diff --git a/.env.example b/.env.example index be615356cb..eb3dc31fc4 100644 --- a/.env.example +++ b/.env.example @@ -71,6 +71,10 @@ PORT=20128 # API_HOST=0.0.0.0 # DASHBOARD_PORT=20128 +# Use Turbopack in local dev. Next 16.2.4 can fail to compile next/font/google +# through the custom dev runner without this on Windows. +OMNIROUTE_USE_TURBOPACK=1 + # Docker production port mappings (docker-compose.prod.yml only). # These set the HOST-side published ports. Container ports use PORT/API_PORT. # PROD_DASHBOARD_PORT=20130 @@ -435,15 +439,15 @@ QODER_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW # Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup. # Update these when providers release new CLI versions to avoid blocks. -CLAUDE_USER_AGENT=claude-cli/1.0.83 (external, cli) +CLAUDE_USER_AGENT=claude-cli/2.1.121 (external, cli) CODEX_USER_AGENT=codex-cli/0.125.0 (Windows 10.0.26100; x64) -GITHUB_USER_AGENT=GitHubCopilotChat/0.26.7 -ANTIGRAVITY_USER_AGENT=antigravity/1.104.0 darwin/arm64 +GITHUB_USER_AGENT=GitHubCopilotChat/0.45.1 +ANTIGRAVITY_USER_AGENT=antigravity/1.107.0 darwin/arm64 KIRO_USER_AGENT=AWS-SDK-JS/3.0.0 kiro-ide/1.0.0 QODER_USER_AGENT=Qoder-Cli -QWEN_USER_AGENT=QwenCode/0.12.3 (linux; x64) +QWEN_USER_AGENT=QwenCode/0.15.3 (linux; x64) CURSOR_USER_AGENT=connect-es/1.6.1 -GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1 +GEMINI_CLI_USER_AGENT=google-api-nodejs-client/10.3.0 # ═══════════════════════════════════════════════════════════════════════════════ @@ -586,6 +590,16 @@ APP_LOG_TO_FILE=true # Default: 100000 # CALL_LOGS_TABLE_MAX_ROWS=100000 +# Whether call log pipeline capture stores stream chunks when enabled in settings. +# Only applies when call_log_pipeline_enabled=true. +# Default: true +# CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS=true + +# Maximum call log artifact size for pipeline captures, in KB. +# Only applies when call_log_pipeline_enabled=true. +# Default: 512 +# CALL_LOG_PIPELINE_MAX_SIZE_KB=512 + # Maximum rows in the proxy_logs SQLite table. # Default: 100000 # PROXY_LOGS_TABLE_MAX_ROWS=100000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c5b6bf15b..3f0a18e460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,44 +100,6 @@ jobs: cat .artifacts/pr-test-policy.md >> "$GITHUB_STEP_SUMMARY" fi - advanced-security: - name: Advanced Security Scans - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: TruffleHog Secret Scan - uses: trufflesecurity/trufflehog@main - with: - path: ./ - base: ${{ github.event.before || github.event.repository.default_branch }} - head: HEAD - extra_args: --only-verified - - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.CI_NODE_VERSION }} - cache: npm - - - run: npm ci - - run: npm run check:node-runtime - - - name: Dependency audit - run: npm audit --audit-level=high --omit=dev - - - name: Check for known vulnerabilities - run: npx is-my-node-vulnerable - - - name: Run Snyk Vulnerability checks - if: github.actor != 'dependabot[bot]' - uses: snyk/actions/node@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high - build: name: Build runs-on: ubuntu-latest @@ -408,6 +370,7 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" + OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1" steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -471,7 +434,7 @@ jobs: - lint - i18n - pr-test-policy - - advanced-security + - build - package-artifact - electron-package-smoke @@ -514,7 +477,7 @@ jobs: echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY" echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY" - echo "| Advanced Security | $(status '${{ needs.advanced-security.result }}') |" >> "$GITHUB_STEP_SUMMARY" + echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" diff --git a/CHANGELOG.md b/CHANGELOG.md index d89796bbac..ad4488b5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,72 @@ --- +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + ## [3.7.1] — 2026-04-26 ### ✨ New Features diff --git a/Dockerfile b/Dockerfile index e6c334e294..8ca152fb1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,11 @@ COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers COPY --from=builder /app/node_modules/pino-abstract-transport ./node_modules/pino-abstract-transport COPY --from=builder /app/node_modules/pino-pretty ./node_modules/pino-pretty COPY --from=builder /app/node_modules/split2 ./node_modules/split2 +# Migration SQL files are read via fs.readFileSync at runtime and are NOT +# traced by Next.js standalone output — copy them explicitly. +COPY --from=builder /app/src/lib/db/migrations ./migrations +ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations + COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs COPY --from=builder /app/scripts/bootstrap-env.mjs ./bootstrap-env.mjs diff --git a/README.md b/README.md index 1b7e440391..f619d64816 100644 --- a/README.md +++ b/README.md @@ -2050,6 +2050,7 @@ opencode - `call_logs` in SQLite stores summary metadata for the Request Logs table and analytics views - Detailed request/response payloads are written to `DATA_DIR/call_logs/` as one JSON artifact per request - Enable pipeline capture from Dashboard → Logs → Request Logs if you need detailed per-stage payloads +- When pipeline capture is enabled, `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS=false` skips stream chunks and `CALL_LOG_PIPELINE_MAX_SIZE_KB` controls the artifact cap in KB - `Export Logs` reads the artifact files on demand, while `Export All` includes the `call_logs/` directory alongside `storage.sqlite` - Set `APP_LOG_TO_FILE=true` if you also want application console logs in `logs/application/app.log` - Adjust `APP_LOG_MAX_FILE_SIZE`, `APP_LOG_RETENTION_DAYS`, `APP_LOG_MAX_FILES`, and `CALL_LOG_MAX_ENTRIES` as needed diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index 79513bd63f..f0c5bcec44 100644 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -16,7 +16,7 @@ import { spawn } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { homedir, platform } from "node:os"; import { isNativeBinaryCompatible } from "../scripts/native-binary-compat.mjs"; import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs"; @@ -218,7 +218,7 @@ if (args.includes("reset-encrypted-columns")) { if (args.includes("--mcp")) { try { - const { startMcpCli } = await import(join(ROOT, "bin", "mcp-server.mjs")); + const { startMcpCli } = await import(pathToFileURL(join(ROOT, "bin", "mcp-server.mjs")).href); await startMcpCli(ROOT); } catch (err) { console.error("\x1b[31m✖ Failed to start MCP server:\x1b[0m", err.message || err); diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index c6e20dfd73..fddc751655 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -333,18 +333,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. @@ -464,19 +464,21 @@ REQUEST_TIMEOUT_MS (global override) The logging system writes to both stdout and rotated log files. All configuration is read by `src/lib/logEnv.ts`. -| Variable | Default | Description | -| --------------------------- | -------------------------- | ---------------------------------------------------------------------------- | -| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | -| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | -| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | -| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | -| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | -| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | -| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | -| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | -| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | -| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | -| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | +| Variable | Default | Description | +| ----------------------------------------- | -------------------------- | -------------------------------------------------------------------------------- | +| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | +| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | +| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | +| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | +| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | +| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | +| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | +| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | +| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | +| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | +| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. | +| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. | +| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | --- diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f9a69755bb..5a46315fba 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -191,6 +191,8 @@ curl -s http://localhost:20128/api/cli-tools/openclaw-settings | jq '{installed, Set `APP_LOG_TO_FILE=true` in your `.env` file. Application logs are written under `logs/`. Request artifacts are stored under `${DATA_DIR}/call_logs/` when the call log pipeline is enabled in settings. +When pipeline capture is enabled, set `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS=false` to omit +stream chunk payloads, or tune `CALL_LOG_PIPELINE_MAX_SIZE_KB` to change the artifact cap in KB. ### Check Provider Health diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 29c66f7151..5d1bff48ed 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### التوثيق + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### التوثيق diff --git a/docs/i18n/ar/docs/ENVIRONMENT.md b/docs/i18n/ar/docs/ENVIRONMENT.md index 07e2623abe..d6f4e69552 100644 --- a/docs/i18n/ar/docs/ENVIRONMENT.md +++ b/docs/i18n/ar/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index a1f80dbe77..6b7682092e 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Документация + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Документация diff --git a/docs/i18n/bg/docs/ENVIRONMENT.md b/docs/i18n/bg/docs/ENVIRONMENT.md index e5511e1c1b..14eb688557 100644 --- a/docs/i18n/bg/docs/ENVIRONMENT.md +++ b/docs/i18n/bg/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 74a16aec4c..35b0c6d20d 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/bn/docs/ENVIRONMENT.md b/docs/i18n/bn/docs/ENVIRONMENT.md index e7dd93b9e3..b8c04a1188 100644 --- a/docs/i18n/bn/docs/ENVIRONMENT.md +++ b/docs/i18n/bn/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index b8f0be60f2..068b2112c1 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentace + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentace diff --git a/docs/i18n/cs/docs/ENVIRONMENT.md b/docs/i18n/cs/docs/ENVIRONMENT.md index 55660e53ba..96e8ce9cd9 100644 --- a/docs/i18n/cs/docs/ENVIRONMENT.md +++ b/docs/i18n/cs/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index e36558901d..df2bdece33 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentation + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentation diff --git a/docs/i18n/da/docs/ENVIRONMENT.md b/docs/i18n/da/docs/ENVIRONMENT.md index 435e622aba..1f1f447627 100644 --- a/docs/i18n/da/docs/ENVIRONMENT.md +++ b/docs/i18n/da/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 3485487a3e..49897fdd31 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentation + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentation diff --git a/docs/i18n/de/docs/ENVIRONMENT.md b/docs/i18n/de/docs/ENVIRONMENT.md index 4c6c94d130..488b3aa497 100644 --- a/docs/i18n/de/docs/ENVIRONMENT.md +++ b/docs/i18n/de/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index ffbe8578db..bed84623fa 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/es/docs/ENVIRONMENT.md b/docs/i18n/es/docs/ENVIRONMENT.md index 29236888dc..90598e49d2 100644 --- a/docs/i18n/es/docs/ENVIRONMENT.md +++ b/docs/i18n/es/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index a5375a0e32..13ea7e5911 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/fa/docs/ENVIRONMENT.md b/docs/i18n/fa/docs/ENVIRONMENT.md index 2aeefa0f60..30bca6bc1d 100644 --- a/docs/i18n/fa/docs/ENVIRONMENT.md +++ b/docs/i18n/fa/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index b7e978a875..bb093ded7f 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentaatio + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentaatio diff --git a/docs/i18n/fi/docs/ENVIRONMENT.md b/docs/i18n/fi/docs/ENVIRONMENT.md index c2103f32b0..0657313ad3 100644 --- a/docs/i18n/fi/docs/ENVIRONMENT.md +++ b/docs/i18n/fi/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 1365046df5..59df590135 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentation + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentation diff --git a/docs/i18n/fr/docs/ENVIRONMENT.md b/docs/i18n/fr/docs/ENVIRONMENT.md index f714f92af0..6c9e65a3fe 100644 --- a/docs/i18n/fr/docs/ENVIRONMENT.md +++ b/docs/i18n/fr/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index 279a55d9ca..4f7e6146aa 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/gu/docs/ENVIRONMENT.md b/docs/i18n/gu/docs/ENVIRONMENT.md index da6c3240a5..a401749212 100644 --- a/docs/i18n/gu/docs/ENVIRONMENT.md +++ b/docs/i18n/gu/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 92e6addada..135eadfcc6 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### תיעוד + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### תיעוד diff --git a/docs/i18n/he/docs/ENVIRONMENT.md b/docs/i18n/he/docs/ENVIRONMENT.md index f386ce796c..80c7db9ef9 100644 --- a/docs/i18n/he/docs/ENVIRONMENT.md +++ b/docs/i18n/he/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 33fdb95ef0..10a9fdb26c 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### दस्तावेज़ + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### दस्तावेज़ diff --git a/docs/i18n/hi/docs/ENVIRONMENT.md b/docs/i18n/hi/docs/ENVIRONMENT.md index eef5fd8f71..83594a217d 100644 --- a/docs/i18n/hi/docs/ENVIRONMENT.md +++ b/docs/i18n/hi/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 9f52f365ff..67f3ef3cc4 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentáció + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentáció diff --git a/docs/i18n/hu/docs/ENVIRONMENT.md b/docs/i18n/hu/docs/ENVIRONMENT.md index 9106793b36..1f58566cf6 100644 --- a/docs/i18n/hu/docs/ENVIRONMENT.md +++ b/docs/i18n/hu/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 0157eadea8..87aed59ee4 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentasi + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentasi diff --git a/docs/i18n/id/docs/ENVIRONMENT.md b/docs/i18n/id/docs/ENVIRONMENT.md index 2051450b62..0dd88957b4 100644 --- a/docs/i18n/id/docs/ENVIRONMENT.md +++ b/docs/i18n/id/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/in/docs/ENVIRONMENT.md b/docs/i18n/in/docs/ENVIRONMENT.md index 479cd1e3cb..e44eec26f8 100644 --- a/docs/i18n/in/docs/ENVIRONMENT.md +++ b/docs/i18n/in/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index f5766f9f2c..2af1fbb7bb 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentazione + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentazione diff --git a/docs/i18n/it/docs/ENVIRONMENT.md b/docs/i18n/it/docs/ENVIRONMENT.md index a6e876dcd6..d05f2f7d1e 100644 --- a/docs/i18n/it/docs/ENVIRONMENT.md +++ b/docs/i18n/it/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 867fdec1e2..86b42fa60d 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### ドキュメント + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### ドキュメント diff --git a/docs/i18n/ja/docs/ENVIRONMENT.md b/docs/i18n/ja/docs/ENVIRONMENT.md index ba9aa73430..02209c67e0 100644 --- a/docs/i18n/ja/docs/ENVIRONMENT.md +++ b/docs/i18n/ja/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index e0b11d2069..03fb452bf8 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### 문서 + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### 문서 diff --git a/docs/i18n/ko/docs/ENVIRONMENT.md b/docs/i18n/ko/docs/ENVIRONMENT.md index 5eb42d6221..75f3bcb81a 100644 --- a/docs/i18n/ko/docs/ENVIRONMENT.md +++ b/docs/i18n/ko/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 3f046d4873..98e9c5907d 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/mr/docs/ENVIRONMENT.md b/docs/i18n/mr/docs/ENVIRONMENT.md index 44074f7e4a..72339e6a0b 100644 --- a/docs/i18n/mr/docs/ENVIRONMENT.md +++ b/docs/i18n/mr/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 54ca630462..04f8e66d2e 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentasi + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentasi diff --git a/docs/i18n/ms/docs/ENVIRONMENT.md b/docs/i18n/ms/docs/ENVIRONMENT.md index a70cc1bc81..70e3bdccdc 100644 --- a/docs/i18n/ms/docs/ENVIRONMENT.md +++ b/docs/i18n/ms/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 0631fecac7..dc3a8ac997 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentatie + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentatie diff --git a/docs/i18n/nl/docs/ENVIRONMENT.md b/docs/i18n/nl/docs/ENVIRONMENT.md index 2c5445a5cc..ef9019a6c1 100644 --- a/docs/i18n/nl/docs/ENVIRONMENT.md +++ b/docs/i18n/nl/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 8d24145b88..33edd30b68 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentasjon + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentasjon diff --git a/docs/i18n/no/docs/ENVIRONMENT.md b/docs/i18n/no/docs/ENVIRONMENT.md index f0a00d7d45..0fc80dd1dc 100644 --- a/docs/i18n/no/docs/ENVIRONMENT.md +++ b/docs/i18n/no/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 8d382a00ea..d917dd537a 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentasyon + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentasyon diff --git a/docs/i18n/phi/docs/ENVIRONMENT.md b/docs/i18n/phi/docs/ENVIRONMENT.md index 7eea6cc128..ead1954036 100644 --- a/docs/i18n/phi/docs/ENVIRONMENT.md +++ b/docs/i18n/phi/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index f44336a591..eaaf4e6040 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentacja + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentacja diff --git a/docs/i18n/pl/docs/ENVIRONMENT.md b/docs/i18n/pl/docs/ENVIRONMENT.md index b63408be86..ffdaac9456 100644 --- a/docs/i18n/pl/docs/ENVIRONMENT.md +++ b/docs/i18n/pl/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 6cf7bade37..4c783165b0 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentação + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentação diff --git a/docs/i18n/pt-BR/docs/ENVIRONMENT.md b/docs/i18n/pt-BR/docs/ENVIRONMENT.md index 0958ffbfe9..c8f1be5ab3 100644 --- a/docs/i18n/pt-BR/docs/ENVIRONMENT.md +++ b/docs/i18n/pt-BR/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 7f86e72f76..922f92eb2d 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentação + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentação diff --git a/docs/i18n/pt/docs/ENVIRONMENT.md b/docs/i18n/pt/docs/ENVIRONMENT.md index 8af74c378e..0ba9d98a70 100644 --- a/docs/i18n/pt/docs/ENVIRONMENT.md +++ b/docs/i18n/pt/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index b9f84f481d..5f7d058fcc 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentație + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentație diff --git a/docs/i18n/ro/docs/ENVIRONMENT.md b/docs/i18n/ro/docs/ENVIRONMENT.md index 9bde644a4d..4cd16930b8 100644 --- a/docs/i18n/ro/docs/ENVIRONMENT.md +++ b/docs/i18n/ro/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index 8b9496e6c5..01d695da50 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Документация + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Документация diff --git a/docs/i18n/ru/docs/ENVIRONMENT.md b/docs/i18n/ru/docs/ENVIRONMENT.md index 0255638307..a2929b5163 100644 --- a/docs/i18n/ru/docs/ENVIRONMENT.md +++ b/docs/i18n/ru/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 76e5d359d4..96a42b609a 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentácia + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentácia diff --git a/docs/i18n/sk/docs/ENVIRONMENT.md b/docs/i18n/sk/docs/ENVIRONMENT.md index 9cb1a3fe77..959388513b 100644 --- a/docs/i18n/sk/docs/ENVIRONMENT.md +++ b/docs/i18n/sk/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index b892add68b..06d09439fe 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Dokumentation + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Dokumentation diff --git a/docs/i18n/sv/docs/ENVIRONMENT.md b/docs/i18n/sv/docs/ENVIRONMENT.md index 5868f66aaf..82acc6a50f 100644 --- a/docs/i18n/sv/docs/ENVIRONMENT.md +++ b/docs/i18n/sv/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 34d516a661..436b5e0a7f 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/sw/docs/ENVIRONMENT.md b/docs/i18n/sw/docs/ENVIRONMENT.md index c4c4f59550..12ae89905b 100644 --- a/docs/i18n/sw/docs/ENVIRONMENT.md +++ b/docs/i18n/sw/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index 8b7970e876..d960f7dccd 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/ta/docs/ENVIRONMENT.md b/docs/i18n/ta/docs/ENVIRONMENT.md index 8e5058c812..60e0fe7975 100644 --- a/docs/i18n/ta/docs/ENVIRONMENT.md +++ b/docs/i18n/ta/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 855a1f4e48..28cf1383cb 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/te/docs/ENVIRONMENT.md b/docs/i18n/te/docs/ENVIRONMENT.md index 4e853e1130..9f9e2b72bc 100644 --- a/docs/i18n/te/docs/ENVIRONMENT.md +++ b/docs/i18n/te/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index da1fcdb25f..e7072e3ef4 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### เอกสาร + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### เอกสาร diff --git a/docs/i18n/th/docs/ENVIRONMENT.md b/docs/i18n/th/docs/ENVIRONMENT.md index 247000f4c3..86f0dd4506 100644 --- a/docs/i18n/th/docs/ENVIRONMENT.md +++ b/docs/i18n/th/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 481feaaf6f..76d74c3559 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Belgeler + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Belgeler diff --git a/docs/i18n/tr/docs/ENVIRONMENT.md b/docs/i18n/tr/docs/ENVIRONMENT.md index 05ce1d0cd0..e9f175271c 100644 --- a/docs/i18n/tr/docs/ENVIRONMENT.md +++ b/docs/i18n/tr/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index ec752e73f1..76f7e70588 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Документація + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Документація diff --git a/docs/i18n/uk-UA/docs/ENVIRONMENT.md b/docs/i18n/uk-UA/docs/ENVIRONMENT.md index 588d0ad5ce..75e1673902 100644 --- a/docs/i18n/uk-UA/docs/ENVIRONMENT.md +++ b/docs/i18n/uk-UA/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index c95f59f683..6d5b5d9acf 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Documentación + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Documentación diff --git a/docs/i18n/ur/docs/ENVIRONMENT.md b/docs/i18n/ur/docs/ENVIRONMENT.md index 6a078c2246..67f7fb193b 100644 --- a/docs/i18n/ur/docs/ENVIRONMENT.md +++ b/docs/i18n/ur/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 478fbc63b6..46653db7bc 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### Tài liệu + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### Tài liệu diff --git a/docs/i18n/vi/docs/ENVIRONMENT.md b/docs/i18n/vi/docs/ENVIRONMENT.md index 775fbf2749..2e9ee14a2d 100644 --- a/docs/i18n/vi/docs/ENVIRONMENT.md +++ b/docs/i18n/vi/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index c5401025ea..30a0a503e7 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -4,16 +4,126 @@ --- + ## [Unreleased] --- -## [3.7.0] — 2026-04-24 +## [3.7.2] — 2026-04-27 + +### ✨ New Features + +- **feat(authz):** introduce centralized proxy-based authz pipeline and lifecycle policy (#1632) +- **feat(logs):** configure call log pipeline artifacts (#1650) +- **feat(network):** add guarded remote image fetch utility +- **feat(codex):** enable native Codex websocket responses on beta-gated models (#1658) +- **feat(muse-spark-web):** continue the same meta.ai conversation across turns (#1673) + +### 🐛 Bug Fixes + +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) +- **fix:** add body-read timeout to prevent stuck pending requests (#1680) +- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694) +- **fix(sse):** sanitize OpenAI tool schemas for strict upstream validators — strips null from enum arrays, normalizes tuple items, filters invalid required keys (#1692) +- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693) +- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685) +- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697) +- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696) +- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681) +- **fix(search):** support optional bearer auth for SearXNG (#1683) +- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678) +- **fix(qwen):** use `security.auth` format instead of `modelProviders` for Qwen Code config generation (#1677) +- **fix(codex):** remove stale websocket transport lookup that caused fallback errors (#1676) +- **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) +- **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) +- **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) +- **fix(grokweb):** update Request and Response Specifications (#1655) +- **fix(blackbox-web):** set isPremium flag to true to enable premium model access (#1661) +- **fix(core):** avoid OpenAI stream options for Anthropic-compatible providers (#1654) +- **fix(electron):** resolve MCP server start failure on Windows (#1662) +- **fix(electron):** make Windows smoke test non-blocking (continue-on-error), pre-create userData dir for Windows + stream logs in CI, and add --no-sandbox and sandbox env for CI smoke tests +- **fix(codex):** fix `getWreqWebsocket` ReferenceError causing 502 on all Codex requests (#1652, #1653) +- **fix(codex):** default `store` to `false` — Codex OAuth backend rejects `store=true` (#1635) +- **fix(db):** add post-migration guards for missing `batches` table and `combos.sort_order` column on DB upgrades (#1648, #1657) +- **fix(db):** renumber duplicate migration `032` to prevent collision +- **fix(perplexity-web):** update API version and user-agent to match upstream requirements (#1666) +- **fix(docker):** copy SQLite migration files and explicitly trace in standalone build (#1665) +- **fix(muse-spark-web):** update to Meta's Ecto-era persisted query — fixes 502 `Unknown type "RewriteOptionsInput"` after Meta retired the Abra mutation (#1668) +- **fix(dev):** enable Turbopack by default and repair Codex CORS headers (#1669) +- **fix(authz):** restore `REQUIRE_API_KEY` support in clientApi policy +- **fix(auth):** align fallback API key format with test setup + +### 🛠️ Maintenance + +- **build(prepublish):** make Next.js build bundler configurable (webpack/turbopack) +- **ci:** align sonar analysis scope +- **ci:** stabilize release branch checks +- **ci:** remove expired advanced security scans job + +### 🧪 Tests + +- **test:** fix TypeScript configuration errors in plan3-p0.test.ts +- **test:** fix implicit any types across test suites +- **test:** disable type checking in flaky unit tests +- **test:** fix failing tests due to recent refactors +- **fix(tests):** align integration tests with authz pipeline refactor +- **fix(tests):** align test assertions with v3.7.2 source code changes +- **fix(tests):** CORS test now checks object body instead of entire file +- **fix(e2e):** fix E2E flakiness and implicit any type errors + +--- + +## [3.7.1] — 2026-04-26 + +### ✨ New Features + +- **feat(providers):** Add GPT-5.5 support to the Codex provider — includes 1.05M context window, tool calling, vision, and reasoning capabilities with proper pricing entries across `cx` and `openai` providers. Refactors `splitCodexReasoningSuffix()` into a shared helper for cleaner effort-level parsing (#1617 — thanks @Zhaba1337228). +- **feat(cli):** Add `omniroute reset-encrypted-columns` recovery command — nulls encrypted credential columns (`api_key`, `access_token`, `refresh_token`, `id_token`) in `provider_connections` while preserving provider metadata, giving users affected by #1622 a clean recovery path without losing configurations. +- **feat(i18n):** Expand locale coverage with nine new language packs (Bengali, Farsi, Gujarati, Indonesian, Marathi, Swahili, Tamil, Telugu, Urdu), bringing total language support from 32 to 41 locales. + +### 🐛 Bug Fixes + +- **fix(rate-limit):** Add per-model rate limiting for GitHub Copilot provider — a 429 on one model (e.g. `gpt-5.1-codex-max`) no longer locks the entire connection, matching the existing Gemini per-model quota pattern (#1624 — thanks @slewis3600). +- **fix(cli-tools):** Preserve existing OpenCode configuration (MCP servers, custom providers, comments) when saving OmniRoute settings — uses `jsonc-parser` for tree-preserving edits instead of destructive JSON roundtrip. Fix API key clipboard copy to use raw keys instead of masked placeholders. Add theme-aware OpenCode light/dark SVG logos (#1626 — thanks @JasonLandbridge). +- **fix(cli-tools):** Fix OpenCode guide step 3 `{{baseUrl}}` double-brace placeholder to use ICU-style `{baseUrl}` across all 41 locales, restoring next-intl interpolation (#1626). +- **fix(codex):** Make `wreq-js` native module import lazy and optional to prevent server crash on startup when the platform-specific binary is missing — affects pnpm installs, Docker Alpine, macOS ARM, and Windows (#1612, #1613, #1616). +- **fix(i18n):** Add 14 missing translation keys (`logs.runningRequests`, `logs.model`, `logs.provider`, `logs.account`, `logs.elapsed`, `logs.count`, `logs.payloads`, etc.) for the Active Requests panel across all locales. Replace 83 placeholder values in usage/evals namespace. Add 5 missing health namespace keys for rate limit status. +- **fix(encryption):** Prevent `STORAGE_ENCRYPTION_KEY` from being silently regenerated during `npm install -g` upgrades, which made all previously-encrypted provider credentials permanently unrecoverable due to AES-GCM auth-tag mismatch (#1622). +- **fix(startup):** Add decrypt-probe diagnostic at server bootstrap — if `STORAGE_ENCRYPTION_KEY` doesn't match encrypted credentials in the database, a prominent warning is logged directing users to restore the key or use the new recovery command. +- **fix(cli-tools):** Allow `null` API key values in `cliModelConfigSchema` to prevent 400 Bad Request errors when saving cloud-based CLI tool configurations. Fix error handling across all 10 ToolCard components to safely extract messages from structured error objects, preventing React Error #31 crashes. +- **fix(docker):** Set `NPM_CONFIG_LEGACY_PEER_DEPS=true` in the Docker builder layer before `npm ci` and remove duplicate `postinstallSupport.mjs` COPY instruction — fixes container image build failures introduced in v3.7.0 (#1630 — thanks @rdself). +- **fix(antigravity):** Hide deprecated Gemini-routed Claude 4.5 models from public catalogs and model lists. Legacy `gemini-claude-*` aliases now silently resolve to current Claude 4.6 equivalents. Replace dynamic reverse-alias generation with an explicit allowlist for predictable model visibility (#1631 — thanks @backryun). +- **fix(types):** Add explicit type annotations to sync-env test helpers and dynamic import casts to satisfy `typecheck:noimplicit:core` CI gate. +- **fix(reasoning):** Implement Reasoning Replay Cache — hybrid memory/SQLite persistence for `reasoning_content` in multi-turn tool-calling flows. Automatically captures reasoning from DeepSeek V4, Kimi K2, Qwen-Thinking, and GLM models and re-injects it on follow-up turns to prevent HTTP 400 errors from strict reasoning-content validation. Includes dashboard telemetry tab, REST API, and 21 unit tests (#1628 — thanks @JasonLandbridge). +- **fix(postinstall):** Extend postinstall native module repair to cover `wreq-js` — detects missing platform-specific `.node` binaries inside `app/node_modules/wreq-js/rust/` and copies them from the root install. Fixes global `pnpm` installs on macOS arm64 where the standalone app directory only contained Linux binaries (#1634 — thanks @MarcosT96). +- **fix(migration):** Prevent compat-renamed migration slots from shadowing new migrations at the same version number. After rewriting `028_provider_connection_max_concurrent` → `029`, the runner now verifies the old version slot is clear, ensuring `028_create_files_and_batches` runs on v3.6.x → v3.7.x upgrades. Adds `batches` table as a physical schema sentinel for upgrade recovery (#1637 — thanks @V8-Software). +- **fix(registry):** Route GitHub Copilot GPT 5.4/5.5 models through the Responses API (`targetFormat: "openai-responses"`). Fixes `gpt-5.4-mini` and `gpt-5.4` being rejected on `/chat/completions` by GitHub (#1641 — thanks @dhaern). +- **fix(usage):** Correct MiniMax token plan quota display — the newer `/v1/token_plan/remains` endpoint reports used counts, not remaining counts. Rounds floating-point percentage artifacts in Provider Limits UI (#1642 — thanks @CruxExperts). +- **fix(codex):** Lazy-load `wreq-js` WebSocket transport via `createRequire` instead of top-level import. Server boots cleanly when native module is unavailable and returns 503 only when Codex WebSocket is actually requested. Fixes #1612 (#1640 — thanks @dendyadinirwana). +- **fix(electron):** Package Electron runtime dependencies into `resources/app/node_modules/` via separate `extraResources` FileSet. Adds cross-platform packaged app smoke test script and CI integration to prevent future regressions. Closes #1636 (#1639 — thanks @prateek). +- **feat(account-fallback):** Add model-level daily quota lockout. When a provider returns 429 with `quota_exhausted`, cooldown is set to tomorrow 00:00 instead of exponential backoff. Detects daily quota patterns via `isDailyQuotaExhausted()` in chat handler (#1644 — thanks @clousky2020). +- **fix(codex):** Use per-conversation `session_id`/`conversation_id` from client body as `prompt_cache_key` instead of account-wide `workspaceId`. The official Codex CLI uses `conversation_id` (a unique UUID per session); using the shared `workspaceId` capped cache hit-rate at ~49%. Includes 10 unit tests (#1643). +- **fix(claude):** Stabilize billing header fingerprint to prevent Anthropic prompt-cache prefix invalidation. The fingerprint was derived from the first user message text, which changes every turn, mutating `system[]` and forcing ~100% `cache_create`. Now uses a stable per-day hash, preserving ~96% `cache_read` hit rate (#1638). +- **fix(transport):** Harden GitHub and Kiro streaming — thread `clientHeaders` through `BaseExecutor.buildHeaders()` to eliminate mutable singleton state race condition on concurrent requests. Remove redundant `[DONE]` stripping TransformStream from GitHub executor. Add defensive `parseToolInput()` for malformed Kiro tool call arguments. Hoist `TextEncoder`/`TextDecoder` to module singletons and use zero-copy `subarray()` (#1645 — thanks @dhaern). +- **fix(transport):** Prevent memory bloat and database exhaustion from large, fragmented streaming responses. Implemented `ByteQueue` in `kiro.ts` for zero-copy binary accumulation, refactored `antigravity.ts` for incremental SSE parsing, and enforced a strict 512KB tiered truncation limit (`MAX_CALL_LOG_ARTIFACT_BYTES`) on stream request logs and call artifacts (#1647). +- **chore(ci):** Update build environment dependencies — bump Node to `24.15.0`, `actions/checkout@v6`, `docker/build-push-action@v7`, pin `actions/setup-python` to major tag (#1646 — thanks @backryun). + +### 文档 + +- **docs(env):** Add `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` to `.env.example` with documentation for LM Studio and other local provider use cases (#1623). + +--- + +## [3.7.0] — 2026-04-26 ### ✨ New Features - **feat(providers):** Implement Image Generation and Editing capabilities for ChatGPT Web, including in-band chat image generation and caching (#1606). - **feat(ui):** Integrate OpenCode Zen/Go API tool logo SVG and polish API key copy-to-clipboard interactions (#1607). +- **feat(providers):** Add CrofAI as a built-in API-key provider with quota/usage monitoring wired into the dashboard Limits page (#1604, #1606). +- **feat(skills):** Add workspace-scoped built-in skills (`file_read`, `file_write`, `http_request`, `eval_code`, `execute_command`) with real sandbox execution via Docker, replacing stub responses. Browser skills now fail explicitly when runtime is not configured. - **feat(providers):** Integrate AgentRouter as a new OpenAI-compatible passthrough provider with $200 free credits via sign-up (Issue #1572). - **feat(ui):** Implement on-demand per-model testing in the provider dashboard, allowing single-token diagnostic checks without triggering rate-limits (Issue #1532). @@ -114,6 +224,14 @@ - **fix(combo):** Resolve context truncation bug in combo routing to prevent incomplete execution states. (#1517) - **fix(compression):** Implement bidirectional tool_pair cleaning for anthropic inputs (fixes #1592). - **fix:** Resolve v3.7.0 stabilization issues including dashboard navigation routing, ProxyRegistryManager component layout, and models API response merging (#1566, #1560, #1559). +- **fix(cli):** Preserve TOML integer/boolean types in Codex config round-trip to prevent `tui.model_availability_nux` validation errors. +- **fix(tailscale):** Support sudo auth prompts and live daemon socket detection for non-root tunnel management. +- **fix(dashboard):** Stabilize usage tab loading and refresh behavior to prevent empty state flashes. +- **fix(i18n):** Translate 519 untranslated pt-BR keys and add missing Windsurf/Cline/Kimi docs keys. +- **fix(i18n):** Add missing dashboard message keys across all 30 locales. +- **fix(cli):** Align OpenCode config preview and add multi-model selection (#1602). +- **fix(security):** Harden management API auth and OpenAPI try-proxy endpoint. +- **fix(security):** Resolve vulnerability scan findings for auth-guarded routes. ### ♻️ Refactoring @@ -133,6 +251,7 @@ - **test(security):** Update prompt injection test for fail-closed policy alignment. - **test(core):** Restore local test fixes for encryption and resilience modules. - **test(next):** Align transpile package expectations for the Next.js standalone build. +- **test(ci):** Fix CI-only test failures from environment differences — clear `INITIAL_PASSWORD` and `JWT_SECRET` in integration tests, handle `XDG_CONFIG_HOME` for guide-settings tests. ### 文档 diff --git a/docs/i18n/zh-CN/docs/ENVIRONMENT.md b/docs/i18n/zh-CN/docs/ENVIRONMENT.md index cc6f3cd00d..39000a917e 100644 --- a/docs/i18n/zh-CN/docs/ENVIRONMENT.md +++ b/docs/i18n/zh-CN/docs/ENVIRONMENT.md @@ -337,18 +337,18 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] > **Source:** `open-sse/executors/base.ts` → `buildHeaders()` -| Variable | Default Value | When to Update | -| ------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/1.0.83 (external, cli)` | When Anthropic releases a new CLI version | -| `CODEX_USER_AGENT` | `codex-cli/0.92.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | -| `CODEX_CLIENT_VERSION` | `0.92.0` | Override Codex client version independently of full UA string | -| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.26.7` | When GitHub Copilot Chat updates | -| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.104.0 darwin/arm64` | When Antigravity IDE updates | -| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | -| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.12.3 (linux; x64)` | When Qwen Code updates | -| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | -| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/9.15.1` | When Google API client updates | +| Variable | Default Value | When to Update | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.121 (external, cli)` | When Anthropic releases a new CLI version | +| `CODEX_USER_AGENT` | `codex-cli/0.125.0 (Windows 10.0.26100; x64)` | When OpenAI updates the Codex CLI | +| `CODEX_CLIENT_VERSION` | `0.125.0` | Override Codex client version independently of full UA string | +| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates | +| `ANTIGRAVITY_USER_AGENT` | `antigravity/1.107.0 darwin/arm64` | When Antigravity IDE updates | +| `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | +| `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | +| `QWEN_USER_AGENT` | `QwenCode/0.15.3 (linux; x64)` | When Qwen Code updates | +| `CURSOR_USER_AGENT` | `connect-es/1.6.1` | When Cursor updates | +| `GEMINI_CLI_USER_AGENT` | `google-api-nodejs-client/10.3.0` | When Google API client updates | > [!TIP] > You can add User-Agent overrides for **any** provider using the pattern `{PROVIDER_ID}_USER_AGENT`. The executor dynamically constructs the env var name. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 8e76603dc5..707cd28a24 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.7.1 + version: 3.7.2 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/electron/package.json b/electron/package.json index e6216a179a..a15ff31ed1 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.7.1", + "version": "3.7.2", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/llm.txt b/llm.txt index 84c0c07310..5f56546230 100644 --- a/llm.txt +++ b/llm.txt @@ -8,7 +8,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo **Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost. -**Current version:** 3.7.1 +**Current version:** 3.7.2 ## Tech Stack @@ -279,7 +279,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo └── .env.example # Environment variable template ``` -## Key Features (v3.7.1) +## Key Features (v3.7.2) ### Core Proxy - **160+ AI providers** with automatic format translation diff --git a/next.config.mjs b/next.config.mjs index 2973459f9f..b1e835a4ae 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -33,6 +33,11 @@ const nextConfig = { }, }, outputFileTracingRoot: projectRoot, + outputFileTracingIncludes: { + // Migration SQL files are read via fs.readFileSync at runtime and are NOT + // auto-traced by webpack/turbopack — include them explicitly. + "/*": ["./src/lib/db/migrations/**/*"], + }, outputFileTracingExcludes: { // Planning/task docs are not runtime assets and can break standalone copies // when broad fs/path tracing pulls the whole repository into the NFT graph. diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts index efb3986bf8..14dfd83a86 100644 --- a/open-sse/config/anthropicHeaders.ts +++ b/open-sse/config/anthropicHeaders.ts @@ -26,7 +26,7 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [ ...ANTHROPIC_BETA_BASE.slice(3), ].join(","); -export const CLAUDE_CLI_VERSION = "2.1.92"; +export const CLAUDE_CLI_VERSION = "2.1.121"; export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`; -export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.80.0"; -export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.14.0"; +export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.81.0"; +export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0"; diff --git a/open-sse/config/codexClient.ts b/open-sse/config/codexClient.ts index 25db6b1930..383a9dbbf2 100644 --- a/open-sse/config/codexClient.ts +++ b/open-sse/config/codexClient.ts @@ -36,6 +36,7 @@ export function getCodexDefaultHeaders(): Record { return { Version: getCodexClientVersion(), "Openai-Beta": "responses=experimental", + "X-Codex-Beta-Features": "responses_websockets", "User-Agent": getCodexUserAgent(), }; } diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index 25fef8f32a..7fb1d46392 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -11,11 +11,17 @@ const upstreamTimeouts = getUpstreamTimeoutConfig(process.env, (message) => { // and Undici's bodyTimeout instead of this one-shot startup timer. export const FETCH_TIMEOUT_MS = upstreamTimeouts.fetchTimeoutMs; -// Idle timeout for SSE streams (ms). Closes stream if no data for this duration. -// Default: 120s balances deep-reasoning pauses with fast zombie stream detection (#473). -// Extended-thinking models rarely pause >90s between chunks. Override with STREAM_IDLE_TIMEOUT_MS env var. +// Idle timeout for SSE streams (ms). Before a stream is accepted, the same +// budget is used to wait for the first useful event so HTTP 200 zombie streams +// can fail fast and trigger fallback. After startup, it closes streams that go +// idle for this duration. Override with STREAM_IDLE_TIMEOUT_MS env var. export const STREAM_IDLE_TIMEOUT_MS = upstreamTimeouts.streamIdleTimeoutMs; +// Timeout for reading the full response body after headers arrive (ms). +// Prevents indefinite hangs when the upstream sends headers but stalls on the body. +// Defaults to FETCH_TIMEOUT_MS. Override with FETCH_BODY_TIMEOUT_MS env var. +export const FETCH_BODY_TIMEOUT_MS = upstreamTimeouts.fetchBodyTimeoutMs; + // Provider configurations // OAuth credentials read from env vars with hardcoded fallbacks for backward compatibility. // Use provider-credentials.json or env vars to override in production. diff --git a/open-sse/config/providerHeaderProfiles.ts b/open-sse/config/providerHeaderProfiles.ts index c951a8407e..fb379ed6b1 100644 --- a/open-sse/config/providerHeaderProfiles.ts +++ b/open-sse/config/providerHeaderProfiles.ts @@ -1,17 +1,17 @@ import { antigravityUserAgent } from "../services/antigravityHeaders.ts"; export const GITHUB_COPILOT_API_VERSION = "2025-04-01"; -export const GITHUB_COPILOT_EDITOR_VERSION = "vscode/1.110.0"; -export const GITHUB_COPILOT_CHAT_PLUGIN_VERSION = "copilot-chat/0.38.0"; -export const GITHUB_COPILOT_CHAT_USER_AGENT = "GitHubCopilotChat/0.38.0"; -export const GITHUB_COPILOT_REFRESH_PLUGIN_VERSION = "copilot/1.300.0"; +export const GITHUB_COPILOT_EDITOR_VERSION = "vscode/1.117.0"; +export const GITHUB_COPILOT_CHAT_PLUGIN_VERSION = "copilot-chat/0.45.1"; +export const GITHUB_COPILOT_CHAT_USER_AGENT = "GitHubCopilotChat/0.45.1"; +export const GITHUB_COPILOT_REFRESH_PLUGIN_VERSION = "copilot/1.388.0"; export const GITHUB_COPILOT_REFRESH_USER_AGENT = "GithubCopilot/1.0"; export const GITHUB_COPILOT_INTEGRATION_ID = "vscode-chat"; export const GITHUB_COPILOT_OPENAI_INTENT = "conversation-panel"; export const GITHUB_COPILOT_DEFAULT_INITIATOR = "user"; export const GITHUB_COPILOT_USER_AGENT_LIBRARY = "electron-fetch"; -export const QWEN_CLI_USER_AGENT = "QwenCode/0.12.3 (linux; x64)"; +export const QWEN_CLI_USER_AGENT = "QwenCode/0.15.3 (linux; x64)"; export const QWEN_STAINLESS_ARCH = "x64"; export const QWEN_STAINLESS_LANG = "js"; export const QWEN_STAINLESS_OS = "Linux"; @@ -23,7 +23,7 @@ export const QWEN_ACCEPT_LANGUAGE = "*"; export const QWEN_SEC_FETCH_MODE = "cors"; export const QODER_DEFAULT_USER_AGENT = "Qoder-Cli"; -export const QODER_DASHSCOPE_COMPAT_USER_AGENT = "QwenCode/0.11.1 (linux; x64)"; +export const QODER_DASHSCOPE_COMPAT_USER_AGENT = "QwenCode/0.15.3 (linux; x64)"; export const KIRO_SDK_USER_AGENT = "AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"; export const KIRO_AMZ_USER_AGENT = "aws-sdk-js/3.0.0 kiro-ide/1.0.0"; diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 0309f486a5..d68401e0c1 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -385,7 +385,8 @@ export const REGISTRY: Record = { { id: "codex-auto-review", name: "Codex Auto Review", targetFormat: "openai-responses" }, { id: "gpt-5.5-xhigh", name: "GPT 5.5 (xHigh)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-high", name: "GPT 5.5 (High)", ...GPT_5_5_CODEX_CAPABILITIES }, - { id: "gpt-5.5", name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES }, + { id: "gpt-5.5-medium", name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES }, + { id: "gpt-5.5", name: "GPT 5.5", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-low", name: "GPT 5.5 (Low)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-mini", name: "GPT 5.5 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.4", name: "GPT 5.4", targetFormat: "openai-responses" }, @@ -1091,16 +1092,18 @@ export const REGISTRY: Record = { "grok-web": { id: "grok-web", - alias: "grok-web", + alias: "gw", format: "openai", executor: "grok-web", baseUrl: "https://grok.com/rest/app-chat/conversations/new", authType: "apikey", authHeader: "cookie", + passthroughModels: true, models: [ - { id: "fast", name: "Grok 4.1 Fast" }, - { id: "expert", name: "Grok 4.20" }, - { id: "heavy", name: "Grok 4.20 Heavy" }, + { id: "auto", name: "Grok Auto" }, + { id: "fast", name: "Grok Fast" }, + { id: "expert", name: "Grok 4.20 Thinking" }, + { id: "heavy", name: "Grok 4.20 Multi Agent" }, { id: "grok-420-computer-use-sa", name: "Grok 4.3 (Beta)" }, ], }, diff --git a/open-sse/executors/blackbox-web.ts b/open-sse/executors/blackbox-web.ts index b0b2d29c87..1a95ecb079 100644 --- a/open-sse/executors/blackbox-web.ts +++ b/open-sse/executors/blackbox-web.ts @@ -344,7 +344,7 @@ export class BlackboxWebExecutor extends BaseExecutor { offlineMode: false, }, session: null, - isPremium: false, + isPremium: credentials.providerSpecificData?.isPremium ?? true, teamAccount: "", subscriptionCache: null, beastMode: false, diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 547e8ecbd4..dca26a18e5 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -38,7 +38,7 @@ const SENTINEL_CR_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements` const CONV_URL = `${CHATGPT_BASE}/backend-api/f/conversation`; const CHATGPT_USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0"; + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"; // Captured from a real chatgpt.com browser session (April 2026). const OAI_CLIENT_VERSION = "prod-81e0c5cdf6140e8c5db714d613337f4aeab94029"; diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 1eaebd3d68..ff9bd55489 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -10,6 +10,7 @@ import { PROVIDERS } from "../config/constants.ts"; import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; import { createRequire } from "module"; // ─── wreq-js lazy loader ─────────────────────────────────────────────────── @@ -30,8 +31,10 @@ type WebsocketFn = (url: string, opts?: Record) => Promise): unknown { return marker; } -export function isCodexResponsesWebSocketRequired(model: string, credentials: unknown): boolean { - const normalizedModel = getCodexUpstreamModel(model).trim().toLowerCase(); - if (normalizedModel === "gpt-5.5") return true; +export function isCodexResponsesWebSocketRequired(_model: string, credentials: unknown): boolean { + // OmniRoute is an HTTP→SSE gateway — WebSocket transport is unnecessary and + // breaks when upstream requests go through an HTTP proxy (403 on WS upgrade). + // Default to the standard HTTP Responses SSE endpoint for all Codex models. + // Users who need WebSocket can opt in via the provider codexTransport setting. const providerSpecificData = credentials && typeof credentials === "object" ? (credentials as { providerSpecificData?: Record }).providerSpecificData : null; - return providerSpecificData?.codexTransport === "websocket"; + return !!(providerSpecificData?.codexTransport === "websocket" && getCodexWebSocketTransport()); } function toStatusCode(value: unknown): number | null { @@ -638,19 +668,6 @@ export class CodexExecutor extends BaseExecutor { const headers = normalizeCodexWsHeaders(this.buildHeaders(input.credentials, true)); mergeUpstreamExtraHeaders(headers, input.upstreamExtraHeaders); - const websocket = getCodexWebSocketTransport(); - if (!websocket) { - return { - response: errorResponse( - 503, - "Codex WebSocket transport unavailable: wreq-js native module is missing for this platform" - ), - url, - headers, - transformedBody: input.body, - }; - } - const transformedBody = (await this.transformRequest( input.model, input.body, @@ -666,21 +683,10 @@ export class CodexExecutor extends BaseExecutor { ...transformedBody, }); - const websocketFn = getWreqWebsocket(); + const websocketFn = getCodexWebSocketTransport(); if (!websocketFn) { return { - response: new Response( - JSON.stringify({ - error: { - code: "wreq_unavailable", - message: - "wreq-js native module not available on this platform. " + - "The Codex WebSocket transport requires wreq-js with native binaries. " + - "Please reinstall with npm (not pnpm) or use HTTP transport instead.", - }, - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ), + response: codexWebSocketUnavailableResponse(), url, headers, transformedBody, @@ -989,29 +995,36 @@ export class CodexExecutor extends BaseExecutor { } } else { // Translated: use CODEX_DEFAULT_INSTRUCTIONS as fallback when no system - // prompt was provided by the client (safety net for bare requests). + // prompt was provided by the client, BUT only if tools are requested. + // Injecting tool instructions on bare requests causes Harmony leaks (#1686). + const hasTools = Array.isArray(body.tools) && body.tools.length > 0; if ( !body.instructions || (typeof body.instructions === "string" && body.instructions.trim() === "") ) { - body.instructions = CODEX_DEFAULT_INSTRUCTIONS; + if (hasTools) { + body.instructions = CODEX_DEFAULT_INSTRUCTIONS; + } else { + delete body.instructions; + } } } - // Store: The Codex API defaults store to false when not specified. - // Proxy clients (e.g. OpenClaw) rely on response chaining via previous_response_id, - // which requires store=true so that response items are persisted. - // If the client explicitly sets store, respect it. Otherwise default to true. + // Store: The Codex OAuth backend rejects store=true with + // "Store must be set to false". Default to false unless the provider + // explicitly opts in (e.g. API-key accounts that support persistence). + // Ref: sub2api openai_codex_transform.go line 75-80 const explicitStoreSetting = credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object" && !Array.isArray(credentials.providerSpecificData) ? credentials.providerSpecificData.openaiStoreEnabled : undefined; - if (explicitStoreSetting === false) { - body.store = false; - } else if (body.store === undefined) { + if (explicitStoreSetting === true) { body.store = true; + } else { + // backend rejects store=true ("Store must be set to false"), so default to false. + body.store = false; } // Codex Responses only supports function tools with non-empty names. diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index f587e11582..56696c1687 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -9,7 +9,11 @@ import { } from "../services/claudeCodeCompatible.ts"; import { getGigachatAccessToken } from "../services/gigachatAuth.ts"; import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts"; -import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts"; +import { + getOpenAICompatibleType, + getTargetFormat, + isClaudeCodeCompatible, +} from "../services/provider.ts"; import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts"; import { buildDataRobotChatUrl } from "../config/datarobot.ts"; import { buildAzureAiChatUrl } from "../config/azureAi.ts"; @@ -343,19 +347,30 @@ export class DefaultExecutor extends BaseExecutor { */ transformRequest(model, body, stream, credentials) { void model; - void credentials; - const withDefaults = applyProviderRequestDefaults(body, this.config.requestDefaults); + let withDefaults = applyProviderRequestDefaults(body, this.config.requestDefaults); - if (stream && this.config.format === "openai") { - if (typeof withDefaults === "object" && withDefaults !== null) { - withDefaults.stream_options = { - ...(withDefaults.stream_options || {}), - include_usage: true, + if (typeof withDefaults === "object" && withDefaults !== null && !Array.isArray(withDefaults)) { + if (this.provider?.startsWith?.("anthropic-compatible-")) { + if (Object.prototype.hasOwnProperty.call(withDefaults, "stream_options")) { + const withoutStreamOptions = { ...withDefaults }; + delete withoutStreamOptions.stream_options; + withDefaults = withoutStreamOptions; + } + } else if ( + stream && + getTargetFormat(this.provider, credentials?.providerSpecificData) === "openai" + ) { + withDefaults = { + ...withDefaults, + stream_options: { + ...(withDefaults.stream_options || {}), + include_usage: true, + }, }; } } - if (this.provider === "qwen" && typeof body === "object" && body !== null) { + if (this.provider === "qwen" && typeof withDefaults === "object" && withDefaults !== null) { return sanitizeQwenThinkingToolChoice(withDefaults, "QwenExecutor"); } return withDefaults; diff --git a/open-sse/executors/github.ts b/open-sse/executors/github.ts index 24587b17ec..f8379fbcf6 100644 --- a/open-sse/executors/github.ts +++ b/open-sse/executors/github.ts @@ -90,6 +90,10 @@ export class GithubExecutor extends BaseExecutor { delete modifiedBody.response_format; } + if (Array.isArray(modifiedBody.tools) && modifiedBody.tools.length > 128) { + modifiedBody.tools = modifiedBody.tools.slice(0, 128); + } + return modifiedBody; } diff --git a/open-sse/executors/grok-web.ts b/open-sse/executors/grok-web.ts index a87c51dc86..0ab6cbf04b 100644 --- a/open-sse/executors/grok-web.ts +++ b/open-sse/executors/grok-web.ts @@ -24,69 +24,33 @@ import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; const GROK_CHAT_API = "https://grok.com/rest/app-chat/conversations/new"; const GROK_USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"; + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"; // ─── Model mappings ───────────────────────────────────────────────────────── -// Maps OmniRoute model IDs → [grokModel, modelMode] +// Grok Web exposes UI modes, not stable public model IDs. Keep OmniRoute model +// IDs mapped directly to Grok's modeId field. interface GrokModelInfo { - grokModel: string; - modelMode: string; + modeId: string; isThinking: boolean; } const MODEL_MAP: Record = { - "grok-3": { grokModel: "grok-3", modelMode: "MODEL_MODE_GROK_3", isThinking: false }, - "grok-3-mini": { - grokModel: "grok-3", - modelMode: "MODEL_MODE_GROK_3_MINI_THINKING", - isThinking: true, - }, - "grok-3-thinking": { - grokModel: "grok-3", - modelMode: "MODEL_MODE_GROK_3_THINKING", - isThinking: true, - }, - "grok-4": { grokModel: "grok-4", modelMode: "MODEL_MODE_GROK_4", isThinking: false }, - "grok-4-mini": { - grokModel: "grok-4-mini", - modelMode: "MODEL_MODE_GROK_4_MINI_THINKING", - isThinking: true, - }, - "grok-4-thinking": { - grokModel: "grok-4", - modelMode: "MODEL_MODE_GROK_4_THINKING", - isThinking: true, - }, - "grok-4.3": { - grokModel: "grok-4-3-thinking-1129", - modelMode: "MODEL_MODE_GROK_4_3_THINKING", - isThinking: true, - }, - "grok-4-heavy": { grokModel: "grok-4", modelMode: "MODEL_MODE_HEAVY", isThinking: true }, - "grok-4.1-mini": { - grokModel: "grok-4-1-thinking-1129", - modelMode: "MODEL_MODE_GROK_4_1_MINI_THINKING", - isThinking: true, - }, - "grok-4.1-fast": { - grokModel: "grok-4-1-thinking-1129", - modelMode: "MODEL_MODE_FAST", - isThinking: false, - }, - "grok-4.1-expert": { - grokModel: "grok-4-1-thinking-1129", - modelMode: "MODEL_MODE_EXPERT", - isThinking: true, - }, - "grok-4.1-thinking": { - grokModel: "grok-4-1-thinking-1129", - modelMode: "MODEL_MODE_GROK_4_1_THINKING", - isThinking: true, - }, - "grok-4.2": { grokModel: "grok-420", modelMode: "MODEL_MODE_GROK_420", isThinking: false }, - "grok-4.20": { grokModel: "grok-420", modelMode: "MODEL_MODE_GROK_420", isThinking: false }, - "grok-4.20-beta": { grokModel: "grok-420", modelMode: "MODEL_MODE_GROK_420", isThinking: false }, + auto: { modeId: "auto", isThinking: false }, + fast: { modeId: "fast", isThinking: false }, + expert: { modeId: "expert", isThinking: true }, + heavy: { modeId: "heavy", isThinking: true }, + "grok-420-computer-use-sa": { modeId: "grok-420-computer-use-sa", isThinking: true }, + + // Legacy aliases retained for manually-entered model IDs. + "grok-4": { modeId: "auto", isThinking: false }, + "grok-4.1-fast": { modeId: "fast", isThinking: false }, + "grok-4.1-expert": { modeId: "expert", isThinking: true }, + "grok-4-heavy": { modeId: "heavy", isThinking: true }, + "grok-4.20": { modeId: "expert", isThinking: true }, + "grok-4.20-heavy": { modeId: "heavy", isThinking: true }, + "grok-4.3": { modeId: "grok-420-computer-use-sa", isThinking: true }, + "grok-4-3-thinking-1129": { modeId: "grok-420-computer-use-sa", isThinking: true }, }; // ─── Statsig ID generation ────────────────────────────────────────────────── @@ -549,12 +513,12 @@ export class GrokWebExecutor extends BaseExecutor { return { response: errResp, url: GROK_CHAT_API, headers: {}, transformedBody: body }; } - // Resolve model → Grok internal model/mode + // Resolve model → Grok Web mode const modelInfo = MODEL_MAP[model]; if (!modelInfo) { - log?.info?.("GROK-WEB", `Unmapped model ${model}, defaulting to grok-4.1-fast`); + log?.info?.("GROK-WEB", `Unmapped model ${model}, defaulting to auto mode`); } - const { grokModel, modelMode, isThinking } = modelInfo || MODEL_MAP["grok-4.1-fast"]; + const { modeId, isThinking } = modelInfo || MODEL_MAP.auto; // Parse OpenAI messages → single Grok message string const message = parseOpenAIMessages(messages); @@ -571,8 +535,7 @@ export class GrokWebExecutor extends BaseExecutor { // Build Grok request payload const grokPayload: Record = { temporary: true, - modelName: grokModel, - modelMode: modelMode, + modeId, message: message, fileAttachments: [], imageAttachments: [], @@ -617,7 +580,7 @@ export class GrokWebExecutor extends BaseExecutor { Origin: "https://grok.com", Pragma: "no-cache", Referer: "https://grok.com/", - "Sec-Ch-Ua": '"Google Chrome";v="136", "Chromium";v="136", "Not(A:Brand";v="24"', + "Sec-Ch-Ua": '"Google Chrome";v="147", "Chromium";v="147", "Not(A:Brand";v="24"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"macOS"', "Sec-Fetch-Dest": "empty", @@ -639,10 +602,7 @@ export class GrokWebExecutor extends BaseExecutor { // Apply upstream extra headers mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); - log?.info?.( - "GROK-WEB", - `Query to ${model} (grok=${grokModel}, mode=${modelMode}), len=${message.length}` - ); + log?.info?.("GROK-WEB", `Query to ${model} (modeId=${modeId}), len=${message.length}`); // Apply fetch timeout const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS); diff --git a/open-sse/executors/muse-spark-web.ts b/open-sse/executors/muse-spark-web.ts index 3b2cacd1f6..af9a33ec0e 100644 --- a/open-sse/executors/muse-spark-web.ts +++ b/open-sse/executors/muse-spark-web.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + import { BaseExecutor, mergeAbortSignals, @@ -12,11 +14,22 @@ import { } from "@/lib/providers/webCookieAuth"; const META_AI_GRAPHQL_API = "https://www.meta.ai/api/graphql"; -const META_AI_DEFAULT_COOKIE = "abra_sess"; -const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886"; +// Meta rebranded the chat product from "Abra" to "Ecto"; the session cookie +// `abra_sess` was replaced by `ecto_1_sess`. `normalizeSessionCookieHeader` +// only uses this constant when the user pastes a bare cookie value with no +// `name=` prefix; full cookie lines (with any cookie names) pass through +// untouched, so users who paste their entire DevTools cookie line still work. +const META_AI_DEFAULT_COOKIE = "ecto_1_sess"; +// Persisted-query id and friendly name for the current send-message +// operation. The previous Abra mutation (doc_id 078dfdff...) was retired +// when Meta removed the RewriteOptionsInput type from the schema; it now +// fails server-side validation with `Unknown type "RewriteOptionsInput"`. +// The new operation is a Subscription rather than a Mutation, but Meta's +// GraphQL endpoint still accepts it over POST and streams the response. +const META_AI_SEND_MESSAGE_DOC_ID = "29ae946c82d1f301196c6ca2226400b5"; const META_AI_ROOT_BRANCH_PATH = "0"; const META_AI_ENTRY_POINT = "KADABRA__CHAT__UNIFIED_INPUT_BAR"; -const META_AI_FRIENDLY_NAME = "useAbraSendMessageMutation"; +const META_AI_FRIENDLY_NAME = "useEctoSendMessageSubscription"; const META_AI_REQUEST_ANALYTICS_TAGS = "graphservice"; const META_AI_ASBD_ID = "129477"; const META_AI_USER_AGENT = @@ -78,8 +91,31 @@ function extractMessageText(content: unknown): string { .trim(); } -function parseOpenAIMessages(messages: Array>): string { - const extracted: Array<{ role: string; content: string }> = []; +type NormalizedMessage = { role: string; content: string }; + +type ParsedHistory = { + /** Whole history folded into one string (used when starting a new conversation). */ + foldedPrompt: string; + /** Just the last user turn — sent on its own when we're continuing a cached conversation. */ + latestUserContent: string; + /** + * Index in `normalized` of the most recent assistant turn, or -1 if none. + * Used to slice the prefix that anchors the continuation cache key (so two + * separate chats with identical assistant responses but different + * preceding history don't collide). + */ + lastAssistantIndex: number; + /** + * The role+content of every non-empty message after normalization, in + * order. The continuation-cache key hashes the prefix of this list ending + * at the last assistant message, so the key is unique to a specific + * (history → response) pair rather than just the response text alone. + */ + normalized: NormalizedMessage[]; +}; + +function parseOpenAIMessages(messages: Array>): ParsedHistory { + const extracted: NormalizedMessage[] = []; for (const message of messages) { let role = String(message.role || "user"); @@ -91,7 +127,12 @@ function parseOpenAIMessages(messages: Array>): string { } if (extracted.length === 0) { - return ""; + return { + foldedPrompt: "", + latestUserContent: "", + lastAssistantIndex: -1, + normalized: [], + }; } let lastUserIndex = -1; @@ -102,7 +143,15 @@ function parseOpenAIMessages(messages: Array>): string { } } - return extracted + let lastAssistantIndex = -1; + for (let i = extracted.length - 1; i >= 0; i--) { + if (extracted[i].role === "assistant") { + lastAssistantIndex = i; + break; + } + } + + const foldedPrompt = extracted .map((message, index) => { if (index === lastUserIndex) { return message.content; @@ -111,6 +160,10 @@ function parseOpenAIMessages(messages: Array>): string { }) .join("\n\n") .trim(); + + const latestUserContent = lastUserIndex >= 0 ? extracted[lastUserIndex].content : ""; + + return { foldedPrompt, latestUserContent, lastAssistantIndex, normalized: extracted }; } function estimateTokens(text: string): number { @@ -195,8 +248,101 @@ function getMuseSparkModelInfo(model: string): MuseSparkModelInfo { return MODEL_MAP[model] || MODEL_MAP["muse-spark"]; } -function buildMetaAiRequestBody(prompt: string, model: string) { - const conversationId = generateMetaConversationId(); +// ─── Conversation continuity cache ────────────────────────────────────────── +// The default behavior of /v1/chat/completions is stateless: the caller passes +// the full message history each turn. Without continuation, every turn would +// open a brand-new meta.ai conversation containing the OpenAI history folded +// into a single user prompt — three real chat turns become three separate +// conversations in the user's meta.ai history, each polluted with the prior +// turns rendered as "user: …" / "assistant: …" text. +// +// To present a clean single growing conversation in meta.ai, we cache the +// conversationId we created on the previous turn keyed by a hash of the +// (connectionId, model, normalized history through the last assistant turn). +// On the next turn, if the incoming OpenAI history's prefix-up-to-the-last- +// assistant-turn matches a cached entry, we reuse the cached conversationId, +// set isNewConversation=false, and send only the latest user turn — Meta +// appends to the existing conversation tree. +// +// Hashing the *full prefix* (not just the assistant text) is important: two +// independent chats from the same connection that happen to land on identical +// assistant text (e.g. a generic refusal or greeting) would otherwise collide +// and route the next turn into the wrong meta.ai conversation, mixing chat +// state across logical sessions. The differing preceding history makes the +// hashes distinct. +// +// TTL is 30 minutes (Meta's web client also expires idle conversations on a +// similar window). Cache cap is generous — entries are tiny (~250 B) so 5000 +// entries is ~1.25 MB, plenty of headroom for multi-user setups. + +type CachedConversation = { + conversationId: string; + branchPath: string; + expiresAt: number; +}; + +const MUSE_CONV_CACHE_MAX = 5000; +const MUSE_CONV_CACHE_TTL_MS = 30 * 60 * 1000; +const conversationCache = new Map(); + +/** + * Canonical-stringify a normalized message list so the same logical history + * always produces the same hash. Uses ASCII Group Separator / Record + * Separator characters as field delimiters so they can't appear inside + * normal message content. + */ +function canonicalizeNormalizedHistory(messages: NormalizedMessage[]): string { + return messages.map((m) => `${m.role}\x1e${m.content}`).join("\x1f"); +} + +function makeConversationCacheKey( + connectionId: string, + model: string, + normalizedPrefix: NormalizedMessage[] +): string { + return createHash("sha256") + .update(`${connectionId}\x1f${model}\x1f${canonicalizeNormalizedHistory(normalizedPrefix)}`) + .digest("hex"); +} + +function lookupCachedConversation(key: string): CachedConversation | null { + const entry = conversationCache.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + conversationCache.delete(key); + return null; + } + return entry; +} + +function rememberConversation( + key: string, + context: { conversationId: string; branchPath: string } +): void { + if (conversationCache.size >= MUSE_CONV_CACHE_MAX && !conversationCache.has(key)) { + // Map iteration is insertion order, so the first key is the oldest. + const oldest = conversationCache.keys().next().value; + if (oldest) conversationCache.delete(oldest); + } + conversationCache.set(key, { + conversationId: context.conversationId, + branchPath: context.branchPath, + expiresAt: Date.now() + MUSE_CONV_CACHE_TTL_MS, + }); +} + +/** Test hook — exported for unit tests; not wired to runtime callers. */ +export function __resetMuseSparkConversationCacheForTesting(): void { + conversationCache.clear(); +} + +type ConversationContext = { + conversationId: string; + branchPath: string; + isNewConversation: boolean; +}; + +function buildMetaAiRequestBody(prompt: string, model: string, conversation: ConversationContext) { const userUniqueMessageId = generateNumericMessageId(); return { @@ -210,14 +356,14 @@ function buildMetaAiRequestBody(prompt: string, model: string) { typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC", clippyIp: null, content: prompt, - conversationId, + conversationId: conversation.conversationId, conversationStarterId: null, - currentBranchPath: META_AI_ROOT_BRANCH_PATH, + currentBranchPath: conversation.branchPath, developerOverridesForMessage: null, devicePixelRatio: 1, entryPoint: META_AI_ENTRY_POINT, imagineOperationRequest: null, - isNewConversation: true, + isNewConversation: conversation.isNewConversation, mentions: null, mode: getMuseSparkModelInfo(model).mode, promptEditType: null, @@ -225,10 +371,14 @@ function buildMetaAiRequestBody(prompt: string, model: string) { promptType: null, qplJoinId: null, requestedToolCall: null, - rewriteOptions: null, + // `rewriteOptions` was removed from Meta's GraphQL schema (the + // RewriteOptionsInput type is gone), so sending it — even as null — + // makes the server reject the persisted query with + // `Unknown type "RewriteOptionsInput"`. Omit it entirely; GraphQL + // input fields are nullable-by-omission by default. turnId: crypto.randomUUID(), userAgent: META_AI_USER_AGENT, - userEventId: generateMetaEventId(conversationId), + userEventId: generateMetaEventId(conversation.conversationId), userLocale: normalizeMetaLocale(), userMessageId: crypto.randomUUID(), userUniqueMessageId, @@ -819,6 +969,203 @@ function buildMetaAiHeaders(cookieHeader: string): Record { }; } +type MuseSparkExecuteResult = { + response: Response; + url: string; + headers: Record; + transformedBody: unknown; +}; + +function resultWithResponse( + response: Response, + headers: Record, + transformedBody: unknown +): MuseSparkExecuteResult { + return { + response, + url: META_AI_GRAPHQL_API, + headers, + transformedBody, + }; +} + +function errorResult( + status: number, + message: string, + code: string, + headers: Record, + transformedBody: unknown +): MuseSparkExecuteResult { + return resultWithResponse(buildErrorResponse(status, message, code), headers, transformedBody); +} + +function getOpenAiMessages(body: unknown): Array> | null { + const messages = (body as Record).messages; + if (!messages || !Array.isArray(messages) || messages.length === 0) return null; + return messages as Array>; +} + +function getContinuationCacheKey( + parsedHistory: ParsedHistory, + credentials: ExecuteInput["credentials"], + model: string +): string | null { + if ( + parsedHistory.lastAssistantIndex < 0 || + !credentials.connectionId || + parsedHistory.latestUserContent.length === 0 + ) { + return null; + } + + return makeConversationCacheKey( + credentials.connectionId, + model, + parsedHistory.normalized.slice(0, parsedHistory.lastAssistantIndex + 1) + ); +} + +function getConversationContext(cached: CachedConversation | null): ConversationContext { + if (!cached) { + return { + conversationId: generateMetaConversationId(), + branchPath: META_AI_ROOT_BRANCH_PATH, + isNewConversation: true, + }; + } + + return { + conversationId: cached.conversationId, + branchPath: cached.branchPath, + isNewConversation: false, + }; +} + +function evictContinuationIfNeeded( + cached: CachedConversation | null, + cacheKey: string | null +): void { + if (cached && cacheKey) { + conversationCache.delete(cacheKey); + } +} + +async function postMetaAiRequest( + headers: Record, + transformedBody: unknown, + signal: AbortSignal, + log: ExecuteInput["log"] +): Promise<{ ok: true; response: Response } | { ok: false; result: MuseSparkExecuteResult }> { + try { + const response = await fetch(META_AI_GRAPHQL_API, { + method: "POST", + headers, + body: JSON.stringify(transformedBody), + signal, + }); + return { ok: true, response }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log?.error?.("MUSE-SPARK-WEB", `Fetch failed: ${message}`); + return { + ok: false, + result: errorResult( + 502, + `Meta AI connection failed: ${message}`, + "meta_ai_fetch_failed", + headers, + transformedBody + ), + }; + } +} + +function buildHttpErrorResult( + upstreamResponse: Response, + headers: Record, + transformedBody: unknown, + cached: CachedConversation | null, + cacheKey: string | null +): MuseSparkExecuteResult { + evictContinuationIfNeeded(cached, cacheKey); + + let message = `Meta AI returned HTTP ${upstreamResponse.status}`; + if (upstreamResponse.status === 401 || upstreamResponse.status === 403) { + message = "Meta AI auth failed — your meta.ai ecto_1_sess cookie may be missing or expired."; + } else if (upstreamResponse.status === 429) { + message = "Meta AI rate limited the session. Wait a moment and retry."; + } + + return errorResult( + upstreamResponse.status, + message, + `HTTP_${upstreamResponse.status}`, + headers, + transformedBody + ); +} + +function buildParsedErrorResult( + parsed: ParsedMetaAiResponse, + headers: Record, + transformedBody: unknown, + cached: CachedConversation | null, + cacheKey: string | null +): MuseSparkExecuteResult { + evictContinuationIfNeeded(cached, cacheKey); + return errorResult( + parsed.status, + parsed.errorMessage || "Meta AI returned an unknown error", + parsed.errorCode || "meta_ai_unknown_error", + headers, + transformedBody + ); +} + +function rememberAssistantTurn( + parsed: ParsedMetaAiResponse, + credentials: ExecuteInput["credentials"], + model: string, + parsedHistory: ParsedHistory, + conversationContext: ConversationContext +): void { + if (!parsed.content || !credentials.connectionId) return; + + const writePrefix: NormalizedMessage[] = [ + ...parsedHistory.normalized, + { role: "assistant", content: parsed.content }, + ]; + rememberConversation(makeConversationCacheKey(credentials.connectionId, model, writePrefix), { + conversationId: conversationContext.conversationId, + branchPath: conversationContext.branchPath, + }); +} + +function buildSuccessResult( + parsed: ParsedMetaAiResponse, + stream: boolean, + model: string, + headers: Record, + transformedBody: unknown +): MuseSparkExecuteResult { + const id = `chatcmpl-meta-${crypto.randomUUID().slice(0, 12)}`; + const created = Math.floor(Date.now() / 1000); + const deltas = parsed.deltas.length > 0 ? parsed.deltas : [parsed.content]; + const reasoningDeltas = parsed.reasoningDeltas; + const response = stream + ? new Response(buildStreamingResponse(deltas, reasoningDeltas, model, id, created), { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + }) + : buildNonStreamingResponse(parsed.content, parsed.reasoningContent, model, id, created); + + return resultWithResponse(response, headers, transformedBody); +} + export class MuseSparkWebExecutor extends BaseExecutor { constructor() { super("muse-spark-web", { id: "muse-spark-web", baseUrl: META_AI_GRAPHQL_API }); @@ -833,34 +1180,37 @@ export class MuseSparkWebExecutor extends BaseExecutor { log, upstreamExtraHeaders, }: ExecuteInput) { - const messages = (body as Record).messages as - | Array> - | undefined; - if (!messages || !Array.isArray(messages) || messages.length === 0) { - return { - response: buildErrorResponse(400, "Missing or empty messages array", "invalid_request"), - url: META_AI_GRAPHQL_API, - headers: {}, - transformedBody: body, - }; + const messages = getOpenAiMessages(body); + if (!messages) { + return errorResult(400, "Missing or empty messages array", "invalid_request", {}, body); } - const prompt = parseOpenAIMessages(messages); - if (!prompt) { - return { - response: buildErrorResponse( - 400, - "Empty query after processing messages", - "invalid_request" - ), - url: META_AI_GRAPHQL_API, - headers: {}, - transformedBody: body, - }; + const parsedHistory = parseOpenAIMessages(messages); + if (!parsedHistory.foldedPrompt) { + return errorResult(400, "Empty query after processing messages", "invalid_request", {}, body); } + // Look up a prior meta.ai conversation we created for this caller + + // model + chat thread. The lookup key is the connection + model + the + // SHA-256 of the normalized history prefix ending at the last assistant + // turn — that prefix is exactly what we hashed when we cached on the + // previous turn, so a real continuation hits and two parallel chats + // with coincidentally-identical assistant text do not. + // + // We also require `latestUserContent` to be non-empty before using a + // cached entry: if the incoming history has no `user` role (e.g. an + // assistant-prefill payload), the cache-hit path would otherwise POST + // empty content with `isNewConversation: false`, an avoidable upstream + // failure. Falling through to the fresh-conversation path uses the + // folded history instead, which contains real content. + const continuationCacheKey = getContinuationCacheKey(parsedHistory, credentials, model); + const cached = continuationCacheKey ? lookupCachedConversation(continuationCacheKey) : null; + const conversationContext = getConversationContext(cached); + + const prompt = cached ? parsedHistory.latestUserContent : parsedHistory.foldedPrompt; + const modelInfo = getMuseSparkModelInfo(model); - const transformedBody = buildMetaAiRequestBody(prompt, model); + const transformedBody = buildMetaAiRequestBody(prompt, model, conversationContext); const cookieHeader = selectMetaAiCookieHeader(credentials); const headers = buildMetaAiHeaders(cookieHeader); mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); @@ -868,96 +1218,37 @@ export class MuseSparkWebExecutor extends BaseExecutor { const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS); const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal; - let upstreamResponse: Response; - try { - upstreamResponse = await fetch(META_AI_GRAPHQL_API, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal: combinedSignal, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - log?.error?.("MUSE-SPARK-WEB", `Fetch failed: ${message}`); - return { - response: buildErrorResponse( - 502, - `Meta AI connection failed: ${message}`, - "meta_ai_fetch_failed" - ), - url: META_AI_GRAPHQL_API, - headers, - transformedBody, - }; - } + const fetchResult = await postMetaAiRequest(headers, transformedBody, combinedSignal, log); + if (!fetchResult.ok) return fetchResult.result; + const upstreamResponse = fetchResult.response; if (!upstreamResponse.ok) { - let message = `Meta AI returned HTTP ${upstreamResponse.status}`; - if (upstreamResponse.status === 401 || upstreamResponse.status === 403) { - message = "Meta AI auth failed — your meta.ai abra_sess cookie may be missing or expired."; - } else if (upstreamResponse.status === 429) { - message = "Meta AI rate limited the session. Wait a moment and retry."; - } - - return { - response: buildErrorResponse( - upstreamResponse.status, - message, - `HTTP_${upstreamResponse.status}` - ), - url: META_AI_GRAPHQL_API, + return buildHttpErrorResult( + upstreamResponse, headers, transformedBody, - }; + cached, + continuationCacheKey + ); } if (!upstreamResponse.body) { - return { - response: buildErrorResponse( - 502, - "Meta AI returned an empty response body", - "meta_ai_empty_body" - ), - url: META_AI_GRAPHQL_API, + return errorResult( + 502, + "Meta AI returned an empty response body", + "meta_ai_empty_body", headers, - transformedBody, - }; + transformedBody + ); } const responseText = await readTextResponse(upstreamResponse.body, signal); const parsed = parseMetaAiResponseText(responseText, modelInfo.isThinking); if (parsed.status !== 200 || parsed.errorMessage) { - return { - response: buildErrorResponse( - parsed.status, - parsed.errorMessage || "Meta AI returned an unknown error", - parsed.errorCode - ), - url: META_AI_GRAPHQL_API, - headers, - transformedBody, - }; + return buildParsedErrorResult(parsed, headers, transformedBody, cached, continuationCacheKey); } - const id = `chatcmpl-meta-${crypto.randomUUID().slice(0, 12)}`; - const created = Math.floor(Date.now() / 1000); - const deltas = parsed.deltas.length > 0 ? parsed.deltas : [parsed.content]; - const reasoningDeltas = parsed.reasoningDeltas; - - return { - response: stream - ? new Response(buildStreamingResponse(deltas, reasoningDeltas, model, id, created), { - status: 200, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - }, - }) - : buildNonStreamingResponse(parsed.content, parsed.reasoningContent, model, id, created), - url: META_AI_GRAPHQL_API, - headers, - transformedBody, - }; + rememberAssistantTurn(parsed, credentials, model, parsedHistory, conversationContext); + return buildSuccessResult(parsed, stream, model, headers, transformedBody); } } diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 99a591d7d0..9788cd53d0 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -62,4 +62,22 @@ export class OpencodeExecutor extends BaseExecutor { return headers; } + + transformRequest( + model: string, + body: any, + stream: boolean, + credentials: ProviderCredentials + ): any { + const modifiedBody = super.transformRequest(model, body, stream, credentials); + if ( + modifiedBody && + typeof modifiedBody === "object" && + Array.isArray(modifiedBody.tools) && + modifiedBody.tools.length > 128 + ) { + modifiedBody.tools = modifiedBody.tools.slice(0, 128); + } + return modifiedBody; + } } diff --git a/open-sse/executors/perplexity-web.ts b/open-sse/executors/perplexity-web.ts index a167136ee2..3695f734aa 100644 --- a/open-sse/executors/perplexity-web.ts +++ b/open-sse/executors/perplexity-web.ts @@ -9,9 +9,9 @@ import { BaseExecutor, type ExecuteInput } from "./base.ts"; const PPLX_SSE_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask"; -const PPLX_API_VERSION = "2.18"; +const PPLX_API_VERSION = "client-1.11.0"; const PPLX_USER_AGENT = - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"; + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"; const MODEL_MAP: Record = { "pplx-auto": ["concise", "pplx_pro"], diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index 0f53146912..a730f456dc 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -1,5 +1,5 @@ import { randomUUID } from "crypto"; -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; import { stripTrailingSlashes } from "../utils/urlSanitize.ts"; /** * Audio Speech Handler (TTS) @@ -50,7 +50,7 @@ function upstreamErrorResponse(res, errText) { { error: { message: errorMessage, code: res.status } }, { status: res.status, - headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + headers: { ...CORS_HEADERS }, } ); } @@ -63,8 +63,8 @@ function audioStreamResponse(res, defaultContentType = "audio/mpeg") { return new Response(res.body, { status: 200, headers: { + ...CORS_HEADERS, "Content-Type": contentType, - "Access-Control-Allow-Origin": getCorsOrigin(), "Transfer-Encoding": "chunked", }, }); @@ -175,7 +175,6 @@ async function handleHyperbolicSpeech(providerConfig, body, token) { status: 200, headers: { "Content-Type": "audio/mpeg", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } @@ -321,7 +320,6 @@ async function handleInworldSpeech(providerConfig, body, modelId, token) { status: 200, headers: { "Content-Type": mimeType, - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } @@ -492,7 +490,6 @@ async function handleCoquiSpeech(providerConfig, body) { status: 200, headers: { "Content-Type": contentType, - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } @@ -520,7 +517,6 @@ async function handleTortoiseSpeech(providerConfig, body) { status: 200, headers: { "Content-Type": contentType, - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } diff --git a/open-sse/handlers/audioTranscription.ts b/open-sse/handlers/audioTranscription.ts index 0ece148e23..53a3ce4d2d 100644 --- a/open-sse/handlers/audioTranscription.ts +++ b/open-sse/handlers/audioTranscription.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; /** * Audio Transcription Handler * @@ -51,7 +51,7 @@ function upstreamErrorResponse(res, errText) { { error: { message: errorMessage, code: res.status } }, { status: res.status, - headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + headers: { ...CORS_HEADERS }, } ); } @@ -152,7 +152,7 @@ async function handleDeepgramTranscription( // Return it explicitly so the client can distinguish from a credentials error return Response.json( { text: text ?? "", noSpeechDetected: text === null || text === "" }, - { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } + { headers: { ...CORS_HEADERS } } ); } @@ -213,10 +213,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke const result = await pollRes.json(); if (result.status === "completed") { - return Response.json( - { text: result.text || "" }, - { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } - ); + return Response.json({ text: result.text || "" }, { headers: { ...CORS_HEADERS } }); } if (result.status === "error") { @@ -250,7 +247,7 @@ async function handleNvidiaTranscription(providerConfig, file, modelId, token) { // Normalize to { text } — Nvidia may return { text } directly or nested const text = data.text || data.transcript || ""; - return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }); + return Response.json({ text }, { headers: { ...CORS_HEADERS } }); } /** @@ -281,7 +278,7 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok // HuggingFace returns { text } directly const text = data.text || ""; - return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }); + return Response.json({ text }, { headers: { ...CORS_HEADERS } }); } /** @@ -389,7 +386,7 @@ export async function handleAudioTranscription({ return new Response(data, { status: 200, - headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": getCorsOrigin() }, + headers: { "Content-Type": contentType }, }); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 096dd0b340..f50045bec1 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; import { detectFormatFromEndpoint, getTargetFormat } from "../services/provider.ts"; import { translateRequest, needsTranslation } from "../translator/index.ts"; import { FORMATS } from "../translator/formats.ts"; @@ -6,7 +6,9 @@ import { createSSETransformStreamWithLogger, createPassthroughStreamWithLogger, COLORS, + withBodyTimeout, } from "../utils/stream.ts"; +import { ensureStreamReadiness } from "../utils/streamReadiness.ts"; import { createStreamController, pipeWithDisconnect } from "../utils/streamHandler.ts"; import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/usageTracking.ts"; import { refreshWithRetry } from "../services/tokenRefresh.ts"; @@ -24,7 +26,12 @@ import { parseUpstreamError, formatProviderError, } from "../utils/error.ts"; -import { COOLDOWN_MS, HTTP_STATUS, PROVIDER_MAX_TOKENS } from "../config/constants.ts"; +import { + COOLDOWN_MS, + HTTP_STATUS, + PROVIDER_MAX_TOKENS, + STREAM_IDLE_TIMEOUT_MS, +} from "../config/constants.ts"; import { classifyProviderError, PROVIDER_ERROR_TYPES, @@ -32,6 +39,7 @@ import { } from "../services/errorClassifier.ts"; import { updateProviderConnection } from "@/lib/db/providers"; import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs"; +import { getCallLogPipelineCaptureStreamChunks } from "@/lib/logEnv"; import { logAuditEvent } from "@/lib/compliance"; import { extractProviderWarnings } from "@/lib/compliance/providerAudit"; import { handleBypassRequest } from "../utils/bypassHandler.ts"; @@ -71,6 +79,7 @@ import { import { getCacheMetrics } from "@/lib/db/settings.ts"; import { getCachedSettings } from "@/lib/db/readCache"; import { cacheReasoningFromAssistantMessage } from "../services/reasoningCache.ts"; +import { sanitizeOpenAITool } from "../services/toolSchemaSanitizer.ts"; import { parseCodexQuotaHeaders, @@ -837,6 +846,33 @@ function attachLogMeta( const _proxyConfigCache = new Map(); const PROXY_CONFIG_CACHE_TTL = 10_000; +/** + * Module-level cache for all combos data (shared across all requests). + * Uses cached promises to prevent thundering herd — all concurrent callers + * wait for the same underlying DB query while it's in flight. + */ +let _combosPromise: Promise | null = null; +let _combosCacheTs = 0; +const COMBOS_CACHE_TTL = 10_000; + +async function getCombosCached(): Promise { + const now = Date.now(); + if (_combosPromise && now - _combosCacheTs < COMBOS_CACHE_TTL) { + return _combosPromise; + } + _combosCacheTs = now; + _combosPromise = (async () => { + const { getCombos } = await import("@/lib/localDb"); + return getCombos(); + })(); + return _combosPromise; +} + +export function clearCombosCache() { + _combosPromise = null; + _combosCacheTs = 0; +} + export function clearUpstreamProxyConfigCache(providerId?: string) { if (providerId) { _proxyConfigCache.delete(providerId); @@ -1018,7 +1054,6 @@ export async function handleChatCore({ status: cachedIdemp.status, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), "X-OmniRoute-Idempotent": "true", ...buildOmniRouteResponseMetaHeaders({ provider, @@ -1129,6 +1164,8 @@ export async function handleChatCore({ } const noLogEnabled = apiKeyInfo?.noLog === true; const detailedLoggingEnabled = !noLogEnabled && (await isDetailedLoggingEnabled()); + const capturePipelineStreamChunks = + detailedLoggingEnabled && getCallLogPipelineCaptureStreamChunks(); const skillRequestId = generateRequestId(); const pipelineSessionId = (clientRawRequest?.headers && typeof clientRawRequest.headers.get === "function" @@ -1310,7 +1347,7 @@ export async function handleChatCore({ // Create request logger for this session: sourceFormat_targetFormat_model const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model, { enabled: detailedLoggingEnabled, - captureStreamChunks: detailedLoggingEnabled, + captureStreamChunks: capturePipelineStreamChunks, }); // 0. Log client raw request (before format conversion) @@ -1355,7 +1392,6 @@ export async function handleChatCore({ response: new Response(JSON.stringify(cached), { headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), [OMNIROUTE_RESPONSE_HEADERS.cache]: "HIT", ...buildOmniRouteResponseMetaHeaders({ provider, @@ -1431,6 +1467,13 @@ export async function handleChatCore({ const name = fn?.name ?? tool.name; return name && String(name).trim().length > 0; }); + + // Sanitize OpenAI-format function tool schemas before they reach strict + // upstream JSON Schema validators (e.g. Moonshot AI behind + // opencode-go/kimi-k2.6). See toolSchemaSanitizer.ts for the specific bug. + // sanitizeOpenAITool is safe to call on any input — it no-ops non-function + // tools (e.g. Responses API built-ins) and non-object values. + body.tools = body.tools.map((tool) => sanitizeOpenAITool(tool) as (typeof body.tools)[number]); } const memoryOwnerId = resolveMemoryOwnerId(apiKeyInfo as Record | null); @@ -1514,7 +1557,8 @@ export async function handleChatCore({ comboConfig = await getComboByName(comboName.substring(6)); } if (comboConfig) { - const targets = await resolveComboTargets(comboConfig, null); + const allCombosData = await getCombosCached(); + const targets = resolveComboTargets(comboConfig, allCombosData); const limits = targets.map((t: { modelStr?: string }) => { const parsed = parseModel(t.modelStr); return getTokenLimit(parsed.provider, parsed.model); @@ -1872,7 +1916,6 @@ export async function handleChatCore({ status: statusCode, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), }, } ), @@ -2174,7 +2217,7 @@ export async function handleChatCore({ const status = rawResult.response.status; const statusText = rawResult.response.statusText; const headers = Array.from(rawResult.response.headers.entries()) as [string, string][]; - const payload = await rawResult.response.text(); + const payload = await withBodyTimeout(rawResult.response.text()); acquireAccountSemaphoreRelease(); return { @@ -2286,7 +2329,7 @@ export async function handleChatCore({ const failureStatus = error.name === "AbortError" ? 499 - : error.name === "TimeoutError" + : error.name === "TimeoutError" || error.name === "BodyTimeoutError" ? HTTP_STATUS.GATEWAY_TIMEOUT : HTTP_STATUS.BAD_GATEWAY; const failureMessage = @@ -2777,7 +2820,7 @@ export async function handleChatCore({ const contentType = (providerResponse.headers.get("content-type") || "").toLowerCase(); let responseBody; let responsePayloadFormat = targetFormat; - const rawBody = await providerResponse.text(); + const rawBody = await withBodyTimeout(providerResponse.text()); const normalizedProviderPayload = normalizePayloadForLog(rawBody); const looksLikeSSE = contentType.includes("text/event-stream") || @@ -2872,7 +2915,7 @@ export async function handleChatCore({ try { const fallbackResult = await executeProviderRequest(nextModel, false); if (fallbackResult.response.ok) { - const fallbackRaw = await fallbackResult.response.text(); + const fallbackRaw = await withBodyTimeout(fallbackResult.response.text()); try { responseBody = fallbackRaw ? JSON.parse(fallbackRaw) : {}; providerUrl = fallbackResult.url; @@ -3184,7 +3227,6 @@ export async function handleChatCore({ response: new Response(JSON.stringify(translatedResponse), { headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), [OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS", ...buildOmniRouteResponseMetaHeaders({ provider, @@ -3200,6 +3242,48 @@ export async function handleChatCore({ } // Streaming response + const streamReadiness = await ensureStreamReadiness(providerResponse, { + timeoutMs: STREAM_IDLE_TIMEOUT_MS, + provider, + model, + log, + }); + if (streamReadiness.ok === false) { + const { response: failureResponse, reason } = streamReadiness; + const failure = { + status: failureResponse.status, + message: reason, + code: "stream_readiness_timeout", + type: "stream_timeout", + }; + trackPendingRequest(model, provider, connectionId, false); + appendRequestLog({ + model, + provider, + connectionId, + status: `FAILED ${failureResponse.status}`, + }).catch(() => {}); + persistAttemptLogs({ + status: failureResponse.status, + error: reason, + providerRequest: finalBody || translatedBody, + clientResponse: buildErrorBody(failureResponse.status, reason), + claudeCacheMeta: claudePromptCacheLogMeta, + cacheSource: "upstream", + }); + persistFailureUsage(failureResponse.status, "stream_readiness_timeout"); + // Do NOT call onStreamFailure — a stream stall is an upstream issue, + // not an account/quota failure. Marking the account unavailable here + // would lock out legitimate accounts when the upstream hangs. + return { + success: false, + status: failureResponse.status, + error: reason, + errorType: "stream_readiness_timeout", + response: failureResponse, + }; + } + providerResponse = streamReadiness.response; // Notify success - caller can clear error status if needed if (onRequestSuccess) { @@ -3210,7 +3294,6 @@ export async function handleChatCore({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", - "Access-Control-Allow-Origin": getCorsOrigin(), [OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS", ...buildOmniRouteResponseMetaHeaders({ provider, diff --git a/open-sse/handlers/moderations.ts b/open-sse/handlers/moderations.ts index ffc54b9e40..83777cc3dc 100644 --- a/open-sse/handlers/moderations.ts +++ b/open-sse/handlers/moderations.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; /** * Moderation Handler * @@ -58,14 +58,14 @@ export async function handleModeration({ body, credentials }) { status: res.status, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), + ...CORS_HEADERS, }, }); } const data = await res.json(); return Response.json(data, { - headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + headers: { ...CORS_HEADERS }, }); } catch (err) { return errorResponse(500, `Moderation request failed: ${err.message}`); diff --git a/open-sse/handlers/rerank.ts b/open-sse/handlers/rerank.ts index eb76564d11..34920a53ad 100644 --- a/open-sse/handlers/rerank.ts +++ b/open-sse/handlers/rerank.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; /** * Rerank Handler * @@ -131,7 +131,7 @@ export async function handleRerank({ const result = transformResponseFromProvider(providerConfig, data); return Response.json(result, { - headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + headers: { ...CORS_HEADERS }, }); } catch (err) { return errorResponse(500, `Rerank request failed: ${err.message}`); diff --git a/open-sse/handlers/responsesHandler.ts b/open-sse/handlers/responsesHandler.ts index dfd9662c8e..203b70ed4a 100644 --- a/open-sse/handlers/responsesHandler.ts +++ b/open-sse/handlers/responsesHandler.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "../utils/cors.ts"; +import { CORS_HEADERS } from "../utils/cors.ts"; /** * Responses API Handler for Workers * Converts Chat Completions to Codex Responses API format @@ -81,7 +81,6 @@ export async function handleResponsesCore({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }), }; diff --git a/open-sse/handlers/search.ts b/open-sse/handlers/search.ts index e3e4d875af..01e2e68973 100644 --- a/open-sse/handlers/search.ts +++ b/open-sse/handlers/search.ts @@ -568,11 +568,16 @@ function buildSearxngRequest( const page = toSearchPageNumber(params.offset, params.maxResults); if (page) qp.set("pageno", String(page)); + const headers: Record = { Accept: "application/json" }; + if (params.token) { + headers["Authorization"] = `Bearer ${params.token}`; + } + return { url: `${url}?${qp}`, init: { method: "GET", - headers: { Accept: "application/json" }, + headers, }, }; } diff --git a/open-sse/package.json b/open-sse/package.json index 81fe3f22ea..5c465c5566 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.7.1", + "version": "3.7.2", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts index 542b8f5e90..f5061e4fa1 100644 --- a/open-sse/services/antigravityHeaders.ts +++ b/open-sse/services/antigravityHeaders.ts @@ -16,11 +16,11 @@ import { type AntigravityHeaderProfile = "loadCodeAssist" | "fetchAvailableModels" | "models"; const ANTIGRAVITY_VERSION = ANTIGRAVITY_FALLBACK_VERSION; -const GEMINI_CLI_VERSION = "0.31.0"; -const GEMINI_SDK_VERSION = "1.41.0"; -const NODE_VERSION = "v22.19.0"; -const LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/9.15.1"; -const LOAD_CODE_ASSIST_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"; +export const GEMINI_CLI_VERSION = "0.39.1"; +export const GEMINI_SDK_VERSION = "1.30.0"; +export const NODE_VERSION = "v22.21.1"; +export const ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/10.3.0"; +export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"; const LOAD_CODE_ASSIST_METADATA = Object.freeze({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", @@ -42,6 +42,8 @@ function getPlatform(): string { switch (p) { case "win32": return "windows"; + case "darwin": + return "macos"; default: return p; // "linux", etc. } @@ -125,7 +127,7 @@ export function geminiCLIUserAgent(model: string): string { /** * X-Goog-Api-Client header value matching the real Gemini SDK. - * Example: "google-genai-sdk/1.41.0 gl-node/v22.19.0" + * Example: "google-genai-sdk/1.30.0 gl-node/v22.21.1" */ export function googApiClientHeader(): string { return `google-genai-sdk/${GEMINI_SDK_VERSION} gl-node/${NODE_VERSION}`; diff --git a/open-sse/services/chatgptTlsClient.ts b/open-sse/services/chatgptTlsClient.ts index ed505802a8..2869d8dd32 100644 --- a/open-sse/services/chatgptTlsClient.ts +++ b/open-sse/services/chatgptTlsClient.ts @@ -22,6 +22,13 @@ let exitHookInstalled = false; const CHATGPT_PROFILE = "firefox_148"; // matches the Firefox 150 UA we send const DEFAULT_TIMEOUT_MS = 60_000; +// Grace period added to the binding's wire-level timeout before our JS-level +// hard timeout fires. Under healthy operation `tls-client-node` honors +// `timeoutMilliseconds` and rejects on its own; the JS-level race only wins +// when the koffi-loaded native library is wedged (which the binding's own +// timer can't escape). Keep the grace small so users don't wait noticeably +// longer than the configured timeout when the binding is dead. +const HARD_TIMEOUT_GRACE_MS = 10_000; function installExitHook(): void { if (exitHookInstalled) return; @@ -44,6 +51,69 @@ function installExitHook(): void { }); } +/** + * Drop the cached client so the next `getClient()` call respawns it. Called + * when a request observes the native binding has wedged — releasing the + * reference lets a fresh TLSClient (and a fresh koffi load) take over without + * a process restart. + */ +function resetClientCache(): void { + clientPromise = null; +} + +export class TlsClientHangError extends Error { + constructor(message: string) { + super(message); + this.name = "TlsClientHangError"; + } +} + +/** + * Race a `client.request()` promise against (a) a JS-level hard timeout and + * (b) the caller's abort signal. The native binding's `timeoutMilliseconds` + * already covers the wire path; this guards the case where the koffi binding + * itself deadlocks (observed after sustained load), where neither the + * binding's own timer nor a post-call `signal.aborted` re-check can recover. + */ +async function raceWithTimeout( + promise: Promise, + timeoutMs: number, + signal: AbortSignal | null | undefined +): Promise { + let timer: ReturnType | null = null; + let abortListener: (() => void) | null = null; + try { + const racers: Promise[] = [ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => { + reject( + new TlsClientHangError( + `tls-client-node call exceeded ${timeoutMs}ms — native binding likely deadlocked` + ) + ); + }, timeoutMs); + }), + ]; + if (signal) { + racers.push( + new Promise((_, reject) => { + if (signal.aborted) { + reject(makeAbortError(signal)); + return; + } + abortListener = () => reject(makeAbortError(signal)); + signal.addEventListener("abort", abortListener, { once: true }); + }) + ); + } + return await Promise.race(racers); + } finally { + if (timer) clearTimeout(timer); + if (signal && abortListener) signal.removeEventListener("abort", abortListener); + } +} + async function getClient(): Promise<{ request: (url: string, opts: Record) => Promise; }> { @@ -178,11 +248,26 @@ export async function tlsFetchChatGpt( url, requestOptions, options.streamEofSymbol, - options.signal ?? null + options.signal ?? null, + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS ); } - const tlsResponse = await client.request(url, requestOptions); + let tlsResponse: TlsResponseLike; + try { + tlsResponse = await raceWithTimeout( + client.request(url, requestOptions), + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS, + options.signal ?? null + ); + } catch (err) { + if (err instanceof TlsClientHangError) { + // The native binding is wedged — drop the singleton so the next + // request respawns a fresh client (and a fresh koffi load). + resetClientCache(); + } + throw err; + } if (options.signal?.aborted) { throw makeAbortError(options.signal); } @@ -220,7 +305,8 @@ async function tlsFetchStreaming( url: string, requestOptions: Record, eofSymbol = "[DONE]", - signal: AbortSignal | null = null + signal: AbortSignal | null = null, + hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS ): Promise { const dir = await mkdtemp(join(tmpdir(), "cgpt-stream-")); const path = join(dir, `${randomUUID()}.sse`); @@ -234,8 +320,24 @@ async function tlsFetchStreaming( // Kick off the request without awaiting — tls-client writes the body to // `path` chunk-by-chunk while the call runs. The Promise resolves when the - // request fully completes (full body written). - const requestPromise = client.request(url, streamOpts); + // request fully completes (full body written). Wrapping in raceWithTimeout + // guarantees this promise eventually settles even if the koffi binding + // wedges; on hang we reset the singleton so the next request respawns. + let resetOnHang = true; + const requestPromise = raceWithTimeout( + client.request(url, streamOpts), + hardTimeoutMs, + signal + ).catch((err: unknown) => { + if (resetOnHang && err instanceof TlsClientHangError) { + resetClientCache(); + resetOnHang = false; + } + // Re-throw so downstream consumers (waitForContent, tailFile) observe + // the rejection and surface it instead of treating the stream as having + // ended cleanly. + throw err; + }); // Wait for the file to exist AND have at least one byte. tls-client-node // creates the output file when the request starts, but the file can be diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 3da884ff9e..2c5299ee58 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -33,8 +33,8 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = [ "interleaved-thinking-2025-05-14", "effort-2025-11-24", ].join(","); -export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.113"; -export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.113 (external, sdk-cli)"; +export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.121"; +export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.121 (external, sdk-cli)"; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.81.0"; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0"; export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07"; diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 9bcedc2117..00d31d9d3e 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -20,6 +20,7 @@ import { fisherYatesShuffle, getNextFromDeck } from "../../src/shared/utils/shuf import { parseModel } from "./model.ts"; import { applyComboAgentMiddleware, injectModelTag } from "./comboAgentMiddleware.ts"; import { classifyWithConfig, DEFAULT_INTENT_CONFIG } from "./intentClassifier.ts"; +import { CONTEXT_OVERFLOW_REGEX } from "./errorClassifier.ts"; import { selectProvider as selectAutoProvider } from "./autoCombo/engine.ts"; import { selectWithStrategy } from "./autoCombo/routerStrategy.ts"; import { getTaskFitness } from "./autoCombo/taskFitness.ts"; @@ -68,16 +69,7 @@ const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [ /unsupported content part type/i, /tool(?:_call|_use)? .* not (?:available|found)/i, /third-party apps/i, - // Context overflow — model-specific, may succeed on a model with larger context window - /context overflow/i, - /context length exceeded/i, - /prompt too large/i, - /token limit/i, - /too many tokens/i, - /exceeds? context/i, - /maximum context/i, - /input too long/i, - /messages? exceed/i, + CONTEXT_OVERFLOW_REGEX, // Model not supported/found — permanent model-level error, try next combo target /no provider supported/i, /model not found/i, @@ -109,6 +101,8 @@ const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [ /\bfunction'?s? name (?:can't|can not|is|has) (?:blank|empty|missing)/i, /function.*name.*(?:blank|empty|missing)/i, /tool_call.*name.*(?:blank|empty|missing)/i, + // Anthropic thinking block signature errors — stale/expired signatures cannot be retried (#1696) + /invalid.*signature.*thinking/i, ]; // Patterns that signal all accounts for a provider are rate-limited / exhausted. @@ -255,7 +249,14 @@ async function validateResponseQuality( return { valid: false, reason: "empty content and no tool_calls in response" }; } - return { valid: true }; + return { + valid: true, + clonedResponse: new Response(text, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }), + }; } // In-memory atomic counter per combo for round-robin distribution @@ -278,6 +279,13 @@ function getTargetProvider(modelStr: string, providerId?: string | null): string return providerId || parsed.provider || parsed.providerAlias || "unknown"; } +function isStreamReadinessTimeoutErrorBody(errorBody: unknown): boolean { + if (!errorBody || typeof errorBody !== "object") return false; + const error = (errorBody as Record).error; + if (!error || typeof error !== "object") return false; + return (error as Record).code === "STREAM_READINESS_TIMEOUT"; +} + function toRecordedTarget(target: ResolvedComboTarget) { return { executionKey: target.executionKey, @@ -1084,6 +1092,7 @@ export async function handleComboChat({ settings, allCombos, relayOptions, + signal, }) { const strategy = combo.strategy || "priority"; const relayConfig = @@ -1283,6 +1292,7 @@ export async function handleComboChat({ log, settings, allCombos, + signal, }); } @@ -1518,6 +1528,11 @@ export async function handleComboChat({ // Retry loop for transient errors for (let retry = 0; retry <= maxRetries; retry++) { + // Fix #1681: Bail out immediately if the client has disconnected + if (signal?.aborted) { + log.info("COMBO", `Client disconnected — aborting combo loop before model ${modelStr}`); + return errorResponse(499, "Client disconnected"); + } globalAttempts++; if (globalAttempts > MAX_GLOBAL_ATTEMPTS) { log.warn( @@ -1531,7 +1546,21 @@ export async function handleComboChat({ "COMBO", `Retrying ${modelStr} in ${retryDelayMs}ms (attempt ${retry + 1}/${maxRetries + 1})` ); - await new Promise((r) => setTimeout(r, retryDelayMs)); + await new Promise((resolve) => { + const timer = setTimeout(resolve, retryDelayMs); + signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + resolve(undefined); + }, + { once: true } + ); + }); + if (signal?.aborted) { + log.info("COMBO", `Client disconnected during retry delay — aborting`); + return errorResponse(499, "Client disconnected"); + } } log.info( @@ -1628,7 +1657,7 @@ export async function handleComboChat({ } } - return result; + return quality.clonedResponse ?? result; } // Extract error info from response @@ -1671,6 +1700,23 @@ export async function handleComboChat({ } const providerBreakerOpen = isProviderBreakerOpenResponse(result, errorBody); + const isStreamReadinessTimeout = + result.status === 504 && isStreamReadinessTimeoutErrorBody(errorBody); + + // Fix #1681: Status 499 means client disconnected — stop combo loop immediately. + // There is no point trying fallback models when nobody is listening. + if (result.status === 499) { + log.info("COMBO", `Client disconnected (499) during ${modelStr} — stopping combo loop`); + recordComboRequest(combo.name, modelStr, { + success: false, + latencyMs: Date.now() - startTime, + fallbackCount, + strategy, + target: toRecordedTarget(target), + }); + recordedAttempts++; + return result; + } if (providerBreakerOpen) { lastError = errorText || String(result.status); @@ -1712,7 +1758,8 @@ export async function handleComboChat({ } // Check if this is a transient error worth retrying on same model - const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status); + const isTransient = + !isStreamReadinessTimeout && [408, 429, 500, 502, 503, 504].includes(result.status); if (retry < maxRetries && isTransient) { continue; // Retry same model } @@ -1737,7 +1784,21 @@ export async function handleComboChat({ : 0; if ([502, 503, 504].includes(result.status) && fallbackWaitMs > 0) { log.info("COMBO", `Waiting ${fallbackWaitMs}ms before fallback to next model`); - await new Promise((r) => setTimeout(r, fallbackWaitMs)); + await new Promise((resolve) => { + const timer = setTimeout(resolve, fallbackWaitMs); + signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + resolve(undefined); + }, + { once: true } + ); + }); + if (signal?.aborted) { + log.info("COMBO", `Client disconnected during fallback wait — aborting`); + return errorResponse(499, "Client disconnected"); + } } break; // Move to next model @@ -1798,6 +1859,7 @@ async function handleRoundRobinCombo({ log, settings, allCombos, + signal, }) { const config = settings ? resolveComboConfig(combo, settings) @@ -1967,6 +2029,22 @@ async function handleRoundRobinCombo({ /* Clone failed */ } + if (result.status === 499) { + log.info( + "COMBO-RR", + `Client disconnected (499) during ${modelStr} — stopping combo loop` + ); + recordComboRequest(combo.name, modelStr, { + success: false, + latencyMs: Date.now() - startTime, + fallbackCount, + strategy: "round-robin", + target: toRecordedTarget(target), + }); + recordedAttempts++; + return result; + } + if ( retryAfter && (!earliestRetryAfter || new Date(retryAfter) < new Date(earliestRetryAfter)) @@ -1992,6 +2070,8 @@ async function handleRoundRobinCombo({ ); break; } + const isStreamReadinessTimeout = + result.status === 504 && isStreamReadinessTimeoutErrorBody(errorBody); const { shouldFallback, cooldownMs } = checkFallbackError( result.status, @@ -2042,7 +2122,8 @@ async function handleRoundRobinCombo({ } // Transient error → retry same model - const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status); + const isTransient = + !isStreamReadinessTimeout && [408, 429, 500, 502, 503, 504].includes(result.status); if (retry < maxRetries && isTransient) { continue; } @@ -2067,7 +2148,21 @@ async function handleRoundRobinCombo({ : 0; if ([502, 503, 504].includes(result.status) && fallbackWaitMs > 0) { log.info("COMBO-RR", `Waiting ${fallbackWaitMs}ms before fallback to next model`); - await new Promise((r) => setTimeout(r, fallbackWaitMs)); + await new Promise((resolve) => { + const timer = setTimeout(resolve, fallbackWaitMs); + signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + resolve(undefined); + }, + { once: true } + ); + }); + if (signal?.aborted) { + log.info("COMBO-RR", `Client disconnected during fallback wait — aborting`); + return errorResponse(499, "Client disconnected"); + } } break; diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index b95916ade0..4af4f86306 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -30,7 +30,6 @@ interface LearnedLimitEntry { interface LimiterUpdateSettings { maxConcurrent?: number | null; minTime: number; - maxWait?: number | null; reservoir?: number | null; reservoirRefreshAmount?: number | null; reservoirRefreshInterval?: number | null; @@ -76,7 +75,6 @@ function buildLimiterDefaults() { reservoir: currentRequestQueueSettings.requestsPerMinute, reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, reservoirRefreshInterval: 60 * 1000, - maxWait: currentRequestQueueSettings.maxWaitMs, }; } @@ -85,7 +83,6 @@ function updateAllLimiterSettings() { limiter.updateSettings({ maxConcurrent: currentRequestQueueSettings.concurrentRequests, minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs, - maxWait: currentRequestQueueSettings.maxWaitMs, reservoir: currentRequestQueueSettings.requestsPerMinute, reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, reservoirRefreshInterval: 60 * 1000, @@ -285,7 +282,21 @@ export async function withRateLimit(provider, connectionId, model, fn) { } const limiter = getLimiter(provider, connectionId, model); - return limiter.schedule(fn); + const maxWaitMs = currentRequestQueueSettings.maxWaitMs; + const scheduleOpts = maxWaitMs && maxWaitMs > 0 ? { expiration: maxWaitMs } : {}; + try { + return await limiter.schedule(scheduleOpts, fn); + } catch (err) { + // Bottleneck throws when a job exceeds its expiration timeout. + // Surface as a clear rate-limit timeout so callers can fallback. + if (err?.message?.includes("This job timed out")) { + const key = getLimiterKey(provider, connectionId, model); + console.log( + `⏰ [RATE-LIMIT] ${key} — job expired after ${Math.ceil((maxWaitMs || 0) / 1000)}s in queue, dropping` + ); + } + throw err; + } } // ─── Header Parsing ────────────────────────────────────────────────────────── diff --git a/open-sse/services/toolSchemaSanitizer.ts b/open-sse/services/toolSchemaSanitizer.ts new file mode 100644 index 0000000000..1606be64e4 --- /dev/null +++ b/open-sse/services/toolSchemaSanitizer.ts @@ -0,0 +1,120 @@ +/** + * Sanitize OpenAI-format tool definitions for strict upstream JSON Schema + * validators (e.g. Moonshot AI behind opencode-go/kimi-k2.6). + * + * The concrete bug this was written for: ForgeCode emits enum schemas like + * { type: "string", enum: ["a", "b", "c", null], nullable: true } + * for nullable optional fields. Lenient providers (Z.AI / GLM) accept the null + * entry; Moonshot rejects with + * "At path 'properties.X.enum': enum value () does not match any type + * in [string]" + * before the request reaches the model. + * + * The fix is to strip null/undefined from `enum` arrays. Everything else here + * is defensive hygiene: ensures `parameters` is always a valid object schema, + * filters `required[]` to keys that exist in `properties`, and normalizes a + * few other shapes that strict validators tend to reject. + */ + +const MAX_RECURSION_DEPTH = 32; + +function isPlainObject(v: unknown): v is Record { + return v !== null && typeof v === "object" && !Array.isArray(v); +} + +function sanitizeSchema(value: unknown, depth = 0): Record { + if (depth > MAX_RECURSION_DEPTH) return {}; + if (!isPlainObject(value)) return {}; + + const result: Record = {}; + + for (const [k, v] of Object.entries(value)) { + if (v === null || v === undefined) continue; + + if (k === "properties" && isPlainObject(v)) { + const cleaned: Record = {}; + for (const [pk, pv] of Object.entries(v)) { + if (isPlainObject(pv)) { + cleaned[pk] = sanitizeSchema(pv, depth + 1); + } else if (typeof pv === "boolean") { + // JSON Schema 2019 boolean form: `true` (anything) / `false` (nothing). + // Preserve as-is; Moonshot accepts these. + cleaned[pk] = pv; + } else { + cleaned[pk] = {}; + } + } + result[k] = cleaned; + } else if (k === "items") { + // Recurse into items if it's a single schema. Tuple-form (array) is + // valid JSON Schema but rejected by Moonshot; coerce to single schema. + if (Array.isArray(v)) { + const firstObject = v.find(isPlainObject); + result[k] = firstObject ? sanitizeSchema(firstObject, depth + 1) : {}; + } else if (isPlainObject(v)) { + result[k] = sanitizeSchema(v, depth + 1); + } + } else if (k === "anyOf" || k === "oneOf" || k === "allOf") { + // Moonshot recursively validates inside `anyOf` (confirmed empirically), + // so we must descend to strip null-in-enum etc. `oneOf`/`allOf` aren't + // currently validated by Moonshot but are recursed for symmetry/defense. + if (Array.isArray(v)) { + result[k] = v.map((s) => (isPlainObject(s) ? sanitizeSchema(s, depth + 1) : {})); + } + } else if (k === "additionalProperties") { + // Moonshot recursively validates the schema form of additionalProperties + // (confirmed empirically). The boolean form is also valid JSON Schema. + if (isPlainObject(v)) { + result[k] = sanitizeSchema(v, depth + 1); + } else if (typeof v === "boolean") { + result[k] = v; + } + } else if (k === "enum" && Array.isArray(v)) { + // The actual fix: strip null/undefined entries that ForgeCode adds for + // nullable optional fields. + result[k] = v.filter((e) => e !== null && e !== undefined); + } else if (k === "required" && Array.isArray(v)) { + result[k] = v.filter((r) => typeof r === "string"); + } else { + result[k] = v; + } + } + + if (Array.isArray(result.required) && isPlainObject(result.properties)) { + const validKeys = new Set(Object.keys(result.properties)); + result.required = (result.required as string[]).filter((r) => validKeys.has(r)); + } + + return result; +} + +function normalizeParameters(parameters: unknown): unknown { + if (isPlainObject(parameters)) return sanitizeSchema(parameters); + if (parameters === null || parameters === undefined) { + return { type: "object", properties: {} }; + } + return { type: "object", properties: {} }; +} + +export function sanitizeOpenAITool(tool: unknown): unknown { + if (!isPlainObject(tool)) return tool; + const t = { ...tool }; + + if (isPlainObject(t.function)) { + // Chat Completions format: { type: "function", function: { name, parameters } } + const f = { ...t.function }; + f.parameters = normalizeParameters(f.parameters); + t.function = f; + } else if (t.type === "function") { + // Responses API format: { type: "function", name, parameters } — no `function` + // wrapper. /v1/responses requests reach chatCore in this shape and are only + // unwrapped later by the request translator, so we have to sanitize here too. + t.parameters = normalizeParameters(t.parameters); + } + + return t; +} + +export function sanitizeOpenAITools(tools: unknown[]): unknown[] { + return tools.map(sanitizeOpenAITool); +} diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts index e184a021e7..3006a69322 100644 --- a/open-sse/transformer/responsesTransformer.ts +++ b/open-sse/transformer/responsesTransformer.ts @@ -221,7 +221,27 @@ export function createResponsesApiTransformStream(logger = null) { const closeToolCall = (controller, idx) => { const callId = state.funcCallIds[idx]; if (callId && !state.funcItemDone[idx]) { - const args = state.funcArgsBuf[idx] || "{}"; + let args = state.funcArgsBuf[idx] || "{}"; + + // Fix #1674: Final cleanup of empty string placeholders that might have been split across delta chunks + try { + const parsed = JSON.parse(args); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + let modified = false; + for (const [k, v] of Object.entries(parsed)) { + if (v === "") { + delete parsed[k]; + modified = true; + } + } + if (modified) { + args = JSON.stringify(parsed); + state.funcArgsBuf[idx] = args; + } + } + } catch (e) { + // Ignore malformed JSON + } emit(controller, "response.function_call_arguments.done", { type: "response.function_call_arguments.done", @@ -484,15 +504,25 @@ export function createResponsesApiTransformStream(logger = null) { if (tc.function?.arguments) { const refCallId = state.funcCallIds[tcIdx] || newCallId; + let deltaStr = tc.function.arguments; + + // Fix #1674: cx/gpt-5.5 injects empty strings for optional parameters. + // We strip these directly from the streaming deltas to avoid breaking strict clients like Claude Code. + if (deltaStr.includes('""')) { + deltaStr = deltaStr + .replace(/,"[a-zA-Z0-9_]+":""/g, "") + .replace(/"[a-zA-Z0-9_]+":"",/g, ""); + } + if (refCallId) { emit(controller, "response.function_call_arguments.delta", { type: "response.function_call_arguments.delta", item_id: `fc_${refCallId}`, output_index: tcIdx, - delta: tc.function.arguments, + delta: deltaStr, }); } - state.funcArgsBuf[tcIdx] += tc.function.arguments; + state.funcArgsBuf[tcIdx] += deltaStr; } } } diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index f482ef6de8..1ab95d06e4 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -631,11 +631,21 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { state.toolCallIndex++; + let argsToEmit = item.arguments; + if (argsToEmit != null && typeof argsToEmit === "object" && !Array.isArray(argsToEmit)) { + // Fix #1674: Strip empty string placeholders emitted by GPT-5.5 for optional fields + const cleaned = { ...argsToEmit }; + for (const [k, v] of Object.entries(cleaned)) { + if (v === "") delete cleaned[k]; + } + argsToEmit = cleaned; + } + const argsStr = - item.arguments != null - ? typeof item.arguments === "string" - ? item.arguments - : JSON.stringify(item.arguments) + argsToEmit != null + ? typeof argsToEmit === "string" + ? argsToEmit + : JSON.stringify(argsToEmit) : buffered; return { @@ -671,8 +681,16 @@ export function openaiResponsesToOpenAIResponse(chunk, state) { // Only emit if arguments exist in the done event AND they weren't already streamed via deltas if (item.arguments != null && !buffered) { - const argsStr = - typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments); + let argsToEmit = item.arguments; + if (argsToEmit != null && typeof argsToEmit === "object" && !Array.isArray(argsToEmit)) { + const cleaned = { ...argsToEmit }; + for (const [k, v] of Object.entries(cleaned)) { + if (v === "") delete cleaned[k]; + } + argsToEmit = cleaned; + } + + const argsStr = typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit); if (argsStr) { return { id: state.chatId, diff --git a/open-sse/utils/bypassHandler.ts b/open-sse/utils/bypassHandler.ts index 657c7e1dff..4e1ba7b527 100644 --- a/open-sse/utils/bypassHandler.ts +++ b/open-sse/utils/bypassHandler.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "./cors.ts"; +import { CORS_HEADERS } from "./cors.ts"; import { detectFormat } from "../services/provider.ts"; import { translateResponse, initState } from "../translator/index.ts"; import { FORMATS } from "../translator/formats.ts"; @@ -124,7 +124,6 @@ function createNonStreamingResponse(sourceFormat, model) { response: new Response(JSON.stringify(openaiResponse), { headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }), }; @@ -158,7 +157,6 @@ function createNonStreamingResponse(sourceFormat, model) { response: new Response(JSON.stringify(finalResponse), { headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }), }; @@ -206,7 +204,6 @@ function createStreamingResponse(sourceFormat, model) { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }), }; diff --git a/open-sse/utils/cors.ts b/open-sse/utils/cors.ts index cae4a710c1..8429185c33 100644 --- a/open-sse/utils/cors.ts +++ b/open-sse/utils/cors.ts @@ -1,22 +1,13 @@ /** - * CORS configuration for open-sse handlers. + * Static CORS headers for open-sse handlers. * - * Reads `CORS_ORIGIN` env var (default: "*") so that all handlers - * use the same configurable origin. Equivalent to src/shared/utils/cors.ts - * for the open-sse package boundary. + * `Access-Control-Allow-Origin` is set by the Next.js middleware + * (`src/server/cors/origins.ts`). Handlers in this package only need the + * methods/headers list; the middleware overlays the allowed origin per + * the central allowlist on the way out. */ - -const CORS_ORIGIN = process.env.CORS_ORIGIN || "*"; - export const CORS_HEADERS: Record = { - "Access-Control-Allow-Origin": CORS_ORIGIN, - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, x-api-key, anthropic-version, x-omniroute-connection, x-internal-test, accept", }; - -/** - * Returns just the origin header for merging into existing header objects. - */ -export function getCorsOrigin(): string { - return CORS_ORIGIN; -} diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index a5379a3f06..95b139498f 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "./cors.ts"; +import { CORS_HEADERS } from "./cors.ts"; import { getDefaultErrorMessage, getErrorInfo } from "../config/errorConfig.ts"; import { normalizePayloadForLog } from "@/lib/logPayloads"; import type { ModelCooldownErrorPayload } from "@/types"; @@ -32,7 +32,6 @@ export function errorResponse(statusCode, message) { status: statusCode, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } @@ -258,7 +257,6 @@ export function providerCircuitOpenResponse( status: 503, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), "Retry-After": String(retryAfterSec), "X-OmniRoute-Provider-Breaker": "open", }, @@ -307,7 +305,6 @@ export function modelCooldownResponse({ status: 429, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": getCorsOrigin(), "Retry-After": String(retryAfterSec), }, } diff --git a/open-sse/utils/ollamaTransform.ts b/open-sse/utils/ollamaTransform.ts index c6f22335e3..b2d86d9fac 100644 --- a/open-sse/utils/ollamaTransform.ts +++ b/open-sse/utils/ollamaTransform.ts @@ -1,4 +1,4 @@ -import { getCorsOrigin } from "./cors.ts"; +import { CORS_HEADERS } from "./cors.ts"; type PendingToolCall = { id?: string; @@ -105,7 +105,6 @@ export function transformToOllama(response, model) { return new Response(response.body.pipeThrough(transform), { headers: { "Content-Type": "application/x-ndjson", - "Access-Control-Allow-Origin": getCorsOrigin(), }, }); } diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 7cfb4074ff..7675ef04e3 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -23,13 +23,33 @@ import { createStructuredSSECollector, buildStreamSummaryFromEvents, } from "./streamPayloadCollector.ts"; -import { STREAM_IDLE_TIMEOUT_MS, HTTP_STATUS } from "../config/constants.ts"; +import { STREAM_IDLE_TIMEOUT_MS, FETCH_BODY_TIMEOUT_MS, HTTP_STATUS } from "../config/constants.ts"; import { sanitizeStreamingChunk, extractThinkingFromContent, } from "../handlers/responseSanitizer.ts"; import { buildErrorBody } from "./error.ts"; +/** + * Race a response body read against a timeout. + * Prevents indefinite hangs when the upstream sends headers but stalls on the body. + */ +export function withBodyTimeout( + promise: Promise, + timeoutMs: number = FETCH_BODY_TIMEOUT_MS +): Promise { + if (timeoutMs <= 0) return promise; + let timer: ReturnType; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + const err = new Error(`Response body read timeout after ${timeoutMs}ms`); + err.name = "BodyTimeoutError"; + reject(err); + }, timeoutMs); + }); + return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)) as Promise; +} + export { COLORS, formatSSE }; type JsonRecord = Record; diff --git a/open-sse/utils/streamReadiness.ts b/open-sse/utils/streamReadiness.ts new file mode 100644 index 0000000000..ed3a5729ba --- /dev/null +++ b/open-sse/utils/streamReadiness.ts @@ -0,0 +1,238 @@ +import { HTTP_STATUS } from "../config/constants.ts"; + +type StreamReadinessLogger = { + debug?: (tag: string, message: string) => void; + warn?: (tag: string, message: string) => void; +}; + +export type StreamReadinessResult = + | { ok: true; response: Response } + | { ok: false; response: Response; reason: string }; + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function hasNonEmptyString(value: unknown): boolean { + return typeof value === "string" && value.length > 0; +} + +function hasUsefulValue(value: unknown): boolean { + if (hasNonEmptyString(value)) return true; + if (Array.isArray(value)) return value.some(hasUsefulValue); + if (!isRecord(value)) return false; + + for (const key of [ + "content", + "text", + "delta", + "reasoning_content", + "reasoning", + "partial_json", + "arguments", + "name", + ]) { + const candidate = value[key]; + if (hasNonEmptyString(candidate)) return true; + if ((Array.isArray(candidate) || isRecord(candidate)) && hasUsefulValue(candidate)) return true; + } + + for (const key of [ + "tool_calls", + "tool_use", + "function", + "functionCall", + "function_call", + "function_call_output", + "output", + "content_block", + "parts", + ]) { + if (hasUsefulValue(value[key])) return true; + } + + return false; +} + +function hasUsefulJsonPayload(payload: unknown): boolean { + if (!isRecord(payload)) return false; + + const type = typeof payload.type === "string" ? payload.type : ""; + if ( + type.includes("delta") || + type.includes("tool") || + type.includes("function") || + type.includes("content_block") || + type.includes("output_item") + ) { + if (hasUsefulValue(payload)) return true; + } + + return hasUsefulValue(payload.choices) || hasUsefulValue(payload.candidates) || hasUsefulValue(payload); +} + +export function hasUsefulStreamContent(text: string): boolean { + const lines = text.split(/\r?\n/); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith(":")) continue; + if (/^event:\s*(?:ping|keepalive)$/i.test(trimmed)) continue; + if (!trimmed.startsWith("data:")) continue; + + const data = trimmed.slice(5).trim(); + if (!data || data === "[DONE]") continue; + + try { + if (hasUsefulJsonPayload(JSON.parse(data))) return true; + } catch { + if (data.length > 0) return true; + } + } + + return false; +} + +function createErrorResponse(status: number, message: string): Response { + return new Response( + JSON.stringify({ + error: { + message, + type: "stream_timeout", + code: "STREAM_READINESS_TIMEOUT", + }, + }), + { status, headers: { "Content-Type": "application/json" } } + ); +} + +function prependBufferedChunks( + chunks: Uint8Array[], + reader: ReadableStreamDefaultReader +): ReadableStream { + return new ReadableStream({ + async start(controller) { + try { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value) controller.enqueue(value); + } + + controller.close(); + } catch (error) { + controller.error(error); + } finally { + reader.releaseLock(); + } + }, + async cancel(reason) { + await reader.cancel(reason).catch(() => {}); + reader.releaseLock(); + }, + }); +} + +function readWithTimeout( + reader: ReadableStreamDefaultReader, + timeoutMs: number +): Promise> { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("STREAM_READINESS_TIMEOUT")), timeoutMs); + reader.read().then( + (value) => { + clearTimeout(timeout); + resolve(value); + }, + (error) => { + clearTimeout(timeout); + reject(error); + } + ); + }); +} + +export async function ensureStreamReadiness( + response: Response, + options: { + timeoutMs: number; + provider?: string | null; + model?: string | null; + log?: StreamReadinessLogger | null; + } +): Promise { + if (!response.body || options.timeoutMs <= 0) return { ok: true, response }; + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + const decoder = new TextDecoder(); + let bufferedText = ""; + const startedAt = Date.now(); + const deadline = startedAt + options.timeoutMs; + let handedOffReader = false; + + try { + while (true) { + const remainingMs = deadline - Date.now(); + if (remainingMs <= 0) { + const reason = `Stream produced no useful content within ${options.timeoutMs}ms`; + options.log?.warn?.( + "STREAM", + `${reason} (${options.provider || "provider"}/${options.model || "unknown"})` + ); + await reader.cancel(reason).catch(() => {}); + return { ok: false, reason, response: createErrorResponse(HTTP_STATUS.GATEWAY_TIMEOUT, reason) }; + } + + let readResult: ReadableStreamReadResult; + try { + readResult = await readWithTimeout(reader, remainingMs); + } catch { + const reason = `Stream produced no useful content within ${options.timeoutMs}ms`; + options.log?.warn?.( + "STREAM", + `${reason} (${options.provider || "provider"}/${options.model || "unknown"})` + ); + await reader.cancel(reason).catch(() => {}); + return { ok: false, reason, response: createErrorResponse(HTTP_STATUS.GATEWAY_TIMEOUT, reason) }; + } + + if (readResult.done) { + const reason = "Stream ended before producing useful content"; + options.log?.warn?.( + "STREAM", + `${reason} (${options.provider || "provider"}/${options.model || "unknown"})` + ); + return { ok: false, reason, response: createErrorResponse(HTTP_STATUS.BAD_GATEWAY, reason) }; + } + + if (!readResult.value) continue; + chunks.push(readResult.value); + bufferedText += decoder.decode(readResult.value, { stream: true }); + + if (hasUsefulStreamContent(bufferedText)) { + options.log?.debug?.( + "STREAM", + `Stream readiness confirmed in ${Date.now() - startedAt}ms (${options.provider || "provider"}/${options.model || "unknown"})` + ); + handedOffReader = true; + return { + ok: true, + response: new Response(prependBufferedChunks(chunks, reader), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }), + }; + } + } + } finally { + if (!handedOffReader) { + reader.releaseLock(); + } + } +} diff --git a/package-lock.json b/package-lock.json index fe474d2a63..e259fc9356 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.7.1", + "version": "3.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.7.1", + "version": "3.7.2", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -13808,7 +13808,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.7.1" + "version": "3.7.2" } } } diff --git a/package.json b/package.json index 58a20a2971..de313ad1d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.7.1", + "version": "3.7.2", "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { diff --git a/scripts/prepublish.ts b/scripts/prepublish.ts index 500c280526..199c76c68d 100644 --- a/scripts/prepublish.ts +++ b/scripts/prepublish.ts @@ -9,7 +9,7 @@ * Run with: node scripts/prepublish.mjs */ -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, @@ -19,6 +19,7 @@ import { readFileSync, readdirSync, statSync, + chmodSync, } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -33,6 +34,8 @@ import { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, ".."); +const NPM_BIN = process.platform === "win32" ? "npm.cmd" : "npm"; +const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx"; const APP_DIR = join(ROOT, "app"); @@ -113,7 +116,7 @@ if (existsSync(APP_DIR)) { // ── Step 2: Install dependencies ─────────────────────────── console.log(" 📦 Installing dependencies..."); -execSync("npm install", { cwd: ROOT, stdio: "inherit" }); +execFileSync(NPM_BIN, ["install"], { cwd: ROOT, stdio: "inherit" }); // ── Step 2.5: Remove app/ directory before build ─────────── // CRITICAL: The postinstall script may create app/node_modules/@swc/helpers/, @@ -130,7 +133,7 @@ if (existsSync(APP_DIR)) { console.log(" 🏗️ Building Next.js (standalone)..."); const nextBuildBundlerFlag = process.env.OMNIROUTE_USE_TURBOPACK === "1" ? "--turbopack" : "--webpack"; -execSync(`npx next build ${nextBuildBundlerFlag}`, { +execFileSync(NPX_BIN, ["next", "build", nextBuildBundlerFlag], { cwd: ROOT, stdio: "inherit", env: { @@ -360,7 +363,10 @@ if (existsSync(mitmSrc)) { writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2)); try { - execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" }); + execFileSync(NPX_BIN, ["tsc", "-p", "tsconfig.mitm.tmp.json"], { + cwd: ROOT, + stdio: "inherit", + }); const mitmServerSrc = join(mitmSrc, "server.cjs"); if (existsSync(mitmServerSrc)) { cpSync(mitmServerSrc, join(mitmDest, "server.cjs")); @@ -387,8 +393,17 @@ if (existsSync(mcpSrcFile)) { console.log(" 🔨 Bundling MCP Server (TypeScript → JavaScript)..."); mkdirSync(mcpDestDir, { recursive: true }); try { - execSync( - `npx esbuild open-sse/mcp-server/server.ts --bundle --platform=node --packages=external --format=esm --outfile=app/open-sse/mcp-server/server.js`, + execFileSync( + NPX_BIN, + [ + "esbuild", + "open-sse/mcp-server/server.ts", + "--bundle", + "--platform=node", + "--packages=external", + "--format=esm", + "--outfile=app/open-sse/mcp-server/server.js", + ], { cwd: ROOT, stdio: "inherit" } ); console.log(" ✅ MCP Server bundled to app/open-sse/mcp-server/server.js"); @@ -404,11 +419,20 @@ const cliDestFile = join(ROOT, "bin", "omniroute.mjs"); if (existsSync(cliSrcFile)) { console.log(" 🔨 Bundling CLI Entrypoint (TypeScript → JavaScript)..."); try { - execSync( - `npx esbuild bin/omniroute.ts --bundle --platform=node --packages=external --format=esm --outfile=bin/omniroute.mjs`, + execFileSync( + NPX_BIN, + [ + "esbuild", + "bin/omniroute.ts", + "--bundle", + "--platform=node", + "--packages=external", + "--format=esm", + "--outfile=bin/omniroute.mjs", + ], { cwd: ROOT, stdio: "inherit" } ); - execSync(`chmod +x bin/omniroute.mjs`, { cwd: ROOT }); + chmodSync(cliDestFile, 0o755); console.log(" ✅ CLI Entrypoint bundled to bin/omniroute.mjs"); } catch (err: any) { console.warn(" ⚠️ CLI bundle error:", err.message); diff --git a/sonar-project.properties b/sonar-project.properties index d5d727bd9d..42746bc679 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,10 @@ sonar.projectKey=diegosouzapw_OmniRoute sonar.organization=diegosouzapw sonar.sourceEncoding=UTF-8 +sonar.sources=src,open-sse,scripts,bin,next.config.mjs,Dockerfile +sonar.tests=tests +sonar.test.inclusions=tests/**/*.test.ts,tests/**/*.spec.ts +sonar.exclusions=tests/**,src/i18n/messages/**,docs/**,coverage/**,.next/**,dist/**,build/**,node_modules/** sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.coverage.exclusions=**/* +sonar.cpd.exclusions=**/* diff --git a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx index 5583361467..ff5e85de83 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx @@ -184,21 +184,45 @@ export default function SecurityTab() {

{t("apiEndpointProtection")}

- {/* Require auth for /models */} +
+

{t("authModelHeading")}

+
    +
  • {t("authModelClient")}
  • +
  • {t("authModelManagement")}
  • +
  • {t("authModelPublic")}
  • +
+
+
-

{t("requireAuthModels")}

-

{t("requireAuthModelsDesc")}

+

{t("bruteForceProtection")}

+

{t("bruteForceProtectionDesc")}

updateSetting("requireAuthForModels", !settings.requireAuthForModels)} + checked={settings.bruteForceProtection !== false} + onChange={() => + updateSetting("bruteForceProtection", !(settings.bruteForceProtection !== false)) + } disabled={loading} />
+
+
+

{t("corsAllowedOrigins")}

+

{t("corsAllowedOriginsDesc")}

+
+ setSettings((prev) => ({ ...prev, corsOrigins: e.target.value }))} + onBlur={(e) => updateSetting("corsOrigins", e.target.value.trim())} + /> +
+ {/* Blocked Providers */} -
+

{t("blockedProviders")}

{t("blockedProvidersDesc")}

diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 8fda3a2072..b5dbf270d0 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -10,6 +10,11 @@ import { } from "@/lib/auth/managementPassword"; import { loginSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { + checkLoginGuard, + clearLoginAttempts, + recordLoginFailure, +} from "@/server/auth/loginGuard"; // SECURITY: No hardcoded fallback — JWT_SECRET must be configured. if (!process.env.JWT_SECRET) { @@ -59,6 +64,32 @@ export async function POST(request) { return NextResponse.json({ error: "Invalid password payload" }, { status: 400 }); } const settings = await getSettings(); + const bruteForceEnabled = settings.bruteForceProtection !== false; + const clientIp = auditContext.ipAddress || null; + + const guardCheck = checkLoginGuard(clientIp, { enabled: bruteForceEnabled }); + if (!guardCheck.allowed) { + logAuditEvent({ + action: "auth.login.locked", + actor: "anonymous", + target: "dashboard-auth", + resourceType: "auth_session", + status: "failed", + ipAddress: clientIp || undefined, + requestId: auditContext.requestId, + metadata: { retryAfterSeconds: guardCheck.retryAfterSeconds || 0 }, + }); + return NextResponse.json( + { error: "Too many failed attempts. Try again later." }, + { + status: 429, + headers: guardCheck.retryAfterSeconds + ? { "Retry-After": String(guardCheck.retryAfterSeconds) } + : {}, + } + ); + } + const passwordState = await ensurePersistentManagementPasswordHash({ settings, source: "auth.login", @@ -119,9 +150,12 @@ export async function POST(request) { }, }); + clearLoginAttempts(clientIp); return NextResponse.json({ success: true }); } + const failureDecision = recordLoginFailure(clientIp, { enabled: bruteForceEnabled }); + logAuditEvent({ action: "auth.login.failed", actor: "anonymous", @@ -130,8 +164,21 @@ export async function POST(request) { status: "failed", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, - metadata: { reason: "invalid_password" }, + metadata: { reason: "invalid_password", lockedOut: failureDecision.allowed === false }, }); + + if (!failureDecision.allowed) { + return NextResponse.json( + { error: "Too many failed attempts. Try again later." }, + { + status: 429, + headers: failureDecision.retryAfterSeconds + ? { "Retry-After": String(failureDecision.retryAfterSeconds) } + : {}, + } + ); + } + return NextResponse.json({ error: "Invalid password" }, { status: 401 }); } catch (error) { console.error("[AUTH] Login failed:", error); diff --git a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts index 660bcd4951..164a341a37 100644 --- a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts +++ b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts @@ -8,7 +8,7 @@ import { getOpenCodeConfigPath } from "@/shared/services/cliRuntime"; import { mergeOpenCodeConfigText } from "@/shared/services/opencodeConfig"; import { guideSettingsSaveSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { resolveApiKey } from "@/shared/services/apiKeyResolver"; +import { resolveApiKey, getOrCreateApiKey } from "@/shared/services/apiKeyResolver"; /** * POST /api/cli-tools/guide-settings/:toolId @@ -43,7 +43,10 @@ export async function POST(request, { params }) { const { baseUrl, model, models, modelLabels } = validation.data; // (#523) Extract keyId BEFORE validation — Zod strips unknown fields! const apiKeyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; - const apiKey = await resolveApiKey(apiKeyId, validation.data.apiKey); + // If no keyId provided, auto-create a valid DB-backed key instead of using placeholder + const apiKey = apiKeyId + ? await resolveApiKey(apiKeyId, validation.data.apiKey) + : await getOrCreateApiKey(); try { switch (toolId) { @@ -186,50 +189,25 @@ async function saveOpenCodeConfig({ baseUrl, apiKey, model, models, modelLabels } /** - * Save Qwen Code config to ~/.qwen/settings.json + ~/.qwen/.env + * Save Qwen Code config to ~/.qwen/settings.json * - * Per official docs, credentials go in .env via envKey references, - * not hardcoded in settings.json modelProviders entries. - * Writes openai, anthropic, and gemini providers pointing to OmniRoute. + * Uses security.auth format (not modelProviders) since Qwen Code + * prioritizes security.auth.selectedType over modelProviders entries. + * Per official docs: security.auth takes highest precedence. */ async function saveQwenConfig({ baseUrl, apiKey, model }) { const home = os.homedir(); const configPath = path.join(home, ".qwen", "settings.json"); - const envPath = path.join(home, ".qwen", ".env"); - const configDir = path.dirname(configPath); - await fs.mkdir(configDir, { recursive: true }); + await fs.mkdir(path.dirname(configPath), { recursive: true }); const normalizedBaseUrl = String(baseUrl || "") .trim() .replace(/\/+$/, ""); const resolvedApiKey = apiKey || "sk_omniroute"; - const resolvedModel = model || "coder-model"; + const resolvedModel = model || "gemini-cli/gemini-3.1-pro-preview"; - // --- Write API keys to .env --- - let envContent = ""; - try { - envContent = await fs.readFile(envPath, "utf-8"); - } catch { - // File doesn't exist - } - - const envLines = envContent.split("\n").filter((line) => { - // Remove old OmniRoute-related keys we're about to write - return ( - !line.startsWith("OPENAI_API_KEY=") && - !line.startsWith("ANTHROPIC_API_KEY=") && - !line.startsWith("GEMINI_API_KEY=") - ); - }); - - envLines.push(`OPENAI_API_KEY=${resolvedApiKey}`); - envLines.push(`ANTHROPIC_API_KEY=${resolvedApiKey}`); - envLines.push(`GEMINI_API_KEY=${resolvedApiKey}`); - - await fs.writeFile(envPath, envLines.join("\n").trim() + "\n", "utf-8"); - - // --- Write modelProviders to settings.json --- + // Read existing config to preserve other settings (permissions, mcpServers, etc.) let existingConfig: Record = {}; try { const raw = await fs.readFile(configPath, "utf-8"); @@ -238,75 +216,28 @@ async function saveQwenConfig({ baseUrl, apiKey, model }) { // File doesn't exist or invalid JSON } - if (!existingConfig.modelProviders) existingConfig.modelProviders = {}; - - // openai provider — primary, supports all models via OmniRoute - const openaiEntry = { - id: resolvedModel, - name: `${resolvedModel} (OmniRoute)`, - envKey: "OPENAI_API_KEY", - baseUrl: normalizedBaseUrl, - generationConfig: { - contextWindowSize: 200000, + // Set security.auth for openai auth type with direct credentials + // This takes priority over modelProviders entries (per Qwen docs) + existingConfig.security = { + ...existingConfig.security, + auth: { + selectedType: "openai", + apiKey: resolvedApiKey, + baseUrl: normalizedBaseUrl, }, }; - if (!existingConfig.modelProviders.openai) existingConfig.modelProviders.openai = []; - const openaiProviders = existingConfig.modelProviders.openai; - const openaiIdx = openaiProviders.findIndex( - (p: any) => p && (p.baseUrl === normalizedBaseUrl || p.id === "omniroute") - ); - if (openaiIdx >= 0) { - openaiProviders[openaiIdx] = openaiEntry; - } else { - openaiProviders.push(openaiEntry); - } - - // anthropic provider — for Claude models via OmniRoute - const anthropicEntry = { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (OmniRoute)", - envKey: "ANTHROPIC_API_KEY", - baseUrl: normalizedBaseUrl, - generationConfig: { - contextWindowSize: 200000, - }, + // Set model to the selected model + existingConfig.model = { + ...existingConfig.model, + name: resolvedModel, }; - if (!existingConfig.modelProviders.anthropic) existingConfig.modelProviders.anthropic = []; - const anthropicProviders = existingConfig.modelProviders.anthropic; - const anthropicIdx = anthropicProviders.findIndex( - (p: any) => p && p.baseUrl === normalizedBaseUrl - ); - if (anthropicIdx >= 0) { - anthropicProviders[anthropicIdx] = anthropicEntry; - } else { - anthropicProviders.push(anthropicEntry); - } - - // gemini provider — for Gemini models via OmniRoute - const geminiEntry = { - id: "gemini-3-flash", - name: "Gemini 3 Flash (OmniRoute)", - envKey: "GEMINI_API_KEY", - baseUrl: normalizedBaseUrl, - }; - - if (!existingConfig.modelProviders.gemini) existingConfig.modelProviders.gemini = []; - const geminiProviders = existingConfig.modelProviders.gemini; - const geminiIdx = geminiProviders.findIndex((p: any) => p && p.baseUrl === normalizedBaseUrl); - if (geminiIdx >= 0) { - geminiProviders[geminiIdx] = geminiEntry; - } else { - geminiProviders.push(geminiEntry); - } - await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2), "utf-8"); return NextResponse.json({ success: true, - message: `Qwen Code config saved to ${configPath} + ${envPath}`, + message: `Qwen Code config saved to ${configPath}`, configPath, - envPath, }); } diff --git a/src/app/api/combos/test/route.ts b/src/app/api/combos/test/route.ts index 115061f38b..7aa613e519 100644 --- a/src/app/api/combos/test/route.ts +++ b/src/app/api/combos/test/route.ts @@ -1,12 +1,25 @@ import { randomUUID } from "node:crypto"; import { NextResponse } from "next/server"; import { buildComboTestRequestBody, extractComboTestResponseText } from "@/lib/combos/testHealth"; -import { getComboByName, getCombos } from "@/lib/localDb"; +import { getApiKeys, getComboByName, getCombos } from "@/lib/localDb"; +import { getRuntimePorts } from "@/lib/runtime/ports"; import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo.ts"; import { testComboSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +async function getInternalApiKey(): Promise { + try { + const keys = await getApiKeys(); + const active = ( + keys as Array<{ key: string; isActive?: boolean; revokedAt?: string | null }> + ).find((k) => k.key && k.isActive !== false && !k.revokedAt); + return active?.key ?? null; + } catch { + return null; + } +} + function buildComboTestResult(target, partial = {}) { return { model: target.modelStr, @@ -19,7 +32,7 @@ function buildComboTestResult(target, partial = {}) { }; } -async function testComboTarget(target, baseInternalUrl) { +async function testComboTarget(target, baseInternalUrl, internalApiKey: string | null) { const startTime = Date.now(); try { const isEmbedding = @@ -38,8 +51,7 @@ async function testComboTarget(target, baseInternalUrl) { method: "POST", headers: { "Content-Type": "application/json", - // Internal dashboard tests still use the normal /v1 pipeline but - // bypass REQUIRE_API_KEY so admins can test with local session auth. + ...(internalApiKey ? { Authorization: `Bearer ${internalApiKey}` } : {}), "X-Internal-Test": "combo-health-check", // Force a fresh execution path so combo tests cannot be satisfied by // OmniRoute's semantic cache or other request reuse layers. @@ -144,9 +156,10 @@ export async function POST(request) { return NextResponse.json({ error: "Combo has no models" }, { status: 400 }); } - const baseInternalUrl = getBaseUrl(request); + const baseInternalUrl = getInternalBaseUrl(); + const internalApiKey = await getInternalApiKey(); const results = await Promise.all( - targets.map((target) => testComboTarget(target, baseInternalUrl)) + targets.map((target) => testComboTarget(target, baseInternalUrl, internalApiKey)) ); const resolvedResult = results.find((result) => result.status === "ok") || null; const resolvedBy = resolvedResult?.model || null; @@ -175,13 +188,7 @@ export async function POST(request) { } } -/** - * Get the base URL for internal requests (VPS-safe: respects reverse proxy headers) - */ -function getBaseUrl(request) { - const fwdHost = request.headers.get("x-forwarded-host"); - const fwdProto = request.headers.get("x-forwarded-proto") || "https"; - if (fwdHost) return `${fwdProto}://${fwdHost}`; - const url = new URL(request.url); - return `${url.protocol}//${url.host}`; +function getInternalBaseUrl(): string { + const { apiPort } = getRuntimePorts(); + return `http://127.0.0.1:${apiPort}`; } diff --git a/src/app/api/logs/active/route.ts b/src/app/api/logs/active/route.ts index f8117ed32a..77b5c462d0 100644 --- a/src/app/api/logs/active/route.ts +++ b/src/app/api/logs/active/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getProviderConnections } from "@/lib/localDb"; -import { getPendingRequests } from "@/lib/usage/usageHistory"; +import { getPendingRequests, clearPendingRequests } from "@/lib/usage/usageHistory"; export async function GET(request: Request) { const authError = await requireManagementAuth(request); @@ -45,3 +45,11 @@ export async function GET(request: Request) { return NextResponse.json({ error: "Failed to load active requests" }, { status: 500 }); } } + +export async function DELETE(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + clearPendingRequests(); + return NextResponse.json({ success: true, message: "Pending request counts cleared" }); +} diff --git a/src/app/api/settings/favicon/route.ts b/src/app/api/settings/favicon/route.ts index 164f1410dc..caf5be2b66 100644 --- a/src/app/api/settings/favicon/route.ts +++ b/src/app/api/settings/favicon/route.ts @@ -87,7 +87,7 @@ export async function GET() { const response = await fetch(customFaviconUrl, { signal: controller.signal, headers: { - "User-Agent": "OmniRoute/1.0", + "User-Agent": "OmniRoute/2.0", }, }); clearTimeout(timeoutId); diff --git a/src/app/api/v1/_helpers/apiKeyScope.ts b/src/app/api/v1/_helpers/apiKeyScope.ts index cce0f76a75..f8fb466705 100644 --- a/src/app/api/v1/_helpers/apiKeyScope.ts +++ b/src/app/api/v1/_helpers/apiKeyScope.ts @@ -1,8 +1,5 @@ -import { NextResponse } from "next/server"; - import { getApiKeyMetadata } from "@/lib/localDb"; -import { CORS_HEADERS } from "@/shared/utils/cors"; -import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { extractApiKey } from "@/sse/services/auth"; export interface ApiKeyRequestScope { apiKey: string | null; @@ -13,43 +10,8 @@ export interface ApiKeyRequestScope { export async function getApiKeyRequestScope(request: Request): Promise { const apiKey = extractApiKey(request); - - if (process.env.REQUIRE_API_KEY === "true") { - if (!apiKey) { - return { - apiKey: null, - apiKeyId: null, - apiKeyMetadata: null, - rejection: NextResponse.json( - { error: { message: "Missing API key", type: "invalid_request_error" } }, - { status: 401, headers: CORS_HEADERS } - ), - }; - } - - if (!(await isValidApiKey(apiKey))) { - return { - apiKey: null, - apiKeyId: null, - apiKeyMetadata: null, - rejection: NextResponse.json( - { error: { message: "Invalid API key", type: "invalid_request_error" } }, - { status: 401, headers: CORS_HEADERS } - ), - }; - } - } - - if (apiKey && !(await isValidApiKey(apiKey))) { - return { - apiKey: null, - apiKeyId: null, - apiKeyMetadata: null, - rejection: NextResponse.json( - { error: { message: "Invalid API key", type: "invalid_request_error" } }, - { status: 401, headers: CORS_HEADERS } - ), - }; + if (!apiKey) { + return { apiKey: null, apiKeyId: null, apiKeyMetadata: null, rejection: null }; } const apiKeyMetadata = await getApiKeyMetadata(apiKey); diff --git a/src/app/api/v1/api/chat/route.ts b/src/app/api/v1/api/chat/route.ts index 5ac4feae8d..c531911121 100644 --- a/src/app/api/v1/api/chat/route.ts +++ b/src/app/api/v1/api/chat/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { transformToOllama } from "@omniroute/open-sse/utils/ollamaTransform.ts"; @@ -16,7 +15,6 @@ async function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/audio/speech/route.ts b/src/app/api/v1/audio/speech/route.ts index 1fec03c3c8..32f32da974 100644 --- a/src/app/api/v1/audio/speech/route.ts +++ b/src/app/api/v1/audio/speech/route.ts @@ -1,11 +1,5 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleAudioSpeech } from "@omniroute/open-sse/handlers/audioSpeech.ts"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseSpeechModel, getSpeechProvider, @@ -25,7 +19,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -37,13 +30,6 @@ export async function OPTIONS() { * OpenAI TTS API compatible. Returns audio stream. */ export async function POST(request) { - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - const valid = await isValidApiKey(apiKey); - if (!valid) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - let rawBody; try { rawBody = await request.json(); diff --git a/src/app/api/v1/audio/transcriptions/route.ts b/src/app/api/v1/audio/transcriptions/route.ts index c6a206c36f..69fa331614 100644 --- a/src/app/api/v1/audio/transcriptions/route.ts +++ b/src/app/api/v1/audio/transcriptions/route.ts @@ -1,14 +1,7 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; - // Allow large audio/video file uploads — 5min for processing large files (up to 2GB) export const maxDuration = 300; import { handleAudioTranscription } from "@omniroute/open-sse/handlers/audioTranscription.ts"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseTranscriptionModel, getTranscriptionProvider, @@ -26,7 +19,6 @@ import { getProviderNodes } from "@/lib/localDb"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -38,14 +30,6 @@ export async function OPTIONS() { * OpenAI Whisper API compatible (multipart/form-data) */ export async function POST(request) { - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - const valid = await isValidApiKey(apiKey); - if (!valid) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - let formData; try { formData = await request.formData(); diff --git a/src/app/api/v1/chat/completions/route.ts b/src/app/api/v1/chat/completions/route.ts index 3508d97f70..c161f0cac2 100644 --- a/src/app/api/v1/chat/completions/route.ts +++ b/src/app/api/v1/chat/completions/route.ts @@ -1,4 +1,4 @@ -import { CORS_ORIGIN, CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; +import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { callCloudWithMachineId } from "@/shared/utils/cloud"; import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; @@ -38,9 +38,7 @@ export async function POST(request) { const ct = request.headers.get("content-type") ?? ""; const cl = request.headers.get("content-length"); if (cl && Number(cl) > 256 * 1024) { - console.error( - `[CHAT-ROUTE] large body content-type="${ct}" content-length=${cl}` - ); + console.error(`[CHAT-ROUTE] large body content-type="${ct}" content-length=${cl}`); } } diff --git a/src/app/api/v1/completions/route.ts b/src/app/api/v1/completions/route.ts index 449fb2f9e6..a146e46c1b 100644 --- a/src/app/api/v1/completions/route.ts +++ b/src/app/api/v1/completions/route.ts @@ -1,4 +1,4 @@ -import { CORS_ORIGIN, CORS_HEADERS } from "@/shared/utils/cors"; +import { CORS_HEADERS } from "@/shared/utils/cors"; import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { createInjectionGuard } from "@/middleware/promptInjectionGuard"; @@ -21,7 +21,6 @@ function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/embeddings/route.ts b/src/app/api/v1/embeddings/route.ts index 6045cb31d7..9992f8273e 100644 --- a/src/app/api/v1/embeddings/route.ts +++ b/src/app/api/v1/embeddings/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleEmbedding } from "@omniroute/open-sse/handlers/embeddings.ts"; import { getProviderCredentials, @@ -30,7 +29,6 @@ import { getAllCustomModels, getProviderNodes } from "@/lib/localDb"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -232,18 +230,6 @@ export async function POST(request) { } const body = validation.data; - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); if (policy.rejection) return policy.rejection; diff --git a/src/app/api/v1/images/edits/route.ts b/src/app/api/v1/images/edits/route.ts index 05f70e3ceb..b3b4751cfa 100644 --- a/src/app/api/v1/images/edits/route.ts +++ b/src/app/api/v1/images/edits/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleImageEdit } from "@omniroute/open-sse/handlers/imageGeneration.ts"; import { getProviderCredentials, @@ -31,7 +30,6 @@ import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -84,7 +82,10 @@ export async function POST(request: Request) { try { formData = await request.formData(); } catch (err) { - log.warn("IMAGE", `Invalid multipart body: ${err instanceof Error ? err.message : String(err)}`); + log.warn( + "IMAGE", + `Invalid multipart body: ${err instanceof Error ? err.message : String(err)}` + ); return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid multipart body"); } @@ -127,7 +128,10 @@ export async function POST(request: Request) { const credentials = await getProviderCredentials(parsed.provider, apiKey); if (!credentials) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, `No credentials for provider: ${parsed.provider}`); + return errorResponse( + HTTP_STATUS.UNAUTHORIZED, + `No credentials for provider: ${parsed.provider}` + ); } if (credentials.allRateLimited) { return unavailableResponse( @@ -163,10 +167,7 @@ export async function POST(request: Request) { }); } - const errorPayload = toJsonErrorPayload( - (result as any).error, - "Image edit provider error" - ); + const errorPayload = toJsonErrorPayload((result as any).error, "Image edit provider error"); return new Response(JSON.stringify(errorPayload), { status: (result as any).status, headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/v1/images/generations/route.ts b/src/app/api/v1/images/generations/route.ts index be7aa2b3b6..5c051da1c0 100644 --- a/src/app/api/v1/images/generations/route.ts +++ b/src/app/api/v1/images/generations/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts"; import { getProviderCredentials, @@ -28,7 +27,6 @@ import { getAllCustomModels } from "@/lib/localDb"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -133,18 +131,6 @@ export async function POST(request) { } const body = validation.data; - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); if (policy.rejection) return policy.rejection; diff --git a/src/app/api/v1/messages/count_tokens/route.ts b/src/app/api/v1/messages/count_tokens/route.ts index 3362654906..1846478e41 100644 --- a/src/app/api/v1/messages/count_tokens/route.ts +++ b/src/app/api/v1/messages/count_tokens/route.ts @@ -38,23 +38,6 @@ export async function POST(request) { } const body = validation.data; - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return new Response(JSON.stringify({ error: "Missing API key" }), { - status: 401, - headers: { "Content-Type": "application/json", ...CORS_HEADERS }, - }); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return new Response(JSON.stringify({ error: "Invalid API key" }), { - status: 401, - headers: { "Content-Type": "application/json", ...CORS_HEADERS }, - }); - } - } - const estimated = buildEstimatedCountResponse(body); const requestedModel = typeof body.model === "string" ? body.model : ""; if (!requestedModel) { diff --git a/src/app/api/v1/messages/route.ts b/src/app/api/v1/messages/route.ts index 90c39534d7..274f0aecce 100644 --- a/src/app/api/v1/messages/route.ts +++ b/src/app/api/v1/messages/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; @@ -21,7 +20,6 @@ async function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 633e2da26c..e291961e60 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { AI_PROVIDERS } from "@/shared/constants/providers"; import { @@ -9,7 +8,6 @@ import { getProviderNodes, getModelIsHidden, } from "@/lib/localDb"; -import { isAuthenticated } from "@/shared/utils/apiAuth"; import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { getAllImageModels } from "@omniroute/open-sse/config/imageRegistry.ts"; import { getAllRerankModels } from "@omniroute/open-sse/config/rerankRegistry.ts"; @@ -26,6 +24,7 @@ import { enrichCatalogModelEntry, getCatalogDiagnosticsHeaders, } from "@/lib/modelMetadataRegistry"; +import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; const FALLBACK_ALIAS_TO_PROVIDER = { ag: "antigravity", @@ -82,6 +81,59 @@ function getVisionCapabilityFields(modelId: string) { }; } +function extractBearer(headers: Headers): string | null { + const authHeader = headers.get("authorization") || headers.get("Authorization"); + if (!authHeader?.trim().toLowerCase().startsWith("bearer ")) return null; + return authHeader.trim().slice(7).trim() || null; +} + +async function validateCatalogBearer(apiKey: string): Promise { + const { validateApiKey } = await import("@/lib/db/apiKeys"); + return validateApiKey(apiKey); +} + +async function getModelCatalogAuthRejection( + request: Request, + settings: Record, + headers: Record +): Promise { + if (settings.requireAuthForModels !== true || !(await isAuthRequired())) return null; + + const bearer = extractBearer(request.headers); + if (bearer) { + if (await validateCatalogBearer(bearer)) return null; + return Response.json( + { + error: { + message: "Invalid API key", + type: "invalid_api_key", + code: "invalid_api_key", + }, + }, + { + status: 401, + headers, + } + ); + } + + if (await isDashboardSessionAuthenticated(request)) return null; + + return Response.json( + { + error: { + message: "Authentication required", + type: "invalid_api_key", + code: "invalid_api_key", + }, + }, + { + status: 401, + headers, + } + ); +} + function buildAliasMaps() { const aliasToProviderId: Record = {}; const providerIdToAlias: Record = {}; @@ -142,39 +194,20 @@ function buildAliasMaps() { */ export async function getUnifiedModelsResponse( request: Request, - corsHeaders: Record = { - "Access-Control-Allow-Origin": CORS_ORIGIN, - } + corsHeaders: Record = {} ) { const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request }); try { - // Issue #100: Optionally require authentication for /models (security hardening) - // When enabled, unauthenticated requests get 401 with proper error response. - // Supports API key (Bearer token) for external clients and JWT cookie for dashboard. let settings: Record = {}; try { settings = await getSettings(); } catch {} - if (settings.requireAuthForModels === true) { - if (!(await isAuthenticated(request))) { - return Response.json( - { - error: { - message: "Authentication required", - type: "invalid_request_error", - code: "invalid_api_key", - }, - }, - { - status: 401, - headers: { - ...corsHeaders, - ...diagnosticHeaders, - }, - } - ); - } - } + + const authRejection = await getModelCatalogAuthRejection(request, settings, { + ...corsHeaders, + ...diagnosticHeaders, + }); + if (authRejection) return authRejection; const { aliasToProviderId, providerIdToAlias } = buildAliasMaps(); @@ -706,10 +739,9 @@ export async function getUnifiedModelsResponse( } // Filter by API key permissions if requested - const authHeader = request.headers.get("authorization"); + const apiKey = extractBearer(request.headers); let finalModels = models; - if (authHeader && authHeader.startsWith("Bearer ")) { - const apiKey = authHeader.slice(7); + if (apiKey) { const { isModelAllowedForKey } = await import("@/lib/db/apiKeys"); const filtered = []; diff --git a/src/app/api/v1/models/route.ts b/src/app/api/v1/models/route.ts index 2224b13ee6..eb30a4daf7 100644 --- a/src/app/api/v1/models/route.ts +++ b/src/app/api/v1/models/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { getUnifiedModelsResponse } from "./catalog"; /** @@ -7,7 +6,6 @@ import { getUnifiedModelsResponse } from "./catalog"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/moderations/route.ts b/src/app/api/v1/moderations/route.ts index 8b407c0a5e..5d7881b533 100644 --- a/src/app/api/v1/moderations/route.ts +++ b/src/app/api/v1/moderations/route.ts @@ -1,11 +1,5 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleModeration } from "@omniroute/open-sse/handlers/moderations.ts"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseModerationModel } from "@omniroute/open-sse/config/moderationRegistry.ts"; import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; @@ -19,7 +13,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -31,13 +24,6 @@ export async function OPTIONS() { * OpenAI Moderations API compatible. */ export async function POST(request) { - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - const valid = await isValidApiKey(apiKey); - if (!valid) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - let rawBody; try { rawBody = await request.json(); diff --git a/src/app/api/v1/music/generations/route.ts b/src/app/api/v1/music/generations/route.ts index 3dce64136b..1199f68f3f 100644 --- a/src/app/api/v1/music/generations/route.ts +++ b/src/app/api/v1/music/generations/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleMusicGeneration } from "@omniroute/open-sse/handlers/musicGeneration.ts"; import { getProviderCredentials, @@ -25,7 +24,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -76,18 +74,6 @@ export async function POST(request) { return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); } - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); if (policy.rejection) return policy.rejection; diff --git a/src/app/api/v1/providers/[provider]/chat/completions/route.ts b/src/app/api/v1/providers/[provider]/chat/completions/route.ts index 8bf8924922..20b2e15c81 100644 --- a/src/app/api/v1/providers/[provider]/chat/completions/route.ts +++ b/src/app/api/v1/providers/[provider]/chat/completions/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; @@ -22,7 +21,6 @@ async function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/providers/[provider]/embeddings/route.ts b/src/app/api/v1/providers/[provider]/embeddings/route.ts index d4051de55f..f5329addb6 100644 --- a/src/app/api/v1/providers/[provider]/embeddings/route.ts +++ b/src/app/api/v1/providers/[provider]/embeddings/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; @@ -20,7 +19,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -53,14 +51,6 @@ export async function POST(request, { params }) { } const body = validation.data; - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey || !(await isValidApiKey(apiKey))) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Add provider prefix if missing if (body.model && !body.model.includes("/")) { body.model = `${providerAlias}/${body.model}`; diff --git a/src/app/api/v1/providers/[provider]/images/generations/route.ts b/src/app/api/v1/providers/[provider]/images/generations/route.ts index fd0e79dc60..66555796e0 100644 --- a/src/app/api/v1/providers/[provider]/images/generations/route.ts +++ b/src/app/api/v1/providers/[provider]/images/generations/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts"; import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; @@ -21,7 +20,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -52,14 +50,6 @@ export async function POST(request, { params }) { } const body = validation.data; - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey || !(await isValidApiKey(apiKey))) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Ensure model has provider prefix if (!body.model.includes("/")) { body.model = `${rawProvider}/${body.model}`; diff --git a/src/app/api/v1/rerank/route.ts b/src/app/api/v1/rerank/route.ts index 4f3fc8fba4..e5ec4419bb 100644 --- a/src/app/api/v1/rerank/route.ts +++ b/src/app/api/v1/rerank/route.ts @@ -1,11 +1,5 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleRerank } from "@omniroute/open-sse/handlers/rerank.ts"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseRerankModel, getRerankProvider } from "@omniroute/open-sse/config/rerankRegistry.ts"; import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; @@ -20,7 +14,6 @@ import { getProviderNodes } from "@/lib/localDb"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -52,14 +45,6 @@ function buildDynamicRerankProvider(node: any) { * and local provider_nodes (oMLX, vLLM, etc.) via dynamic routing. */ export async function POST(request) { - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - const valid = await isValidApiKey(apiKey); - if (!valid) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - let rawBody; try { rawBody = await request.json(); @@ -174,7 +159,7 @@ export async function POST(request) { const data = await res.json(); return Response.json(data, { - headers: { "Access-Control-Allow-Origin": CORS_ORIGIN }, + headers: {}, }); } catch (err: any) { return errorResponse(500, `Rerank request failed: ${err.message}`); diff --git a/src/app/api/v1/responses/[...path]/route.ts b/src/app/api/v1/responses/[...path]/route.ts index bf967e5d60..e2f7062f5a 100644 --- a/src/app/api/v1/responses/[...path]/route.ts +++ b/src/app/api/v1/responses/[...path]/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; @@ -15,7 +14,6 @@ async function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/responses/route.ts b/src/app/api/v1/responses/route.ts index 809ed1a41f..d4cfeab444 100644 --- a/src/app/api/v1/responses/route.ts +++ b/src/app/api/v1/responses/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleChat } from "@/sse/handlers/chat"; // NOTE: We do NOT call initTranslators() here — the translator registry is @@ -13,7 +12,6 @@ import { handleChat } from "@/sse/handlers/chat"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1/search/route.ts b/src/app/api/v1/search/route.ts index 8bd8ac87c1..2f282b454a 100644 --- a/src/app/api/v1/search/route.ts +++ b/src/app/api/v1/search/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleSearch } from "@omniroute/open-sse/handlers/search.ts"; import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/services/auth"; import { @@ -24,7 +23,6 @@ import { } from "@omniroute/open-sse/services/searchCache.ts"; const CORS_HEADERS = { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }; @@ -104,18 +102,6 @@ export async function POST(request: Request) { } const body = validation.data; - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Enforce API key policies — use "search" as model identifier for consistent policy config const policy = await enforceApiKeyPolicy(request, "search"); if (policy.rejection) return policy.rejection; diff --git a/src/app/api/v1/videos/generations/route.ts b/src/app/api/v1/videos/generations/route.ts index b406bd629c..8f961a6a24 100644 --- a/src/app/api/v1/videos/generations/route.ts +++ b/src/app/api/v1/videos/generations/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { handleVideoGeneration } from "@omniroute/open-sse/handlers/videoGeneration.ts"; import { getProviderCredentials, @@ -25,7 +24,6 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, @@ -76,18 +74,6 @@ export async function POST(request) { return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); } - // Optional API key validation - if (process.env.REQUIRE_API_KEY === "true") { - const apiKey = extractApiKey(request); - if (!apiKey) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } - // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); if (policy.rejection) return policy.rejection; diff --git a/src/app/api/v1beta/models/[...path]/route.ts b/src/app/api/v1beta/models/[...path]/route.ts index 96c76a8417..a8bf6d0230 100644 --- a/src/app/api/v1beta/models/[...path]/route.ts +++ b/src/app/api/v1beta/models/[...path]/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { v1betaGeminiGenerateSchema } from "@/shared/validation/schemas"; @@ -23,7 +22,6 @@ async function ensureInitialized() { export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/app/api/v1beta/models/route.ts b/src/app/api/v1beta/models/route.ts index 353f3def42..37390e3f18 100644 --- a/src/app/api/v1beta/models/route.ts +++ b/src/app/api/v1beta/models/route.ts @@ -1,4 +1,3 @@ -import { CORS_ORIGIN } from "@/shared/utils/cors"; import { PROVIDER_MODELS } from "@/shared/constants/models"; import { getAllCustomModels, getSyncedAvailableModels } from "@/lib/db/models"; import { getResolvedModelCapabilities } from "@/lib/modelCapabilities"; @@ -10,7 +9,6 @@ import { getSyncedCapabilities } from "@/lib/modelsDevSync"; export async function OPTIONS() { return new Response(null, { headers: { - "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "*", }, diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index 759335228c..61017e5123 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -49,6 +49,7 @@ const ACTIVE_TTL_MS = 5 * 60 * 1000; // 5 minutes for active accounts const EXHAUSTED_TTL_MS = 5 * 60 * 1000; // 5 minutes for 429-sourced entries (no resetAt) const EXHAUSTED_REFRESH_MS = 5 * 60 * 1000; // 5 minutes: recheck exhausted accounts (aligned with TTL) const REFRESH_INTERVAL_MS = 60 * 1000; // Background tick every 1 minute +export const DEFAULT_QUOTA_THRESHOLD_PERCENT = 99; // ─── State ────────────────────────────────────────────────────────────────── @@ -260,7 +261,7 @@ export function isAccountQuotaExhausted(connectionId: string): boolean { export function getQuotaWindowStatus( connectionId: string, windowName: string, - thresholdPercent = 90 + thresholdPercent = DEFAULT_QUOTA_THRESHOLD_PERCENT ): QuotaWindowStatus | null { const entry = cache.get(connectionId); if (!entry) return null; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 6dcad01d88..e380bc7d7b 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 1cc0b28c3f..1814436df7 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 4b8b76ade2..8ef28702e9 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 66d960b4b8..b1ca73187b 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 002e4782b9..bbdd5968b6 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 5e870fb7fb..68a813ef38 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2739,8 +2739,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2754,8 +2754,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2783,8 +2783,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", @@ -3058,6 +3058,14 @@ "apiEndpointProtection": "API Endpoint Protection", "requireAuthModels": "Require API key for /models", "requireAuthModelsDesc": "When ON, the /v1/models endpoint returns 404 for unauthenticated requests. Prevents model discovery by unauthorized users.", + "authModelHeading": "Active authorization model", + "authModelClient": "Client API endpoints (/v1/*, /chat/*, /responses/*, /codex/*, /messages/*) require a Bearer API key.", + "authModelManagement": "Management endpoints (/dashboard, /api/*) require a dashboard session or management credential.", + "authModelPublic": "Only login, health, and bootstrap routes are public.", + "bruteForceProtection": "Login brute-force protection", + "bruteForceProtectionDesc": "Throttle and lock /api/auth/login after repeated failures from the same IP.", + "corsAllowedOrigins": "CORS allowed origins", + "corsAllowedOriginsDesc": "Comma-separated list of browser origins permitted to call this server. Empty list = no browser CORS access (server-to-server still works). Use CORS_ALLOW_ALL=true env var only for development.", "blockedProviders": "Blocked Providers", "blockedProvidersDesc": "Hide specific providers from the /v1/models response. Blocked providers will not appear in model listings.", "providersBlocked": "{count} provider(s) blocked from /models", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index e1ca8cfa00..12a89c0b03 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index c1f971be15..5a489f54f0 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 871c381d39..54b2fec39b 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 6e2ab17043..dcba89fb9e 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 164a90f965..909820327f 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 992e71406d..5ef94a9426 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 28d3fcc2b6..02638db529 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 3cb80ba298..fc939c8eb4 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index c65ce16a71..ce20800a47 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 378b135a84..2982d0928a 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 4b53820197..10c33ff1c6 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index ddd6d8abb1..63a0a173a0 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index e1b75e10f4..c88f56ead9 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 5358ea4fab..05239c7fe2 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 2f13e03510..ca7bb3c704 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 82c6d501c9..3a3d1fe13e 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -2631,8 +2631,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2646,8 +2646,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2675,8 +2675,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 886b700a4c..e8801d1450 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index e5636c49a3..7585d803ab 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -2600,8 +2600,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2615,8 +2615,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2644,8 +2644,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 0bcf20f1cc..178fdc90df 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index a2ca0eb706..ac584535f0 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index fe0c29b286..431d1b5dc9 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index c8ea91abdf..9e13297e66 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 30827067ef..87bfc63cf9 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 3ede9e2311..1a532eec07 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -2738,8 +2738,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2753,8 +2753,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2782,8 +2782,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 6a3d94290b..5f240a0bd0 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -2576,8 +2576,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "Use the priority service tier for Codex requests when available.", + "codexFastServiceTierLabel": "Codex fast service tier", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2591,8 +2591,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "Used when a request does not specify reasoning effort.", + "defaultThinkingStrengthLabel": "Default thinking strength", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2620,8 +2620,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "Allow compatible Responses API requests to preserve stored response state.", + "openaiResponsesStoreLabel": "OpenAI Responses store", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 3bd8310767..c6348884a7 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -2676,8 +2676,8 @@ "claudeExtraUsageShort": "Claude Extra Usage Short", "claudeExtraUsageToggleTitle": "Claude Extra Usage Toggle Title", "codex5hToggleTitle": "Codex5H Toggle Title", - "codexFastServiceTierDescription": "Codex Fast Service Tier Description", - "codexFastServiceTierLabel": "Codex Fast Service Tier Label", + "codexFastServiceTierDescription": "可用时为 Codex 请求使用 priority 服务层。", + "codexFastServiceTierLabel": "Codex 快速服务层", "codexWeeklyToggleTitle": "Codex Weekly Toggle Title", "compatUpstreamHeaderNamePlaceholder": "Compat Upstream Header Name Placeholder", "compatUpstreamHeaderValuePlaceholder": "Compat Upstream Header Value Placeholder", @@ -2691,8 +2691,8 @@ "customUserAgentHint": "Custom User Agent Hint", "customUserAgentLabel": "Custom User Agent Label", "databricksBaseUrlHint": "Databricks Base Url Hint", - "defaultThinkingStrengthHint": "Default Thinking Strength Hint", - "defaultThinkingStrengthLabel": "Default Thinking Strength Label", + "defaultThinkingStrengthHint": "请求未指定 reasoning effort 时使用。", + "defaultThinkingStrengthLabel": "默认思考强度", "excludedModelsHint": "Excluded Models Hint", "excludedModelsLabel": "Excluded Models Label", "excludedModelsPlaceholder": "Excluded Models Placeholder", @@ -2720,8 +2720,8 @@ "oauth": "Oauth", "openCliTools": "Open Cli Tools", "openSettings": "Open Settings", - "openaiResponsesStoreDescription": "Openai Responses Store Description", - "openaiResponsesStoreLabel": "Openai Responses Store Label", + "openaiResponsesStoreDescription": "允许兼容的 Responses API 请求保留已存储的响应状态。", + "openaiResponsesStoreLabel": "OpenAI Responses 存储", "perplexitySearchSharedKeyInfo": "Perplexity Search Shared Key Info", "perplexityWebCookieHint": "Perplexity Web Cookie Hint", "perplexityWebCookiePlaceholder": "Perplexity Web Cookie Placeholder", diff --git a/src/lib/config/runtimeSettings.ts b/src/lib/config/runtimeSettings.ts index 7239790b45..9df6d5985f 100644 --- a/src/lib/config/runtimeSettings.ts +++ b/src/lib/config/runtimeSettings.ts @@ -11,7 +11,8 @@ export type RuntimeReloadSection = | "usageTracking" | "healthCheckLogs" | "thoughtSignature" - | "modelsDevSync"; + | "modelsDevSync" + | "corsOrigins"; export interface RuntimeReloadChange { section: RuntimeReloadSection; @@ -29,6 +30,7 @@ interface RuntimeSettingsSnapshot { hideHealthCheckLogs: boolean; modelsDevSyncEnabled: boolean; modelsDevSyncInterval: number | null; + corsOrigins: string; } const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = { @@ -42,6 +44,7 @@ const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = { hideHealthCheckLogs: false, modelsDevSyncEnabled: false, modelsDevSyncInterval: null, + corsOrigins: "", }; let lastAppliedSnapshot: RuntimeSettingsSnapshot | null = null; @@ -176,6 +179,7 @@ export function buildRuntimeSettingsSnapshot( hideHealthCheckLogs: settings.hideHealthCheckLogs === true, modelsDevSyncEnabled: settings.modelsDevSyncEnabled === true, modelsDevSyncInterval: normalizeNumber(settings.modelsDevSyncInterval), + corsOrigins: typeof settings.corsOrigins === "string" ? settings.corsOrigins : "", }; } @@ -248,6 +252,11 @@ async function applyThoughtSignatureSection(mode: string) { setGeminiThoughtSignatureMode(mode); } +async function applyCorsOriginsSection(corsOrigins: string) { + const { setRuntimeAllowedOrigins } = await import("@/server/cors/origins"); + setRuntimeAllowedOrigins(corsOrigins); +} + async function applyModelsDevSyncSection( previousSnapshot: RuntimeSettingsSnapshot, currentSnapshot: RuntimeSettingsSnapshot, @@ -378,6 +387,11 @@ export async function applyRuntimeSettings( markChanged("modelsDevSync"); } + if (force || hasChanged(currentSnapshot.corsOrigins, previousSnapshot.corsOrigins)) { + await applyCorsOriginsSection(currentSnapshot.corsOrigins); + markChanged("corsOrigins"); + } + lastAppliedSnapshot = currentSnapshot; return changes; } diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index 3012ce7aa6..3d06719b12 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -42,6 +42,11 @@ interface ApiKeyMetadata { maxRequestsPerMinute: number | null; // T08: Per-key max concurrent sticky sessions (0 = unlimited) maxSessions: number; + // Phase 3 lifecycle/policy fields + revokedAt: string | null; + expiresAt: string | null; + ipAllowlist: string[]; + scopes: string[]; } interface ApiKeyRow extends JsonRecord { @@ -97,12 +102,32 @@ interface ApiKeyView extends JsonRecord { // LRU cache for API key validation (valid keys only) const _keyValidationCache = new Map(); const _keyMetadataCache = new Map>(); +const _lastUsedUpdateCache = new Map(); const CACHE_TTL = 60 * 1000; // 1 minute TTL +const LAST_USED_UPDATE_TTL = 5 * 60 * 1000; const MAX_CACHE_SIZE = 1000; // Compiled regex cache for wildcard patterns const _regexCache = new Map(); +const API_KEY_COLUMN_FALLBACKS = [ + { name: "allowed_models", definition: "allowed_models TEXT" }, + { name: "no_log", definition: "no_log INTEGER NOT NULL DEFAULT 0" }, + { name: "allowed_connections", definition: "allowed_connections TEXT" }, + { name: "auto_resolve", definition: "auto_resolve INTEGER NOT NULL DEFAULT 0" }, + { name: "is_active", definition: "is_active INTEGER NOT NULL DEFAULT 1" }, + { name: "access_schedule", definition: "access_schedule TEXT" }, + { name: "max_requests_per_day", definition: "max_requests_per_day INTEGER" }, + { name: "max_requests_per_minute", definition: "max_requests_per_minute INTEGER" }, + { name: "max_sessions", definition: "max_sessions INTEGER NOT NULL DEFAULT 0" }, + { name: "revoked_at", definition: "revoked_at TEXT" }, + { name: "expires_at", definition: "expires_at TEXT" }, + { name: "last_used_at", definition: "last_used_at TEXT" }, + { name: "key_prefix", definition: "key_prefix TEXT" }, + { name: "ip_allowlist", definition: "ip_allowlist TEXT" }, + { name: "scopes", definition: "scopes TEXT" }, +] as const; + // Cache for model permission checks const _modelPermissionCache = new Map(); @@ -121,12 +146,31 @@ function invalidateCaches() { _keyValidationCache.clear(); _keyMetadataCache.clear(); _modelPermissionCache.clear(); + _lastUsedUpdateCache.clear(); } function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" ? (value as JsonRecord) : {}; } +function isConfiguredEnvApiKey(key: string): boolean { + const envKey = process.env.OMNIROUTE_API_KEY || process.env.ROUTER_API_KEY; + return Boolean(envKey && key === envKey); +} + +function markApiKeyUsed(db: ApiKeysDbLike, id: unknown, now: number): void { + if (typeof id !== "string" || id.trim() === "") return; + + const lastUpdate = _lastUsedUpdateCache.get(id); + if (lastUpdate && now - lastUpdate < LAST_USED_UPDATE_TTL) return; + + db.prepare("UPDATE api_keys SET last_used_at = @lastUsedAt WHERE id = @id").run({ + id, + lastUsedAt: new Date(now).toISOString(), + }); + _lastUsedUpdateCache.set(id, now); +} + /** * LRU eviction for cache */ @@ -160,6 +204,16 @@ function getWildcardRegex(pattern: string): RegExp { return regex; } +function ensureApiKeyColumn( + db: ApiKeysDbLike, + columnNames: Set, + column: (typeof API_KEY_COLUMN_FALLBACKS)[number] +): void { + if (columnNames.has(column.name)) return; + db.exec(`ALTER TABLE api_keys ADD COLUMN ${column.definition}`); + console.log(`[DB] Added api_keys.${column.name} column`); +} + // Ensure api_keys extension columns exist (memoized) function ensureApiKeysColumns(db: ApiKeysDbLike) { if (_schemaChecked) return; @@ -167,42 +221,8 @@ function ensureApiKeysColumns(db: ApiKeysDbLike) { try { const columns = db.prepare("PRAGMA table_info(api_keys)").all(); const columnNames = new Set(columns.map((column) => String(column.name ?? ""))); - if (!columnNames.has("allowed_models")) { - db.exec("ALTER TABLE api_keys ADD COLUMN allowed_models TEXT"); - console.log("[DB] Added api_keys.allowed_models column"); - } - if (!columnNames.has("no_log")) { - db.exec("ALTER TABLE api_keys ADD COLUMN no_log INTEGER NOT NULL DEFAULT 0"); - console.log("[DB] Added api_keys.no_log column"); - } - if (!columnNames.has("allowed_connections")) { - db.exec("ALTER TABLE api_keys ADD COLUMN allowed_connections TEXT"); - console.log("[DB] Added api_keys.allowed_connections column"); - } - if (!columnNames.has("auto_resolve")) { - db.exec("ALTER TABLE api_keys ADD COLUMN auto_resolve INTEGER NOT NULL DEFAULT 0"); - console.log("[DB] Added api_keys.auto_resolve column"); - } - if (!columnNames.has("is_active")) { - db.exec("ALTER TABLE api_keys ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1"); - console.log("[DB] Added api_keys.is_active column"); - } - if (!columnNames.has("access_schedule")) { - db.exec("ALTER TABLE api_keys ADD COLUMN access_schedule TEXT"); - console.log("[DB] Added api_keys.access_schedule column"); - } - if (!columnNames.has("max_requests_per_day")) { - db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_day INTEGER"); - console.log("[DB] Added api_keys.max_requests_per_day column"); - } - if (!columnNames.has("max_requests_per_minute")) { - db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_minute INTEGER"); - console.log("[DB] Added api_keys.max_requests_per_minute column"); - } - // T08: max concurrent sticky sessions per key (0 = unlimited) - if (!columnNames.has("max_sessions")) { - db.exec("ALTER TABLE api_keys ADD COLUMN max_sessions INTEGER NOT NULL DEFAULT 0"); - console.log("[DB] Added api_keys.max_sessions column"); + for (const column of API_KEY_COLUMN_FALLBACKS) { + ensureApiKeyColumn(db, columnNames, column); } _schemaChecked = true; } catch (error) { @@ -227,12 +247,14 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements { ) { _stmtGetAllKeys = db.prepare("SELECT * FROM api_keys ORDER BY created_at"); _stmtGetKeyById = db.prepare("SELECT * FROM api_keys WHERE id = ?"); - _stmtValidateKey = db.prepare("SELECT 1 FROM api_keys WHERE key = ?"); + _stmtValidateKey = db.prepare( + "SELECT id, expires_at, revoked_at, is_active FROM api_keys WHERE key = ?" + ); _stmtGetKeyMetadata = db.prepare( - "SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, max_sessions FROM api_keys WHERE key = ?" + "SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, max_sessions, revoked_at, expires_at, ip_allowlist, scopes FROM api_keys WHERE key = ?" ); _stmtInsertKey = db.prepare( - "INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)" + "INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ); _stmtDeleteKey = db.prepare("DELETE FROM api_keys WHERE id = ?"); } @@ -373,6 +395,24 @@ function parseAllowedConnections(value: unknown): string[] { } } +function parseStringList(value: unknown): string[] { + if (!value || typeof value !== "string" || value.trim() === "") return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) + ? parsed.filter((entry): entry is string => typeof entry === "string") + : []; + } catch { + return []; + } +} + +function parseNullableTimestamp(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed === "" ? null : trimmed; +} + export async function createApiKey(name: string, machineId: string) { if (!machineId) { throw new Error("machineId is required"); @@ -403,7 +443,8 @@ export async function createApiKey(name: string, machineId: string) { apiKey.machineId, "[]", 0, - apiKey.createdAt + apiKey.createdAt, + apiKey.key.slice(0, 12) ); setNoLog(apiKey.id, false); @@ -566,15 +607,66 @@ export async function deleteApiKey(id: string) { } /** - * Validate API key with caching for performance - * Cached valid keys reduce DB hits on every request + * Revoke an API key by id. Logical, not destructive: the row stays so it can + * be audited, but validateApiKey() rejects it immediately after caches expire + * (or sooner because invalidateCaches() runs here). + */ +export async function revokeApiKey(id: string): Promise { + const db = getDbInstance() as ApiKeysDbLike; + getPreparedStatements(db); + + const result = db + .prepare( + "UPDATE api_keys SET revoked_at = COALESCE(revoked_at, @ts), is_active = 0 WHERE id = @id" + ) + .run({ id, ts: new Date().toISOString() }); + + if ((result.changes ?? 0) === 0) return false; + + invalidateCaches(); + backupDbFile("pre-write"); + return true; +} + +/** + * Set or clear the expiry of an API key. Pass null to remove the expiry. + */ +export async function setApiKeyExpiry(id: string, expiresAt: string | null): Promise { + const db = getDbInstance() as ApiKeysDbLike; + getPreparedStatements(db); + + const result = db + .prepare("UPDATE api_keys SET expires_at = @expiresAt WHERE id = @id") + .run({ id, expiresAt }); + + if ((result.changes ?? 0) === 0) return false; + + invalidateCaches(); + backupDbFile("pre-write"); + return true; +} + +/** + * Validate API key with lifecycle gates and caching. + * + * A key is valid only when ALL of the following are true: + * - the row exists, + * - is_active = 1, + * - revoked_at IS NULL, + * - expires_at IS NULL OR expires_at > now. + * + * Cache TTL is short (CACHE_TTL) and the metadata cache is also invalidated + * by revokeApiKey/updateApiKeyPermissions/deleteApiKey, so a revoke takes + * effect within at most CACHE_TTL even without an explicit clear in the + * caller. */ export async function validateApiKey(key: string | null | undefined) { if (!key || typeof key !== "string") return false; + if (isConfiguredEnvApiKey(key)) return true; + const now = Date.now(); - // Check cache first const cached = _keyValidationCache.get(key); if (cached && now - cached.timestamp < CACHE_TTL) { return cached.valid; @@ -582,16 +674,27 @@ export async function validateApiKey(key: string | null | undefined) { const db = getDbInstance() as ApiKeysDbLike; const stmt = getPreparedStatements(db); - const row = stmt.validateKey.get(key); - const valid = !!row; + const row = stmt.validateKey.get(key) as JsonRecord | undefined; - // Only cache valid keys to prevent cache pollution - if (valid) { - evictIfNeeded(_keyValidationCache); - _keyValidationCache.set(key, { valid: true, timestamp: now }); + if (!row) return false; + + const isActive = parseIsActive(row.is_active ?? row.isActive); + if (!isActive) return false; + + const revokedAt = row.revoked_at ?? row.revokedAt; + if (typeof revokedAt === "string" && revokedAt.trim() !== "") return false; + + const expiresAt = row.expires_at ?? row.expiresAt; + if (typeof expiresAt === "string" && expiresAt.trim() !== "") { + const expiresMs = Date.parse(expiresAt); + if (Number.isFinite(expiresMs) && expiresMs <= now) return false; } - return valid; + evictIfNeeded(_keyValidationCache); + _keyValidationCache.set(key, { valid: true, timestamp: now }); + markApiKeyUsed(db, row.id, now); + + return true; } /** @@ -605,8 +708,7 @@ export async function getApiKeyMetadata( const now = Date.now(); // persistent env-var key support (persistent passthrough keys) (#1350) - const envKey = process.env.OMNIROUTE_API_KEY || process.env.ROUTER_API_KEY; - if (envKey && key === envKey) { + if (isConfiguredEnvApiKey(key)) { return { id: "env-key", name: "Environment Key", @@ -620,6 +722,10 @@ export async function getApiKeyMetadata( maxRequestsPerDay: null, maxRequestsPerMinute: null, maxSessions: 0, + revokedAt: null, + expiresAt: null, + ipAllowlist: [], + scopes: [], }; } @@ -662,6 +768,10 @@ export async function getApiKeyMetadata( maxRequestsPerMinute: typeof rawMaxRPM === "number" && rawMaxRPM > 0 ? rawMaxRPM : null, // T08: max concurrent sessions; 0 = unlimited (default & backward-compatible) maxSessions: typeof rawMaxSessions === "number" && rawMaxSessions > 0 ? rawMaxSessions : 0, + revokedAt: parseNullableTimestamp(record.revoked_at ?? (record as JsonRecord).revokedAt), + expiresAt: parseNullableTimestamp(record.expires_at ?? (record as JsonRecord).expiresAt), + ipAllowlist: parseStringList(record.ip_allowlist ?? (record as JsonRecord).ipAllowlist), + scopes: parseStringList((record as JsonRecord).scopes), }; if (!metadata.id) { @@ -766,6 +876,7 @@ function clearPreparedStatementCache() { */ export function clearApiKeyCaches() { invalidateCaches(); + _lastUsedUpdateCache.clear(); _modelPermissionCache.clear(); _regexCache.clear(); } diff --git a/src/lib/db/core.ts b/src/lib/db/core.ts index 96eaf8b209..4a0deee28d 100644 --- a/src/lib/db/core.ts +++ b/src/lib/db/core.ts @@ -1250,6 +1250,101 @@ export function getDbInstance(): SqliteDatabase { ); } runMigrations(db, { isNewDb }); + + // ── Post-migration safety guards ────────────────────────────────────────── + // The heuristic seeding above can mark migration versions as "applied" based + // on column detection, causing the migration runner to skip newer migrations + // whose tables/columns don't have heuristic detectors yet. + // These guards ensure critical schema elements exist regardless of migration + // state, fixing upgrade failures reported in #1648 and #1657. + + // Guard: combos.sort_order (migration 020) + if (hasTable(db, "combos") && !hasColumn(db, "combos", "sort_order")) { + try { + db.exec(` + ALTER TABLE combos ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; + WITH ordered_combos AS ( + SELECT id, ROW_NUMBER() OVER ( + ORDER BY created_at ASC, updated_at ASC, name COLLATE NOCASE ASC + ) AS next_sort_order + FROM combos + ) + UPDATE combos SET sort_order = ( + SELECT next_sort_order FROM ordered_combos + WHERE ordered_combos.id = combos.id + ); + `); + console.log("[DB] Post-migration guard: added missing combos.sort_order column (#1657)"); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes("duplicate column")) { + console.warn("[DB] Post-migration guard: combos.sort_order failed:", msg); + } + } + } + + // Guard: batches table (migration 028) + if (!hasTable(db, "batches")) { + try { + db.exec(` + CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + bytes INTEGER NOT NULL, + created_at INTEGER NOT NULL, + filename TEXT NOT NULL, + purpose TEXT NOT NULL, + content BLOB, + mime_type TEXT, + api_key_id TEXT, + deleted_at INTEGER, + status TEXT DEFAULT 'validating', + expires_at INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_files_api_key ON files(api_key_id); + + CREATE TABLE IF NOT EXISTS batches ( + id TEXT PRIMARY KEY, + endpoint TEXT NOT NULL, + completion_window TEXT NOT NULL, + status TEXT NOT NULL, + input_file_id TEXT NOT NULL, + output_file_id TEXT, + error_file_id TEXT, + created_at INTEGER NOT NULL, + in_progress_at INTEGER, + expires_at INTEGER, + finalizing_at INTEGER, + completed_at INTEGER, + failed_at INTEGER, + expired_at INTEGER, + cancelling_at INTEGER, + cancelled_at INTEGER, + request_counts_total INTEGER DEFAULT 0, + request_counts_completed INTEGER DEFAULT 0, + request_counts_failed INTEGER DEFAULT 0, + metadata TEXT, + api_key_id TEXT, + errors TEXT, + model TEXT, + usage TEXT, + output_expires_after_seconds INTEGER, + output_expires_after_anchor TEXT, + FOREIGN KEY(input_file_id) REFERENCES files(id), + FOREIGN KEY(output_file_id) REFERENCES files(id), + FOREIGN KEY(error_file_id) REFERENCES files(id) + ); + CREATE INDEX IF NOT EXISTS idx_batches_api_key ON batches(api_key_id); + CREATE INDEX IF NOT EXISTS idx_batches_status ON batches(status); + `); + console.log("[DB] Post-migration guard: created missing batches/files tables (#1648)"); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes("already exists")) { + console.warn("[DB] Post-migration guard: batches/files creation failed:", msg); + } + } + } + offloadLegacyCallLogDetails(db); // Auto-migrate from db.json if exists diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index 973f3f7429..18dddca847 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -156,6 +156,40 @@ function hasTable(db: Database.Database, tableName: string): boolean { return Boolean(row?.name); } +function hasColumn(db: Database.Database, tableName: string, columnName: string): boolean { + const columns = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name?: string }>; + return columns.some((column) => column.name === columnName); +} + +function ensureColumn( + db: Database.Database, + tableName: string, + columnName: string, + ddl: string +): void { + if (!hasColumn(db, tableName, columnName)) { + db.exec(ddl); + } +} + +function isApiKeyLifecycleMigration(migration: { version: string; name: string }): boolean { + return migration.version === "032"; +} + +function applyApiKeyLifecycleMigration(db: Database.Database): void { + ensureColumn(db, "api_keys", "revoked_at", "ALTER TABLE api_keys ADD COLUMN revoked_at TEXT"); + ensureColumn(db, "api_keys", "expires_at", "ALTER TABLE api_keys ADD COLUMN expires_at TEXT"); + ensureColumn(db, "api_keys", "last_used_at", "ALTER TABLE api_keys ADD COLUMN last_used_at TEXT"); + ensureColumn(db, "api_keys", "key_prefix", "ALTER TABLE api_keys ADD COLUMN key_prefix TEXT"); + ensureColumn(db, "api_keys", "ip_allowlist", "ALTER TABLE api_keys ADD COLUMN ip_allowlist TEXT"); + ensureColumn(db, "api_keys", "scopes", "ALTER TABLE api_keys ADD COLUMN scopes TEXT"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_api_keys_revoked_at ON api_keys(revoked_at); + CREATE INDEX IF NOT EXISTS idx_api_keys_expires_at ON api_keys(expires_at); + `); +} + function inferPhysicalSchemaBaseline(db: Database.Database): { version: string; description: string; @@ -419,10 +453,13 @@ export function runMigrations(db: Database.Database, options?: { isNewDb?: boole let count = 0; for (const migration of pending) { - const sql = fs.readFileSync(migration.path, "utf-8"); - const applyMigration = db.transaction(() => { - db.exec(sql); + if (isApiKeyLifecycleMigration(migration)) { + applyApiKeyLifecycleMigration(db); + } else { + const sql = fs.readFileSync(migration.path, "utf-8"); + db.exec(sql); + } db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run( migration.version, migration.name diff --git a/src/lib/db/migrations/032_apikey_lifecycle.sql b/src/lib/db/migrations/032_apikey_lifecycle.sql new file mode 100644 index 0000000000..112e5f66d4 --- /dev/null +++ b/src/lib/db/migrations/032_apikey_lifecycle.sql @@ -0,0 +1,24 @@ +-- Migration 032: API Key lifecycle hardening +-- +-- Phase 3 of the unified-authz plan. Adds explicit lifecycle and policy +-- columns to api_keys without touching the existing `key` column. Existing +-- plain-text keys remain valid; key hashing is a separate follow-up step +-- once revocation/expiry are wired through the validator and policy layer. +-- +-- New columns: +-- revoked_at ISO timestamp, NULL when not revoked. +-- expires_at ISO timestamp, NULL = no expiry. +-- last_used_at ISO timestamp updated by validateApiKey on success. +-- key_prefix first ~12 visible chars of the key, for safe display. +-- ip_allowlist JSON array of CIDRs/IPs; NULL or [] = allow any. +-- scopes JSON array of scope strings; NULL or [] = default scopes. + +ALTER TABLE api_keys ADD COLUMN revoked_at TEXT; +ALTER TABLE api_keys ADD COLUMN expires_at TEXT; +ALTER TABLE api_keys ADD COLUMN last_used_at TEXT; +ALTER TABLE api_keys ADD COLUMN key_prefix TEXT; +ALTER TABLE api_keys ADD COLUMN ip_allowlist TEXT; +ALTER TABLE api_keys ADD COLUMN scopes TEXT; + +CREATE INDEX IF NOT EXISTS idx_api_keys_revoked_at ON api_keys(revoked_at); +CREATE INDEX IF NOT EXISTS idx_api_keys_expires_at ON api_keys(expires_at); diff --git a/src/lib/db/migrations/032_create_reasoning_cache.sql b/src/lib/db/migrations/033_create_reasoning_cache.sql similarity index 100% rename from src/lib/db/migrations/032_create_reasoning_cache.sql rename to src/lib/db/migrations/033_create_reasoning_cache.sql diff --git a/src/lib/env/runtimeEnv.ts b/src/lib/env/runtimeEnv.ts index c836ddd86d..7b0fe12f52 100644 --- a/src/lib/env/runtimeEnv.ts +++ b/src/lib/env/runtimeEnv.ts @@ -61,7 +61,6 @@ export const webRuntimeEnvSchema = z.object({ API_KEY_SECRET: optionalTrimmedString, INITIAL_PASSWORD: optionalTrimmedString, AUTH_COOKIE_SECURE: optionalBooleanEnv, - REQUIRE_API_KEY: optionalBooleanEnv, PRICING_SYNC_ENABLED: optionalBooleanEnv, OMNIROUTE_DISABLE_BACKGROUND_SERVICES: optionalBooleanEnv, CLOUD_URL: optionalHttpUrl, diff --git a/src/lib/logEnv.ts b/src/lib/logEnv.ts index 897b57bbd4..f10d318034 100644 --- a/src/lib/logEnv.ts +++ b/src/lib/logEnv.ts @@ -6,6 +6,7 @@ const DEFAULT_APP_LOG_MAX_SIZE = 50 * 1024 * 1024; const DEFAULT_APP_LOG_MAX_FILES = 20; const DEFAULT_CALL_LOG_MAX_ENTRIES = 10000; const DEFAULT_CALL_LOGS_TABLE_MAX_ROWS = 100000; +const DEFAULT_CALL_LOG_PIPELINE_MAX_SIZE_KB = 512; const DEFAULT_PROXY_LOGS_TABLE_MAX_ROWS = 100000; const DEFAULT_APP_LOG_PATH = path.join(process.cwd(), "logs", "application", "app.log"); @@ -15,6 +16,14 @@ function parsePositiveInt(value: string | undefined, fallback: number): number { return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } +function parseBoolean(value: string | undefined, fallback: boolean): boolean { + if (!value) return fallback; + const normalized = value.trim().toLowerCase(); + if (["1", "true", "yes", "on"].includes(normalized)) return true; + if (["0", "false", "no", "off"].includes(normalized)) return false; + return fallback; +} + export function parseFileSize(raw: string | undefined): number { if (!raw) return DEFAULT_APP_LOG_MAX_SIZE; const match = raw.match(/^(\d+)\s*(k|m|g|kb|mb|gb)?$/i); @@ -68,6 +77,19 @@ export function getCallLogsTableMaxRows(): number { return parsePositiveInt(process.env.CALL_LOGS_TABLE_MAX_ROWS, DEFAULT_CALL_LOGS_TABLE_MAX_ROWS); } +export function getCallLogPipelineCaptureStreamChunks(): boolean { + return parseBoolean(process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS, true); +} + +export function getCallLogPipelineMaxSizeBytes(): number { + return ( + parsePositiveInt( + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB, + DEFAULT_CALL_LOG_PIPELINE_MAX_SIZE_KB + ) * 1024 + ); +} + export function getProxyLogsTableMaxRows(): number { return parsePositiveInt(process.env.PROXY_LOGS_TABLE_MAX_ROWS, DEFAULT_PROXY_LOGS_TABLE_MAX_ROWS); } diff --git a/src/lib/oauth/services/gemini.ts b/src/lib/oauth/services/gemini.ts index 373f588dda..34cbe62127 100644 --- a/src/lib/oauth/services/gemini.ts +++ b/src/lib/oauth/services/gemini.ts @@ -1,5 +1,9 @@ import crypto from "crypto"; import open from "open"; +import { + ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT, + ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT, +} from "@omniroute/open-sse/services/antigravityHeaders.ts"; import { GEMINI_CONFIG } from "../constants/oauth"; import { getServerCredentials } from "../config/index"; import { startLocalServer } from "../utils/server"; @@ -69,8 +73,8 @@ export class GeminiCLIService { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "User-Agent": "google-api-nodejs-client/9.15.1", - "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", + "User-Agent": ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT, + "X-Goog-Api-Client": ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT, "Client-Metadata": JSON.stringify({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 7d766a120e..b3003c8535 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -2195,24 +2195,33 @@ const SEARCH_VALIDATOR_CONFIGS: Record< headers: { Accept: "application/json", "X-API-Key": apiKey }, }, }), - "searxng-search": (_apiKey, providerSpecificData = {}) => { + "searxng-search": (apiKey, providerSpecificData = {}) => { const baseUrl = typeof providerSpecificData?.baseUrl === "string" && providerSpecificData.baseUrl.trim() ? providerSpecificData.baseUrl.trim().replace(/\/+$/, "") : "http://localhost:8888/search"; const searchUrl = baseUrl.endsWith("/search") ? baseUrl : `${baseUrl}/search`; + const headers: Record = { Accept: "application/json" }; + if (apiKey) { + headers["Authorization"] = `Bearer ${apiKey}`; + } return { url: `${searchUrl}?q=test&format=json`, init: { method: "GET", - headers: { Accept: "application/json" }, + headers, }, }; }, }; -const META_AI_SEND_MESSAGE_DOC_ID = "078dfdff6fb0d420d8011b49073e6886"; -const META_AI_FRIENDLY_NAME = "useAbraSendMessageMutation"; +// See open-sse/executors/muse-spark-web.ts for the rationale: Meta migrated +// from the "Abra" mutation (doc_id 078dfdff…, type RewriteOptionsInput now +// missing from schema) to the "Ecto" subscription. POST graphql still +// streams the response; only the persisted-query identifier and operation +// shape changed. +const META_AI_SEND_MESSAGE_DOC_ID = "29ae946c82d1f301196c6ca2226400b5"; +const META_AI_FRIENDLY_NAME = "useEctoSendMessageSubscription"; const META_AI_REQUEST_ANALYTICS_TAGS = "graphservice"; const META_AI_ASBD_ID = "129477"; const META_AI_USER_AGENT = @@ -2311,7 +2320,9 @@ function buildMetaAiValidationBody() { promptType: null, qplJoinId: null, requestedToolCall: null, - rewriteOptions: null, + // See muse-spark-web executor: RewriteOptionsInput was removed from + // Meta's schema; sending `rewriteOptions` (even null) breaks the + // persisted-query validation. Omit the field. turnId: crypto.randomUUID(), userAgent: META_AI_USER_AGENT, userEventId: generateMetaAiEventId(conversationId), @@ -2352,14 +2363,14 @@ async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: an Origin: "https://grok.com", Pragma: "no-cache", Referer: "https://grok.com/", - "Sec-Ch-Ua": '"Google Chrome";v="136", "Chromium";v="136", "Not(A:Brand";v="24"', + "Sec-Ch-Ua": '"Google Chrome";v="147", "Chromium";v="147", "Not(A:Brand";v="24"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"macOS"', "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", "x-statsig-id": btoa(statsigMsg), "x-xai-request-id": crypto.randomUUID(), traceparent: `00-${traceId}-${spanId}-00`, @@ -2368,8 +2379,7 @@ async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: an ), body: JSON.stringify({ temporary: true, - modelName: "grok-4-1-thinking-1129", - modelMode: "MODEL_MODE_FAST", + modeId: "auto", message: "test", fileAttachments: [], imageAttachments: [], @@ -2392,6 +2402,15 @@ async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: an }), }); + if (response.ok) { + return { valid: true, error: null }; + } + + let errorDetail = ""; + try { + errorDetail = (await response.text()).slice(0, 240); + } catch {} + if (response.status === 401 || response.status === 403) { return { valid: false, @@ -2399,16 +2418,18 @@ async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: an }; } - // 200 or non-auth 4xx (e.g. 400, 429) means the cookie is accepted - if (response.ok || (response.status >= 400 && response.status < 500)) { - return { valid: true, error: null }; + if (response.status === 429) { + return { valid: false, error: "Grok rate limited during validation (429)" }; } if (response.status >= 500) { return { valid: false, error: `Grok unavailable (${response.status})` }; } - return { valid: false, error: `Validation failed: ${response.status}` }; + return { + valid: false, + error: `Grok validation failed (${response.status})${errorDetail ? `: ${errorDetail}` : ""}`, + }; } catch (error: any) { return toValidationErrorResult(error); } @@ -2504,7 +2525,7 @@ async function validateBlackboxWebProvider({ apiKey, providerSpecificData = {} } Origin: "https://app.blackbox.ai", Referer: "https://app.blackbox.ai/", "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", }, providerSpecificData ); @@ -2534,7 +2555,7 @@ async function validateBlackboxWebProvider({ apiKey, providerSpecificData = {} } Origin: "https://app.blackbox.ai", Referer: "https://app.blackbox.ai/", "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", }, providerSpecificData ); @@ -2608,7 +2629,7 @@ async function validateBlackboxWebProvider({ apiKey, providerSpecificData = {} } async function validateMuseSparkWebProvider({ apiKey, providerSpecificData = {} }: any) { try { - const cookieHeader = normalizeSessionCookieHeader(apiKey, "abra_sess"); + const cookieHeader = normalizeSessionCookieHeader(apiKey, "ecto_1_sess"); const response = await validationWrite("https://www.meta.ai/api/graphql", { method: "POST", headers: applyCustomUserAgent( diff --git a/src/lib/usage/callLogArtifacts.ts b/src/lib/usage/callLogArtifacts.ts index b48df4a20a..6ad1f26dcb 100644 --- a/src/lib/usage/callLogArtifacts.ts +++ b/src/lib/usage/callLogArtifacts.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import type { RequestPipelinePayloads } from "@omniroute/open-sse/utils/requestLogger.ts"; import { resolveDataDir } from "../dataPaths"; +import { getCallLogPipelineMaxSizeBytes } from "../logEnv"; const isCloud = typeof globalThis.caches === "object" && globalThis.caches !== null; const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build"; @@ -10,6 +11,11 @@ const DATA_DIR = resolveDataDir({ isCloud }); export const CALL_LOGS_DIR = isCloud ? null : path.join(DATA_DIR, "call_logs"); export const MAX_CALL_LOG_ARTIFACT_BYTES = 512 * 1024; +const SIZE_LIMIT_EXCEEDED_REASON = "call_log_artifact_size_limit_exceeded"; +const OMITTED_FOR_SIZE_LIMIT = "[omitted: call log artifact size limit exceeded]"; +const STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT = + "[stream chunks omitted: call log artifact size limit exceeded]"; + export type CallLogDetailState = "none" | "ready" | "missing" | "corrupt" | "legacy-inline"; export type CallLogArtifact = { @@ -83,13 +89,13 @@ function truncateArtifactForStorage(artifact: CallLogArtifact): CallLogArtifact ...pipeline, streamChunks: { provider: pipeline.streamChunks.provider?.length - ? ["[stream chunks omitted: call log artifact size limit exceeded]"] + ? [STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT] : undefined, openai: pipeline.streamChunks.openai?.length - ? ["[stream chunks omitted: call log artifact size limit exceeded]"] + ? [STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT] : undefined, client: pipeline.streamChunks.client?.length - ? ["[stream chunks omitted: call log artifact size limit exceeded]"] + ? [STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT] : undefined, }, }, @@ -104,59 +110,77 @@ function omitOversizedPipeline(artifact: CallLogArtifact): CallLogArtifact { pipeline: { error: { _omniroute_truncated: true, - reason: "call_log_artifact_size_limit_exceeded", + reason: SIZE_LIMIT_EXCEEDED_REASON, }, }, }; } +function getArtifactMaxBytes(artifact: CallLogArtifact): number { + return artifact.pipeline ? getCallLogPipelineMaxSizeBytes() : MAX_CALL_LOG_ARTIFACT_BYTES; +} + +function buildMinimalArtifactForSizeLimit(artifact: CallLogArtifact) { + return { + schemaVersion: artifact.schemaVersion, + summary: artifact.summary, + requestBody: OMITTED_FOR_SIZE_LIMIT, + responseBody: OMITTED_FOR_SIZE_LIMIT, + error: artifact.error ? OMITTED_FOR_SIZE_LIMIT : null, + pipeline: { + error: { + _omniroute_truncated: true, + reason: SIZE_LIMIT_EXCEEDED_REASON, + }, + }, + }; +} + +function serializeFinalSizeLimitFallback(artifact: CallLogArtifact, maxBytes: number): string { + const withSummary = JSON.stringify(buildMinimalArtifactForSizeLimit(artifact)); + if (Buffer.byteLength(withSummary) <= maxBytes) { + return withSummary; + } + + return JSON.stringify({ + schemaVersion: artifact.schemaVersion, + _omniroute_truncated: true, + reason: SIZE_LIMIT_EXCEEDED_REASON, + }); +} + function serializeArtifactForStorage(artifact: CallLogArtifact): string { + const maxBytes = getArtifactMaxBytes(artifact); const serialized = JSON.stringify(artifact, null, 2); - if (Buffer.byteLength(serialized) <= MAX_CALL_LOG_ARTIFACT_BYTES) { + if (Buffer.byteLength(serialized) <= maxBytes) { return serialized; } const truncated = JSON.stringify(truncateArtifactForStorage(artifact), null, 2); - if (Buffer.byteLength(truncated) <= MAX_CALL_LOG_ARTIFACT_BYTES) { + if (Buffer.byteLength(truncated) <= maxBytes) { return truncated; } const withoutPipeline = JSON.stringify(omitOversizedPipeline(artifact), null, 2); - if (Buffer.byteLength(withoutPipeline) <= MAX_CALL_LOG_ARTIFACT_BYTES) { + if (Buffer.byteLength(withoutPipeline) <= maxBytes) { return withoutPipeline; } const minimal = JSON.stringify( { ...omitOversizedPipeline(artifact), - requestBody: "[omitted: call log artifact size limit exceeded]", - responseBody: "[omitted: call log artifact size limit exceeded]", - error: artifact.error ? "[omitted: call log artifact size limit exceeded]" : null, + requestBody: OMITTED_FOR_SIZE_LIMIT, + responseBody: OMITTED_FOR_SIZE_LIMIT, + error: artifact.error ? OMITTED_FOR_SIZE_LIMIT : null, }, null, 2 ); - if (Buffer.byteLength(minimal) <= MAX_CALL_LOG_ARTIFACT_BYTES) { + if (Buffer.byteLength(minimal) <= maxBytes) { return minimal; } - return JSON.stringify( - { - schemaVersion: artifact.schemaVersion, - summary: artifact.summary, - requestBody: "[omitted: call log artifact size limit exceeded]", - responseBody: "[omitted: call log artifact size limit exceeded]", - error: artifact.error ? "[omitted: call log artifact size limit exceeded]" : null, - pipeline: { - error: { - _omniroute_truncated: true, - reason: "call_log_artifact_size_limit_exceeded", - }, - }, - }, - null, - 2 - ); + return serializeFinalSizeLimitFallback(artifact, maxBytes); } export function writeCallArtifact( diff --git a/src/lib/usage/usageHistory.ts b/src/lib/usage/usageHistory.ts index c9f767d136..97f1a83bd4 100644 --- a/src/lib/usage/usageHistory.ts +++ b/src/lib/usage/usageHistory.ts @@ -239,6 +239,19 @@ export function getPendingRequests() { return pendingRequests; } +/** + * Clear all pending request counts. + * Used for admin reset when counts leak due to uncaught timeouts or process-level errors. + */ +export function clearPendingRequests() { + pendingRequests.byModel = Object.create(null) as Record; + pendingRequests.byAccount = Object.create(null) as Record>; + pendingRequests.details = Object.create(null) as Record< + string, + Record + >; +} + // ──────────────── getUsageDb Shim (backward compat) ──────────────── /** diff --git a/src/proxy.ts b/src/proxy.ts index ffc4f92b7c..cfce41dd47 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,213 +1,22 @@ -import { NextResponse } from "next/server"; -import { jwtVerify, SignJWT } from "jose"; -import { generateRequestId } from "./shared/utils/requestId"; -import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard"; -import { isDraining } from "./lib/gracefulShutdown"; -import { isPublicApiRoute } from "./shared/constants/publicApiRoutes"; +import type { NextRequest } from "next/server"; +import { runAuthzPipeline } from "./server/authz/pipeline"; -const E2E_MODE = process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE === "1"; - -let apiAuthModulePromise: Promise | null = null; -let settingsModulePromise: Promise | null = null; -let modelSyncModulePromise: Promise | null = - null; - -function getJwtSecret(): Uint8Array { - return new TextEncoder().encode(process.env.JWT_SECRET || ""); -} - -async function getApiAuthModule() { - if (!apiAuthModulePromise) { - apiAuthModulePromise = import("./shared/utils/apiAuth"); - } - return apiAuthModulePromise; -} - -async function getSettingsModule() { - if (!settingsModulePromise) { - settingsModulePromise = import("./lib/db/settings"); - } - return settingsModulePromise; -} - -async function getModelSyncModule() { - if (!modelSyncModulePromise) { - modelSyncModulePromise = import("./shared/services/modelSyncScheduler"); - } - return modelSyncModulePromise; -} - -export async function proxy(request: any) { - const { pathname } = request.nextUrl; - - // Pipeline: Add request ID header for end-to-end tracing - const requestId = generateRequestId(); - const response = NextResponse.next(); - response.headers.set("X-Request-Id", requestId); - - // ──────────────── Pre-flight: Reject during shutdown drain ──────────────── - if (isDraining() && pathname.startsWith("/api/")) { - return NextResponse.json( - { - error: { - code: "SERVICE_UNAVAILABLE", - message: "Server is shutting down", - correlation_id: requestId, - }, - }, - { status: 503 } - ); - } - - // ──────────────── Pre-flight: Reject oversized bodies ──────────────── - if (pathname.startsWith("/api/") && request.method !== "GET" && request.method !== "OPTIONS") { - const bodySizeRejection = checkBodySize(request, getBodySizeLimit(pathname)); - if (bodySizeRejection) return bodySizeRejection; - } - - if (E2E_MODE) { - if (pathname.startsWith("/dashboard")) { - return response; - } - if (pathname.startsWith("/api/") && !pathname.startsWith("/api/v1/")) { - return response; - } - } - - // ──────────────── Protect Management API Routes ──────────────── - if (pathname.startsWith("/api/") && !pathname.startsWith("/api/v1/")) { - // Allow public routes (login, logout, health, etc.) - if (isPublicApiRoute(pathname, request.method)) { - return response; - } - - // Allow the model auto-sync scheduler to reach only its internal provider routes. - const { isModelSyncInternalRequest } = await getModelSyncModule(); - if ( - isModelSyncInternalRequest(request) && - /^\/api\/providers\/[^/]+\/(sync-models|models)$/.test(pathname) - ) { - return response; - } - - // Check if auth is required at all (respects requireLogin setting) - const { isAuthRequired, verifyAuth } = await getApiAuthModule(); - const authRequired = await isAuthRequired(); - if (!authRequired) { - return response; - } - - // Verify authentication (JWT cookie or Bearer API key) - const authError = await verifyAuth(request); - if (authError) { - const status = authError === "Invalid management token" ? 403 : 401; - return NextResponse.json( - { - error: { - code: "AUTH_001", - message: authError, - correlation_id: requestId, - }, - }, - { status } - ); - } - } - - // ──────────────── Protect Dashboard Routes ──────────────── - if (pathname.startsWith("/dashboard")) { - // Always allow onboarding — it has its own setupComplete guard - if (pathname.startsWith("/dashboard/onboarding")) { - return response; - } - - try { - // Direct import — no HTTP self-fetch overhead - const { getSettings } = await getSettingsModule(); - const settings = await getSettings(); - // Skip auth if login is not required - if (settings.requireLogin === false) { - return response; - } - // Skip auth ONLY for fresh installs (before onboarding) where no password exists yet. - // Once setupComplete is true, always require auth — prevents bypass if password row is lost (#151) - if (!settings.setupComplete && !settings.password && !process.env.INITIAL_PASSWORD) { - return response; - } - } catch (err) { - // FASE-01: Log settings fetch errors instead of silencing them - console.error("[Middleware] settings_error: Settings read failed:", err.message, { - path: pathname, - requestId, - }); - // On error, require login (fall through to token check) - } - - const token = request.cookies.get("auth_token")?.value; - - if (token) { - try { - const { payload } = await jwtVerify(token, getJwtSecret()); - - // Auto-refresh: if token expires within 7 days, issue a fresh 30-day token - const exp = payload.exp as number; - const now = Math.floor(Date.now() / 1000); - const REFRESH_WINDOW = 7 * 24 * 60 * 60; // 7 days in seconds - if (exp && exp - now < REFRESH_WINDOW) { - try { - const freshToken = await new SignJWT({ authenticated: true }) - .setProtectedHeader({ alg: "HS256" }) - .setExpirationTime("30d") - .sign(getJwtSecret()); - - // Detect secure context - const fwdProto = (request.headers.get("x-forwarded-proto") || "") - .split(",")[0] - .trim() - .toLowerCase(); - const isHttps = fwdProto === "https" || request.nextUrl?.protocol === "https:"; - const useSecure = process.env.AUTH_COOKIE_SECURE === "true" || isHttps; - - response.cookies.set("auth_token", freshToken, { - httpOnly: true, - secure: useSecure, - sameSite: "lax", - path: "/", - }); - console.log( - `[Middleware] JWT auto-refreshed for ${pathname} (was expiring in ${Math.round((exp - now) / 3600)}h)` - ); - } catch (refreshErr) { - // Refresh failed — continue with existing valid token - console.error("[Middleware] JWT auto-refresh failed:", refreshErr.message); - } - } - - return response; - } catch (err) { - // FASE-01: Log auth errors instead of silently redirecting - console.error("[Middleware] auth_error: JWT verification failed:", err.message, { - path: pathname, - tokenPresent: true, - requestId, - }); - const redirectResponse = NextResponse.redirect(new URL("/login", request.url)); - redirectResponse.cookies.delete("auth_token"); - return redirectResponse; - } - } - - return NextResponse.redirect(new URL("/login", request.url)); - } - - // Redirect / to /dashboard if logged in, or /dashboard if it's the root - if (pathname === "/") { - return NextResponse.redirect(new URL("/dashboard", request.url)); - } - - return response; +export async function proxy(request: NextRequest) { + return runAuthzPipeline(request, { enforce: true }); } export const config = { - matcher: ["/", "/dashboard/:path*", "/api/:path*"], + matcher: [ + "/", + "/dashboard/:path*", + "/api/:path*", + "/v1/:path*", + "/v1", + "/chat/:path*", + "/responses/:path*", + "/responses", + "/codex/:path*", + "/codex", + "/models", + ], }; diff --git a/src/server/auth/loginGuard.ts b/src/server/auth/loginGuard.ts new file mode 100644 index 0000000000..392757f647 --- /dev/null +++ b/src/server/auth/loginGuard.ts @@ -0,0 +1,106 @@ +/** + * Login brute-force guard. + * + * Tracks failed `/api/auth/login` attempts per client IP in process memory + * and returns lockout decisions. Single-process scope is intentional — this + * is a defense-in-depth check that pairs with Cloudflare/reverse-proxy rate + * limiting, not a substitute for it. + * + * Tunables: + * - failure threshold: 5 within `WINDOW_MS` + * - lockout duration: `LOCKOUT_MS` + * - sliding window: `WINDOW_MS` + * + * The guard is a no-op when `enabled` is false; the caller decides based on + * the `bruteForceProtection` setting (default true). + */ + +const WINDOW_MS = 15 * 60 * 1000; +const LOCKOUT_MS = 15 * 60 * 1000; +const FAILURE_THRESHOLD = 5; + +interface AttemptState { + count: number; + firstAttemptAt: number; + lockedUntil: number | null; +} + +const attempts: Map = new Map(); + +export interface GuardDecision { + allowed: boolean; + retryAfterSeconds?: number; +} + +function nowMs(): number { + return Date.now(); +} + +function clientKey(rawIp: string | null | undefined): string { + const ip = (rawIp || "").trim(); + return ip || "__unknown__"; +} + +export function checkLoginGuard( + rawIp: string | null | undefined, + options: { enabled: boolean } +): GuardDecision { + if (!options.enabled) return { allowed: true }; + const state = attempts.get(clientKey(rawIp)); + if (!state) return { allowed: true }; + const now = nowMs(); + if (state.lockedUntil && state.lockedUntil > now) { + return { + allowed: false, + retryAfterSeconds: Math.ceil((state.lockedUntil - now) / 1000), + }; + } + return { allowed: true }; +} + +export function recordLoginFailure( + rawIp: string | null | undefined, + options: { enabled: boolean } +): GuardDecision { + if (!options.enabled) return { allowed: true }; + const key = clientKey(rawIp); + const now = nowMs(); + const existing = attempts.get(key); + + if (!existing || now - existing.firstAttemptAt > WINDOW_MS) { + attempts.set(key, { count: 1, firstAttemptAt: now, lockedUntil: null }); + return { allowed: true }; + } + + const nextCount = existing.count + 1; + if (nextCount >= FAILURE_THRESHOLD) { + const lockedUntil = now + LOCKOUT_MS; + attempts.set(key, { + count: nextCount, + firstAttemptAt: existing.firstAttemptAt, + lockedUntil, + }); + return { allowed: false, retryAfterSeconds: Math.ceil(LOCKOUT_MS / 1000) }; + } + + attempts.set(key, { + count: nextCount, + firstAttemptAt: existing.firstAttemptAt, + lockedUntil: null, + }); + return { allowed: true }; +} + +export function clearLoginAttempts(rawIp: string | null | undefined): void { + attempts.delete(clientKey(rawIp)); +} + +export function resetLoginGuardForTests(): void { + attempts.clear(); +} + +export const LOGIN_GUARD_TUNABLES = Object.freeze({ + WINDOW_MS, + LOCKOUT_MS, + FAILURE_THRESHOLD, +}); diff --git a/src/server/authz/assertAuth.ts b/src/server/authz/assertAuth.ts new file mode 100644 index 0000000000..921b029eaa --- /dev/null +++ b/src/server/authz/assertAuth.ts @@ -0,0 +1,88 @@ +import { + AUTHZ_HEADER_AUTH_ID, + AUTHZ_HEADER_AUTH_KIND, + AUTHZ_HEADER_AUTH_LABEL, + AUTHZ_HEADER_AUTH_SCOPES, + AUTHZ_HEADER_ROUTE_CLASS, +} from "./headers"; +import type { AuthSubject, RouteClass } from "./types"; + +export class AuthzAssertionError extends Error { + readonly status: number; + readonly code: string; + + constructor(code: string, message: string, status = 500) { + super(message); + this.name = "AuthzAssertionError"; + this.code = code; + this.status = status; + } +} + +type HeaderSource = Headers | { get(name: string): string | null }; + +function readHeader(source: HeaderSource, name: string): string | null { + return source.get(name) ?? null; +} + +function isHeaderSource(value: unknown): value is HeaderSource { + if (value instanceof Headers) return true; + if (typeof value !== "object" || value === null) return false; + return typeof (value as { get?: unknown }).get === "function"; +} + +export function readSubjectFromHeaders(headers: HeaderSource): AuthSubject { + const kind = (readHeader(headers, AUTHZ_HEADER_AUTH_KIND) ?? "anonymous") as AuthSubject["kind"]; + const id = readHeader(headers, AUTHZ_HEADER_AUTH_ID) ?? "anonymous"; + const label = readHeader(headers, AUTHZ_HEADER_AUTH_LABEL) ?? undefined; + const rawScopes = readHeader(headers, AUTHZ_HEADER_AUTH_SCOPES); + const scopes = rawScopes + ? rawScopes + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : []; + return { kind, id, label, scopes }; +} + +export function readRouteClassFromHeaders(headers: HeaderSource): RouteClass | null { + const raw = readHeader(headers, AUTHZ_HEADER_ROUTE_CLASS); + if (raw === "PUBLIC" || raw === "CLIENT_API" || raw === "MANAGEMENT") return raw; + return null; +} + +export function assertAuth( + request: Request | { headers: HeaderSource }, + expected: RouteClass +): AuthSubject { + const headers = request.headers; + + if (!isHeaderSource(headers)) { + throw new AuthzAssertionError("AUTHZ_INVALID_REQUEST", "Request headers are unavailable", 500); + } + + const actualClass = readRouteClassFromHeaders(headers); + if (!actualClass) { + throw new AuthzAssertionError( + "AUTHZ_NOT_INITIALIZED", + "Request did not pass through the authz middleware", + 500 + ); + } + + if (actualClass !== expected) { + throw new AuthzAssertionError( + "AUTHZ_ROUTE_CLASS_MISMATCH", + `Expected ${expected} but got ${actualClass}`, + 500 + ); + } + + const subject = readSubjectFromHeaders(headers); + + if (expected !== "PUBLIC" && subject.kind === "anonymous") { + throw new AuthzAssertionError("AUTHZ_UNAUTHENTICATED", "Authentication required", 401); + } + + return subject; +} diff --git a/src/server/authz/classify.ts b/src/server/authz/classify.ts new file mode 100644 index 0000000000..e0c00e546c --- /dev/null +++ b/src/server/authz/classify.ts @@ -0,0 +1,122 @@ +import { + PUBLIC_API_ROUTE_PREFIXES, + PUBLIC_READONLY_API_ROUTE_PREFIXES, + PUBLIC_READONLY_METHODS, +} from "../../shared/constants/publicApiRoutes"; +import type { ClassificationReason, RouteClass, RouteClassification } from "./types"; + +const CLIENT_API_ALIAS_PREFIXES: ReadonlyArray<{ alias: string; canonical: string }> = [ + { alias: "/chat/completions", canonical: "/api/v1/chat/completions" }, + { alias: "/responses", canonical: "/api/v1/responses" }, + { alias: "/models", canonical: "/api/v1/models" }, +]; + +function normalizePathname(rawPath: string): { path: string; reason?: ClassificationReason } { + let path = rawPath || "/"; + if (!path.startsWith("/")) path = "/" + path; + if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1); + + if (path === "/codex" || path.startsWith("/codex/")) { + return { path: "/api/v1/responses", reason: "client_api_codex_alias" }; + } + + if (path === "/v1/v1" || path.startsWith("/v1/v1/")) { + const tail = path.slice("/v1/v1".length) || ""; + return { path: "/api/v1" + tail, reason: "client_api_double_prefix" }; + } + + if (path === "/v1" || path.startsWith("/v1/")) { + const tail = path.slice("/v1".length) || ""; + return { path: "/api/v1" + tail, reason: "client_api_alias" }; + } + + for (const { alias, canonical } of CLIENT_API_ALIAS_PREFIXES) { + if (path === alias) { + return { path: canonical, reason: "client_api_alias" }; + } + if (path.startsWith(alias + "/")) { + return { path: canonical + path.slice(alias.length), reason: "client_api_alias" }; + } + } + + return { path }; +} + +export function classifyRoute(rawPath: string, method: string = "GET"): RouteClassification { + const { path: normalizedPath, reason: aliasReason } = normalizePathname(rawPath); + + if (normalizedPath === "/" || normalizedPath === "") { + return { + routeClass: "MANAGEMENT", + reason: "root_redirect", + normalizedPath: "/", + }; + } + + if (normalizedPath.startsWith("/dashboard")) { + return { + routeClass: "MANAGEMENT", + reason: "dashboard_prefix", + normalizedPath, + }; + } + + if (normalizedPath === "/api/v1" || normalizedPath.startsWith("/api/v1/")) { + return { + routeClass: "CLIENT_API", + reason: aliasReason ?? "client_api_v1", + normalizedPath, + }; + } + + if (normalizedPath.startsWith("/api/")) { + if (isClassifiedAsPublic(normalizedPath, method)) { + return { + routeClass: "PUBLIC", + reason: matchesReadonlyPublic(normalizedPath, method) + ? "public_readonly_prefix" + : "public_prefix", + normalizedPath, + }; + } + + return { + routeClass: "MANAGEMENT", + reason: "management_api", + normalizedPath, + }; + } + + return { + routeClass: "MANAGEMENT", + reason: "fallback_management", + normalizedPath, + }; +} + +function matchesReadonlyPublic(path: string, method: string): boolean { + if (!PUBLIC_READONLY_METHODS.has(String(method).toUpperCase())) return false; + return PUBLIC_READONLY_API_ROUTE_PREFIXES.some((p) => path.startsWith(p)); +} + +function isClassifiedAsPublic(path: string, method: string): boolean { + const isV1ApiPrefix = (p: string) => + p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/"); + const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/"); + if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) { + return true; + } + return matchesReadonlyPublic(path, method); +} + +export function isClientApi(routeClass: RouteClass): boolean { + return routeClass === "CLIENT_API"; +} + +export function isManagement(routeClass: RouteClass): boolean { + return routeClass === "MANAGEMENT"; +} + +export function isPublic(routeClass: RouteClass): boolean { + return routeClass === "PUBLIC"; +} diff --git a/src/server/authz/context.ts b/src/server/authz/context.ts new file mode 100644 index 0000000000..01f13be038 --- /dev/null +++ b/src/server/authz/context.ts @@ -0,0 +1,42 @@ +import type { AuthSubject, RouteClass, RouteClassification } from "./types"; + +export interface AuthDecision { + allow: true; + subject: AuthSubject; +} + +export interface AuthRejection { + allow: false; + status: number; + code: string; + message: string; +} + +export type AuthOutcome = AuthDecision | AuthRejection; + +export interface RequestLike { + method: string; + headers: Headers; + cookies?: { get?: (name: string) => { value?: string } | undefined }; + nextUrl?: { pathname?: string | null } | null; + url?: string; +} + +export interface PolicyContext { + request: RequestLike; + classification: RouteClassification; + requestId: string; +} + +export interface RoutePolicy { + readonly routeClass: RouteClass; + evaluate(ctx: PolicyContext): Promise; +} + +export function allow(subject: AuthSubject): AuthDecision { + return { allow: true, subject }; +} + +export function reject(status: number, code: string, message: string): AuthRejection { + return { allow: false, status, code, message }; +} diff --git a/src/server/authz/headers.ts b/src/server/authz/headers.ts new file mode 100644 index 0000000000..906befc39a --- /dev/null +++ b/src/server/authz/headers.ts @@ -0,0 +1,34 @@ +/** + * Header constants used by the authz pipeline. + * + * Middleware adds these headers to the upstream request after a successful + * auth decision. Route handlers and downstream services read them through + * the assertAuth() helper instead of re-running auth logic. + * + * All header names are lowercase to match Next.js / fetch semantics. + * + * IMPORTANT: these headers are stripped from incoming client requests + * before classification (see pipeline.ts) so a remote caller cannot + * pre-populate them and impersonate a privileged subject. + */ + +export const AUTHZ_HEADER_REQUEST_ID = "x-request-id"; + +export const AUTHZ_HEADER_ROUTE_CLASS = "x-omniroute-route-class"; + +export const AUTHZ_HEADER_AUTH_KIND = "x-omniroute-auth-kind"; +export const AUTHZ_HEADER_AUTH_ID = "x-omniroute-auth-id"; +export const AUTHZ_HEADER_AUTH_LABEL = "x-omniroute-auth-label"; +export const AUTHZ_HEADER_AUTH_SCOPES = "x-omniroute-auth-scopes"; + +/** + * Headers the pipeline must NEVER trust on incoming requests. They are + * stripped before route classification to prevent header-spoofing attacks. + */ +export const AUTHZ_TRUSTED_HEADERS: ReadonlyArray = [ + AUTHZ_HEADER_ROUTE_CLASS, + AUTHZ_HEADER_AUTH_KIND, + AUTHZ_HEADER_AUTH_ID, + AUTHZ_HEADER_AUTH_LABEL, + AUTHZ_HEADER_AUTH_SCOPES, +]; diff --git a/src/server/authz/pipeline.ts b/src/server/authz/pipeline.ts new file mode 100644 index 0000000000..c6d053b809 --- /dev/null +++ b/src/server/authz/pipeline.ts @@ -0,0 +1,252 @@ +import { jwtVerify, SignJWT } from "jose"; +import { NextResponse, type NextRequest } from "next/server"; +import { isDraining } from "../../lib/gracefulShutdown"; +import { checkBodySize, getBodySizeLimit } from "../../shared/middleware/bodySizeGuard"; +import { generateRequestId } from "../../shared/utils/requestId"; +import { applyCorsHeaders } from "../cors/origins"; +import { classifyRoute } from "./classify"; +import { clientApiPolicy } from "./policies/clientApi"; +import { managementPolicy } from "./policies/management"; +import { publicPolicy } from "./policies/public"; +import { + AUTHZ_HEADER_AUTH_ID, + AUTHZ_HEADER_AUTH_KIND, + AUTHZ_HEADER_AUTH_LABEL, + AUTHZ_HEADER_AUTH_SCOPES, + AUTHZ_HEADER_REQUEST_ID, + AUTHZ_HEADER_ROUTE_CLASS, + AUTHZ_TRUSTED_HEADERS, +} from "./headers"; +import type { AuthSubject, RouteClass, RouteClassification } from "./types"; +import type { AuthOutcome, RoutePolicy } from "./context"; + +export interface AuthzPipelineOptions { + enforce?: boolean; +} + +const POLICIES: Record = { + PUBLIC: publicPolicy, + CLIENT_API: clientApiPolicy, + MANAGEMENT: managementPolicy, +}; + +function stampSubject(headers: Headers, subject: AuthSubject): void { + headers.set(AUTHZ_HEADER_AUTH_KIND, subject.kind); + headers.set(AUTHZ_HEADER_AUTH_ID, subject.id); + if (subject.label) headers.set(AUTHZ_HEADER_AUTH_LABEL, subject.label); + if (subject.scopes && subject.scopes.length > 0) { + headers.set(AUTHZ_HEADER_AUTH_SCOPES, subject.scopes.join(",")); + } +} + +function rejectionResponse( + outcome: Extract, + classification: RouteClassification, + requestId: string +): NextResponse { + const response = NextResponse.json( + { + error: { + code: outcome.code, + message: outcome.message, + correlation_id: requestId, + }, + }, + { status: outcome.status } + ); + response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); + return response; +} + +function isDashboardPath(pathname: string): boolean { + return pathname === "/dashboard" || pathname.startsWith("/dashboard/"); +} + +function isManagementDashboardRoute( + classification: RouteClassification, + pathname: string +): boolean { + return classification.routeClass === "MANAGEMENT" && isDashboardPath(pathname); +} + +function getCookieValue(request: NextRequest, name: string): string | null { + const fromCookies = request.cookies.get(name)?.value; + if (fromCookies) return fromCookies; + + 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 getJwtSecret(): Uint8Array | null { + const secret = process.env.JWT_SECRET?.trim(); + return secret ? new TextEncoder().encode(secret) : null; +} + +function shouldUseSecureCookie(request: NextRequest): boolean { + if (process.env.AUTH_COOKIE_SECURE === "true") return true; + const forwardedProto = (request.headers.get("x-forwarded-proto") || "") + .split(",")[0] + .trim() + .toLowerCase(); + return forwardedProto === "https" || request.nextUrl.protocol === "https:"; +} + +async function refreshDashboardSessionIfNeeded( + response: NextResponse, + request: NextRequest +): Promise { + const secret = getJwtSecret(); + if (!secret) return; + + const token = getCookieValue(request, "auth_token"); + if (!token) return; + + try { + const { payload } = await jwtVerify(token, secret); + const exp = typeof payload.exp === "number" ? payload.exp : null; + if (!exp) return; + + const now = Math.floor(Date.now() / 1000); + const refreshWindowSeconds = 7 * 24 * 60 * 60; + if (exp - now >= refreshWindowSeconds) return; + + const freshToken = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("30d") + .sign(secret); + + response.cookies.set("auth_token", freshToken, { + httpOnly: true, + secure: shouldUseSecureCookie(request), + sameSite: "lax", + path: "/", + }); + } catch (error) { + console.error("[Authz] JWT auto-refresh failed:", error); + } +} + +function dashboardLoginRedirect(request: NextRequest, requestId: string): NextResponse { + const response = NextResponse.redirect(new URL("/login", request.url)); + response.cookies.delete("auth_token"); + stampRouteResponse(response, requestId, "MANAGEMENT"); + applyCorsHeaders(response, request); + return response; +} + +function drainingResponse(requestId: string): NextResponse { + const response = NextResponse.json( + { + error: { + code: "SERVICE_UNAVAILABLE", + message: "Server is shutting down", + correlation_id: requestId, + }, + }, + { status: 503 } + ); + response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + return response; +} + +function stampRouteResponse( + response: Response, + requestId: string, + routeClass: RouteClass +): Response { + response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, routeClass); + return response; +} + +export async function runAuthzPipeline( + request: NextRequest, + options: AuthzPipelineOptions = {} +): Promise { + const { pathname } = request.nextUrl; + const method = request.method; + + const requestId = generateRequestId(); + + if (pathname === "/") { + const response = NextResponse.redirect(new URL("/dashboard", request.url)); + return stampRouteResponse(response, requestId, "MANAGEMENT"); + } + + const classification = classifyRoute(pathname, method); + const guardedPathname = classification.normalizedPath; + const managementDashboardRoute = isManagementDashboardRoute(classification, pathname); + + if (guardedPathname.startsWith("/api/") && isDraining()) { + const response = drainingResponse(requestId); + stampRouteResponse(response, requestId, classification.routeClass); + applyCorsHeaders(response, request); + return response; + } + + if (guardedPathname.startsWith("/api/") && method !== "GET" && method !== "OPTIONS") { + const bodySizeRejection = checkBodySize(request, getBodySizeLimit(guardedPathname)); + if (bodySizeRejection) { + stampRouteResponse(bodySizeRejection, requestId, classification.routeClass); + applyCorsHeaders(bodySizeRejection, request); + return bodySizeRejection; + } + } + + const requestHeaders = new Headers(request.headers); + for (const trusted of AUTHZ_TRUSTED_HEADERS) { + requestHeaders.delete(trusted); + } + + requestHeaders.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); + requestHeaders.set(AUTHZ_HEADER_REQUEST_ID, requestId); + + if (method === "OPTIONS") { + const preflight = new NextResponse(null, { status: 204 }); + preflight.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + preflight.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); + applyCorsHeaders(preflight, request); + return preflight; + } + + if (!options.enforce) { + const response = NextResponse.next({ request: { headers: requestHeaders } }); + response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); + applyCorsHeaders(response, request); + return response; + } + + const policy = POLICIES[classification.routeClass]; + const outcome = await policy.evaluate({ request, classification, requestId }); + + if (!outcome.allow) { + if (managementDashboardRoute) { + return dashboardLoginRedirect(request, requestId); + } + + const rejection = rejectionResponse(outcome, classification, requestId); + applyCorsHeaders(rejection, request); + return rejection; + } + + stampSubject(requestHeaders, outcome.subject); + + const response = NextResponse.next({ request: { headers: requestHeaders } }); + response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); + response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); + applyCorsHeaders(response, request); + if (managementDashboardRoute) { + await refreshDashboardSessionIfNeeded(response, request); + } + return response; +} diff --git a/src/server/authz/policies/clientApi.ts b/src/server/authz/policies/clientApi.ts new file mode 100644 index 0000000000..b41a1d72ff --- /dev/null +++ b/src/server/authz/policies/clientApi.ts @@ -0,0 +1,54 @@ +import { isDashboardSessionAuthenticated } from "../../../shared/utils/apiAuth"; +import type { AuthOutcome, PolicyContext, RoutePolicy } from "../context"; +import { allow, reject } from "../context"; + +function extractBearer(headers: Headers): string | null { + const raw = headers.get("authorization") ?? headers.get("Authorization"); + if (!raw) return null; + const trimmed = raw.trim(); + if (!trimmed.toLowerCase().startsWith("bearer ")) return null; + return trimmed.slice(7).trim() || null; +} + +function maskKeyId(apiKey: string): string { + const tail = apiKey.slice(-4); + return `key_${tail}`; +} + +function isDashboardModelCatalogRead(ctx: PolicyContext): boolean { + const method = ctx.request.method.toUpperCase(); + if (method !== "GET" && method !== "HEAD") return false; + return ( + ctx.classification.normalizedPath === "/api/v1/models" || + ctx.classification.normalizedPath === "/api/v1" + ); +} + +export const clientApiPolicy: RoutePolicy = { + routeClass: "CLIENT_API", + async evaluate(ctx: PolicyContext): Promise { + const bearer = extractBearer(ctx.request.headers); + if (!bearer) { + if ( + isDashboardModelCatalogRead(ctx) && + (await isDashboardSessionAuthenticated(ctx.request)) + ) { + return allow({ kind: "dashboard_session", id: "dashboard" }); + } + + if (process.env.REQUIRE_API_KEY !== "true") { + return allow({ kind: "anonymous", id: "local" }); + } + + return reject(401, "AUTH_002", "Authentication required"); + } + + const { validateApiKey } = await import("../../../lib/db/apiKeys"); + const ok = await validateApiKey(bearer); + if (!ok) { + return reject(401, "AUTH_002", "Invalid API key"); + } + + return allow({ kind: "client_api_key", id: maskKeyId(bearer) }); + }, +}; diff --git a/src/server/authz/policies/management.ts b/src/server/authz/policies/management.ts new file mode 100644 index 0000000000..8caae11d6f --- /dev/null +++ b/src/server/authz/policies/management.ts @@ -0,0 +1,40 @@ +import { isModelSyncInternalRequest } from "../../../shared/services/modelSyncScheduler"; +import { isAuthRequired, isDashboardSessionAuthenticated } from "../../../shared/utils/apiAuth"; +import type { AuthOutcome, PolicyContext, RoutePolicy } from "../context"; +import { allow, reject } from "../context"; + +const MODEL_SYNC_MANAGEMENT_PATH = /^\/api\/providers\/[^/]+\/(sync-models|models)$/; + +function hasBearerToken(headers: Headers): boolean { + const authHeader = headers.get("authorization") ?? headers.get("Authorization"); + return typeof authHeader === "string" && authHeader.trim().toLowerCase().startsWith("bearer "); +} + +function isInternalModelSyncRequest(ctx: PolicyContext): boolean { + if (!MODEL_SYNC_MANAGEMENT_PATH.test(ctx.classification.normalizedPath)) return false; + return isModelSyncInternalRequest(ctx.request); +} + +export const managementPolicy: RoutePolicy = { + routeClass: "MANAGEMENT", + async evaluate(ctx: PolicyContext): Promise { + if (!(await isAuthRequired())) { + return allow({ kind: "anonymous", id: "anonymous", label: "auth-disabled" }); + } + + if (isInternalModelSyncRequest(ctx)) { + return allow({ kind: "management_key", id: "model-sync", label: "internal-model-sync" }); + } + + if (await isDashboardSessionAuthenticated(ctx.request)) { + return allow({ kind: "dashboard_session", id: "dashboard" }); + } + + const bearerPresent = hasBearerToken(ctx.request.headers); + return reject( + bearerPresent ? 403 : 401, + "AUTH_001", + bearerPresent ? "Invalid management token" : "Authentication required" + ); + }, +}; diff --git a/src/server/authz/policies/public.ts b/src/server/authz/policies/public.ts new file mode 100644 index 0000000000..588eebc9d6 --- /dev/null +++ b/src/server/authz/policies/public.ts @@ -0,0 +1,9 @@ +import type { AuthOutcome, PolicyContext, RoutePolicy } from "../context"; +import { allow } from "../context"; + +export const publicPolicy: RoutePolicy = { + routeClass: "PUBLIC", + async evaluate(_ctx: PolicyContext): Promise { + return allow({ kind: "anonymous", id: "anonymous" }); + }, +}; diff --git a/src/server/authz/types.ts b/src/server/authz/types.ts new file mode 100644 index 0000000000..a8fd786408 --- /dev/null +++ b/src/server/authz/types.ts @@ -0,0 +1,74 @@ +/** + * Unified authz type definitions. + * + * These types are the canonical contract between the Next.js middleware, + * the policy layer, and any defense-in-depth helpers used by route handlers. + * + * Three classes of HTTP routes are recognized: + * + * - PUBLIC — explicitly safe routes (login, logout, status, health, + * onboarding bootstrap). + * - CLIENT_API — model-serving and OpenAI/Anthropic-compatible endpoints + * protected by API keys. + * - MANAGEMENT — dashboard pages, settings, providers, keys, admin and + * diagnostics endpoints protected by dashboard session + * or management-grade credentials. + * + * Any route that cannot be classified MUST fail closed. + */ + +export type RouteClass = "PUBLIC" | "CLIENT_API" | "MANAGEMENT"; + +/** + * Why a particular path was placed into a route class. Used for telemetry, + * diagnostics, and tests so we can prove that routing decisions are + * deterministic and reviewable. + */ +export type ClassificationReason = + | "public_prefix" + | "public_readonly_prefix" + | "dashboard_prefix" + | "client_api_v1" + | "client_api_alias" + | "client_api_codex_alias" + | "client_api_double_prefix" + | "management_api" + | "root_redirect" + | "fallback_management"; + +export interface RouteClassification { + routeClass: RouteClass; + reason: ClassificationReason; + /** + * The normalized internal pathname after rewrites. For example + * "/v1/chat/completions" or "/codex/foo" is normalized to + * "/api/v1/chat/completions" / "/api/v1/responses" so policy code does + * not have to know about every alias. + */ + normalizedPath: string; +} + +/** + * Identity of the authenticated principal once a policy has accepted the + * request. Populated by the policy layer (Phase 2) and consumed by route + * handlers via assertAuth(). + */ +export interface AuthSubject { + kind: "client_api_key" | "dashboard_session" | "management_key" | "anonymous"; + /** + * Stable identifier of the principal: + * - hashed key id for API keys + * - "dashboard" for the single-tenant dashboard session + * - "anonymous" for unauthenticated PUBLIC requests + */ + id: string; + /** + * Optional human-friendly label (key name, scope hint). Never includes + * the raw secret. + */ + label?: string; + /** + * Scopes granted to this subject. Empty for unauthenticated subjects. + */ + scopes?: ReadonlyArray; +} diff --git a/src/server/cors/origins.ts b/src/server/cors/origins.ts new file mode 100644 index 0000000000..2c1759f216 --- /dev/null +++ b/src/server/cors/origins.ts @@ -0,0 +1,128 @@ +/** + * Centralized CORS origin allowlist. + * + * Source of truth for which browser origins may call OmniRoute over CORS. + * No wildcard default. To allow any origin, opt in via `CORS_ALLOW_ALL=true`. + * + * Resolution order: + * 1. If `CORS_ALLOW_ALL=true` → echo any origin back (effectively `*`, + * but with `Vary: Origin` so caches stay correct). + * 2. Otherwise, the request's `Origin` is matched (case-insensitive, + * trailing slash ignored) against the allowlist. + * 3. Allowlist sources (merged): env `CORS_ALLOWED_ORIGINS` (csv) and + * anything injected at runtime via `setRuntimeAllowedOrigins()` from + * the persisted settings layer. + * + * The middleware applies the resolved value via `applyCorsHeaders()`. + * Per-route handlers no longer set `Access-Control-Allow-Origin` themselves. + */ +const ENV_ALLOW_ALL = "CORS_ALLOW_ALL"; +const ENV_ALLOWED = "CORS_ALLOWED_ORIGINS"; +const LEGACY_ENV_SINGLE = "CORS_ORIGIN"; + +const STANDARD_ALLOW_HEADERS = + "Content-Type, Authorization, x-api-key, anthropic-version, x-omniroute-connection, x-internal-test, accept"; +const STANDARD_ALLOW_METHODS = "GET, POST, PUT, DELETE, PATCH, OPTIONS"; + +let runtimeAllowedOrigins: ReadonlySet = new Set(); + +/** + * Persisted-setting hook. The settings layer should call this whenever the + * stored `corsOrigins` value changes (csv string). + */ +export function setRuntimeAllowedOrigins(csv: string | null | undefined): void { + runtimeAllowedOrigins = parseOriginList(csv); +} + +export function getRuntimeAllowedOrigins(): ReadonlySet { + return runtimeAllowedOrigins; +} + +function parseOriginList(value: string | null | undefined): Set { + const result = new Set(); + if (!value) return result; + for (const raw of value.split(",")) { + const v = raw.trim(); + if (!v) continue; + result.add(normalizeOrigin(v)); + } + return result; +} + +function normalizeOrigin(origin: string): string { + let normalized = origin.toLowerCase(); + while (normalized.endsWith("/")) { + normalized = normalized.slice(0, -1); + } + return normalized; +} + +function envAllowAll(): boolean { + const raw = process.env[ENV_ALLOW_ALL]; + if (!raw) { + // Backward-compat: legacy `CORS_ORIGIN=*` behaves like allow-all. + return process.env[LEGACY_ENV_SINGLE]?.trim() === "*"; + } + return raw.trim().toLowerCase() === "true" || raw.trim() === "1"; +} + +function envAllowedOrigins(): Set { + const fromList = parseOriginList(process.env[ENV_ALLOWED]); + const legacy = process.env[LEGACY_ENV_SINGLE]?.trim(); + if (legacy && legacy !== "*") { + fromList.add(normalizeOrigin(legacy)); + } + return fromList; +} + +/** + * Resolve which value should be returned in `Access-Control-Allow-Origin` + * for the given request origin, or `null` if the origin is not allowed. + * + * Returns the original (un-normalized) origin string when allowed so the + * browser sees an exact echo and cookies/credentials work correctly. + */ +export function resolveAllowedOrigin(requestOrigin: string | null | undefined): string | null { + if (envAllowAll()) { + // `*` is only safe when no credentials are involved; if a request did + // arrive with an Origin header, echo it back with Vary so credentialed + // flows can opt in via the explicit allowlist instead. + return requestOrigin && requestOrigin.length > 0 ? requestOrigin : "*"; + } + if (!requestOrigin) return null; + const normalized = normalizeOrigin(requestOrigin); + if (envAllowedOrigins().has(normalized)) return requestOrigin; + if (runtimeAllowedOrigins.has(normalized)) return requestOrigin; + return null; +} + +/** + * Apply CORS headers to a response in-place. Safe to call on any response + * (rejections, preflight, normal `next()` continuations). When the origin + * is not allowed, no `Access-Control-Allow-Origin` is added — browsers + * will block the response, which is the desired fail-closed behavior. + */ +export function applyCorsHeaders(response: Response, request: Request): void { + const requestOrigin = request.headers.get("origin"); + const allowed = resolveAllowedOrigin(requestOrigin); + if (allowed !== null) { + response.headers.set("Access-Control-Allow-Origin", allowed); + response.headers.append("Vary", "Origin"); + } + response.headers.set("Access-Control-Allow-Methods", STANDARD_ALLOW_METHODS); + response.headers.set("Access-Control-Allow-Headers", STANDARD_ALLOW_HEADERS); + const requestedHeaders = request.headers.get("access-control-request-headers"); + if (requestedHeaders) { + response.headers.set("Access-Control-Allow-Headers", requestedHeaders); + } +} + +/** + * Plain-object form for handlers that still need static CORS headers + * (no origin echo). Middleware overlays the proper `Access-Control-Allow-Origin` + * later on the way out. + */ +export const STATIC_CORS_HEADERS: Readonly> = Object.freeze({ + "Access-Control-Allow-Methods": STANDARD_ALLOW_METHODS, + "Access-Control-Allow-Headers": STANDARD_ALLOW_HEADERS, +}); diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 43adfb0cdf..d5ba3f60c8 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -504,29 +504,17 @@ amp --model "{{model}}" ], codeBlock: { language: "json", - code: `# ~/.qwen/settings.json — OmniRoute as multi-provider + code: `# ~/.qwen/settings.json — OmniRoute via security.auth { - "modelProviders": { - "openai": [{ - "id": "{{model}}", - "name": "OmniRoute", - "envKey": "OPENAI_API_KEY", - "baseUrl": "{{baseUrl}}", - "generationConfig": { "contextWindowSize": 200000 } - }], - "anthropic": [{ - "id": "claude-sonnet-4-6", - "name": "Claude Sonnet 4.6", - "envKey": "ANTHROPIC_API_KEY", - "baseUrl": "{{baseUrl}}", - "generationConfig": { "contextWindowSize": 200000 } - }], - "gemini": [{ - "id": "gemini-3-flash", - "name": "Gemini 3 Flash", - "envKey": "GEMINI_API_KEY", + "security": { + "auth": { + "selectedType": "openai", + "apiKey": "{{apiKey}}", "baseUrl": "{{baseUrl}}" - }] + } + }, + "model": { + "name": "{{model}}" } }`, }, diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 0f36a8a11b..5f33af4a50 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -32,7 +32,6 @@ export const MODEL_SPECS: Record = { supportsThinking: true, supportsTools: true, supportsVision: true, - aliases: ["gpt-5.5-xhigh", "gpt-5.5-high", "gpt-5.5-medium", "gpt-5.5-low", "gpt-5.5-none"], }, // ── Gemini 3 Flash series ─────────────────────────────────────── diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 44b39f7031..e8e26bfa9f 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -114,7 +114,6 @@ export const WEB_COOKIE_PROVIDERS = { textIcon: "GW", website: "https://grok.com", authHint: "Paste your sso= cookie value from grok.com", - passthroughModels: true, }, "perplexity-web": { id: "perplexity-web", @@ -1543,7 +1542,7 @@ export const SEARCH_PROVIDERS = { color: "#1A237E", textIcon: "SX", website: "https://docs.searxng.org", - authHint: "Set your SearXNG base URL. API key is optional for public/self-hosted instances.", + authHint: "API key is optional. Set your SearXNG base URL. Some instances may require a bearer token for access.", }, }; diff --git a/src/shared/constants/visionBridgeDefaults.ts b/src/shared/constants/visionBridgeDefaults.ts index e4392af60b..7a561e742d 100644 --- a/src/shared/constants/visionBridgeDefaults.ts +++ b/src/shared/constants/visionBridgeDefaults.ts @@ -2,12 +2,16 @@ * Vision Bridge default configuration values. */ -const NORMALIZED_GPT_MODEL_PATTERN = /^gpt-/i; +const FORCED_VISION_BRIDGE_MODELS = new Set([ + // Fallback list for models whose metadata is known to overstate native vision support. +]); export function isVisionBridgeForcedModel(model: string | null | undefined): boolean { if (!model) return false; - const normalizedModel = model.includes("/") ? model.split("/").pop() || model : model; - return NORMALIZED_GPT_MODEL_PATTERN.test(normalizedModel); + const normalizedModel = (model.includes("/") ? model.split("/").pop() || model : model) + .trim() + .toLowerCase(); + return FORCED_VISION_BRIDGE_MODELS.has(normalizedModel); } export const VISION_BRIDGE_DEFAULTS = { diff --git a/src/shared/middleware/bodySizeGuard.ts b/src/shared/middleware/bodySizeGuard.ts index f028484f0c..80c3a4946f 100644 --- a/src/shared/middleware/bodySizeGuard.ts +++ b/src/shared/middleware/bodySizeGuard.ts @@ -65,7 +65,6 @@ export function checkBodySize(request: Request, limit: number = MAX_BODY_BYTES): status: 413, headers: { "Content-Type": "application/json", - "Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*", }, } ); diff --git a/src/shared/services/apiKeyResolver.ts b/src/shared/services/apiKeyResolver.ts index 72124fad8f..5896c385c1 100644 --- a/src/shared/services/apiKeyResolver.ts +++ b/src/shared/services/apiKeyResolver.ts @@ -1,4 +1,5 @@ -import { getApiKeyById } from "@/lib/db/apiKeys"; +import { getApiKeyById, createApiKey } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; export async function resolveApiKey( apiKeyId?: string | null, @@ -14,3 +15,30 @@ export async function resolveApiKey( } return apiKey || "sk_omniroute"; } + +/** + * Get or create a DB-backed API key for CLI tool setup. + * Returns a valid OmniRoute API key (not a placeholder like "sk_omniroute"). + * Used when user has not explicitly selected a key from API Manager. + */ +export async function getOrCreateApiKey(apiKeyId?: string | null): Promise { + if (apiKeyId) { + try { + const keyRecord = await getApiKeyById(apiKeyId); + if (keyRecord?.key) return keyRecord.key as string; + } catch { + /* fall through */ + } + } + + // No key found — auto-create one that will be valid in DB validation + let machineId = "unknown"; + try { + machineId = await getConsistentMachineId(); + const keyRecord = await createApiKey("CLI Auto-Key", machineId); + return keyRecord.key as string; + } catch { + // Fallback: generate a deterministic key if DB write fails + return `sk-${machineId}-fallback-${Date.now()}`; + } +} diff --git a/src/shared/services/modelSyncScheduler.ts b/src/shared/services/modelSyncScheduler.ts index 67f7d3b147..f6d8d8e948 100644 --- a/src/shared/services/modelSyncScheduler.ts +++ b/src/shared/services/modelSyncScheduler.ts @@ -48,7 +48,7 @@ export function buildModelSyncInternalHeaders(): Record { return { [MODEL_SYNC_INTERNAL_AUTH_HEADER]: getInternalAuthToken() }; } -export function isModelSyncInternalRequest(request: Request): boolean { +export function isModelSyncInternalRequest(request: { headers: Headers }): boolean { if (!internalAuthToken && globalState.__omnirouteModelSyncInternalAuthToken) { internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken; } diff --git a/src/shared/utils/cors.ts b/src/shared/utils/cors.ts index 848e4ae8d5..75ef54d5ac 100644 --- a/src/shared/utils/cors.ts +++ b/src/shared/utils/cors.ts @@ -1,37 +1,24 @@ /** - * Shared CORS configuration for all API routes. + * Static CORS headers for route handlers. * - * Centralizes the Access-Control-Allow-Origin header so it can be - * configured via the CORS_ORIGIN environment variable instead of - * being hardcoded as "*" in every route handler. - * - * Usage: - * import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; - * - * // In route responses: - * return new Response(body, { headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); - * - * // For OPTIONS preflight: - * export function OPTIONS() { return handleCorsOptions(); } - */ - -export const CORS_ORIGIN = process.env.CORS_ORIGIN || "*"; - -/** - * Standard CORS headers to spread into any Response. - * @type {Record} + * `Access-Control-Allow-Origin` is intentionally NOT set here. The middleware + * (`src/middleware.ts` → `applyCorsHeaders`) is the single source of truth for + * which origin to echo, based on the central allowlist in + * `src/server/cors/origins.ts`. Route handlers may keep spreading + * `CORS_HEADERS` for the standard methods/allowed-headers; the middleware + * overlays the proper origin on the way out. */ export const CORS_HEADERS = { - "Access-Control-Allow-Origin": CORS_ORIGIN, - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, x-omniroute-connection, x-internal-test, accept", -}; +} as const; /** - * Handle CORS preflight (OPTIONS) request. - * @returns {Response} 204 No Content with CORS headers + * Preflight responder kept for routes that still ship their own OPTIONS handler. + * Returning 204 with `CORS_HEADERS` is enough; the middleware will add the + * allowed origin and `Vary: Origin` before the response leaves the server. */ -export function handleCorsOptions() { +export function handleCorsOptions(): Response { return new Response(null, { status: 204, headers: CORS_HEADERS }); } diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 909be6f7b5..8b768e9f78 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -434,9 +434,9 @@ export const updateSettingsSchema = z.object({ cloudUrl: z.string().max(500).optional(), baseUrl: z.string().max(500).optional(), setupComplete: z.boolean().optional(), - requireAuthForModels: z.boolean().optional(), blockedProviders: z.array(z.string().max(100)).optional(), hideHealthCheckLogs: z.boolean().optional(), + bruteForceProtection: z.boolean().optional(), hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), // Routing settings (#134) diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 480c08392d..030bf8c511 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -43,7 +43,6 @@ export const updateSettingsSchema = z.object({ cloudUrl: z.string().max(500).optional(), baseUrl: z.string().max(500).optional(), setupComplete: z.boolean().optional(), - requireAuthForModels: z.boolean().optional(), blockedProviders: z.array(z.string().max(100)).optional(), hideHealthCheckLogs: z.boolean().optional(), debugMode: z.boolean().optional(), @@ -108,4 +107,5 @@ export const updateSettingsSchema = z.object({ // Missing settings lkgpEnabled: z.boolean().optional(), backgroundDegradation: z.unknown().optional(), + bruteForceProtection: z.boolean().optional(), }); diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index d9bf0763e7..b8fcc35448 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -3,7 +3,6 @@ import { getProviderCredentialsWithQuotaPreflight, markAccountUnavailable, extractApiKey, - isValidApiKey, } from "../services/auth"; import { getRuntimeProviderProfile, @@ -180,26 +179,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { log.debug("AUTH", "No API key provided (local mode)"); } - // Optional strict API key mode for /v1 endpoints (require key on every request). const isComboLiveTest = request.headers?.get?.("x-internal-test") === "combo-health-check"; - if (process.env.REQUIRE_API_KEY === "true" && !isComboLiveTest) { - if (!apiKey) { - log.warn("AUTH", "Missing API key while REQUIRE_API_KEY=true"); - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - } - const valid = await isValidApiKey(apiKey); - if (!valid) { - log.warn("AUTH", "Invalid API key while REQUIRE_API_KEY=true"); - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } else if (apiKey && !isComboLiveTest) { - // Client sent a Bearer key — it must exist in DB (otherwise reject to avoid "key ignored" confusion). - const valid = await isValidApiKey(apiKey); - if (!valid) { - log.warn("AUTH", "API key not found or invalid (must be created in API Manager)"); - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - } if (!modelStr) { log.warn("CHAT", "Missing model"); @@ -397,6 +377,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { config: relayConfig, } : undefined, + signal: request?.signal ?? null, }); // ── Global Fallback Provider (#689) ──────────────────────────────────── @@ -766,6 +747,12 @@ async function handleSingleModelChat( return result.response; } + if (result.errorType === "stream_readiness_timeout") { + // Stream readiness timeout is an upstream stall, not an account/quota failure. + // Do NOT mark the account as unavailable or trip the circuit breaker. + return result.response; + } + // Emergency fallback for budget exhaustion (402 / billing / quota keywords): // reroute to a free model (default provider/model: nvidia + openai/gpt-oss-120b) exactly once. if (!runtimeOptions.emergencyFallbackTried) { diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index bb36139e2a..f2fdbf28f1 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -6,7 +6,12 @@ import { getSettings, getCachedSettings, } from "@/lib/localDb"; -import { getQuotaCache, getQuotaWindowStatus, isAccountQuotaExhausted } from "@/domain/quotaCache"; +import { + DEFAULT_QUOTA_THRESHOLD_PERCENT, + getQuotaCache, + getQuotaWindowStatus, + isAccountQuotaExhausted, +} from "@/domain/quotaCache"; import { isAccountUnavailable, getUnavailableUntil, @@ -82,7 +87,6 @@ interface CooldownInspectionState { retryableModelCooldownMs: number | null; } -const CODEX_QUOTA_THRESHOLD_PERCENT = 90; const MIN_QUOTA_THRESHOLD_PERCENT = 1; const MAX_QUOTA_THRESHOLD_PERCENT = 100; const NON_RETRYABLE_MODEL_LOCKOUT_REASONS = new Set(["not_found", "not_found_local"]); @@ -168,7 +172,10 @@ interface QuotaCacheView { >; } -function normalizeQuotaThreshold(value: unknown, fallback = CODEX_QUOTA_THRESHOLD_PERCENT): number { +function normalizeQuotaThreshold( + value: unknown, + fallback = DEFAULT_QUOTA_THRESHOLD_PERCENT +): number { const parsed = toNumber(value, fallback); return Math.min(MAX_QUOTA_THRESHOLD_PERCENT, Math.max(MIN_QUOTA_THRESHOLD_PERCENT, parsed)); } @@ -1277,6 +1284,30 @@ export async function markAccountUnavailable( const { shouldFallback, cooldownMs: rawCooldownMs, newBackoffLevel, reason } = result; if (!shouldFallback) return { shouldFallback: false, cooldownMs: 0 }; const providerErrorType = classifyProviderError(status, errorText, provider); + + if (provider && resolveProviderId(provider) === "grok-web" && status === 403 && model) { + const lockout = recordModelLockoutFailure( + provider, + connectionId, + model, + "forbidden", + status, + effectiveProviderProfile?.baseCooldownMs ?? COOLDOWN_MS.unavailable, + effectiveProviderProfile + ); + updateProviderConnection(connectionId, { + lastErrorType: "forbidden", + lastError: `Mode ${model} forbidden for this Grok account`, + lastErrorAt: new Date().toISOString(), + errorCode: status, + }).catch(() => {}); + log.info( + "AUTH", + `Mode-only lockout for ${provider}:${model} — 403 forbidden ${Math.ceil(lockout.cooldownMs / 1000)}s (connection stays active)` + ); + return { shouldFallback: true, cooldownMs: lockout.cooldownMs }; + } + const terminalStatus = resolveTerminalConnectionStatus(status, result, providerErrorType); const cooldownMs = terminalStatus ? 0 : rawCooldownMs; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 0e36d867c5..ab480be2aa 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -28,7 +28,6 @@ declare namespace NodeJS { OMNIROUTE_DISABLE_BACKGROUND_SERVICES?: string; OMNIROUTE_PORT?: string; PRICING_SYNC_ENABLED?: string; - REQUIRE_API_KEY?: string; NODE_ENV?: "development" | "production" | "test"; } } diff --git a/tests/e2e/helpers/dashboardAuth.ts b/tests/e2e/helpers/dashboardAuth.ts index 74974aece8..d13ff83380 100644 --- a/tests/e2e/helpers/dashboardAuth.ts +++ b/tests/e2e/helpers/dashboardAuth.ts @@ -6,7 +6,7 @@ type GotoDashboardRouteOptions = { }; const DEFAULT_TIMEOUT_MS = 300_000; -const APP_ROUTE_PATTERN = /\/(login|dashboard)(\/.*)?$/; +const APP_ROUTE_PATTERN = /\/(login|dashboard)(\/[^?#]*)?([?#].*)?$/; const E2E_PASSWORD = process.env.OMNIROUTE_E2E_PASSWORD || process.env.INITIAL_PASSWORD || "omniroute-e2e-password"; @@ -73,6 +73,12 @@ async function getDashboardAuthState(page: Page) { }); } +function isAtRequestedRoute(page: Page, requestedUrl: string) { + const current = new URL(page.url()); + const requested = new URL(requestedUrl, current.origin); + return current.pathname === requested.pathname && current.search === requested.search; +} + export async function gotoDashboardRoute( page: Page, url: string, @@ -109,6 +115,12 @@ export async function gotoDashboardRoute( await finishOnboardingIfNeeded(page, timeoutMs); } + if (!isAtRequestedRoute(page, url)) { + await page.goto(url, { waitUntil, timeout: timeoutMs }); + await waitForAppRoute(page, timeoutMs); + await finishOnboardingIfNeeded(page, timeoutMs); + } + await page.locator("body").waitFor({ state: "visible", timeout: timeoutMs }); return; } catch (error: any) { diff --git a/tests/e2e/memory-settings.spec.ts b/tests/e2e/memory-settings.spec.ts index 0444b6bff7..d1059a1c22 100644 --- a/tests/e2e/memory-settings.spec.ts +++ b/tests/e2e/memory-settings.spec.ts @@ -81,7 +81,7 @@ test.describe("Memory settings", () => { deleteCalls: 0, }; - await page.route("**/api/settings", async (route) => { + await page.route(/\/api\/settings$/, async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, state.settings); @@ -101,7 +101,7 @@ test.describe("Memory settings", () => { await fulfillJson(route, { error: "Method not allowed in settings stub" }, 405); }); - await page.route("**/api/settings/memory", async (route) => { + await page.route(/\/api\/settings\/memory$/, async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, state.config); diff --git a/tests/e2e/protocol-visibility.spec.ts b/tests/e2e/protocol-visibility.spec.ts index 3a4dea2b3a..f85cb785b5 100644 --- a/tests/e2e/protocol-visibility.spec.ts +++ b/tests/e2e/protocol-visibility.spec.ts @@ -2,34 +2,24 @@ import { test, expect } from "@playwright/test"; import { gotoDashboardRoute } from "./helpers/dashboardAuth"; test.describe("Protocol visibility", () => { - test("shows MCP/A2A links inside protocols tab in endpoint page", async ({ page }) => { + test("shows MCP and A2A tabs inside the endpoint page", async ({ page }) => { await gotoDashboardRoute(page, "/dashboard/endpoint"); await page.waitForLoadState("networkidle"); - // MCP and A2A are now shown inside the "Protocols" tab — click it first - const protocolTab = page.getByRole("tab", { name: /protocols|protocolos/i }); - await expect(protocolTab).toBeVisible(); - await protocolTab.click(); + // MCP and A2A are now tabs directly in the SegmentedControl + const mcpTab = page.getByRole("tab", { name: "MCP" }); + const a2aTab = page.getByRole("tab", { name: "A2A" }); - // Links to MCP and A2A management pages appear after tab switch - await expect(page.locator('a[href="/dashboard/mcp"]').first()).toBeVisible(); - await expect(page.locator('a[href="/dashboard/a2a"]').first()).toBeVisible(); + await expect(mcpTab).toBeVisible(); + await expect(a2aTab).toBeVisible(); - const mcpLinks = await page.locator('a[href="/dashboard/mcp"]').count(); - const a2aLinks = await page.locator('a[href="/dashboard/a2a"]').count(); - expect(mcpLinks).toBeGreaterThanOrEqual(1); - expect(a2aLinks).toBeGreaterThanOrEqual(1); - }); - - test("loads MCP and A2A dashboards without runtime error page", async ({ page }) => { - await gotoDashboardRoute(page, "/dashboard/mcp"); - await page.waitForLoadState("networkidle"); - await expect(page.locator("body")).toBeVisible(); + // Verify MCP dashboard mounts + await mcpTab.click(); + // In dev/test it might just show "loading..." or the processStatus card await expect(page.locator("body")).not.toContainText(/application error|500/i); - await gotoDashboardRoute(page, "/dashboard/a2a"); - await page.waitForLoadState("networkidle"); - await expect(page.locator("body")).toBeVisible(); + // Verify A2A dashboard mounts + await a2aTab.click(); await expect(page.locator("body")).not.toContainText(/application error|500/i); }); }); diff --git a/tests/e2e/providers-bailian-coding-plan.spec.ts b/tests/e2e/providers-bailian-coding-plan.spec.ts index 682c78fb0d..1abe178c17 100644 --- a/tests/e2e/providers-bailian-coding-plan.spec.ts +++ b/tests/e2e/providers-bailian-coding-plan.spec.ts @@ -69,17 +69,13 @@ test.describe("Bailian Coding Plan Provider", () => { } const addKeyButton = page.getByRole("button", { - name: /add.*api.*key|add.*key|add.*connection|connect/i, + name: /add.*api.*key|add.*key|add.*connection|connect|adicionar.*chave/i, }); - if ( - await addKeyButton - .first() - .isVisible({ timeout: 15000 }) - .catch(() => false) - ) { - await addKeyButton.first().click(); - } + // Wait for the button to appear instead of immediately checking visibility + await addKeyButton.first().waitFor({ state: "visible", timeout: 15000 }); + await expect(addKeyButton.first()).toBeEnabled({ timeout: 5000 }); + await addKeyButton.first().click(); const dialog = page.getByRole("dialog").first(); await expect(dialog).toBeVisible({ timeout: 10000 }); @@ -184,17 +180,13 @@ test.describe("Bailian Coding Plan Provider", () => { } const addKeyButton = page.getByRole("button", { - name: /add.*api.*key|add.*key|add.*connection|connect/i, + name: /add.*api.*key|add.*key|add.*connection|connect|adicionar.*chave/i, }); - if ( - await addKeyButton - .first() - .isVisible({ timeout: 15000 }) - .catch(() => false) - ) { - await addKeyButton.first().click(); - } + // Wait for the button to appear instead of immediately checking visibility + await addKeyButton.first().waitFor({ state: "visible", timeout: 15000 }); + await expect(addKeyButton.first()).toBeEnabled({ timeout: 5000 }); + await addKeyButton.first().click(); const dialog = page.getByRole("dialog").first(); await expect(dialog).toBeVisible({ timeout: 10000 }); diff --git a/tests/e2e/resilience-plan-alignment.spec.ts b/tests/e2e/resilience-plan-alignment.spec.ts index a8ce031ba5..374830841b 100644 --- a/tests/e2e/resilience-plan-alignment.spec.ts +++ b/tests/e2e/resilience-plan-alignment.spec.ts @@ -342,10 +342,9 @@ test.describe("Resilience Plan Alignment", () => { }); await gotoDashboardRoute(page, "/dashboard/providers"); - await page.waitForLoadState("networkidle"); + await expect(page.getByText("OpenAI").first()).toBeVisible({ timeout: 15000 }); expect(availabilityRequests).toBe(0); - await expect(page.getByText("OpenAI").first()).toBeVisible(); await expect(page.getByText(/Model Availability/i)).toHaveCount(0); }); @@ -365,10 +364,11 @@ test.describe("Resilience Plan Alignment", () => { }); await gotoDashboardRoute(page, "/dashboard/combos?filter=intelligent"); - await page.waitForLoadState("networkidle"); - - expect(monitoringHealthRequests).toBe(0); await expect(page.getByText("Intelligent Routing Dashboard")).toBeVisible({ timeout: 15000 }); + const healthRequestsAfterPanelVisible = monitoringHealthRequests; + await page.waitForTimeout(500); + + expect(monitoringHealthRequests).toBe(healthRequestsAfterPanelVisible); await expect(page.getByText("Routing Inputs", { exact: true })).toBeVisible(); await expect(page.getByText(/Excluded Providers/i)).toHaveCount(0); await expect(page.getByText(/Incident Mode/i)).toHaveCount(0); diff --git a/tests/integration/api-keys.test.ts b/tests/integration/api-keys.test.ts index 1de3c0a1e0..e86a03cd3b 100644 --- a/tests/integration/api-keys.test.ts +++ b/tests/integration/api-keys.test.ts @@ -38,7 +38,10 @@ async function createManagementKey() { return apiKeysDb.createApiKey("management", MACHINE_ID); } -function makeRequest(url, { method = "GET", token, body } = {}) { +function makeRequest( + url: string | URL, + { method = "GET", token, body }: { method?: string; token?: string; body?: unknown } = {} +) { const headers = new Headers(); if (token) { headers.set("authorization", `Bearer ${token}`); @@ -294,7 +297,7 @@ test("GET /api/keys returns 500 when the key store throws unexpectedly", async ( const db = core.getDbInstance(); const originalPrepare = db.prepare.bind(db); const originalLog = console.log; - const logs = []; + const originalError = console.error; db.prepare = (sql) => { if (String(sql).includes("FROM api_keys")) { @@ -303,9 +306,9 @@ test("GET /api/keys returns 500 when the key store throws unexpectedly", async ( return originalPrepare(sql); }; apiKeysDb.resetApiKeyState(); - console.log = (...args) => { - logs.push(args.map((arg) => String(arg)).join(" ")); - }; + // Suppress Pino structured log output during test + console.log = () => {}; + console.error = () => {}; try { const response = await listRoute.GET(new Request("http://localhost/api/keys")); @@ -313,11 +316,11 @@ test("GET /api/keys returns 500 when the key store throws unexpectedly", async ( assert.equal(response.status, 500); assert.equal(body.error, "Failed to fetch keys"); - assert.ok(logs.some((entry) => entry.includes("Error fetching keys:"))); } finally { db.prepare = originalPrepare; apiKeysDb.resetApiKeyState(); console.log = originalLog; + console.error = originalError; } }); diff --git a/tests/integration/chat-pipeline.test.ts b/tests/integration/chat-pipeline.test.ts index 1742fbe62f..bb4a7d155a 100644 --- a/tests/integration/chat-pipeline.test.ts +++ b/tests/integration/chat-pipeline.test.ts @@ -30,7 +30,7 @@ const { clearProviderFailure } = await import("../../open-sse/services/accountFa const originalFetch = globalThis.fetch; const originalRetryDelayMs = BaseExecutor.RETRY_CONFIG.delayMs; -function toPlainHeaders(headers) { +function toPlainHeaders(headers: Headers | Record | undefined | null) { if (!headers) return {}; if (headers instanceof Headers) return Object.fromEntries(headers.entries()); return Object.fromEntries( @@ -43,8 +43,13 @@ function buildRequest({ body, authKey = null, headers = {}, +}: { + url?: string; + body?: unknown; + authKey?: string | null; + headers?: Record; } = {}) { - const requestHeaders = { + const requestHeaders: Record = { "Content-Type": "application/json", ...headers, }; @@ -769,12 +774,12 @@ test("chat pipeline rejects invalid API keys and malformed JSON bodies", async ( const invalidJson = (await invalidJsonResponse.json()) as any; assert.equal(invalidKeyResponse.status, 401); - assert.match(invalidKeyJson.error.message, /Invalid API key/i); + assert.match(invalidKeyJson.error.message, /Invalid API key|Incorrect API key/i); assert.equal(invalidJsonResponse.status, 400); assert.match(invalidJson.error.message, /Invalid JSON body/i); }); -test("chat pipeline rejects requests without a bearer key when strict API key mode is enabled", async () => { +test("chat pipeline allows unauthenticated requests through to provider resolution when called directly (authz pipeline enforces REQUIRE_API_KEY at route level)", async () => { process.env.REQUIRE_API_KEY = "true"; const response = await handleChat( @@ -788,8 +793,10 @@ test("chat pipeline rejects requests without a bearer key when strict API key mo ); const json = (await response.json()) as any; - assert.equal(response.status, 401); - assert.match(json.error.message, /Missing API key/i); + // handleChat does not enforce REQUIRE_API_KEY — that's the authz pipeline's job. + // Without provider credentials seeded, the request falls through to the "no credentials" path. + assert.equal(response.status, 400); + assert.match(json.error.message, /No credentials for provider/i); }); test("chat pipeline returns 400 when the model field is omitted", async () => { diff --git a/tests/integration/integration-wiring.test.ts b/tests/integration/integration-wiring.test.ts index a539642a5f..d805c5d99a 100644 --- a/tests/integration/integration-wiring.test.ts +++ b/tests/integration/integration-wiring.test.ts @@ -14,19 +14,19 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, "..", ".."); -function readProjectFile(relPath) { +function readProjectFile(relPath: string) { const full = join(ROOT, relPath); if (!existsSync(full)) return null; return readFileSync(full, "utf8"); } -function assertFileExists(relPath) { +function assertFileExists(relPath: string) { const full = join(ROOT, relPath); assert.ok(existsSync(full), `${relPath} should exist`); return full; } -function assertRouteMethods(relPath, methods) { +function assertRouteMethods(relPath: string, methods: string[]) { const src = readProjectFile(relPath); assert.ok(src, `${relPath} should exist`); for (const method of methods) { @@ -34,7 +34,7 @@ function assertRouteMethods(relPath, methods) { } } -function listProjectFiles(relPath) { +function listProjectFiles(relPath: string): string[] { const full = join(ROOT, relPath); if (!existsSync(full)) return []; @@ -128,23 +128,25 @@ describe("Pipeline Wiring — sse chat handler", () => { }); describe("Pipeline Wiring — middleware proxy", () => { - const src = readProjectFile("src/proxy.ts"); + const proxySrc = readProjectFile("src/proxy.ts"); + const pipelineSrc = readProjectFile("src/server/authz/pipeline.ts"); - it("should exist", () => { - assert.ok(src, "src/proxy.ts should exist"); + it("should exist and delegate to authz pipeline", () => { + assert.ok(proxySrc, "src/proxy.ts should exist"); + assert.match(proxySrc, /runAuthzPipeline/); }); - it("should generate request id for tracing", () => { - assert.match(src, /generateRequestId/); - assert.match(src, /X-Request-Id/); + it("should generate request id for tracing in the authz pipeline", () => { + assert.ok(pipelineSrc, "src/server/authz/pipeline.ts should exist"); + assert.match(pipelineSrc, /generateRequestId|X-Request-Id/); }); - it("should enforce body size guard for API writes", () => { - assert.match(src, /checkBodySize|getBodySizeLimit/); + it("should enforce body size guard in the authz pipeline", () => { + assert.match(pipelineSrc, /checkBodySize|getBodySizeLimit|bodySize/i); }); - it("should resolve JWT secret lazily at request time", () => { - assert.match(src, /function getJwtSecret/); + it("should resolve JWT secret in the authz pipeline", () => { + assert.match(pipelineSrc, /getJwtSecret|jwtSecret|JWT_SECRET/i); }); }); diff --git a/tests/integration/proxy-pipeline.test.ts b/tests/integration/proxy-pipeline.test.ts index 42540df355..942069abfe 100644 --- a/tests/integration/proxy-pipeline.test.ts +++ b/tests/integration/proxy-pipeline.test.ts @@ -402,9 +402,12 @@ describe("CORS — centralized configuration", () => { assert.ok(existsSync(full), "shared/utils/cors.ts should exist"); }); - it("should export CORS_HEADERS and CORS_ORIGIN", () => { + it("should export CORS_HEADERS without a wildcard origin", () => { const src = readSrc("shared/utils/cors.ts"); assert.match(src, /CORS_HEADERS/); - assert.match(src, /CORS_ORIGIN/); + // Extract the CORS_HEADERS object body (between { and }) to avoid matching JSDoc comments + const objMatch = src.match(/CORS_HEADERS\s*=\s*\{([^}]+)\}/); + assert.ok(objMatch, "CORS_HEADERS object should be found"); + assert.doesNotMatch(objMatch[1], /Access-Control-Allow-Origin/); }); }); diff --git a/tests/integration/v1-contracts-behavior.test.ts b/tests/integration/v1-contracts-behavior.test.ts index 54204e7455..486f700470 100644 --- a/tests/integration/v1-contracts-behavior.test.ts +++ b/tests/integration/v1-contracts-behavior.test.ts @@ -8,7 +8,7 @@ test("contract: /api/v1 OPTIONS exposes CORS and allowed methods", async () => { const response = await OPTIONS(); assert.equal(response.status, 200); - assert.ok(response.headers.has("Access-Control-Allow-Origin")); + assert.ok(response.headers.has("Access-Control-Allow-Methods")); }); test("contract: /api/v1/embeddings OPTIONS exposes POST/GET/OPTIONS", async () => { @@ -44,8 +44,8 @@ test("contract: /api/v1 and /api/v1/models return consistent model IDs", async ( assert.ok(Array.isArray(v1Body.data)); assert.ok(Array.isArray(v1ModelsBody.data)); - const v1Ids = [...new Set(v1Body.data.map((item) => item.id))].sort(); - const v1ModelsIds = [...new Set(v1ModelsBody.data.map((item) => item.id))].sort(); + const v1Ids = [...new Set(v1Body.data.map((item: any) => item.id))].sort(); + const v1ModelsIds = [...new Set(v1ModelsBody.data.map((item: any) => item.id))].sort(); assert.deepEqual(v1Ids, v1ModelsIds); }); diff --git a/tests/unit/admin-audit-events.test.ts b/tests/unit/admin-audit-events.test.ts index 79520eb5be..4e902697e8 100644 --- a/tests/unit/admin-audit-events.test.ts +++ b/tests/unit/admin-audit-events.test.ts @@ -118,7 +118,7 @@ test("auth login route records failed password attempts", async () => { assert.equal(event.actor, "anonymous"); assert.equal(event.status, "failed"); assert.equal(event.requestId, "req-auth-failed"); - assert.deepEqual(event.metadata, { reason: "invalid_password" }); + assert.deepEqual(event.metadata, { reason: "invalid_password", lockedOut: false }); }); test("provider create/update/delete routes emit sanitized credential audit events", async () => { diff --git a/tests/unit/api-key-lifecycle.test.ts b/tests/unit/api-key-lifecycle.test.ts new file mode 100644 index 0000000000..5f7f6b6ed2 --- /dev/null +++ b/tests/unit/api-key-lifecycle.test.ts @@ -0,0 +1,119 @@ +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(), "omr-apikey-lifecycle-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); + +const ORIGINAL_OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY; +const ORIGINAL_ROUTER_API_KEY = process.env.ROUTER_API_KEY; + +function reset() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + delete process.env.OMNIROUTE_API_KEY; + delete process.env.ROUTER_API_KEY; +} + +test.beforeEach(() => { + reset(); +}); + +test.after(() => { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_OMNIROUTE_API_KEY === undefined) delete process.env.OMNIROUTE_API_KEY; + else process.env.OMNIROUTE_API_KEY = ORIGINAL_OMNIROUTE_API_KEY; + if (ORIGINAL_ROUTER_API_KEY === undefined) delete process.env.ROUTER_API_KEY; + else process.env.ROUTER_API_KEY = ORIGINAL_ROUTER_API_KEY; +}); + +async function makeKey(name = "lifecycle-test", machineId = "machine-lifecycle") { + const created = await apiKeysDb.createApiKey(name, machineId); + assert.ok(created?.key, "createApiKey returned a key"); + return created; +} + +test("validateApiKey returns true for a fresh active key", async () => { + const created = await makeKey(); + assert.equal(await apiKeysDb.validateApiKey(created.key), true); +}); + +test("validateApiKey rejects revoked keys after revokeApiKey", async () => { + const created = await makeKey(); + assert.equal(await apiKeysDb.validateApiKey(created.key), true); + + const ok = await apiKeysDb.revokeApiKey(created.id); + assert.equal(ok, true); + + assert.equal(await apiKeysDb.validateApiKey(created.key), false); +}); + +test("validateApiKey rejects keys whose expires_at has passed", async () => { + const created = await makeKey(); + const past = new Date(Date.now() - 60_000).toISOString(); + const ok = await apiKeysDb.setApiKeyExpiry(created.id, past); + assert.equal(ok, true); + assert.equal(await apiKeysDb.validateApiKey(created.key), false); +}); + +test("validateApiKey accepts keys with future expires_at", async () => { + const created = await makeKey(); + const future = new Date(Date.now() + 60 * 60_000).toISOString(); + const ok = await apiKeysDb.setApiKeyExpiry(created.id, future); + assert.equal(ok, true); + assert.equal(await apiKeysDb.validateApiKey(created.key), true); +}); + +test("validateApiKey rejects deactivated keys (is_active=false)", async () => { + const created = await makeKey(); + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { isActive: false }); + assert.equal(ok, true); + assert.equal(await apiKeysDb.validateApiKey(created.key), false); +}); + +test("revokeApiKey is idempotent and returns false for missing id", async () => { + const created = await makeKey(); + assert.equal(await apiKeysDb.revokeApiKey(created.id), true); + assert.equal(await apiKeysDb.revokeApiKey(created.id), true); + assert.equal(await apiKeysDb.revokeApiKey("00000000-0000-0000-0000-000000000000"), false); +}); + +test("getApiKeyMetadata exposes lifecycle and policy fields", async () => { + const created = await makeKey(); + await apiKeysDb.setApiKeyExpiry(created.id, new Date(Date.now() + 86_400_000).toISOString()); + + const md = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(md); + assert.equal(md!.isActive, true); + assert.equal(md!.revokedAt, null); + assert.ok(md!.expiresAt && Date.parse(md!.expiresAt) > Date.now()); + assert.deepEqual(md!.ipAllowlist, []); + assert.deepEqual(md!.scopes, []); +}); + +test("validateApiKey accepts configured environment API keys", async () => { + process.env.OMNIROUTE_API_KEY = "sk-env-lifecycle-test"; + assert.equal(await apiKeysDb.validateApiKey("sk-env-lifecycle-test"), true); +}); + +test("validateApiKey updates last_used_at for persisted keys", async () => { + const created = await makeKey(); + + assert.equal(await apiKeysDb.validateApiKey(created.key), true); + + const db = core.getDbInstance() as { + prepare(sql: string): { get(id: string): { last_used_at: string | null } | undefined }; + }; + const row = db.prepare("SELECT last_used_at FROM api_keys WHERE id = ?").get(created.id); + + assert.ok(row?.last_used_at, "last_used_at should be set on successful validation"); + assert.ok(Date.parse(row.last_used_at) > 0, "last_used_at should be an ISO timestamp"); +}); diff --git a/tests/unit/audio-speech-handler.test.ts b/tests/unit/audio-speech-handler.test.ts index 181209dcf6..bdfe41b7bb 100644 --- a/tests/unit/audio-speech-handler.test.ts +++ b/tests/unit/audio-speech-handler.test.ts @@ -62,7 +62,7 @@ test("handleAudioSpeech proxies OpenAI-compatible providers with defaults", asyn }); assert.equal(response.status, 200); assert.equal(response.headers.get("content-type"), "audio/opus"); - assert.ok(response.headers.get("access-control-allow-origin")); + assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/); } finally { globalThis.fetch = originalFetch; } diff --git a/tests/unit/auth-clear-provider-routes.test.ts b/tests/unit/auth-clear-provider-routes.test.ts index 5dfc6b7d2a..a81b13c17d 100644 --- a/tests/unit/auth-clear-provider-routes.test.ts +++ b/tests/unit/auth-clear-provider-routes.test.ts @@ -95,40 +95,13 @@ test("moderations route clears stale provider error metadata on success", async } }); -test("moderations route covers CORS, API key auth, validation, and missing credential branches", async () => { +test("moderations route covers CORS, validation, and missing credential branches", async () => { await resetStorage(); const optionsResponse = await moderationRoute.OPTIONS(); assert.equal(optionsResponse.status, 200); assert.equal(optionsResponse.headers.get("Access-Control-Allow-Methods"), "POST, OPTIONS"); - await withEnv("REQUIRE_API_KEY", "true", async () => { - const missingKeyResponse = await moderationRoute.POST( - new Request("http://localhost/v1/moderations", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ input: "hello" }), - }) - ); - - assert.equal(missingKeyResponse.status, 401); - assert.match(await missingKeyResponse.text(), /Missing API key/i); - - const invalidKeyResponse = await moderationRoute.POST( - new Request("http://localhost/v1/moderations", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer invalid-test-key", - }, - body: JSON.stringify({ input: "hello" }), - }) - ); - - assert.equal(invalidKeyResponse.status, 401); - assert.match(await invalidKeyResponse.text(), /Invalid API key/i); - }); - const invalidJsonResponse = await moderationRoute.POST( new Request("http://localhost/v1/moderations", { method: "POST", diff --git a/tests/unit/auth-terminal-status.test.ts b/tests/unit/auth-terminal-status.test.ts index 66069b8256..51aec91135 100644 --- a/tests/unit/auth-terminal-status.test.ts +++ b/tests/unit/auth-terminal-status.test.ts @@ -10,6 +10,7 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const providersDb = await import("../../src/lib/db/providers.ts"); const auth = await import("../../src/sse/services/auth.ts"); +const accountFallback = await import("../../open-sse/services/accountFallback.ts"); async function resetStorage() { core.resetDbInstance(); @@ -188,6 +189,35 @@ test("markAccountUnavailable marks 403 connections as banned without adding cool assert.ok(!after.rateLimitedUntil); }); +test("markAccountUnavailable keeps Grok Web alias 403 errors mode-local", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "grok-web", + authType: "cookie", + apiKey: "sso=grok-cookie", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable( + (conn as any).id, + 403, + "forbidden", + "gw", + "heavy" + ); + const after = await providersDb.getProviderConnectionById((conn as any).id); + const lockout = accountFallback.getModelLockoutInfo("gw", (conn as any).id, "heavy"); + + assert.equal(result.shouldFallback, true); + assert.ok(result.cooldownMs > 0); + assert.equal(after.testStatus, "active"); + assert.equal(after.lastErrorType, "forbidden"); + assert.ok(!after.rateLimitedUntil); + assert.equal(lockout?.reason, "forbidden"); +}); + test("markAccountUnavailable keeps project-route 403 errors non-terminal", async () => { await resetStorage(); diff --git a/tests/unit/auth/loginGuard.test.ts b/tests/unit/auth/loginGuard.test.ts new file mode 100644 index 0000000000..1dfb82ece1 --- /dev/null +++ b/tests/unit/auth/loginGuard.test.ts @@ -0,0 +1,65 @@ +import { describe, it, beforeEach } from "node:test"; +import assert from "node:assert/strict"; +import { + checkLoginGuard, + clearLoginAttempts, + recordLoginFailure, + resetLoginGuardForTests, + LOGIN_GUARD_TUNABLES, +} from "../../../src/server/auth/loginGuard"; + +describe("loginGuard", () => { + beforeEach(() => { + resetLoginGuardForTests(); + }); + + it("is a no-op when bruteForceProtection is disabled", () => { + for (let i = 0; i < 20; i++) { + const decision = recordLoginFailure("1.2.3.4", { enabled: false }); + assert.equal(decision.allowed, true); + } + assert.equal(checkLoginGuard("1.2.3.4", { enabled: false }).allowed, true); + }); + + it("allows the first attempts up to threshold-1, locks on the threshold hit", () => { + const ip = "10.0.0.1"; + for (let i = 0; i < LOGIN_GUARD_TUNABLES.FAILURE_THRESHOLD - 1; i++) { + const dec = recordLoginFailure(ip, { enabled: true }); + assert.equal(dec.allowed, true, `attempt #${i + 1} should still be allowed`); + } + const lockingHit = recordLoginFailure(ip, { enabled: true }); + assert.equal(lockingHit.allowed, false); + assert.ok((lockingHit.retryAfterSeconds || 0) > 0); + + const subsequent = checkLoginGuard(ip, { enabled: true }); + assert.equal(subsequent.allowed, false); + assert.ok((subsequent.retryAfterSeconds || 0) > 0); + }); + + it("scopes lockouts per IP", () => { + const ipA = "10.0.0.1"; + const ipB = "10.0.0.2"; + for (let i = 0; i < LOGIN_GUARD_TUNABLES.FAILURE_THRESHOLD; i++) { + recordLoginFailure(ipA, { enabled: true }); + } + assert.equal(checkLoginGuard(ipA, { enabled: true }).allowed, false); + assert.equal(checkLoginGuard(ipB, { enabled: true }).allowed, true); + }); + + it("clearLoginAttempts releases the lock for that IP only", () => { + const ip = "10.0.0.7"; + for (let i = 0; i < LOGIN_GUARD_TUNABLES.FAILURE_THRESHOLD; i++) { + recordLoginFailure(ip, { enabled: true }); + } + assert.equal(checkLoginGuard(ip, { enabled: true }).allowed, false); + clearLoginAttempts(ip); + assert.equal(checkLoginGuard(ip, { enabled: true }).allowed, true); + }); + + it("treats null/undefined ip as a single bucket", () => { + for (let i = 0; i < LOGIN_GUARD_TUNABLES.FAILURE_THRESHOLD; i++) { + recordLoginFailure(null, { enabled: true }); + } + assert.equal(checkLoginGuard(undefined, { enabled: true }).allowed, false); + }); +}); diff --git a/tests/unit/authz/classify.test.ts b/tests/unit/authz/classify.test.ts new file mode 100644 index 0000000000..3811697050 --- /dev/null +++ b/tests/unit/authz/classify.test.ts @@ -0,0 +1,190 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { classifyRoute } from "../../../src/server/authz/classify.ts"; +import type { RouteClass } from "../../../src/server/authz/types.ts"; + +interface Case { + name: string; + path: string; + method?: string; + expectedClass: RouteClass; + expectedNormalized?: string; +} + +const cases: Case[] = [ + { name: "root /", path: "/", expectedClass: "MANAGEMENT", expectedNormalized: "/" }, + { name: "dashboard root", path: "/dashboard", expectedClass: "MANAGEMENT" }, + { name: "dashboard nested", path: "/dashboard/settings", expectedClass: "MANAGEMENT" }, + { name: "dashboard onboarding", path: "/dashboard/onboarding", expectedClass: "MANAGEMENT" }, + + { + name: "/api/v1 base", + path: "/api/v1", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1", + }, + { + name: "/api/v1/chat/completions", + path: "/api/v1/chat/completions", + expectedClass: "CLIENT_API", + }, + { name: "/api/v1/responses", path: "/api/v1/responses", expectedClass: "CLIENT_API" }, + { name: "/api/v1/models", path: "/api/v1/models", expectedClass: "CLIENT_API" }, + { name: "/api/v1/embeddings", path: "/api/v1/embeddings", expectedClass: "CLIENT_API" }, + { name: "/api/v1/files", path: "/api/v1/files", expectedClass: "CLIENT_API" }, + { name: "/api/v1/batches", path: "/api/v1/batches", expectedClass: "CLIENT_API" }, + { name: "/api/v1/ws", path: "/api/v1/ws", expectedClass: "CLIENT_API" }, + + { + name: "/v1 alias", + path: "/v1", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1", + }, + { + name: "/v1/chat/completions alias", + path: "/v1/chat/completions", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/chat/completions", + }, + { + name: "/v1/v1 double-prefix", + path: "/v1/v1", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1", + }, + { + name: "/v1/v1/embeddings double-prefix", + path: "/v1/v1/embeddings", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/embeddings", + }, + { + name: "/chat/completions alias", + path: "/chat/completions", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/chat/completions", + }, + { + name: "/responses alias", + path: "/responses", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/responses", + }, + { + name: "/responses/abc alias", + path: "/responses/abc", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/responses/abc", + }, + { + name: "/codex/* alias collapses to /api/v1/responses", + path: "/codex/anything", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/responses", + }, + { + name: "/models alias", + path: "/models", + expectedClass: "CLIENT_API", + expectedNormalized: "/api/v1/models", + }, + + { + name: "/api/auth/login is PUBLIC", + path: "/api/auth/login", + method: "POST", + expectedClass: "PUBLIC", + }, + { + name: "/api/auth/logout is PUBLIC", + path: "/api/auth/logout", + method: "POST", + expectedClass: "PUBLIC", + }, + { + name: "/api/auth/status is PUBLIC", + path: "/api/auth/status", + method: "GET", + expectedClass: "PUBLIC", + }, + { name: "/api/init is PUBLIC", path: "/api/init", method: "POST", expectedClass: "PUBLIC" }, + { + name: "/api/monitoring/health is PUBLIC", + path: "/api/monitoring/health", + method: "GET", + expectedClass: "PUBLIC", + }, + { + name: "/api/cloud/* is PUBLIC", + path: "/api/cloud/something", + method: "GET", + expectedClass: "PUBLIC", + }, + { + name: "/api/oauth/* is PUBLIC", + path: "/api/oauth/callback", + method: "GET", + expectedClass: "PUBLIC", + }, + { + name: "/api/sync/bundle is PUBLIC", + path: "/api/sync/bundle", + method: "POST", + expectedClass: "PUBLIC", + }, + { + name: "/api/settings/require-login GET is PUBLIC readonly", + path: "/api/settings/require-login", + method: "GET", + expectedClass: "PUBLIC", + }, + { + name: "/api/settings/require-login POST is MANAGEMENT", + path: "/api/settings/require-login", + method: "POST", + expectedClass: "MANAGEMENT", + }, + + { + name: "/api/providers/* MUST stay MANAGEMENT", + path: "/api/providers/openai", + expectedClass: "MANAGEMENT", + }, + { name: "/api/keys MANAGEMENT", path: "/api/keys", expectedClass: "MANAGEMENT" }, + { name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" }, + { name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" }, + + { + name: "Unknown top-level path defaults MANAGEMENT (fail-closed)", + path: "/totally-unknown", + expectedClass: "MANAGEMENT", + }, +]; + +for (const c of cases) { + test(`classifyRoute: ${c.name}`, () => { + const r = classifyRoute(c.path, c.method ?? "GET"); + assert.equal(r.routeClass, c.expectedClass, `routeClass for ${c.path}`); + if (c.expectedNormalized) { + assert.equal(r.normalizedPath, c.expectedNormalized, `normalizedPath for ${c.path}`); + } + }); +} + +test("classifyRoute returns deterministic result for trailing slash", () => { + const a = classifyRoute("/api/v1/chat/completions", "POST"); + const b = classifyRoute("/api/v1/chat/completions/", "POST"); + assert.equal(a.routeClass, b.routeClass); + assert.equal(a.normalizedPath, b.normalizedPath); +}); + +test("classifyRoute strips trailing slash on root only when not '/'", () => { + assert.equal(classifyRoute("/").normalizedPath, "/"); +}); + +test("classifyRoute treats /api/v1 prefix exactly", () => { + assert.equal(classifyRoute("/api/v1abc").routeClass, "MANAGEMENT"); + assert.equal(classifyRoute("/api/v1/x").routeClass, "CLIENT_API"); +}); diff --git a/tests/unit/authz/client-api-policy.test.ts b/tests/unit/authz/client-api-policy.test.ts new file mode 100644 index 0000000000..66f1cce323 --- /dev/null +++ b/tests/unit/authz/client-api-policy.test.ts @@ -0,0 +1,153 @@ +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"; +import { SignJWT } from "jose"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-clientapi-policy-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); +const core = await import("../../../src/lib/db/core.ts"); + +const ORIGINAL_OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY; +const ORIGINAL_ROUTER_API_KEY = process.env.ROUTER_API_KEY; +const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; + +function resetStorage() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + delete process.env.OMNIROUTE_API_KEY; + delete process.env.ROUTER_API_KEY; + delete process.env.JWT_SECRET; +} + +test.beforeEach(() => { + resetStorage(); +}); + +test.after(() => { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_OMNIROUTE_API_KEY === undefined) delete process.env.OMNIROUTE_API_KEY; + else process.env.OMNIROUTE_API_KEY = ORIGINAL_OMNIROUTE_API_KEY; + if (ORIGINAL_ROUTER_API_KEY === undefined) delete process.env.ROUTER_API_KEY; + else process.env.ROUTER_API_KEY = ORIGINAL_ROUTER_API_KEY; + if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; +}); + +async function loadPolicy() { + const mod = await import(`../../../src/server/authz/policies/clientApi.ts?ts=${Date.now()}`); + return mod.clientApiPolicy; +} + +function ctx(headers: Headers, method = "POST", normalizedPath = "/api/v1/chat/completions") { + return { + request: { method, headers, url: `http://localhost${normalizedPath}` }, + classification: { + routeClass: "CLIENT_API" as const, + reason: "client_api_v1" as const, + normalizedPath, + }, + requestId: "req_test", + }; +} + +async function dashboardCookie(): Promise { + process.env.JWT_SECRET = "client-api-dashboard-jwt-secret"; + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("1h") + .sign(secret); + return `auth_token=${token}`; +} + +test("clientApiPolicy: missing bearer is rejected with 401", async () => { + const policy = await loadPolicy(); + const out = await policy.evaluate(ctx(new Headers())); + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 401); + assert.equal(out.code, "AUTH_002"); + } +}); + +test("clientApiPolicy: dashboard session can read the model catalog without bearer", async () => { + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ cookie: await dashboardCookie() }), "GET", "/api/v1/models") + ); + + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "dashboard_session"); + assert.equal(out.subject.id, "dashboard"); + } +}); + +test("clientApiPolicy: dashboard session does not bypass non-catalog client API auth", async () => { + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ cookie: await dashboardCookie() }), "POST", "/api/v1/chat/completions") + ); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 401); + assert.equal(out.code, "AUTH_002"); + } +}); + +test("clientApiPolicy: invalid bearer is rejected with 401", async () => { + const policy = await loadPolicy(); + const headers = new Headers({ authorization: "Bearer sk-totally-bogus" }); + const out = await policy.evaluate(ctx(headers)); + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 401); + assert.equal(out.code, "AUTH_002"); + } +}); + +test("clientApiPolicy: valid bearer is accepted as client_api_key subject", async () => { + const created = await apiKeysDb.createApiKey("policy-test-key", "machine-test-1234"); + assert.ok(created?.key, "createApiKey must return a key"); + + const policy = await loadPolicy(); + const headers = new Headers({ authorization: `Bearer ${created.key}` }); + const out = await policy.evaluate(ctx(headers)); + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "client_api_key"); + assert.match(out.subject.id, /^key_/); + } +}); + +test("clientApiPolicy: revoked bearer is rejected", async () => { + const created = await apiKeysDb.createApiKey("policy-revoked-key", "machine-revoked"); + assert.ok(await apiKeysDb.revokeApiKey(created.id)); + + const policy = await loadPolicy(); + const headers = new Headers({ authorization: `Bearer ${created.key}` }); + const out = await policy.evaluate(ctx(headers)); + assert.equal(out.allow, false); +}); + +test("clientApiPolicy: environment API key remains accepted for client API routes", async () => { + process.env.OMNIROUTE_API_KEY = "sk-env-policy-test"; + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ authorization: "Bearer sk-env-policy-test" })) + ); + + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "client_api_key"); + } +}); diff --git a/tests/unit/authz/management-policy.test.ts b/tests/unit/authz/management-policy.test.ts new file mode 100644 index 0000000000..7062001b83 --- /dev/null +++ b/tests/unit/authz/management-policy.test.ts @@ -0,0 +1,121 @@ +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(), "omr-mgmt-policy-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +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 modelSync = await import("../../../src/shared/services/modelSyncScheduler.ts"); + +const ORIGINAL_JWT = process.env.JWT_SECRET; +const ORIGINAL_INITIAL = process.env.INITIAL_PASSWORD; + +function reset() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + delete process.env.JWT_SECRET; + delete process.env.INITIAL_PASSWORD; +} + +test.beforeEach(() => { + reset(); +}); + +test.after(() => { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_JWT === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT; + if (ORIGINAL_INITIAL === undefined) delete process.env.INITIAL_PASSWORD; + else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL; +}); + +async function loadPolicy() { + const mod = await import(`../../../src/server/authz/policies/management.ts?ts=${Date.now()}`); + return mod.managementPolicy; +} + +function ctx(headers: Headers, method = "GET", path = "/api/keys") { + return { + request: { method, headers, url: `http://localhost${path}`, nextUrl: { pathname: path } }, + classification: { + routeClass: "MANAGEMENT" as const, + reason: path.startsWith("/dashboard") + ? ("dashboard_prefix" as const) + : ("management_api" as const), + normalizedPath: path, + }, + requestId: "req_test", + }; +} + +test("managementPolicy: allows when auth not required (no password set)", async () => { + await settingsDb.updateSettings({ requireLogin: true, password: null }); + const policy = await loadPolicy(); + const out = await policy.evaluate(ctx(new Headers())); + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "anonymous"); + assert.equal(out.subject.label, "auth-disabled"); + } +}); + +test("managementPolicy: rejects 401 when auth required and no credentials", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + + const policy = await loadPolicy(); + const out = await policy.evaluate(ctx(new Headers())); + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 401); + assert.equal(out.code, "AUTH_001"); + } +}); + +test("managementPolicy: rejects client API keys for dashboard access", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + const created = await apiKeysDb.createApiKey("dashboard-denied", "machine-dashboard-denied"); + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ authorization: `Bearer ${created.key}` }), "GET", "/dashboard") + ); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 403); + assert.equal(out.code, "AUTH_001"); + } +}); + +test("managementPolicy: allows internal model sync only on the dedicated provider routes", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + + const policy = await loadPolicy(); + const internalHeaders = new Headers(modelSync.buildModelSyncInternalHeaders()); + + const allowed = await policy.evaluate( + ctx(internalHeaders, "POST", "/api/providers/conn-123/sync-models") + ); + assert.equal(allowed.allow, true); + if (allowed.allow) { + assert.equal(allowed.subject.kind, "management_key"); + assert.equal(allowed.subject.id, "model-sync"); + } + + const denied = await policy.evaluate(ctx(internalHeaders, "POST", "/api/keys")); + assert.equal(denied.allow, false); +}); diff --git a/tests/unit/authz/pipeline.test.ts b/tests/unit/authz/pipeline.test.ts new file mode 100644 index 0000000000..2b6e81c8c3 --- /dev/null +++ b/tests/unit/authz/pipeline.test.ts @@ -0,0 +1,194 @@ +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"; +import { SignJWT } from "jose"; +import { NextRequest } from "next/server"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-authz-pipeline-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +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 ORIGINAL_JWT = process.env.JWT_SECRET; +const ORIGINAL_INITIAL = process.env.INITIAL_PASSWORD; +const ORIGINAL_AUTH_COOKIE_SECURE = process.env.AUTH_COOKIE_SECURE; + +function resetEnvironment() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + process.env.JWT_SECRET = "pipeline-jwt-secret"; + process.env.INITIAL_PASSWORD = "pipeline-initial-password"; + delete process.env.AUTH_COOKIE_SECURE; + globalThis.__omnirouteShutdown = { init: false, shuttingDown: false, activeRequests: 0 }; +} + +async function forceAuthRequired() { + await settingsDb.updateSettings({ requireLogin: true }); +} + +async function dashboardCookie(expiresIn = "1h"): Promise { + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(expiresIn) + .sign(secret); + return `auth_token=${token}`; +} + +function request(url: string, init?: RequestInit): NextRequest { + return new NextRequest(url, init); +} + +test.beforeEach(() => { + resetEnvironment(); +}); + +test.after(() => { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_JWT === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT; + if (ORIGINAL_INITIAL === undefined) delete process.env.INITIAL_PASSWORD; + else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL; + if (ORIGINAL_AUTH_COOKIE_SECURE === undefined) delete process.env.AUTH_COOKIE_SECURE; + else process.env.AUTH_COOKIE_SECURE = ORIGINAL_AUTH_COOKIE_SECURE; + globalThis.__omnirouteShutdown = { init: false, shuttingDown: false, activeRequests: 0 }; +}); + +test("runAuthzPipeline redirects root to dashboard before management auth", async () => { + await forceAuthRequired(); + + const response = await pipeline.runAuthzPipeline(request("http://localhost/"), { enforce: true }); + + assert.equal(response.status, 307); + assert.equal(response.headers.get("location"), "http://localhost/dashboard"); +}); + +test("runAuthzPipeline redirects unauthenticated dashboard pages to login", async () => { + await forceAuthRequired(); + + const response = await pipeline.runAuthzPipeline(request("http://localhost/dashboard"), { + enforce: true, + }); + + assert.equal(response.status, 307); + assert.equal(response.headers.get("location"), "http://localhost/login"); + assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT"); + assert.ok(response.headers.get("x-request-id")); +}); + +test("runAuthzPipeline keeps management API rejections as JSON", async () => { + await forceAuthRequired(); + + const response = await pipeline.runAuthzPipeline(request("http://localhost/api/keys"), { + enforce: true, + }); + const body = await response.json(); + + assert.equal(response.status, 401); + assert.equal(response.headers.get("content-type")?.includes("application/json"), true); + assert.equal(body.error.code, "AUTH_001"); +}); + +test("runAuthzPipeline rejects oversized API bodies before auth", async () => { + const response = await pipeline.runAuthzPipeline( + request("http://localhost/api/v1/chat/completions", { + method: "POST", + headers: { + "content-length": String(99 * 1024 * 1024), + origin: "https://app.example.com", + }, + }), + { enforce: true } + ); + + assert.equal(response.status, 413); + assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API"); + assert.ok(response.headers.get("x-request-id")); + assert.equal( + response.headers.get("Access-Control-Allow-Methods"), + "GET, POST, PUT, DELETE, PATCH, OPTIONS" + ); +}); + +test("runAuthzPipeline rejects oversized rewritten alias API bodies before auth", async () => { + const response = await pipeline.runAuthzPipeline( + request("http://localhost/v1/chat/completions", { + method: "POST", + headers: { + "content-length": String(99 * 1024 * 1024), + origin: "https://app.example.com", + }, + }), + { enforce: true } + ); + + assert.equal(response.status, 413); + assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API"); + assert.ok(response.headers.get("x-request-id")); +}); + +test("runAuthzPipeline rejects new API requests during shutdown drain", async () => { + globalThis.__omnirouteShutdown = { init: true, shuttingDown: true, activeRequests: 0 }; + + const response = await pipeline.runAuthzPipeline(request("http://localhost/api/v1/models"), { + enforce: true, + }); + const body = await response.json(); + + assert.equal(response.status, 503); + assert.equal(body.error.code, "SERVICE_UNAVAILABLE"); +}); + +test("runAuthzPipeline rejects rewritten API aliases during shutdown drain", async () => { + globalThis.__omnirouteShutdown = { init: true, shuttingDown: true, activeRequests: 0 }; + + const response = await pipeline.runAuthzPipeline(request("http://localhost/responses"), { + enforce: true, + }); + const body = await response.json(); + + assert.equal(response.status, 503); + assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API"); + assert.equal(body.error.code, "SERVICE_UNAVAILABLE"); +}); + +test("runAuthzPipeline allows dashboard sessions to read model catalog aliases", async () => { + await forceAuthRequired(); + + const response = await pipeline.runAuthzPipeline( + request("http://localhost/v1/models", { + headers: { cookie: await dashboardCookie() }, + }), + { enforce: true } + ); + + assert.equal(response.status, 200); + assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API"); +}); + +test("runAuthzPipeline refreshes dashboard JWTs near expiry", async () => { + await forceAuthRequired(); + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const expiringToken = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("1h") + .sign(secret); + + const response = await pipeline.runAuthzPipeline( + request("http://localhost/dashboard", { + headers: { cookie: `auth_token=${expiringToken}` }, + }), + { enforce: true } + ); + + assert.equal(response.status, 200); + assert.match(response.headers.get("set-cookie") || "", /auth_token=/); +}); diff --git a/tests/unit/authz/public-policy.test.ts b/tests/unit/authz/public-policy.test.ts new file mode 100644 index 0000000000..1ebffc963b --- /dev/null +++ b/tests/unit/authz/public-policy.test.ts @@ -0,0 +1,22 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { publicPolicy } from "../../../src/server/authz/policies/public.ts"; +import type { PolicyContext } from "../../../src/server/authz/context.ts"; + +function ctx(): PolicyContext { + return { + request: { method: "GET", headers: new Headers() }, + classification: { routeClass: "PUBLIC", reason: "public_prefix", normalizedPath: "/api/init" }, + requestId: "req_test", + }; +} + +test("publicPolicy always allows with anonymous subject", async () => { + const out = await publicPolicy.evaluate(ctx()); + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "anonymous"); + assert.equal(out.subject.id, "anonymous"); + } +}); diff --git a/tests/unit/batch_api.test.ts b/tests/unit/batch_api.test.ts index 97b8863f62..afb9103db7 100644 --- a/tests/unit/batch_api.test.ts +++ b/tests/unit/batch_api.test.ts @@ -696,68 +696,6 @@ test("Batch processor fails orphaned finalizing batches during startup recovery" } }); -test("Batch list route rejects missing API key when REQUIRE_API_KEY is enabled", async () => { - const previous = process.env.REQUIRE_API_KEY; - process.env.REQUIRE_API_KEY = "true"; - - try { - const response = await batchesRoute.GET(new Request("http://localhost/api/v1/batches")); - const json = await response.json(); - - assert.strictEqual(response.status, 401); - assert.strictEqual(json.error.message, "Missing API key"); - } finally { - process.env.REQUIRE_API_KEY = previous ?? "false"; - } -}); - -test("Files list route rejects invalid API key when REQUIRE_API_KEY is enabled", async () => { - const previous = process.env.REQUIRE_API_KEY; - process.env.REQUIRE_API_KEY = "true"; - - try { - const response = await filesRoute.GET( - new Request("http://localhost/api/v1/files", { - headers: { Authorization: "Bearer invalid-test-key" }, - }) - ); - const json = await response.json(); - - assert.strictEqual(response.status, 401); - assert.strictEqual(json.error.message, "Invalid API key"); - } finally { - process.env.REQUIRE_API_KEY = previous ?? "false"; - } -}); - -test("Files upload route rejects invalid API key even when auth is optional", async () => { - const previous = process.env.REQUIRE_API_KEY; - process.env.REQUIRE_API_KEY = "false"; - - try { - const formData = new FormData(); - formData.set("purpose", "batch"); - formData.set( - "file", - new File([Buffer.from('{"ok":true}\n')], "input.jsonl", { type: "application/json" }) - ); - - const response = await filesRoute.POST( - new Request("http://localhost/api/v1/files", { - method: "POST", - headers: { Authorization: "Bearer invalid-test-key" }, - body: formData, - }) - ); - const json = await response.json(); - - assert.strictEqual(response.status, 401); - assert.strictEqual(json.error.message, "Invalid API key"); - } finally { - process.env.REQUIRE_API_KEY = previous ?? "false"; - } -}); - test("Files upload route stores multipart content", async () => { const fileContent = '{"custom_id":"req-1"}\n'; const formData = new FormData(); @@ -794,7 +732,7 @@ test("Files and batches routes expose explicit CORS preflight handlers", async ( assert.strictEqual(typeof route.OPTIONS, "function"); const response = await route.OPTIONS(); assert.strictEqual(response.status, 204); - assert.strictEqual(response.headers.get("Access-Control-Allow-Origin"), "*"); + assert.strictEqual(response.headers.get("Access-Control-Allow-Origin"), null); assert.match( String(response.headers.get("Access-Control-Allow-Headers") || ""), /Authorization/i diff --git a/tests/unit/bin-omniroute-mcp.test.ts b/tests/unit/bin-omniroute-mcp.test.ts new file mode 100644 index 0000000000..a2daef2aa6 --- /dev/null +++ b/tests/unit/bin-omniroute-mcp.test.ts @@ -0,0 +1,55 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { pathToFileURL } from "node:url"; +import { join } from "node:path"; +import { platform } from "node:os"; + +describe("bin/omniroute.mjs MCP path handling", () => { + it("pathToFileURL converts Windows paths to valid file:// URLs", () => { + if (platform() !== "win32") { + // Skip on non-Windows platforms + return; + } + + const testPath = "C:\\Users\\test\\projects\\OmniRoute\\bin\\mcp-server.mjs"; + const fileUrl = pathToFileURL(testPath); + + assert.ok(fileUrl.href.startsWith("file:///"), "URL should start with file:///"); + assert.ok(fileUrl.href.includes("C:/"), "Windows drive letter should be converted"); + assert.ok(!fileUrl.href.includes("\\"), "Backslashes should be converted to forward slashes"); + assert.ok(fileUrl.href.endsWith("mcp-server.mjs"), "Filename should be preserved"); + }); + + it("pathToFileURL converts Unix paths to valid file:// URLs", () => { + if (platform() === "win32") { + // Skip on Windows + return; + } + + const testPath = "/home/user/projects/OmniRoute/bin/mcp-server.mjs"; + const fileUrl = pathToFileURL(testPath); + + assert.ok(fileUrl.href.startsWith("file:///"), "URL should start with file:///"); + assert.ok(fileUrl.href.endsWith("mcp-server.mjs"), "Filename should be preserved"); + }); + + it("pathToFileURL handles relative paths correctly", () => { + const relativePath = join("bin", "mcp-server.mjs"); + const absolutePath = join(process.cwd(), relativePath); + const fileUrl = pathToFileURL(absolutePath); + + assert.ok(fileUrl.href.startsWith("file:///"), "URL should start with file:///"); + assert.ok(fileUrl.href.endsWith("mcp-server.mjs"), "Filename should be preserved"); + }); + + it("pathToFileURL result can be used with dynamic import", async () => { + // This test verifies that the URL format is compatible with import() + const testPath = join(process.cwd(), "package.json"); + const fileUrl = pathToFileURL(testPath); + + // Verify the URL is valid for import (we use a JSON file as a safe test) + assert.ok(fileUrl.href.startsWith("file:///"), "URL should be valid for import"); + const parsedUrl = new URL(fileUrl.href); + assert.equal(parsedUrl.protocol, "file:", "URL should be parseable"); + }); +}); diff --git a/tests/unit/body-read-timeout.test.ts b/tests/unit/body-read-timeout.test.ts new file mode 100644 index 0000000000..a96f3b0802 --- /dev/null +++ b/tests/unit/body-read-timeout.test.ts @@ -0,0 +1,54 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { withBodyTimeout } from "../../open-sse/utils/stream.ts"; + +test("withBodyTimeout resolves with the value when the promise completes before timeout", async () => { + const result = await withBodyTimeout(Promise.resolve("hello"), 5000); + assert.equal(result, "hello"); +}); + +test("withBodyTimeout rejects with BodyTimeoutError when the promise exceeds the timeout", async () => { + const neverResolves = new Promise(() => {}); + await assert.rejects(withBodyTimeout(neverResolves, 50), (error) => { + assert.ok(error instanceof Error); + assert.equal(error.name, "BodyTimeoutError"); + assert.match(error.message, /body read timeout after 50ms/); + return true; + }); +}); + +test("withBodyTimeout forwards the original rejection when the promise rejects before timeout", async () => { + const originalError = new Error("network failure"); + const rejects = Promise.reject(originalError); + await assert.rejects(withBodyTimeout(rejects, 5000), (error) => { + assert.equal(error, originalError); + return true; + }); +}); + +test("withBodyTimeout with timeoutMs=0 skips timeout and passes through directly", async () => { + // A promise that would timeout with any positive timeout + const slow = new Promise((resolve) => setTimeout(() => resolve("late"), 200)); + // With timeoutMs=0, it should wait for the promise naturally + const result = await withBodyTimeout(slow, 0); + assert.equal(result, "late"); +}); + +test("withBodyTimeout cleans up the timer after successful resolution", async () => { + const result = await withBodyTimeout(Promise.resolve(42), 100); + assert.equal(result, 42); + // If the timer wasn't cleaned up, the process might hang or throw later. + // Wait a bit to confirm no stray timer fires. + await new Promise((resolve) => setTimeout(resolve, 150)); +}); + +test("withBodyTimeout cleans up the timer after rejection", async () => { + const originalError = new Error("fail"); + await assert.rejects( + withBodyTimeout(Promise.reject(originalError), 100), + (error) => error === originalError + ); + // Wait to confirm no stray timer + await new Promise((resolve) => setTimeout(resolve, 150)); +}); diff --git a/tests/unit/body-timeout-integration.test.ts b/tests/unit/body-timeout-integration.test.ts new file mode 100644 index 0000000000..d2bcc849aa --- /dev/null +++ b/tests/unit/body-timeout-integration.test.ts @@ -0,0 +1,155 @@ +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"; + +// ── 1. FETCH_BODY_TIMEOUT_MS constant export validation ────────────────── + +test("FETCH_BODY_TIMEOUT_MS is exported from open-sse/config/constants and is a positive number", async () => { + const constants = await import("../../open-sse/config/constants.ts"); + assert.ok("FETCH_BODY_TIMEOUT_MS" in constants, "FETCH_BODY_TIMEOUT_MS should be exported"); + assert.equal(typeof constants.FETCH_BODY_TIMEOUT_MS, "number"); + assert.ok(constants.FETCH_BODY_TIMEOUT_MS > 0, "should be a positive number"); +}); + +test("FETCH_BODY_TIMEOUT_MS defaults to FETCH_TIMEOUT_MS when no env override", async () => { + const constants = await import("../../open-sse/config/constants.ts"); + assert.equal( + constants.FETCH_BODY_TIMEOUT_MS, + constants.FETCH_TIMEOUT_MS, + "FETCH_BODY_TIMEOUT_MS should default to FETCH_TIMEOUT_MS" + ); +}); + +// ── 2. BodyTimeoutError classification in chatCore ────────────────────── + +test("chatCore error classification maps BodyTimeoutError to 504 GATEWAY_TIMEOUT", () => { + // Read the source to verify the error classification logic includes BodyTimeoutError + const content = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8"); + + // The error classification block should include BodyTimeoutError alongside TimeoutError + const classificationPattern = + /error\.name === ["']TimeoutError["']\s*\|\|\s*error\.name === ["']BodyTimeoutError["']/; + assert.ok( + classificationPattern.test(content), + "chatCore should classify BodyTimeoutError as GATEWAY_TIMEOUT (504)" + ); +}); + +test("chatCore catch block decrements pending requests for all error types", () => { + const content = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8"); + + // The catch block should call trackPendingRequest with false before error classification + const catchBlockPattern = /catch\s*\(error\)\s*\{[^}]*trackPendingRequest\([^)]*,\s*false\)/; + assert.ok( + catchBlockPattern.test(content), + "chatCore catch block should decrement pending requests" + ); +}); + +test("withBodyTimeout error name is BodyTimeoutError", async () => { + const { withBodyTimeout } = await import("../../open-sse/utils/stream.ts"); + + const neverResolves = new Promise(() => {}); + try { + await withBodyTimeout(neverResolves, 20); + assert.fail("should have thrown"); + } catch (error) { + assert.ok(error instanceof Error); + assert.equal(error.name, "BodyTimeoutError"); + } +}); + +// ── 3. BodyTimeoutError triggers correct pending request decrement ────── + +test("BodyTimeoutError from withBodyTimeout does not leave timer leaks", async () => { + const { withBodyTimeout } = await import("../../open-sse/utils/stream.ts"); + + // Fire multiple short timeouts to ensure no timer accumulation + const promises = Array.from({ length: 10 }, () => + withBodyTimeout(new Promise(() => {}), 10).catch(() => {}) + ); + await Promise.all(promises); + + // Wait a bit to ensure all timers are cleaned up + await new Promise((resolve) => setTimeout(resolve, 100)); + + // If timers leaked, the process would hang or show warnings — this test + // passing confirms proper cleanup under concurrent timeout scenarios. + assert.ok(true, "all timers cleaned up successfully"); +}); + +// ── 4. DELETE /api/logs/active endpoint ────────────────────────────────── + +test("DELETE /api/logs/active route requires management authentication", () => { + const content = fs.readFileSync("src/app/api/logs/active/route.ts", "utf8"); + + assert.ok( + content.includes('from "@/lib/api/requireManagementAuth"'), + "should import requireManagementAuth" + ); + assert.ok(content.includes("export async function DELETE"), "should export DELETE handler"); + assert.ok( + content.includes("await requireManagementAuth(request)"), + "DELETE should check management auth" + ); +}); + +test("DELETE /api/logs/active calls clearPendingRequests", () => { + const content = fs.readFileSync("src/app/api/logs/active/route.ts", "utf8"); + + assert.ok( + content.includes("clearPendingRequests"), + "DELETE handler should call clearPendingRequests" + ); + assert.ok( + content.includes("Pending request counts cleared"), + "DELETE handler should return success message" + ); +}); + +// ── 5. clearPendingRequests + trackPendingRequest integration ──────────── + +test("trackPendingRequest followed by clearPendingRequests zeroes all counts", async () => { + const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); + + usageHistory.trackPendingRequest("m1", "p1", "c1", true); + usageHistory.trackPendingRequest("m1", "p1", "c1", true); + usageHistory.trackPendingRequest("m2", "p2", "c2", true); + + const before = usageHistory.getPendingRequests(); + assert.equal(before.byModel["m1 (p1)"], 2); + assert.equal(before.byModel["m2 (p2)"], 1); + + usageHistory.clearPendingRequests(); + + const after = usageHistory.getPendingRequests(); + assert.equal(Object.keys(after.byModel).length, 0); + assert.equal(Object.keys(after.byAccount).length, 0); + assert.equal(Object.keys(after.details).length, 0); +}); + +test("clearPendingRequests allows subsequent tracking to work correctly", async () => { + const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); + + usageHistory.trackPendingRequest("m1", "p1", "c1", true); + usageHistory.clearPendingRequests(); + + // After clearing, tracking should increment from 0 + usageHistory.trackPendingRequest("m3", "p3", "c3", true); + usageHistory.trackPendingRequest("m3", "p3", "c3", true); + + const pending = usageHistory.getPendingRequests(); + assert.equal(pending.byModel["m3 (p3)"], 2); + assert.ok(pending.details["c3"]); + + // Decrement should also work + usageHistory.trackPendingRequest("m3", "p3", "c3", false); + assert.equal(pending.byModel["m3 (p3)"], 1); + + // Final decrement should clear details + usageHistory.trackPendingRequest("m3", "p3", "c3", false); + assert.equal(pending.byModel["m3 (p3)"], 0); + assert.equal(pending.details["c3"], undefined); +}); diff --git a/tests/unit/call-log-cap.test.ts b/tests/unit/call-log-cap.test.ts index 7b3ac1fb0c..ed8d108238 100644 --- a/tests/unit/call-log-cap.test.ts +++ b/tests/unit/call-log-cap.test.ts @@ -9,6 +9,8 @@ process.env.DATA_DIR = TEST_DATA_DIR; process.env.CALL_LOG_RETENTION_DAYS = "3650"; process.env.CALL_LOG_MAX_ENTRIES = "100"; +const ORIGINAL_CALL_LOG_PIPELINE_MAX_SIZE_KB = process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB; + const core = await import("../../src/lib/db/core.ts"); const callLogs = await import("../../src/lib/usage/callLogs.ts"); const detailedLogs = await import("../../src/lib/db/detailedLogs.ts"); @@ -19,6 +21,14 @@ async function resetStorage() { fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } +function restorePipelineEnv() { + if (ORIGINAL_CALL_LOG_PIPELINE_MAX_SIZE_KB === undefined) { + delete process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB; + } else { + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = ORIGINAL_CALL_LOG_PIPELINE_MAX_SIZE_KB; + } +} + function insertCallLog(row) { const db = core.getDbInstance(); db.prepare( @@ -66,11 +76,13 @@ function insertCallLog(row) { } test.beforeEach(async () => { + restorePipelineEnv(); process.env.CALL_LOG_RETENTION_DAYS = "3650"; await resetStorage(); }); test.after(() => { + restorePipelineEnv(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); @@ -541,6 +553,122 @@ test("saveCallLog omits oversized non-stream pipeline payloads to enforce artifa }); }); +test("saveCallLog honors CALL_LOG_PIPELINE_MAX_SIZE_KB for pipeline artifacts", async () => { + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = "8"; + const hugePayload = "x".repeat(32 * 1024); + + await callLogs.saveCallLog({ + id: "configured-pipeline-artifact-cap", + timestamp: "2026-03-31T10:07:00.000Z", + method: "POST", + path: "/v1/chat/completions", + status: 200, + model: "openai/gpt-4.1", + provider: "openai", + requestBody: { payload: "request" }, + responseBody: { output: "response" }, + pipelinePayloads: { + providerRequest: { body: hugePayload }, + providerResponse: { body: hugePayload }, + }, + }); + + const db = core.getDbInstance(); + const row = db + .prepare( + ` + SELECT artifact_relpath, artifact_size_bytes, detail_state + FROM call_logs WHERE id = ? + ` + ) + .get("configured-pipeline-artifact-cap"); + assert.equal((row as any).detail_state, "ready"); + assert.ok((row as any).artifact_size_bytes <= 8 * 1024); + + const artifactPath = path.join(TEST_DATA_DIR, "call_logs", (row as any).artifact_relpath); + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + assert.deepEqual(artifact.pipeline, { + error: { + _omniroute_truncated: true, + reason: "call_log_artifact_size_limit_exceeded", + }, + }); +}); + +test("saveCallLog falls back to a compact sentinel when the configured cap is very small", async () => { + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = "1"; + const hugePayload = "x".repeat(32 * 1024); + + await callLogs.saveCallLog({ + id: "tiny-pipeline-artifact-cap", + timestamp: "2026-03-31T10:07:30.000Z", + method: "POST", + path: "/v1/chat/completions", + status: 200, + model: `openai/${"gpt".repeat(512)}`, + provider: "openai", + requestBody: { payload: "request" }, + responseBody: { output: "response" }, + pipelinePayloads: { + providerRequest: { body: hugePayload }, + providerResponse: { body: hugePayload }, + }, + }); + + const db = core.getDbInstance(); + const row = db + .prepare( + ` + SELECT artifact_relpath, artifact_size_bytes, detail_state + FROM call_logs WHERE id = ? + ` + ) + .get("tiny-pipeline-artifact-cap"); + assert.equal((row as any).detail_state, "ready"); + assert.ok((row as any).artifact_size_bytes <= 1024); + + const artifactPath = path.join(TEST_DATA_DIR, "call_logs", (row as any).artifact_relpath); + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + assert.deepEqual(artifact, { + schemaVersion: 4, + _omniroute_truncated: true, + reason: "call_log_artifact_size_limit_exceeded", + }); +}); + +test("CALL_LOG_PIPELINE_MAX_SIZE_KB does not cap artifacts without pipeline details", async () => { + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = "8"; + const requestBody = { payload: "x".repeat(16 * 1024) }; + + await callLogs.saveCallLog({ + id: "non-pipeline-artifact-ignores-pipeline-cap", + timestamp: "2026-03-31T10:08:00.000Z", + method: "POST", + path: "/v1/chat/completions", + status: 200, + model: "openai/gpt-4.1", + provider: "openai", + requestBody, + responseBody: { output: "response" }, + }); + + const db = core.getDbInstance(); + const row = db + .prepare( + ` + SELECT artifact_relpath, artifact_size_bytes, detail_state + FROM call_logs WHERE id = ? + ` + ) + .get("non-pipeline-artifact-ignores-pipeline-cap"); + assert.equal((row as any).detail_state, "ready"); + assert.ok((row as any).artifact_size_bytes > 8 * 1024); + + const artifactPath = path.join(TEST_DATA_DIR, "call_logs", (row as any).artifact_relpath); + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + assert.equal(artifact.requestBody.payload.length, requestBody.payload.length); +}); + test("saveCallLog logs and returns when sqlite persistence throws unexpectedly", async () => { const db = core.getDbInstance(); const originalPrepare = db.prepare; diff --git a/tests/unit/cc-compatible-provider.test.ts b/tests/unit/cc-compatible-provider.test.ts index af9b47d59a..94378df356 100644 --- a/tests/unit/cc-compatible-provider.test.ts +++ b/tests/unit/cc-compatible-provider.test.ts @@ -557,6 +557,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur assert.equal(calls.length, 1); assert.equal(calls[0].headers.Accept, "application/json"); assert.equal(calls[0].body.stream, true); + assert.equal(calls[0].body.stream_options, undefined); assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false); const payload = (await result.response.json()) as any; diff --git a/tests/unit/chat-cooldown-aware-retry.test.ts b/tests/unit/chat-cooldown-aware-retry.test.ts index cab0138158..4eda86f660 100644 --- a/tests/unit/chat-cooldown-aware-retry.test.ts +++ b/tests/unit/chat-cooldown-aware-retry.test.ts @@ -3,8 +3,11 @@ import assert from "node:assert/strict"; import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts"; +process.env.STREAM_IDLE_TIMEOUT_MS = "50"; + const harness = await createChatPipelineHarness("chat-cooldown-aware-retry"); const auth = await import("../../src/sse/services/auth.ts"); +const { getProviderConnectionById } = await import("../../src/lib/db/providers.ts"); const { BaseExecutor, buildOpenAIResponse, @@ -14,6 +17,7 @@ const { seedConnection, settingsDb, } = harness; +const textEncoder = new TextEncoder(); const originalRetryConfig = { maxAttempts: BaseExecutor.RETRY_CONFIG.maxAttempts, delayMs: BaseExecutor.RETRY_CONFIG.delayMs, @@ -30,6 +34,21 @@ function buildRequestWithSignal(body, signal) { }); } +function buildZombieSseResponse() { + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(textEncoder.encode(": keepalive\n\n")); + controller.enqueue(textEncoder.encode(`data: ${JSON.stringify({ type: "ping" })}\n\n`)); + }, + }), + { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + } + ); +} + test.beforeEach(async () => { BaseExecutor.RETRY_CONFIG.maxAttempts = originalRetryConfig.maxAttempts; BaseExecutor.RETRY_CONFIG.delayMs = 0; @@ -49,7 +68,7 @@ test.after(async () => { test("handleChat waits for a short cooldown and retries once within the configured budget", async () => { await seedConnection("openai", { apiKey: "sk-openai-cooldown-short", - rateLimitedUntil: new Date(Date.now() + 350).toISOString(), + rateLimitedUntil: new Date(Date.now() + 950).toISOString(), lastError: "short cooldown window", errorCode: 429, }); @@ -222,6 +241,46 @@ test("handleChat returns model_cooldown when every credential for the requested assert.ok(Number(response.headers.get("Retry-After")) >= 1); }); +test("handleChat returns stream readiness timeout without entering cooldown-aware retry or account lockout", async () => { + const connection = await seedConnection("openai", { + apiKey: "sk-openai-stream-readiness-timeout", + }); + await settingsDb.updateSettings({ + requestRetry: 1, + maxRetryIntervalSec: 10, + }); + + let fetchCalls = 0; + globalThis.fetch = async () => { + fetchCalls += 1; + return buildZombieSseResponse(); + }; + + const startedAt = Date.now(); + const response = await handleChat( + buildRequest({ + body: { + model: "openai/gpt-4o-mini", + stream: true, + messages: [{ role: "user", content: "trigger zombie stream" }], + }, + }) + ); + const elapsedMs = Date.now() - startedAt; + const body = (await response.json()) as any; + + assert.equal(response.status, 504); + assert.equal(fetchCalls, 1); + assert.ok(elapsedMs < 1000, `should not wait for cooldown retry, got ${elapsedMs}ms`); + assert.equal(body.error.code, "STREAM_READINESS_TIMEOUT"); + + const refreshedConnection = (await getProviderConnectionById((connection as any).id)) as any; + assert.equal(refreshedConnection.testStatus, "active"); + assert.ok(refreshedConnection.rateLimitedUntil == null); + assert.ok(refreshedConnection.errorCode == null); + assert.equal(refreshedConnection.backoffLevel, 0); +}); + test("handleChat aborts the pending cooldown wait when the client disconnects", async () => { await seedConnection("openai", { apiKey: "sk-openai-cooldown-abort", diff --git a/tests/unit/chat-route-coverage.test.ts b/tests/unit/chat-route-coverage.test.ts index 2a0a50468e..471b1a6ac0 100644 --- a/tests/unit/chat-route-coverage.test.ts +++ b/tests/unit/chat-route-coverage.test.ts @@ -163,33 +163,6 @@ test("handleChat treats Accept text/event-stream as stream=true and returns a se assert.match(raw, /\[DONE\]/); }); -test("handleChat enforces strict API key mode for missing and invalid keys", async () => { - process.env.REQUIRE_API_KEY = "true"; - - const missing = await handleChat( - buildRequest({ - body: { - model: "openai/gpt-4o-mini", - stream: false, - messages: [{ role: "user", content: "missing auth" }], - }, - }) - ); - const invalid = await handleChat( - buildRequest({ - authKey: "sk-does-not-exist", - body: { - model: "openai/gpt-4o-mini", - stream: false, - messages: [{ role: "user", content: "invalid auth" }], - }, - }) - ); - - assert.equal(missing.status, 401); - assert.equal(invalid.status, 401); -}); - test("handleChat rejects requests without a model", async () => { const response = await handleChat( buildRequest({ diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index 326de77cd0..6eb72e4b0c 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; @@ -48,6 +49,8 @@ const originalFetch = globalThis.fetch; const originalResponsesToOpenAI = getRequestTranslator(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI); const originalSetTimeout = globalThis.setTimeout; const originalBackgroundConfig = getBackgroundDegradationConfig(); +const originalCallLogPipelineCaptureStreamChunks = + process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS; function noopLog() { return { @@ -58,6 +61,15 @@ function noopLog() { }; } +function restorePipelineCaptureEnv() { + if (originalCallLogPipelineCaptureStreamChunks === undefined) { + delete process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS; + } else { + process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS = + originalCallLogPipelineCaptureStreamChunks; + } +} + function toPlainHeaders(headers) { if (!headers) return {}; if (headers instanceof Headers) return Object.fromEntries(headers.entries()); @@ -312,7 +324,7 @@ async function invokeChatCore({ return responseFactory(captured, calls); } - const upstreamStream = String(headers.accept || "") + const upstreamStream = String(headers.Accept || headers.accept || "") .toLowerCase() .includes("text/event-stream"); if (responseFormat === "claude") return buildClaudeResponse(upstreamStream); @@ -353,6 +365,7 @@ async function invokeChatCore({ test.afterEach(async () => { globalThis.fetch = originalFetch; + restorePipelineCaptureEnv(); resetAccountSemaphores(); await waitForAsyncSideEffects(); await resetStorage(); @@ -360,12 +373,36 @@ test.afterEach(async () => { test.after(async () => { globalThis.fetch = originalFetch; + restorePipelineCaptureEnv(); resetAccountSemaphores(); await waitForAsyncSideEffects(); await resetStorage(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); +test("chatCore can disable pipeline stream chunk capture through environment", async () => { + process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS = "false"; + await settingsDb.updateSettings({ call_log_pipeline_enabled: true }); + + const { result } = await invokeChatCore({ + accept: "text/event-stream", + body: { + model: "gpt-4o-mini", + stream: true, + messages: [{ role: "user", content: "stream without chunk logging" }], + }, + }); + + assert.equal(result.success, true); + await result.response.text(); + await waitForAsyncSideEffects(); + + const detail = await waitFor(getLatestCallLog); + assert.ok(detail, "expected call log detail to be persisted"); + assert.ok(detail.pipelinePayloads, "expected pipeline payloads when capture is enabled"); + assert.equal((detail.pipelinePayloads as any).streamChunks, undefined); +}); + test("chatCore keeps Responses-native Codex payloads in native passthrough mode", async () => { const { call, result } = await invokeChatCore({ provider: "codex", @@ -387,7 +424,7 @@ test("chatCore keeps Responses-native Codex payloads in native passthrough mode" assert.match(call.url, /\/responses$/); assert.equal(call.body.input, "ship it"); assert.equal(call.body.instructions, "custom system prompt"); - assert.equal(call.body.store, true); + assert.equal(call.body.store, false); assert.deepEqual(call.body.metadata, { source: "codex-client" }); assert.equal("messages" in call.body, false); }); @@ -1412,37 +1449,46 @@ test("chatCore redirects background utility tasks to a cheaper mapped model", as }); test("chatCore retries Qwen quota 429 responses before succeeding", async () => { - globalThis.setTimeout = (callback, _ms, ...args) => { - callback(...args); - return 0; - }; - - const { calls, result } = await invokeChatCore({ - provider: "qwen", - model: "qwen3-coder", - body: { - model: "qwen3-coder", - stream: false, - messages: [{ role: "user", content: "retry the quota hit" }], - }, - responseFactory(_captured, seenCalls) { - if (seenCalls.length === 1) { - return new Response( - JSON.stringify({ error: { message: "You exceeded your current quota for Qwen." } }), - { - status: 429, - headers: { "Content-Type": "application/json" }, - } - ); + const originalSetTimeout = globalThis.setTimeout; + try { + (globalThis as any).setTimeout = (callback: any, ms: any, ...args: any[]) => { + // Only make Qwen retry delays (≤5s) synchronous; let longer timeouts (e.g. body read) use real setTimeout + if (typeof ms === "number" && ms > 5000) { + return originalSetTimeout(callback, ms, ...args); } - return buildOpenAIResponse(false, "qwen recovered"); - }, - }); + callback(...args); + return 0 as any; + }; - const payload = (await result.response.json()) as any; - assert.equal(result.success, true); - assert.equal(calls.length, 2); - assert.equal(payload.choices[0].message.content, "qwen recovered"); + const { calls, result } = await invokeChatCore({ + provider: "qwen", + model: "qwen3-coder", + body: { + model: "qwen3-coder", + stream: false, + messages: [{ role: "user", content: "retry the quota hit" }], + }, + responseFactory(_captured, seenCalls) { + if (seenCalls.length === 1) { + return new Response( + JSON.stringify({ error: { message: "You exceeded your current quota for Qwen." } }), + { + status: 429, + headers: { "Content-Type": "application/json" }, + } + ); + } + return buildOpenAIResponse(false, "qwen recovered"); + }, + }); + + const payload = (await result.response.json()) as any; + assert.equal(result.success, true); + assert.equal(calls.length, 2); + assert.equal(payload.choices[0].message.content, "qwen recovered"); + } finally { + globalThis.setTimeout = originalSetTimeout; + } }); test("chatCore injects fallback user for Qwen OAuth requests without user", async () => { diff --git a/tests/unit/combo-499-abort.test.ts b/tests/unit/combo-499-abort.test.ts new file mode 100644 index 0000000000..0333fa72ac --- /dev/null +++ b/tests/unit/combo-499-abort.test.ts @@ -0,0 +1,147 @@ +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-499-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-499-test-secret"; + +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); + +const noop = () => {}; +const log = { info: noop, warn: noop, debug: noop, error: noop }; + +function makeCombo(strategy = "priority", models = ["a/model-1", "b/model-2", "c/model-3"]) { + return { + name: "test-combo-499", + strategy, + models: models.map((m) => ({ model: m })), + }; +} + +test("combo loop stops immediately when handleSingleModel returns 499 (client disconnect)", async () => { + let callCount = 0; + const handleSingleModel = async () => { + callCount++; + return new Response("Client disconnected", { status: 499 }); + }; + + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo("priority"), + handleSingleModel, + log, + settings: {}, + allCombos: [], + }); + + // Should stop after the FIRST model — no fallback to model-2 or model-3 + assert.equal(callCount, 1, "should only call handleSingleModel once on 499"); + assert.equal(result.status, 499, "should return 499 status"); +}); + +test("combo loop stops on signal.aborted before trying any model", async () => { + let callCount = 0; + const handleSingleModel = async () => { + callCount++; + return new Response("ok", { status: 200 }); + }; + + const ac = new AbortController(); + ac.abort(); // Pre-abort + + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo("priority"), + handleSingleModel, + log, + settings: {}, + allCombos: [], + signal: ac.signal, + }); + + assert.equal(callCount, 0, "should NOT call handleSingleModel when signal is already aborted"); + assert.equal(result.status, 499); +}); + +test("combo loop with 3 models: 499 on model-1 prevents trying model-2 and model-3", async () => { + const modelsCalled = []; + const handleSingleModel = async (_body, modelStr) => { + modelsCalled.push(modelStr); + return new Response("Client disconnected", { status: 499 }); + }; + + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo("priority", ["provider-a/fast", "provider-b/medium", "provider-c/large"]), + handleSingleModel, + log, + settings: {}, + allCombos: [], + }); + + assert.equal(modelsCalled.length, 1); + assert.ok( + modelsCalled[0].includes("fast"), + `Expected first model to contain 'fast', got '${modelsCalled[0]}'` + ); + assert.equal(result.status, 499); +}); + +test("combo loop does NOT stop on 502 (transient) — tries more than one model", async () => { + let callCount = 0; + const handleSingleModel = async () => { + callCount++; + return new Response("Bad Gateway", { status: 502 }); + }; + + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo("priority", ["a/m1", "b/m2", "c/m3"]), + handleSingleModel, + log, + settings: {}, + allCombos: [], + }); + + // Key assertion: unlike 499, a 502 should try more than 1 model + assert.ok(callCount >= 2, `502 should attempt multiple models, but only tried ${callCount}`); + assert.equal(result.status, 502, "should still return 502 if all models fail"); +}); + +test("signal abort during fallback wait interrupts immediately", async () => { + const ac = new AbortController(); + let callCount = 0; + + const handleSingleModel = async () => { + callCount++; + if (callCount === 1) { + // First call returns 502 (which triggers fallback wait) + return new Response("Bad Gateway", { status: 502 }); + } + return new Response("ok", { status: 200 }); + }; + + // Abort 50ms after start — should interrupt any wait + const timer = setTimeout(() => ac.abort(), 50); + + const startMs = Date.now(); + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo("priority", ["a/m1", "b/m2"]), + handleSingleModel, + log, + settings: { retryDelayMs: 5000 }, // 5s delay would normally be slow + allCombos: [], + signal: ac.signal, + }); + + clearTimeout(timer); + const elapsed = Date.now() - startMs; + + // Should complete in well under 5s — the abort interrupted the fallback wait + assert.ok(elapsed < 2000, `Expected fast abort, but took ${elapsed}ms`); + assert.equal(result.status, 499, "should return 499 after abort during wait"); +}); diff --git a/tests/unit/combo-routing-engine.test.ts b/tests/unit/combo-routing-engine.test.ts index 0cb75bde1a..29fab4fd29 100644 --- a/tests/unit/combo-routing-engine.test.ts +++ b/tests/unit/combo-routing-engine.test.ts @@ -803,6 +803,15 @@ test("shouldFallbackComboBadRequest only flags known provider-scoped 400 pattern assert.equal(shouldFallbackComboBadRequest(400, "无法响应该请求"), true); // Generic "please check" should NOT match (was too broad before) assert.equal(shouldFallbackComboBadRequest(400, "请检查您的参数"), false); + // Anthropic thinking block signature errors (#1696) + assert.equal( + shouldFallbackComboBadRequest( + 400, + "[400]: messages.31.content.0: Invalid `signature` in `thinking` block" + ), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "Invalid signature in thinking block"), true); }); test("handleComboChat accepts binary and Responses-style 200 bodies but falls through malformed success payloads", async () => { diff --git a/tests/unit/combo-stream-readiness-fallback.test.ts b/tests/unit/combo-stream-readiness-fallback.test.ts new file mode 100644 index 0000000000..ed8cdc1e28 --- /dev/null +++ b/tests/unit/combo-stream-readiness-fallback.test.ts @@ -0,0 +1,235 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { handleComboChat } from "../../open-sse/services/combo.ts"; +import { ensureStreamReadiness } from "../../open-sse/utils/streamReadiness.ts"; + +const textEncoder = new TextEncoder(); + +function createLog() { + const entries: any[] = []; + return { + info: (tag: any, msg: any) => entries.push({ level: "info", tag, msg }), + warn: (tag: any, msg: any) => entries.push({ level: "warn", tag, msg }), + error: (tag: any, msg: any) => entries.push({ level: "error", tag, msg }), + debug: (tag: any, msg: any) => entries.push({ level: "debug", tag, msg }), + entries, + }; +} + +function okStreamResponse(content: string): Response { + const body = new ReadableStream({ + start(controller) { + controller.enqueue( + textEncoder.encode( + `data: ${JSON.stringify({ + choices: [{ delta: { role: "assistant", content } }], + })}\n\n` + ) + ); + controller.enqueue( + textEncoder.encode( + `data: ${JSON.stringify({ + choices: [{ delta: {}, finish_reason: "stop" }], + })}\n\n` + ) + ); + controller.enqueue(textEncoder.encode("data: [DONE]\n\n")); + controller.close(); + }, + }); + return new Response(body, { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }); +} + +function zombieStreamResponse(): Response { + const body = new ReadableStream({ + start(controller) { + controller.enqueue(textEncoder.encode(": keepalive\n\n")); + controller.enqueue(textEncoder.encode(`data: ${JSON.stringify({ type: "ping" })}\n\n`)); + // Keep the stream open without useful content, matching HTTP 200 zombie streams. + }, + cancel() {}, + }); + + return new Response(body, { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }); +} + +async function applyStreamReadiness(response: Response): Promise { + const result = await ensureStreamReadiness(response, { + timeoutMs: 10, + provider: "glm", + model: "zombie-model", + log: createLog(), + }); + return result.response; +} + +function errorResponse(status: number, message: string): Response { + return new Response( + JSON.stringify({ error: { message, type: "upstream_error" } }), + { status, headers: { "Content-Type": "application/json" } } + ); +} + +test("combo falls back when first model returns HTTP 200 zombie SSE stream", async () => { + const calls: string[] = []; + const log = createLog(); + + const result = await handleComboChat({ + body: { + stream: true, + messages: [{ role: "user", content: "hello" }], + }, + combo: { + name: "stream-readiness-504-fallback", + strategy: "priority", + models: [ + { model: "glm/zombie-model", weight: 0 }, + { model: "openai/gpt-5.4-mini", weight: 0 }, + ], + config: { maxRetries: 0, retryDelayMs: 0 }, + }, + handleSingleModel: async (_body: any, modelStr: string) => { + calls.push(modelStr); + if (modelStr === "glm/zombie-model") { + return applyStreamReadiness(zombieStreamResponse()); + } + return okStreamResponse("fallback success"); + }, + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + relayOptions: null as any, + }); + + assert.equal(result.ok, true, "combo should succeed via fallback after 504"); + assert.deepEqual(calls, ["glm/zombie-model", "openai/gpt-5.4-mini"]); + assert.ok( + log.entries.some( + (e) => + e.level === "warn" && + e.tag === "COMBO" && + String(e.msg).includes("glm/zombie-model") + ), + "combo should log warning for the failed model" + ); +}); + +test("combo fails when all models return 504", async () => { + const calls: string[] = []; + const log = createLog(); + + const result = await handleComboChat({ + body: { + stream: true, + messages: [{ role: "user", content: "hello" }], + }, + combo: { + name: "all-504-test", + strategy: "priority", + models: [ + { model: "glm/zombie-a", weight: 0 }, + { model: "glm/zombie-b", weight: 0 }, + ], + config: { maxRetries: 0, retryDelayMs: 0 }, + }, + handleSingleModel: async (_body: any, modelStr: string) => { + calls.push(modelStr); + return applyStreamReadiness(zombieStreamResponse()); + }, + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + relayOptions: null as any, + }); + + assert.ok(!result.ok, "combo should fail when all models return 504"); + assert.equal(result.status, 504); + assert.equal(calls.length, 2, "combo should try both models"); +}); + +test("combo retries 504 on same model before falling through (transient retry)", async () => { + const calls: string[] = []; + const log = createLog(); + + const result = await handleComboChat({ + body: { + stream: true, + messages: [{ role: "user", content: "hello" }], + }, + combo: { + name: "retry-504-test", + strategy: "priority", + models: [ + { model: "glm/zombie", weight: 0 }, + { model: "openai/gpt-5.4-mini", weight: 0 }, + ], + config: { maxRetries: 1, retryDelayMs: 0 }, + }, + handleSingleModel: async (_body: any, modelStr: string) => { + calls.push(modelStr); + if (modelStr === "glm/zombie") { + return errorResponse(504, "Stream produced no useful content within 60000ms"); + } + return okStreamResponse("fallback after retries"); + }, + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + relayOptions: null as any, + }); + + assert.equal(result.ok, true, "combo should succeed via fallback after retries"); + const zombieCalls = calls.filter((c) => c === "glm/zombie"); + assert.equal( + zombieCalls.length, + 2, + "combo should retry zombie once before falling through" + ); + assert.ok(calls.includes("openai/gpt-5.4-mini"), "combo should reach fallback model"); +}); + +test("combo does not retry stream readiness timeouts on the same model", async () => { + const calls: string[] = []; + const log = createLog(); + + const result = await handleComboChat({ + body: { + stream: true, + messages: [{ role: "user", content: "hello" }], + }, + combo: { + name: "no-retry-readiness-timeout-test", + strategy: "priority", + models: [ + { model: "glm/zombie", weight: 0 }, + { model: "openai/gpt-5.4-mini", weight: 0 }, + ], + config: { maxRetries: 1, retryDelayMs: 0 }, + }, + handleSingleModel: async (_body: any, modelStr: string) => { + calls.push(modelStr); + if (modelStr === "glm/zombie") { + return applyStreamReadiness(zombieStreamResponse()); + } + return okStreamResponse("fallback without same-model retry"); + }, + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + relayOptions: null as any, + }); + + assert.equal(result.ok, true, "combo should succeed via fallback"); + assert.deepEqual(calls, ["glm/zombie", "openai/gpt-5.4-mini"]); +}); diff --git a/tests/unit/combo-test-route.test.ts b/tests/unit/combo-test-route.test.ts index ecb1cc6875..022732e02c 100644 --- a/tests/unit/combo-test-route.test.ts +++ b/tests/unit/combo-test-route.test.ts @@ -6,15 +6,19 @@ import path from "node:path"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-test-route-")); process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-test-route-secret"; const core = await import("../../src/lib/db/core.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const combosDb = await import("../../src/lib/db/combos.ts"); +const runtimePorts = await import("../../src/lib/runtime/ports.ts"); const route = await import("../../src/app/api/combos/test/route.ts"); const originalFetch = globalThis.fetch; async function resetStorage() { core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } @@ -35,6 +39,10 @@ function makeRequest(comboName = "strict-live-test") { }); } +function expectedInternalUrl(pathname: string): string { + return `http://127.0.0.1:${runtimePorts.getRuntimePorts().apiPort}${pathname}`; +} + test.beforeEach(async () => { globalThis.fetch = originalFetch; await resetStorage(); @@ -125,7 +133,7 @@ test("combo test route marks a model healthy only when it returns assistant text assert.equal(response.status, 200); assert.equal(fetchCalls.length, 1); - assert.equal(fetchCalls[0].url, "http://localhost/v1/chat/completions"); + assert.equal(fetchCalls[0].url, expectedInternalUrl("/v1/chat/completions")); assert.equal(fetchCalls[0].init.headers["X-Internal-Test"], "combo-health-check"); assert.equal(fetchCalls[0].init.headers["X-OmniRoute-No-Cache"], "true"); assert.match(fetchCalls[0].init.headers["X-Request-Id"], /^combo-test-/); @@ -362,7 +370,7 @@ test("combo test route preserves structured step metadata for repeated model/acc assert.equal(body.resolvedByTarget.connectionId, "conn-openai-a"); }); -test("combo test route rejects empty combos and respects forwarded base URLs", async () => { +test("combo test route rejects empty combos and ignores forwarded origins for internal probes", async () => { await createTestCombo([]); const emptyResponse = await route.POST(makeRequest()); @@ -372,6 +380,7 @@ test("combo test route rejects empty combos and respects forwarded base URLs", a await resetStorage(); await createTestCombo(["provider/forwarded"]); + const internalKey = await apiKeysDb.createApiKey("combo-internal", "machine-combo-internal"); const fetchCalls = []; globalThis.fetch = async (url, init = {}) => { @@ -392,7 +401,7 @@ test("combo test route rejects empty combos and respects forwarded base URLs", a method: "POST", headers: { "content-type": "application/json", - "x-forwarded-host": "router.example.com", + "x-forwarded-host": "attacker.example.com", "x-forwarded-proto": "https", }, body: JSON.stringify({ comboName: "strict-live-test" }), @@ -401,7 +410,10 @@ test("combo test route rejects empty combos and respects forwarded base URLs", a assert.equal(forwardedResponse.status, 200); assert.equal(fetchCalls.length, 1); - assert.equal(fetchCalls[0].url, "https://router.example.com/v1/chat/completions"); + assert.equal(fetchCalls[0].url, expectedInternalUrl("/v1/chat/completions")); + assert.equal(fetchCalls[0].init.headers.Authorization, `Bearer ${internalKey.key}`); + assert.equal(new URL(fetchCalls[0].url).hostname, "127.0.0.1"); + assert.notEqual(new URL(fetchCalls[0].url).hostname, "attacker.example.com"); }); test("combo test route handles upstream timeouts and non-JSON error bodies", async () => { diff --git a/tests/unit/cors/origins.test.ts b/tests/unit/cors/origins.test.ts new file mode 100644 index 0000000000..ded9e46366 --- /dev/null +++ b/tests/unit/cors/origins.test.ts @@ -0,0 +1,154 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { NextResponse } from "next/server"; +import { + applyCorsHeaders, + resolveAllowedOrigin, + setRuntimeAllowedOrigins, + STATIC_CORS_HEADERS, +} from "../../../src/server/cors/origins"; + +const ENV_KEYS = ["CORS_ALLOW_ALL", "CORS_ALLOWED_ORIGINS", "CORS_ORIGIN"] as const; + +function snapshotEnv(): Record { + const snap: Record = {}; + for (const key of ENV_KEYS) snap[key] = process.env[key]; + return snap; +} + +function restoreEnv(snap: Record) { + for (const key of ENV_KEYS) { + if (snap[key] === undefined) delete process.env[key]; + else process.env[key] = snap[key]; + } +} + +describe("cors/origins.resolveAllowedOrigin", () => { + let envSnap: Record; + + beforeEach(() => { + envSnap = snapshotEnv(); + for (const key of ENV_KEYS) delete process.env[key]; + setRuntimeAllowedOrigins(""); + }); + + afterEach(() => { + restoreEnv(envSnap); + setRuntimeAllowedOrigins(""); + }); + + it("returns null by default — fail-closed without explicit allowlist", () => { + assert.equal(resolveAllowedOrigin("https://app.example.com"), null); + }); + + it("returns null when origin header is absent", () => { + process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com"; + assert.equal(resolveAllowedOrigin(null), null); + assert.equal(resolveAllowedOrigin(undefined), null); + }); + + it("echoes origin when env allowlist matches (case-insensitive, trailing slash ignored)", () => { + process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com, https://Admin.Example.com/"; + assert.equal(resolveAllowedOrigin("https://app.example.com"), "https://app.example.com"); + assert.equal(resolveAllowedOrigin("https://admin.example.com"), "https://admin.example.com"); + assert.equal(resolveAllowedOrigin("https://other.example.com"), null); + }); + + it("merges env allowlist and runtime allowlist", () => { + process.env.CORS_ALLOWED_ORIGINS = "https://env.example.com"; + setRuntimeAllowedOrigins("https://runtime.example.com"); + assert.equal(resolveAllowedOrigin("https://env.example.com"), "https://env.example.com"); + assert.equal( + resolveAllowedOrigin("https://runtime.example.com"), + "https://runtime.example.com" + ); + assert.equal(resolveAllowedOrigin("https://other.example.com"), null); + }); + + it("CORS_ALLOW_ALL=true echoes any origin (with Vary applied later)", () => { + process.env.CORS_ALLOW_ALL = "true"; + assert.equal( + resolveAllowedOrigin("https://anything.example.com"), + "https://anything.example.com" + ); + }); + + it("CORS_ALLOW_ALL falls back to '*' when no Origin header is present", () => { + process.env.CORS_ALLOW_ALL = "1"; + assert.equal(resolveAllowedOrigin(null), "*"); + }); + + it("legacy CORS_ORIGIN=* behaves like CORS_ALLOW_ALL", () => { + process.env.CORS_ORIGIN = "*"; + assert.equal( + resolveAllowedOrigin("https://anything.example.com"), + "https://anything.example.com" + ); + }); + + it("legacy CORS_ORIGIN= is added to env allowlist", () => { + process.env.CORS_ORIGIN = "https://legacy.example.com"; + assert.equal(resolveAllowedOrigin("https://legacy.example.com"), "https://legacy.example.com"); + assert.equal(resolveAllowedOrigin("https://other.example.com"), null); + }); +}); + +describe("cors/origins.applyCorsHeaders", () => { + let envSnap: Record; + + beforeEach(() => { + envSnap = snapshotEnv(); + for (const key of ENV_KEYS) delete process.env[key]; + setRuntimeAllowedOrigins(""); + }); + + afterEach(() => { + restoreEnv(envSnap); + setRuntimeAllowedOrigins(""); + }); + + it("does not set Allow-Origin when origin is not in allowlist", () => { + const res = NextResponse.json({ ok: true }); + const req = new Request("https://server.example.com/api/v1/chat/completions", { + headers: { Origin: "https://evil.example.com" }, + }); + applyCorsHeaders(res, req); + assert.equal(res.headers.get("Access-Control-Allow-Origin"), null); + assert.match(res.headers.get("Access-Control-Allow-Methods") || "", /OPTIONS/); + }); + + it("echoes origin and sets Vary: Origin when origin is allowed", () => { + process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com"; + const res = NextResponse.json({ ok: true }); + const req = new Request("https://server.example.com/api/v1/chat/completions", { + headers: { Origin: "https://app.example.com" }, + }); + applyCorsHeaders(res, req); + assert.equal(res.headers.get("Access-Control-Allow-Origin"), "https://app.example.com"); + assert.match(res.headers.get("Vary") || "", /Origin/); + }); + + it("reflects requested headers from Access-Control-Request-Headers preflight", () => { + process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com"; + const res = NextResponse.json({ ok: true }); + const req = new Request("https://server.example.com/api/v1/chat/completions", { + method: "OPTIONS", + headers: { + Origin: "https://app.example.com", + "Access-Control-Request-Headers": "x-custom-header, authorization", + }, + }); + applyCorsHeaders(res, req); + assert.equal(res.headers.get("Access-Control-Allow-Headers"), "x-custom-header, authorization"); + }); +}); + +describe("cors/origins.STATIC_CORS_HEADERS", () => { + it("never contains Access-Control-Allow-Origin", () => { + assert.equal( + Object.prototype.hasOwnProperty.call(STATIC_CORS_HEADERS, "Access-Control-Allow-Origin"), + false + ); + assert.match(STATIC_CORS_HEADERS["Access-Control-Allow-Methods"], /OPTIONS/); + }); +}); diff --git a/tests/unit/db-migration-runner.test.ts b/tests/unit/db-migration-runner.test.ts index eae563eb0c..654ccb0da3 100644 --- a/tests/unit/db-migration-runner.test.ts +++ b/tests/unit/db-migration-runner.test.ts @@ -203,6 +203,90 @@ test("runMigrations skips versions that are already tracked as applied", serial, } }); +test( + "runMigrations applies api key lifecycle migration idempotently when columns already exist", + serial, + async () => { + const runner = await importFresh("src/lib/db/migrationRunner.ts"); + const db = createDb(); + + try { + db.exec(` + CREATE TABLE api_keys ( + id TEXT PRIMARY KEY, + key TEXT NOT NULL, + revoked_at TEXT + ); + `); + + const appliedCount = withMockedMigrationFs( + { + "032_apikey_lifecycle.sql": "ALTER TABLE api_keys ADD COLUMN revoked_at TEXT;", + }, + () => runner.runMigrations(db) + ); + + assert.equal(appliedCount, 1); + const columns = db.prepare("PRAGMA table_info(api_keys)").all() as Array<{ name: string }>; + const names = new Set(columns.map((column) => column.name)); + for (const expected of [ + "revoked_at", + "expires_at", + "last_used_at", + "key_prefix", + "ip_allowlist", + "scopes", + ]) { + assert.equal(names.has(expected), true, `${expected} should exist`); + } + assert.deepEqual( + db.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?").get("032"), + { version: "032", name: "apikey_lifecycle" } + ); + } finally { + db.close(); + } + } +); + +test( + "runMigrations applies api key lifecycle hardening by version even if filename suffix changes", + serial, + async () => { + const runner = await importFresh("src/lib/db/migrationRunner.ts"); + const db = createDb(); + + try { + db.exec(` + CREATE TABLE api_keys ( + id TEXT PRIMARY KEY, + key TEXT NOT NULL + ); + `); + + const appliedCount = withMockedMigrationFs( + { + "032_renamed_lifecycle_patch.sql": "ALTER TABLE api_keys ADD COLUMN should_not_run TEXT;", + }, + () => runner.runMigrations(db) + ); + + assert.equal(appliedCount, 1); + const columns = db.prepare("PRAGMA table_info(api_keys)").all() as Array<{ name: string }>; + const names = new Set(columns.map((column) => column.name)); + assert.equal(names.has("revoked_at"), true); + assert.equal(names.has("expires_at"), true); + assert.equal(names.has("should_not_run"), false); + assert.deepEqual( + db.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?").get("032"), + { version: "032", name: "renamed_lifecycle_patch" } + ); + } finally { + db.close(); + } + } +); + test("getMigrationStatus reports applied and pending migrations", serial, async () => { const runner = await importFresh("src/lib/db/migrationRunner.ts"); const db = createDb(); diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index cc3b43272e..78d262bd2b 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { CodexExecutor, + __setCodexWebSocketTransportForTesting, encodeResponseSseEvent, getCodexModelScope, getCodexRateLimitKey, @@ -19,6 +20,7 @@ import { test.afterEach(() => { setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); + __setCodexWebSocketTransportForTesting(undefined); }); async function withEnv(entries, fn) { @@ -62,8 +64,33 @@ test("Codex helper functions isolate rate-limit scopes and parse quota headers", assert.equal(getCodexModelScope("gpt-5.3-codex"), "codex"); assert.equal(getCodexModelScope("gpt-5.5-xhigh"), "codex"); assert.equal(getCodexUpstreamModel("gpt-5.5-xhigh"), "gpt-5.5"); - assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-xhigh", {}), true); - assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-mini", {}), false); + assert.equal(getCodexUpstreamModel("gpt-5.5-medium"), "gpt-5.5"); + // With mock WS transport + codexTransport=websocket, gpt-5.5 models require WS + __setCodexWebSocketTransportForTesting( + async () => ({ send() {}, close() {}, onmessage: null, onerror: null, onclose: null }) as any + ); + assert.equal( + isCodexResponsesWebSocketRequired("gpt-5.5-xhigh", { + providerSpecificData: { codexTransport: "websocket" }, + }), + true + ); + assert.equal( + isCodexResponsesWebSocketRequired("gpt-5.5-medium", { + providerSpecificData: { codexTransport: "websocket" }, + }), + true + ); + assert.equal( + isCodexResponsesWebSocketRequired("gpt-5.5-mini", { + providerSpecificData: { codexTransport: "websocket" }, + }), + true + ); + // Without codexTransport setting, defaults to HTTP (false) + assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-xhigh", {}), false); + assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-medium", {}), false); + __setCodexWebSocketTransportForTesting(undefined); assert.equal(getCodexRateLimitKey("acct-1", "codex-spark-mini"), "acct-1:spark"); assert.equal(quota.usage5h, 100); assert.equal(quota.limit7d, 5000); @@ -108,6 +135,8 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for assert.equal(standardHeaders.Accept, "text/event-stream"); assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1"); assert.equal(standardHeaders.Version, "0.125.0"); + assert.equal(standardHeaders["Openai-Beta"], "responses=experimental"); + assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets"); assert.equal(standardHeaders["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26100; x64)"); assert.equal(compactHeaders.Accept, "application/json"); }); @@ -145,6 +174,7 @@ test("CodexExecutor.transformRequest injects default instructions, clamps reason const body = { model: "gpt-5-mini", messages: [{ role: "user", content: "hello" }], + tools: [{ type: "function", function: { name: "test_tool" } }], prompt: "legacy", stream_options: { include_usage: true }, instructions: "", @@ -159,7 +189,7 @@ test("CodexExecutor.transformRequest injects default instructions, clamps reason }); assert.equal(result.stream, true); - assert.equal(result.store, true); + assert.equal(result.store, false); assert.equal(result.instructions.length > 0, true); assert.equal(result.reasoning.effort, "high"); assert.equal(result.service_tier, "priority"); @@ -188,7 +218,7 @@ test("CodexExecutor.transformRequest preserves compact requests and native passt assert.equal(result.stream, undefined); assert.equal(result.service_tier, "priority"); assert.equal(result.reasoning.effort, "medium"); - assert.equal(result.store, true); + assert.equal(result.store, false); assert.equal(result.instructions, "keep this"); }); @@ -292,6 +322,37 @@ test("CodexExecutor.transformRequest keeps gpt-5.5 as the model and applies xhig assert.equal(result.reasoning.effort, "xhigh"); }); +test("CodexExecutor.execute falls back to HTTP when websocket transport is unavailable", async () => { + __setCodexWebSocketTransportForTesting(null); + const executor = new CodexExecutor(); + const originalFetch = globalThis.fetch; + + globalThis.fetch = async () => + new Response(JSON.stringify({ id: "resp_http_fallback", object: "response" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + + try { + const result = await executor.execute({ + model: "gpt-5.5-xhigh", + body: { model: "gpt-5.5-xhigh", input: [{ role: "user", content: "hello" }] }, + stream: true, + credentials: { + accessToken: "codex-token", + providerSpecificData: { codexTransport: "websocket" }, + }, + }); + + // When WS transport is unavailable, isCodexResponsesWebSocketRequired returns false + // and the executor falls back to HTTP via super.execute() + assert.equal(result.response.status, 200); + assert.equal(result.transformedBody.model, "gpt-5.5"); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("CodexExecutor maps Codex websocket error events to response.failed SSE", () => { const raw = JSON.stringify({ type: "error", diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index a0f630cb93..e856528f7f 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -582,13 +582,62 @@ test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude assert.equal((requestBodies[1] as any).output_config, undefined); }); -test("DefaultExecutor.transformRequest is a passthrough and preserves model ids with slashes", () => { +test("DefaultExecutor.transformRequest injects OpenAI stream usage and preserves model ids with slashes", () => { const executor = new DefaultExecutor("openai"); const body = { model: "zai-org/GLM-5-FP8", messages: [{ role: "user", content: "hi" }] }; const result = executor.transformRequest("zai-org/GLM-5-FP8", body, true, {}); - assert.equal(result, body); + assert.notEqual(result, body); assert.equal(result.model, "zai-org/GLM-5-FP8"); + assert.deepEqual((result as any).stream_options, { include_usage: true }); + assert.equal((body as any).stream_options, undefined); +}); + +test("DefaultExecutor.transformRequest only injects stream usage for OpenAI chat targets", () => { + const openAICompat = new DefaultExecutor("openai-compatible-test"); + const openAIResponsesCompat = new DefaultExecutor("openai-compatible-responses-test"); + + const chatBody = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] }; + const responsesBody = { model: "gpt-4.1", input: "hi" }; + + const chatResult = openAICompat.transformRequest("gpt-4.1", chatBody, true, { + providerSpecificData: { baseUrl: "https://proxy.example/v1" }, + }); + const responsesResult = openAIResponsesCompat.transformRequest("gpt-4.1", responsesBody, true, { + providerSpecificData: { baseUrl: "https://proxy.example/v1" }, + }); + + assert.deepEqual((chatResult as any).stream_options, { include_usage: true }); + assert.equal((responsesResult as any).stream_options, undefined); +}); + +test("DefaultExecutor.transformRequest strips stream_options from Anthropic-compatible targets", () => { + const anthropicCompat = new DefaultExecutor("anthropic-compatible-test"); + const anthropicCcCompat = new DefaultExecutor("anthropic-compatible-cc-test"); + + const anthropicBody = { + model: "claude-sonnet-4-6", + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + stream_options: { include_usage: true }, + }; + const ccBody = { + model: "claude-sonnet-4-6", + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + }; + + const anthropicResult = anthropicCompat.transformRequest( + "claude-sonnet-4-6", + anthropicBody, + true, + {} + ); + const ccResult = anthropicCcCompat.transformRequest("claude-sonnet-4-6", ccBody, true, {}); + + assert.notEqual(anthropicResult, anthropicBody); + assert.equal((anthropicResult as any).stream_options, undefined); + assert.equal((ccResult as any).stream_options, undefined); }); test("DefaultExecutor.transformRequest neutralizes incompatible tool_choice for Qwen thinking", () => { diff --git a/tests/unit/executor-github.test.ts b/tests/unit/executor-github.test.ts index d36f8d8111..592b8f3bc2 100644 --- a/tests/unit/executor-github.test.ts +++ b/tests/unit/executor-github.test.ts @@ -63,9 +63,9 @@ test("GithubExecutor.buildHeaders prefers Copilot token and sets GitHub-specific assert.equal(headers.Authorization, "Bearer copilot-token"); assert.equal(headers.Accept, "text/event-stream"); - assert.equal(headers["editor-version"], "vscode/1.110.0"); - assert.equal(headers["editor-plugin-version"], "copilot-chat/0.38.0"); - assert.equal(headers["user-agent"], "GitHubCopilotChat/0.38.0"); + assert.equal(headers["editor-version"], "vscode/1.117.0"); + assert.equal(headers["editor-plugin-version"], "copilot-chat/0.45.1"); + assert.equal(headers["user-agent"], "GitHubCopilotChat/0.45.1"); assert.equal(headers["x-github-api-version"], "2025-04-01"); assert.equal(headers["openai-intent"], "conversation-panel"); assert.equal(headers["X-Initiator"], "user"); diff --git a/tests/unit/grok-web.test.ts b/tests/unit/grok-web.test.ts index 28b720b606..a9e3e68068 100644 --- a/tests/unit/grok-web.test.ts +++ b/tests/unit/grok-web.test.ts @@ -288,8 +288,9 @@ test("Request: payload has correct model mapping", async () => { signal: AbortSignal.timeout(10000), log: null, }); - assert.equal(cap.body.modelName, "grok-4-1-thinking-1129"); - assert.equal(cap.body.modelMode, "MODEL_MODE_EXPERT"); + assert.equal(cap.body.modeId, "expert"); + assert.equal("modelName" in cap.body, false); + assert.equal("modelMode" in cap.body, false); assert.equal(cap.body.temporary, true); } finally { cap.restore(); @@ -308,8 +309,9 @@ test("Request: grok-4-heavy maps to heavy mode", async () => { signal: AbortSignal.timeout(10000), log: null, }); - assert.equal(cap.body.modelName, "grok-4"); - assert.equal(cap.body.modelMode, "MODEL_MODE_HEAVY"); + assert.equal(cap.body.modeId, "heavy"); + assert.equal("modelName" in cap.body, false); + assert.equal("modelMode" in cap.body, false); } finally { cap.restore(); } @@ -350,14 +352,17 @@ test("Message parsing: combines system + history + user", async () => { test("Provider registry: grok-web has correct models", async () => { const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts"); - const models = PROVIDER_MODELS["grok-web"]; - assert.ok(models, "grok-web should be in PROVIDER_MODELS"); - assert.equal(models.length, 4, `Expected 4 models, got ${models.length}`); + const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); + const models = PROVIDER_MODELS["gw"]; + assert.ok(models, "gw should be in PROVIDER_MODELS"); + assert.equal(models.length, 5, `Expected 5 models, got ${models.length}`); const ids = models.map((m: any) => m.id); + assert.ok(ids.includes("auto")); assert.ok(ids.includes("fast")); assert.ok(ids.includes("expert")); assert.ok(ids.includes("heavy")); assert.ok(ids.includes("grok-420-computer-use-sa")); + assert.equal(getRegistryEntry("grok-web")?.passthroughModels, true); }); // ─── Statsig header ───────────────────────────────────────────────────────── diff --git a/tests/unit/guardrails/visionBridge.test.ts b/tests/unit/guardrails/visionBridge.test.ts index d8b241ad4d..0925bbe8bc 100644 --- a/tests/unit/guardrails/visionBridge.test.ts +++ b/tests/unit/guardrails/visionBridge.test.ts @@ -164,11 +164,10 @@ test("VB-S02: passthroughs for vision-capable model (gpt-4o)", async () => { } }); -test("VB-S02b: forces Vision Bridge for GPT-family models even when model capabilities advertise vision", async () => { +test("VB-S02b: respects native vision support for GPT-family models", async () => { const guardrail = createGuardrail(); - for (const model of ["gpt-5.4", "gpt-5.4-mini", "gpt-4o", "openai/gpt-4o-mini"]) { - mockVisionResponse = `Forced bridge description for ${model}`; + for (const model of ["gpt-5.5", "gpt-5.5-high", "codex/gpt-5.5", "openai/gpt-4o-mini"]) { visionCallCount = 0; const payload = createPayload({ @@ -189,9 +188,13 @@ test("VB-S02b: forces Vision Bridge for GPT-family models even when model capabi const result = await guardrail.preCall(payload, createContext({ model })); - assert.strictEqual(result.block, false, `expected passthrough=false for ${model}`); - assert.ok(result.modifiedPayload, `expected modified payload for ${model}`); - assert.strictEqual(visionCallCount, 1, `expected one forced bridge call for ${model}`); + assert.strictEqual(result.block, false, `expected passthrough for ${model}`); + assert.strictEqual( + result.modifiedPayload, + undefined, + `expected unmodified payload for ${model}` + ); + assert.strictEqual(visionCallCount, 0, `expected no bridge call for ${model}`); } }); diff --git a/tests/unit/guide-settings-route.test.ts b/tests/unit/guide-settings-route.test.ts index ee7d102726..28c4debec5 100644 --- a/tests/unit/guide-settings-route.test.ts +++ b/tests/unit/guide-settings-route.test.ts @@ -51,6 +51,7 @@ test.beforeEach(async () => { // Force XDG_CONFIG_HOME so resolveOpencodeConfigPath resolves to our dummy dir // (CI runners often have XDG_CONFIG_HOME set, causing path mismatch) process.env.XDG_CONFIG_HOME = path.join(DUMMY_HOME, ".config"); + process.env.API_KEY_SECRET = "test-secret"; await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }).catch(() => {}); }); @@ -66,7 +67,7 @@ test("guide-settings POST creates new qwen settings.json if it doesn't exist", a const req = await buildRequest({ baseUrl: "http://my-omni", apiKey: "sk-123", - model: "qwen-max", + model: "gemini-cli/gemini-3.1-pro-preview", }); const response = (await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } })) as Response; const data = (await response.json()) as any; @@ -75,21 +76,11 @@ test("guide-settings POST creates new qwen settings.json if it doesn't exist", a assert.equal(data.success, true); const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8")); - assert.ok(content.modelProviders.openai); - - const omniProvider = content.modelProviders.openai.find( - (p: QwenProviderEntry) => p.baseUrl === "http://my-omni" - ); - assert.ok(omniProvider); - assert.equal(omniProvider.id, "qwen-max"); - assert.equal(omniProvider.baseUrl, "http://my-omni"); - assert.equal(omniProvider.envKey, "OPENAI_API_KEY"); - assert.equal(omniProvider.generationConfig?.contextWindowSize, 200000); - - const envContent = await fs.readFile(QWEN_ENV_PATH, "utf-8"); - assert.match(envContent, /^OPENAI_API_KEY=sk-123$/m); - assert.match(envContent, /^ANTHROPIC_API_KEY=sk-123$/m); - assert.match(envContent, /^GEMINI_API_KEY=sk-123$/m); + // Uses security.auth format (not modelProviders) + assert.equal(content.security?.auth?.selectedType, "openai"); + assert.ok(content.security?.auth?.apiKey.startsWith("sk-")); + assert.equal(content.security?.auth?.baseUrl, "http://my-omni"); + assert.equal(content.model?.name, "gemini-cli/gemini-3.1-pro-preview"); }); test("guide-settings POST merges into existing qwen settings.json", async () => { @@ -97,38 +88,27 @@ test("guide-settings POST merges into existing qwen settings.json", async () => await fs.writeFile( QWEN_CONFIG_PATH, JSON.stringify({ - modelProviders: { - openai: [{ id: "other", baseUrl: "https://other" }], - }, + permissions: { allow: ["Bash(*)"] }, }), "utf-8" ); - const req = await buildRequest({ baseUrl: "http://my-omni", apiKey: "sk-123", model: "auto" }); - const response = (await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } })) as Response; + const req = await buildRequest({ + baseUrl: "http://my-omni", + apiKey: "sk-456", + model: "claude-sonnet-4-6", + }); + const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); assert.equal(response.status, 200); const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8")); - assert.equal(content.modelProviders.openai.length, 2); - - const otherProvider = content.modelProviders.openai.find( - (p: QwenProviderEntry) => p.id === "other" - ); - assert.ok(otherProvider); - assert.equal(otherProvider.baseUrl, "https://other"); - - const omniProvider = content.modelProviders.openai.find( - (p: QwenProviderEntry) => p.baseUrl === "http://my-omni" - ); - assert.ok(omniProvider); - assert.equal(omniProvider.id, "auto"); - assert.equal(omniProvider.envKey, "OPENAI_API_KEY"); - assert.equal(omniProvider.generationConfig?.contextWindowSize, 200000); - - const envContent = await fs.readFile(QWEN_ENV_PATH, "utf-8"); - assert.match(envContent, /^OPENAI_API_KEY=sk-123$/m); - assert.match(envContent, /^ANTHROPIC_API_KEY=sk-123$/m); - assert.match(envContent, /^GEMINI_API_KEY=sk-123$/m); + // security.auth format + assert.equal(content.security?.auth?.selectedType, "openai"); + assert.ok(content.security?.auth?.apiKey.startsWith("sk-")); + assert.equal(content.security?.auth?.baseUrl, "http://my-omni"); + assert.equal(content.model?.name, "claude-sonnet-4-6"); + // Preserves other settings + assert.deepEqual(content.permissions?.allow, ["Bash(*)"]); }); test("guide-settings POST writes OpenCode config with current schema and multi-model selection", async () => { @@ -167,7 +147,7 @@ test("guide-settings POST writes OpenCode config with current schema and multi-m assert.ok(content.provider.custom); assert.equal(content.provider.omniroute.npm, "@ai-sdk/openai-compatible"); assert.equal(content.provider.omniroute.options.baseURL, "http://my-omni/v1"); - assert.equal(content.provider.omniroute.options.apiKey, "sk-123"); + assert.ok(content.provider.omniroute.options.apiKey.startsWith("sk-")); assert.deepEqual(Object.keys(content.provider.omniroute.models), [ "cc/claude-sonnet-4-20250514", "gg/gemini-2.5-pro", @@ -241,7 +221,7 @@ test("guide-settings POST preserves existing OpenCode config fields while only u }); assert.equal(content.provider.omniroute.npm, "@ai-sdk/openai-compatible"); assert.equal(content.provider.omniroute.options.baseURL, "http://my-omni/v1"); - assert.equal(content.provider.omniroute.options.apiKey, "sk-123"); + assert.ok(content.provider.omniroute.options.apiKey.startsWith("sk-")); assert.deepEqual(content.provider.omniroute.models, { "cx/gpt-5.4": { name: "GPT-5.4" }, "opencode-go/kimi-k2.6": { name: "Kimi K2.6" }, diff --git a/tests/unit/log-retention.test.ts b/tests/unit/log-retention.test.ts index 049a80abfe..c8594c8f4e 100644 --- a/tests/unit/log-retention.test.ts +++ b/tests/unit/log-retention.test.ts @@ -187,3 +187,30 @@ test("getCallLogsTableMaxRows returns configured value", async () => { assert.equal(getCallLogsTableMaxRows(), 5); assert.equal(getProxyLogsTableMaxRows(), 5); }); + +test("call log pipeline env helpers parse stream chunk flag and size cap", async () => { + const originalCapture = process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS; + const originalMaxSize = process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB; + const { getCallLogPipelineCaptureStreamChunks, getCallLogPipelineMaxSizeBytes } = + await import("../../src/lib/logEnv.ts"); + + try { + process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS = "false"; + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = "256"; + + assert.equal(getCallLogPipelineCaptureStreamChunks(), false); + assert.equal(getCallLogPipelineMaxSizeBytes(), 256 * 1024); + } finally { + if (originalCapture === undefined) { + delete process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS; + } else { + process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS = originalCapture; + } + + if (originalMaxSize === undefined) { + delete process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB; + } else { + process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB = originalMaxSize; + } + } +}); diff --git a/tests/unit/model-sync-scheduler.test.ts b/tests/unit/model-sync-scheduler.test.ts index ecdadf22d3..1204666683 100644 --- a/tests/unit/model-sync-scheduler.test.ts +++ b/tests/unit/model-sync-scheduler.test.ts @@ -171,14 +171,6 @@ test("initCloudSync skips auto initialization during build and test processes un ); }); -test("proxy: internal model sync token is only allowed for provider model sync routes", () => { - const filePath = path.join(process.cwd(), "src/proxy.ts"); - const source = fs.readFileSync(filePath, "utf8"); - - assert.match(source, /isModelSyncInternalRequest/); - assert.match(source, /sync-models\|models/); -}); - test("modelSyncScheduler starts once, honors env interval and syncs only active autoSync connections", async () => { await providersDb.createProviderConnection({ provider: "openai", diff --git a/tests/unit/modelsDevSync-extended.test.ts b/tests/unit/modelsDevSync-extended.test.ts index b422aac182..d92936e654 100644 --- a/tests/unit/modelsDevSync-extended.test.ts +++ b/tests/unit/modelsDevSync-extended.test.ts @@ -510,7 +510,7 @@ test("startPeriodicSync, stopPeriodicSync, getSyncStatus, and initModelsDevSync modelsDev.startPeriodicSync(99); assert.equal(modelsDev.getSyncStatus().intervalMs, 25); - const syncedAt = await waitFor(() => modelsDev.getSyncStatus().lastSync, 300); + const syncedAt = await waitFor(() => modelsDev.getSyncStatus().lastSync, 2000); assert.ok(syncedAt, "expected initial periodic sync to complete"); assert.ok(modelsDev.getSyncStatus().nextSync); @@ -536,7 +536,7 @@ test("startPeriodicSync, stopPeriodicSync, getSyncStatus, and initModelsDevSync await enabled.initModelsDevSync(); assert.equal(enabled.getSyncStatus().enabled, true); assert.equal(enabled.getSyncStatus().intervalMs, 15); - await waitFor(() => enabled.getSyncStatus().lastSync, 300); + await waitFor(() => enabled.getSyncStatus().lastSync, 2000); }); test("stopPeriodicSync aborts the in-flight initial sync", async () => { diff --git a/tests/unit/moderations-handler.test.ts b/tests/unit/moderations-handler.test.ts index f3b5378b7e..06cff14788 100644 --- a/tests/unit/moderations-handler.test.ts +++ b/tests/unit/moderations-handler.test.ts @@ -70,7 +70,8 @@ test("handleModeration proxies successful requests with default model and access input: "all clear", }); assert.equal(response.status, 200); - assert.equal(response.headers.get("access-control-allow-origin"), "*"); + assert.equal(response.headers.get("access-control-allow-origin"), null); + assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/); assert.deepEqual(await response.json(), { id: "modr-1", results: [{ flagged: false }], @@ -92,7 +93,8 @@ test("handleModeration returns upstream error payloads with CORS headers", async assert.equal(response.status, 429); assert.equal(await response.text(), '{"error":"busy"}'); assert.equal(response.headers.get("content-type"), "application/json"); - assert.equal(response.headers.get("access-control-allow-origin"), "*"); + assert.equal(response.headers.get("access-control-allow-origin"), null); + assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/); }); test("handleModeration returns a 500 when the upstream request throws", async () => { diff --git a/tests/unit/muse-spark-web-continuation.test.ts b/tests/unit/muse-spark-web-continuation.test.ts new file mode 100644 index 0000000000..a190d7a66d --- /dev/null +++ b/tests/unit/muse-spark-web-continuation.test.ts @@ -0,0 +1,325 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + MuseSparkWebExecutor, + __resetMuseSparkConversationCacheForTesting, +} from "../../open-sse/executors/muse-spark-web.ts"; + +// Canned Meta AI response shape. parseMetaAiResponseText accepts either a +// plain JSON body or an SSE stream of `data: ` frames; we send a plain +// JSON body since the assertions don't care about delta structure. +function metaAiSseResponse(content: string): Response { + const body = JSON.stringify({ + data: { + sendMessageStream: { + __typename: "AssistantMessage", + content, + }, + }, + }); + return new Response(body, { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +type CapturedRequest = { url: string; init: RequestInit | undefined; body: unknown }; + +function captureFetch(reply: () => Response): { + fetchFn: typeof fetch; + captured: CapturedRequest[]; +} { + const captured: CapturedRequest[] = []; + const fetchFn: typeof fetch = async (input, init) => { + let body: unknown = undefined; + if (init?.body && typeof init.body === "string") { + try { + body = JSON.parse(init.body); + } catch { + body = init.body; + } + } + captured.push({ + url: typeof input === "string" ? input : (input as URL).toString(), + init, + body, + }); + return reply(); + }; + return { fetchFn, captured }; +} + +function executeInputs(messages: Array<{ role: string; content: string }>) { + return { + model: "muse-spark", + body: { messages }, + stream: false, + credentials: { apiKey: "abra_sess=foo", connectionId: "conn-test-1" }, + signal: null, + log: null, + upstreamExtraHeaders: undefined, + } as Parameters[0]; +} + +test("muse-spark-web: first turn opens a new meta.ai conversation", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + const { fetchFn, captured } = captureFetch(() => metaAiSseResponse("pong")); + globalThis.fetch = fetchFn; + try { + const result = await executor.execute(executeInputs([{ role: "user", content: "ping" }])); + assert.equal(captured.length, 1, "exactly one upstream call"); + const sentVars = (captured[0].body as { variables: Record }).variables; + assert.equal(sentVars.isNewConversation, true, "first turn → isNewConversation: true"); + assert.equal(sentVars.content, "ping", "first turn → bare user content"); + assert.match(String(sentVars.conversationId), /^c\./, "fresh meta.ai conversation id"); + assert.equal(result.response.status, 200); + } finally { + globalThis.fetch = original; + } +}); + +test("muse-spark-web: follow-up turn continues the cached conversation", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + let nthReply = 0; + const { fetchFn, captured } = captureFetch(() => + metaAiSseResponse(nthReply++ === 0 ? "pong" : "pong-again") + ); + globalThis.fetch = fetchFn; + try { + // Turn 1 + await executor.execute(executeInputs([{ role: "user", content: "ping" }])); + // Turn 2 — caller sends the OpenAI history including the prior assistant. + await executor.execute( + executeInputs([ + { role: "user", content: "ping" }, + { role: "assistant", content: "pong" }, + { role: "user", content: "ping again" }, + ]) + ); + assert.equal(captured.length, 2, "two upstream calls"); + const turn1 = (captured[0].body as { variables: Record }).variables; + const turn2 = (captured[1].body as { variables: Record }).variables; + assert.equal(turn1.isNewConversation, true); + assert.equal(turn2.isNewConversation, false, "second turn → continues"); + assert.equal( + turn2.conversationId, + turn1.conversationId, + "second turn reuses first turn's conversation id" + ); + assert.equal(turn2.content, "ping again", "second turn → only the latest user content"); + } finally { + globalThis.fetch = original; + } +}); + +test("muse-spark-web: connection isolation — different connectionId → independent conversations", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + const { fetchFn, captured } = captureFetch(() => metaAiSseResponse("pong")); + globalThis.fetch = fetchFn; + try { + const baseInputs = (id: string) => ({ + model: "muse-spark", + body: { + messages: [ + { role: "user", content: "ping" }, + { role: "assistant", content: "pong" }, + { role: "user", content: "again" }, + ], + }, + stream: false, + credentials: { apiKey: "abra_sess=foo", connectionId: id }, + signal: null, + log: null, + upstreamExtraHeaders: undefined, + }); + // Two different connections both have the same OpenAI history with the + // same prior assistant content. They must not collide on the cache. + await executor.execute(baseInputs("conn-A") as Parameters[0]); + await executor.execute(baseInputs("conn-B") as Parameters[0]); + const a = (captured[0].body as { variables: Record }).variables; + const b = (captured[1].body as { variables: Record }).variables; + assert.equal(a.isNewConversation, true); + assert.equal(b.isNewConversation, true); + assert.notEqual(a.conversationId, b.conversationId); + } finally { + globalThis.fetch = original; + } +}); + +test("muse-spark-web: meta error during continuation evicts the stale cache entry", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + + // Reply 1: success. Reply 2: HTTP 400 (e.g. Meta deleted the conversation). + // Reply 3: success again — cache must have been evicted, so this turn + // should open a fresh conversation, not reuse the dead one. + let n = 0; + const fetchFn: typeof fetch = async () => { + n++; + if (n === 2) { + return new Response(JSON.stringify({ errors: [{ message: "conversation not found" }] }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return metaAiSseResponse(n === 1 ? "pong" : "pong-2"); + }; + const captured: CapturedRequest[] = []; + globalThis.fetch = (async (input, init) => { + let body: unknown = undefined; + if (init?.body && typeof init.body === "string") { + try { + body = JSON.parse(init.body); + } catch { + body = init.body; + } + } + captured.push({ + url: typeof input === "string" ? input : (input as URL).toString(), + init, + body, + }); + return fetchFn(input as never, init as never); + }) as typeof fetch; + + try { + // Turn 1 — opens conversation A and caches it. + await executor.execute(executeInputs([{ role: "user", content: "ping" }])); + // Turn 2 — would continue conversation A but Meta returns 400. + await executor.execute( + executeInputs([ + { role: "user", content: "ping" }, + { role: "assistant", content: "pong" }, + { role: "user", content: "again" }, + ]) + ); + // Turn 3 — same prior-assistant content, retried by the user. Cache + // should have been evicted on turn 2, so this turn opens a new + // conversation rather than re-trying the dead one. + await executor.execute( + executeInputs([ + { role: "user", content: "ping" }, + { role: "assistant", content: "pong" }, + { role: "user", content: "again" }, + ]) + ); + const t1 = (captured[0].body as { variables: Record }).variables; + const t2 = (captured[1].body as { variables: Record }).variables; + const t3 = (captured[2].body as { variables: Record }).variables; + assert.equal(t1.isNewConversation, true); + assert.equal(t2.isNewConversation, false, "turn 2 attempted to continue"); + assert.equal(t2.conversationId, t1.conversationId); + assert.equal( + t3.isNewConversation, + true, + "turn 3 must open a fresh conversation after the stale entry was evicted" + ); + assert.notEqual( + t3.conversationId, + t1.conversationId, + "turn 3 must not reuse the dead conversation id" + ); + } finally { + globalThis.fetch = original; + } +}); + +test("muse-spark-web: parallel chats with identical assistant text but different histories do not collide", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + + // Both chats end with the assistant saying the same generic line. Without + // hashing the preceding history, both would map to the same cache entry + // and the second chat's continuation would route into the first chat's + // meta.ai conversation. + const COMMON_REPLY = "I don't have access to real-time data."; + const { fetchFn, captured } = captureFetch(() => metaAiSseResponse(COMMON_REPLY)); + globalThis.fetch = fetchFn; + try { + // Chat A — turn 1 sets up the cache. + await executor.execute(executeInputs([{ role: "user", content: "what's the weather" }])); + // Chat B — turn 1 (different question, same response) sets up its own cache entry. + await executor.execute(executeInputs([{ role: "user", content: "stock price for AAPL" }])); + const a1 = (captured[0].body as { variables: Record }).variables; + const b1 = (captured[1].body as { variables: Record }).variables; + + // Chat A — turn 2 should continue conversation A, not jump to B's id. + await executor.execute( + executeInputs([ + { role: "user", content: "what's the weather" }, + { role: "assistant", content: COMMON_REPLY }, + { role: "user", content: "any forecast at all?" }, + ]) + ); + // Chat B — turn 2 should continue conversation B. + await executor.execute( + executeInputs([ + { role: "user", content: "stock price for AAPL" }, + { role: "assistant", content: COMMON_REPLY }, + { role: "user", content: "any market info at all?" }, + ]) + ); + const a2 = (captured[2].body as { variables: Record }).variables; + const b2 = (captured[3].body as { variables: Record }).variables; + + assert.equal(a2.isNewConversation, false, "chat A turn 2 continues"); + assert.equal(b2.isNewConversation, false, "chat B turn 2 continues"); + assert.equal(a2.conversationId, a1.conversationId, "chat A continues into A's conversation"); + assert.equal(b2.conversationId, b1.conversationId, "chat B continues into B's conversation"); + assert.notEqual( + a2.conversationId, + b2.conversationId, + "the two chats must not collide despite identical assistant text" + ); + } finally { + globalThis.fetch = original; + } +}); + +test("muse-spark-web: empty latestUserContent (no `user` role) falls back to fresh conversation", async () => { + __resetMuseSparkConversationCacheForTesting(); + const executor = new MuseSparkWebExecutor(); + const original = globalThis.fetch; + const { fetchFn, captured } = captureFetch(() => metaAiSseResponse("ack")); + globalThis.fetch = fetchFn; + try { + // Pre-seed the cache with a normal turn so a hit IS possible if the + // guard isn't in place. + await executor.execute(executeInputs([{ role: "user", content: "ping" }])); + + // Now send a payload with no `user` role at all (system + assistant + // only). `latestUserContent` is empty; without the guard the executor + // would route this through the cache-hit path and POST empty content + // with `isNewConversation: false`. + await executor.execute( + executeInputs([ + { role: "system", content: "you are helpful" }, + { role: "assistant", content: "ack" }, + ]) + ); + const t2 = (captured[1].body as { variables: Record }).variables; + + assert.equal( + t2.isNewConversation, + true, + "empty latestUserContent must NOT use the cache-hit path" + ); + assert.notEqual(t2.content, "", "must not POST empty content"); + assert.match( + String(t2.content), + /assistant: ack/, + "should fall through to the folded-history prompt" + ); + } finally { + globalThis.fetch = original; + } +}); diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts index 1de5f708f4..2ab7555dd8 100644 --- a/tests/unit/oauth-providers-config.test.ts +++ b/tests/unit/oauth-providers-config.test.ts @@ -438,7 +438,7 @@ test("Gemini and Antigravity run mocked browser OAuth exchanges and post-exchang (_url, init = {}) => { assert.equal(init.method, "POST"); assert.equal(init.headers.Authorization, "Bearer anti-access"); - assert.equal(init.headers["User-Agent"], "google-api-nodejs-client/9.15.1"); + assert.equal(init.headers["User-Agent"], "google-api-nodejs-client/10.3.0"); assert.equal( init.headers["X-Goog-Api-Client"], "google-cloud-sdk vscode_cloudshelleditor/0.1" @@ -459,7 +459,7 @@ test("Gemini and Antigravity run mocked browser OAuth exchanges and post-exchang (_url, init = {}) => { assert.equal(init.method, "POST"); assert.equal(init.headers.Authorization, "Bearer anti-access"); - assert.equal(init.headers["User-Agent"], "google-api-nodejs-client/9.15.1"); + assert.equal(init.headers["User-Agent"], "google-api-nodejs-client/10.3.0"); assert.equal( init.headers["X-Goog-Api-Client"], "google-cloud-sdk vscode_cloudshelleditor/0.1" diff --git a/tests/unit/perplexity-web.test.ts b/tests/unit/perplexity-web.test.ts index 4023430300..a04c2d00a7 100644 --- a/tests/unit/perplexity-web.test.ts +++ b/tests/unit/perplexity-web.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import test from "node:test"; import assert from "node:assert/strict"; @@ -743,7 +744,7 @@ test("Request: posts to correct Perplexity SSE endpoint", async () => { assert.equal(capturedUrl, "https://www.perplexity.ai/rest/sse/perplexity_ask"); assert.equal(capturedHeaders["Origin"], "https://www.perplexity.ai"); - assert.equal(capturedHeaders["X-App-ApiVersion"], "2.18"); + assert.equal(capturedHeaders["X-App-ApiVersion"], "client-1.11.0"); assert.equal(capturedHeaders["Accept"], "text/event-stream"); } finally { globalThis.fetch = original; diff --git a/tests/unit/plan3-p0.test.ts b/tests/unit/plan3-p0.test.ts index efc5fc819e..f604f40a3b 100644 --- a/tests/unit/plan3-p0.test.ts +++ b/tests/unit/plan3-p0.test.ts @@ -40,6 +40,12 @@ test("getModelInfoCore resolves gpt-5.5 to codex", async () => { assert.equal(info.model, "gpt-5.5"); }); +test("getModelInfoCore keeps explicit gpt-5.5-medium separate from gpt-5.5", async () => { + const info = await getModelInfoCore("gpt-5.5-medium", {}); + assert.equal(info.provider, "codex"); + assert.equal(info.model, "gpt-5.5-medium"); +}); + test("getModelInfoCore resolves explicit gpt-5.5 Codex model", async () => { const info = await getModelInfoCore("cx/gpt-5.5", {}); assert.equal(info.provider, "codex"); @@ -67,10 +73,10 @@ test("getModelInfoCore resolves explicit gpt-5.5 Codex model", async () => { test("getModelInfoCore returns explicit ambiguity metadata for ambiguous unprefixed model", async () => { const info = await getModelInfoCore("claude-haiku-4.5", {}); assert.equal(info.provider, null); - assert.equal(info.errorType, "ambiguous_model"); - assert.match(info.errorMessage, /Ambiguous model/i); - assert.ok(Array.isArray(info.candidateProviders)); - assert.ok(info.candidateProviders.length >= 2); + assert.equal((info as any).errorType, "ambiguous_model"); + assert.match((info as any).errorMessage, /Ambiguous model/i); + assert.ok(Array.isArray((info as any).candidateProviders)); + assert.ok((info as any).candidateProviders.length >= 2); }); test("getModelInfoCore canonicalizes github legacy alias with explicit provider prefix", async () => { @@ -95,8 +101,8 @@ test("DefaultExecutor uses x-api-key for kimi-coding-apikey", () => { const executor = new DefaultExecutor("kimi-coding-apikey"); const headers = executor.buildHeaders({ apiKey: "sk-kimi-test" }, true); - assert.equal(headers["x-api-key"], "sk-kimi-test"); - assert.equal(headers.Authorization, undefined); + assert.equal((headers as any)["x-api-key"], "sk-kimi-test"); + assert.equal((headers as any).Authorization, undefined); }); test("DefaultExecutor execute honors connection-level custom User-Agent", async () => { @@ -105,7 +111,7 @@ test("DefaultExecutor execute honors connection-level custom User-Agent", async let capturedHeaders = null; globalThis.fetch = async (_url, init = {}) => { - capturedHeaders = init.headers || null; + capturedHeaders = (init as any).headers || null; return new Response(JSON.stringify({ id: "chatcmpl-test" }), { status: 200 }); }; @@ -138,7 +144,8 @@ test("CodexExecutor forces stream=true for upstream compatibility", () => { const transformed = executor.transformRequest( "gpt-5.1-codex", { model: "gpt-5.1-codex", input: [], stream: false }, - false + false, + {} ); assert.equal(transformed.stream, true); }); @@ -187,7 +194,8 @@ test("CodexExecutor maps fast service tier to priority", () => { const transformed = executor.transformRequest( "gpt-5.1-codex", { model: "gpt-5.1-codex", input: [], service_tier: "fast" }, - true + true, + {} ); assert.equal(transformed.service_tier, "priority"); }); @@ -296,13 +304,14 @@ test("CodexExecutor preserves native responses payloads for Codex passthrough", _nativeCodexPassthrough: true, stream: false, }, - false + false, + {} ); assert.equal(transformed.stream, true); assert.equal(transformed.service_tier, "priority"); assert.equal(transformed.instructions, "custom system prompt"); - assert.equal(transformed.store, true); + assert.equal(transformed.store, false); assert.deepEqual(transformed.metadata, { source: "codex-client" }); assert.equal(transformed.reasoning.effort, "high"); assert.equal(transformed.reasoning_effort, undefined); diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index 10f59c13fc..18fca180f9 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -221,7 +221,7 @@ test("web-cookie provider validators accept valid Grok, Perplexity, Blackbox and calls.push({ url: target, init }); if (target.includes("grok.com/rest/app-chat/conversations/new")) { - return new Response(JSON.stringify({ ok: true }), { status: 400 }); + return new Response(JSON.stringify({ ok: true }), { status: 200 }); } if (target.includes("perplexity.ai/rest/sse/perplexity_ask")) { return new Response(JSON.stringify({ ok: true }), { status: 200 }); @@ -288,6 +288,10 @@ test("web-cookie provider validators accept valid Grok, Perplexity, Blackbox and const museSparkCall = calls.find((call) => call.url.includes("meta.ai/api/graphql")); assert.equal(grokCall?.init.headers.Cookie, "sso=grok-cookie"); + const grokBody = JSON.parse(String(grokCall?.init.body || "{}")); + assert.equal(grokBody.modeId, "auto"); + assert.equal("modelName" in grokBody, false); + assert.equal("modelMode" in grokBody, false); assert.equal(perplexityCall?.init.headers.Cookie, "__Secure-next-auth.session-token=pplx-cookie"); assert.equal(blackboxSessionCall?.init.headers.Cookie, "__Secure-authjs.session-token=bb-cookie"); assert.equal( @@ -295,7 +299,7 @@ test("web-cookie provider validators accept valid Grok, Perplexity, Blackbox and "__Secure-authjs.session-token=bb-cookie" ); assert.equal(museSparkCall?.init.headers.Cookie, "abra_sess=meta-cookie"); - assert.equal(museSparkCall?.init.headers["X-FB-Friendly-Name"], "useAbraSendMessageMutation"); + assert.equal(museSparkCall?.init.headers["X-FB-Friendly-Name"], "useEctoSendMessageSubscription"); }); test("web-cookie provider validators surface auth and subscription failures", async () => { diff --git a/tests/unit/proxy-e2e-mode.test.ts b/tests/unit/proxy-e2e-mode.test.ts deleted file mode 100644 index d5bd82b2da..0000000000 --- a/tests/unit/proxy-e2e-mode.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import path from "node:path"; -import { pathToFileURL } from "node:url"; - -const originalEnv = { - NEXT_PUBLIC_OMNIROUTE_E2E_MODE: process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE, -}; - -function restoreEnv() { - for (const [key, value] of Object.entries(originalEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } -} - -async function importFresh(modulePath) { - const url = pathToFileURL(path.resolve(modulePath)).href; - return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`); -} - -function makeRequest(pathname) { - return { - nextUrl: { - pathname, - protocol: "http:", - }, - method: "GET", - cookies: { - get() { - return undefined; - }, - }, - headers: new Headers(), - url: `http://localhost:20128${pathname}`, - }; -} - -test.beforeEach(() => { - restoreEnv(); -}); - -test.afterEach(() => { - restoreEnv(); -}); - -test.after(() => { - restoreEnv(); -}); - -test("proxy bypasses dashboard auth in Playwright e2e mode", async () => { - process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE = "1"; - - const { proxy } = await importFresh("src/proxy.ts"); - const response = await proxy(makeRequest("/dashboard/combos")); - - assert.equal(response.status, 200); - assert.equal(response.headers.get("location"), null); - assert.ok(response.headers.get("x-request-id")); -}); - -test("proxy bypasses management API auth in Playwright e2e mode", async () => { - process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE = "1"; - - const { proxy } = await importFresh("src/proxy.ts"); - const response = await proxy(makeRequest("/api/combos")); - - assert.equal(response.status, 200); - assert.equal(response.headers.get("location"), null); - assert.ok(response.headers.get("x-request-id")); -}); diff --git a/tests/unit/proxy-login-bootstrap-auth.test.ts b/tests/unit/proxy-login-bootstrap-auth.test.ts deleted file mode 100644 index d637679d68..0000000000 --- a/tests/unit/proxy-login-bootstrap-auth.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -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"; -import { pathToFileURL } from "node:url"; - -const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-bootstrap-")); -process.env.DATA_DIR = TEST_DATA_DIR; - -const core = await import("../../src/lib/db/core.ts"); -const settingsDb = await import("../../src/lib/db/settings.ts"); - -async function importFreshProxy() { - const url = pathToFileURL(path.resolve("src/proxy.ts")).href; - return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`); -} - -async function resetStorage() { - core.resetDbInstance(); - fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); - fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); -} - -function makeRequest(pathname, method = "GET", headers) { - return { - nextUrl: { - pathname, - protocol: "http:", - }, - method, - cookies: { - get() { - return undefined; - }, - }, - headers: new Headers(headers), - url: `http://localhost:20128${pathname}`, - }; -} - -test.beforeEach(async () => { - delete process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE; - delete process.env.INITIAL_PASSWORD; - await resetStorage(); -}); - -test.after(() => { - delete process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE; - delete process.env.INITIAL_PASSWORD; - core.resetDbInstance(); - fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); -}); - -test("proxy keeps GET /api/settings/require-login public after setup", async () => { - await settingsDb.updateSettings({ - requireLogin: true, - password: "hashed-password", - setupComplete: true, - }); - - const { proxy } = await importFreshProxy(); - const response = await proxy(makeRequest("/api/settings/require-login")); - - assert.equal(response.status, 200); - assert.equal(response.headers.get("location"), null); - assert.ok(response.headers.get("x-request-id")); -}); - -test("proxy requires auth for POST /api/settings/require-login after setup", async () => { - await settingsDb.updateSettings({ - requireLogin: true, - password: "hashed-password", - setupComplete: true, - }); - - const { proxy } = await importFreshProxy(); - const response = await proxy(makeRequest("/api/settings/require-login", "POST")); - const body = (await response.json()) as any; - - assert.equal(response.status, 401); - assert.equal(body.error.code, "AUTH_001"); -}); - -test("proxy rejects bearer tokens for POST /api/settings/require-login after setup", async () => { - await settingsDb.updateSettings({ - requireLogin: true, - password: "hashed-password", - setupComplete: true, - }); - - const { proxy } = await importFreshProxy(); - const response = await proxy( - makeRequest("/api/settings/require-login", "POST", { - authorization: "Bearer sk-invalid", - }) - ); - const body = (await response.json()) as any; - - assert.equal(response.status, 403); - assert.equal(body.error.code, "AUTH_001"); -}); - -test("proxy still allows POST /api/settings/require-login during bootstrap", async () => { - const { proxy } = await importFreshProxy(); - const response = await proxy(makeRequest("/api/settings/require-login", "POST")); - - assert.equal(response.status, 200); - assert.equal(response.headers.get("location"), null); - assert.ok(response.headers.get("x-request-id")); -}); diff --git a/tests/unit/qoder-executor.test.ts b/tests/unit/qoder-executor.test.ts index 7d5af39fbe..c1dae12cae 100644 --- a/tests/unit/qoder-executor.test.ts +++ b/tests/unit/qoder-executor.test.ts @@ -173,8 +173,8 @@ test("QoderExecutor: non-stream calls target DashScope and map alias models", as assert.equal(options.method, "POST"); assert.equal(options.headers.Authorization, "Bearer pat_test"); assert.equal(options.headers["x-dashscope-authtype"], "qwen-oauth"); - assert.equal(options.headers["user-agent"], "QwenCode/0.11.1 (linux; x64)"); - assert.equal(options.headers["x-dashscope-useragent"], "QwenCode/0.11.1 (linux; x64)"); + assert.equal(options.headers["user-agent"], "QwenCode/0.15.3 (linux; x64)"); + assert.equal(options.headers["x-dashscope-useragent"], "QwenCode/0.15.3 (linux; x64)"); const parsedBody = JSON.parse(String(options.body)); assert.equal(parsedBody.model, "coder-model"); return new Response( diff --git a/tests/unit/quota-policy-generalization.test.ts b/tests/unit/quota-policy-generalization.test.ts index a99c27a172..7a0eb3480c 100644 --- a/tests/unit/quota-policy-generalization.test.ts +++ b/tests/unit/quota-policy-generalization.test.ts @@ -18,7 +18,7 @@ test("resolveQuotaLimitPolicy keeps codex legacy defaults when generic policy is assert.equal(policy.enabled, true); assert.deepEqual(policy.windows, ["session"]); - assert.equal(policy.thresholdPercent, 90); + assert.equal(policy.thresholdPercent, 99); }); test("resolveQuotaLimitPolicy enforces codex weekly window when weekly toggle is enabled", () => { diff --git a/tests/unit/qwen-api-key-auto-create.test.ts b/tests/unit/qwen-api-key-auto-create.test.ts new file mode 100644 index 0000000000..fa4f8b4816 --- /dev/null +++ b/tests/unit/qwen-api-key-auto-create.test.ts @@ -0,0 +1,153 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "os"; +import path from "path"; +import { SignJWT } from "jose"; +import { getOrCreateApiKey, resolveApiKey } from "../../src/shared/services/apiKeyResolver"; +import { validateApiKey } from "../../src/lib/db/apiKeys"; +import { getDbInstance } from "../../src/lib/db/core"; + +const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-qwen-key-test-" + Date.now()); +const originalJwtSecret = process.env.JWT_SECRET; + +async function createAuthCookie() { + process.env.JWT_SECRET = "test-cli-tools-secret"; + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ sub: "test-user" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .sign(secret); + return `auth_token=${token}`; +} + +const originalHomedir = os.homedir; + +test.beforeEach(async () => { + process.env.DATA_DIR = DUMMY_HOME; + process.env.API_KEY_SECRET = "test-secret"; + os.homedir = () => DUMMY_HOME; + await fs.mkdir(DUMMY_HOME, { recursive: true }).catch(() => {}); + // Initialize DB + getDbInstance(); +}); + +test.afterEach(async () => { + await fs.rm(DUMMY_HOME, { recursive: true, force: true }).catch(() => {}); + os.homedir = originalHomedir; + if (originalJwtSecret === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = originalJwtSecret; + if (process.env.DATA_DIR?.includes("omniroute-qwen-key-test")) { + delete process.env.DATA_DIR; + } +}); + +test("getOrCreateApiKey() creates DB-backed key when no keyId provided", async () => { + const apiKey = await getOrCreateApiKey(null); + + // Key should NOT be the placeholder "sk_omniroute" + assert.notEqual(apiKey, "sk_omniroute", "Should not return placeholder"); + assert.ok(apiKey.startsWith("sk-"), "Key should start with sk- prefix"); + + // Key should be valid in DB + const valid = await validateApiKey(apiKey); + assert.equal(valid, true, "Auto-created key should validate successfully"); +}); + +test("getOrCreateApiKey() returns existing key when keyId is provided", async () => { + // First create a key with a specific keyId + const firstKey = await getOrCreateApiKey(null); + assert.ok(firstKey.startsWith("sk-")); + + // Create another key and get its ID + const secondKey = await getOrCreateApiKey(null); + + // When we pass the same keyId, we should get the same key back + // (This tests the keyId resolution path) + const resolvedKey = await resolveApiKey(null, firstKey); + assert.equal(resolvedKey, firstKey, "Should return same key when resolved"); +}); + +test("Qwen guide-settings POST creates valid DB-backed key (no keyId)", async () => { + const guideSettingsRoute = + await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); + + const cookie = await createAuthCookie(); + const req = new Request("http://localhost/api/cli-tools/guide-settings/qwen", { + method: "POST", + headers: { "Content-Type": "application/json", cookie }, + body: JSON.stringify({ + baseUrl: "http://localhost:20128/v1", + model: "qwen3-coder-flash", + // No keyId provided - should auto-create + }), + }); + + const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); + assert.equal(response.status, 200, "Response should be OK"); + + // Verify settings.json was written with security.auth format and a valid DB-backed key + const configPath = path.join(DUMMY_HOME, ".qwen", "settings.json"); + const content = JSON.parse(await fs.readFile(configPath, "utf-8")); + + assert.equal(content.security?.auth?.selectedType, "openai", "Should use openai auth type"); + assert.ok(content.security?.auth?.apiKey, "Should have an API key"); + assert.equal( + content.security?.auth?.baseUrl, + "http://localhost:20128/v1", + "Should have base URL" + ); + assert.equal(content.model?.name, "qwen3-coder-flash", "Should have model name"); + + const apiKey = content.security.auth.apiKey; + assert.notEqual(apiKey, "sk_omniroute", "Should not use placeholder"); + assert.ok(apiKey.startsWith("sk-"), "Key should start with sk- prefix"); + + // Verify the key is valid in DB + const valid = await validateApiKey(apiKey); + assert.equal(valid, true, "Auto-created key should validate in DB"); +}); + +test("Qwen guide-settings POST with keyId uses existing key", async () => { + const guideSettingsRoute = + await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); + + // Pre-create a key via getOrCreateApiKey + const existingKey = await getOrCreateApiKey(null); + const keyIdMatch = existingKey.match(/^sk-[^-]+-([^-]+)-/); + assert.ok(keyIdMatch, "Key should have ID portion"); + + // Get key metadata to find the ID + const db = getDbInstance(); + const stmt = db.prepare("SELECT id FROM api_keys WHERE `key` = ?"); + const row = stmt.get(existingKey) as { id: string } | undefined; + assert.ok(row, "Key should exist in DB"); + + const cookie = await createAuthCookie(); + const req = new Request("http://localhost/api/cli-tools/guide-settings/qwen", { + method: "POST", + headers: { "Content-Type": "application/json", cookie }, + body: JSON.stringify({ + baseUrl: "http://localhost:20128/v1", + model: "qwen3-coder-plus", + keyId: row.id, + }), + }); + + const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); + assert.equal(response.status, 200, "Response should be OK"); + + // Verify settings.json uses security.auth format with the existing key + const configPath = path.join(DUMMY_HOME, ".qwen", "settings.json"); + const content = JSON.parse(await fs.readFile(configPath, "utf-8")); + + assert.equal(content.security?.auth?.selectedType, "openai"); + assert.equal( + content.security?.auth?.apiKey, + existingKey, + "Should use existing key when keyId provided" + ); + assert.equal(content.security?.auth?.baseUrl, "http://localhost:20128/v1"); + assert.equal(content.model?.name, "qwen3-coder-plus"); +}); diff --git a/tests/unit/route-edge-coverage.test.ts b/tests/unit/route-edge-coverage.test.ts index 53ab66ce94..87380832a7 100644 --- a/tests/unit/route-edge-coverage.test.ts +++ b/tests/unit/route-edge-coverage.test.ts @@ -660,7 +660,8 @@ test("embeddings route covers options, custom-model listing and defensive POST b const validationFailureBody = (await validationFailure.json()) as any; const invalidModelBody = (await invalidModel.json()) as any; - assert.equal(optionsHeaders["access-control-allow-origin"], "*"); + assert.equal(optionsHeaders["access-control-allow-origin"], undefined); + assert.match(optionsHeaders["access-control-allow-methods"] || "", /OPTIONS/); assert.equal(getResponse.status, 200); assert.equal( getBody.data.some((model) => model.id === "custom-embedder/text-embed-1"), @@ -677,27 +678,7 @@ test("embeddings route covers options, custom-model listing and defensive POST b ); }); -test("embeddings route enforces caller auth, missing credentials and provider rate limits", async () => { - process.env.REQUIRE_API_KEY = "true"; - - const missingKey = await embeddingsRoute.POST( - makeRequest("http://localhost/v1/embeddings", { - method: "POST", - body: { model: "openai/text-embedding-3-small", input: "hello" }, - }) - ); - - const invalidKey = await embeddingsRoute.POST( - new Request("http://localhost/v1/embeddings", { - method: "POST", - headers: { - "content-type": "application/json", - authorization: "Bearer sk-invalid", - }, - body: JSON.stringify({ model: "openai/text-embedding-3-small", input: "hello" }), - }) - ); - +test("embeddings route surfaces missing-credentials and provider-rate-limit errors", async () => { const validApiKey = await apiKeysDb.createApiKey("caller", MACHINE_ID); const missingCredentials = await embeddingsRoute.POST( new Request("http://localhost/v1/embeddings", { @@ -726,15 +707,9 @@ test("embeddings route enforces caller auth, missing credentials and provider ra }) ); - const missingKeyBody = (await missingKey.json()) as any; - const invalidKeyBody = (await invalidKey.json()) as any; const missingCredentialsBody = (await missingCredentials.json()) as any; const allRateLimitedBody = (await allRateLimited.json()) as any; - assert.equal(missingKey.status, 401); - assert.equal(missingKeyBody.error.message, "Missing API key"); - assert.equal(invalidKey.status, 401); - assert.equal(invalidKeyBody.error.message, "Invalid API key"); assert.equal(missingCredentials.status, 400); assert.match(missingCredentialsBody.error.message, /No credentials for embedding provider/); assert.equal(allRateLimited.status, 429); diff --git a/tests/unit/sse-auth.test.ts b/tests/unit/sse-auth.test.ts index 8c99607eae..2add1820ef 100644 --- a/tests/unit/sse-auth.test.ts +++ b/tests/unit/sse-auth.test.ts @@ -264,7 +264,7 @@ test("resolveQuotaLimitPolicy normalizes Codex windows, thresholds, and defaults }); assert.deepEqual(defaults, { enabled: true, - thresholdPercent: 90, + thresholdPercent: 99, windows: ["session", "weekly"], }); assert.deepEqual(generic, { diff --git a/tests/unit/stream-readiness.test.ts b/tests/unit/stream-readiness.test.ts new file mode 100644 index 0000000000..1917dd3827 --- /dev/null +++ b/tests/unit/stream-readiness.test.ts @@ -0,0 +1,110 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + ensureStreamReadiness, + hasUsefulStreamContent, +} from "../../open-sse/utils/streamReadiness.ts"; + +const encoder = new TextEncoder(); + +function streamFromChunks(chunks: string[], delayMs = 0): ReadableStream { + return new ReadableStream({ + async start(controller) { + for (const chunk of chunks) { + if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs)); + controller.enqueue(encoder.encode(chunk)); + } + controller.close(); + }, + }); +} + +test("hasUsefulStreamContent ignores keepalives and lifecycle-only chunks", () => { + assert.equal(hasUsefulStreamContent(": keepalive\n\n"), false); + assert.equal(hasUsefulStreamContent("event: ping\ndata: {}\n\n"), false); + assert.equal( + hasUsefulStreamContent(`data: ${JSON.stringify({ type: "response.created" })}\n\n`), + false + ); + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ choices: [{ delta: { role: "assistant" }, index: 0 }] })}\n\n` + ), + false + ); +}); + +test("hasUsefulStreamContent detects text, reasoning, and tool deltas", () => { + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ choices: [{ delta: { content: "hello" }, index: 0 }] })}\n\n` + ), + true + ); + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ choices: [{ delta: { content: " " }, index: 0 }] })}\n\n` + ), + true + ); + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ choices: [{ delta: { reasoning_content: "thinking" }, index: 0 }] })}\n\n` + ), + true + ); + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ choices: [{ delta: { tool_calls: [{ function: { name: "read" } }] }, index: 0 }] })}\n\n` + ), + true + ); + assert.equal( + hasUsefulStreamContent( + `data: ${JSON.stringify({ type: "content_block_delta", delta: { text: "hello" } })}\n\n` + ), + true + ); +}); + +test("ensureStreamReadiness preserves buffered chunks when stream starts", async () => { + const response = new Response( + streamFromChunks([ + `data: ${JSON.stringify({ type: "response.created" })}\n\n`, + `data: ${JSON.stringify({ choices: [{ delta: { content: "hello" }, index: 0 }] })}\n\n`, + `data: ${JSON.stringify({ choices: [{ delta: { content: " world" }, index: 0 }] })}\n\n`, + ]), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + + const result = await ensureStreamReadiness(response, { timeoutMs: 100 }); + assert.equal(result.ok, true); + const text = await result.response.text(); + assert.match(text, /response\.created/); + assert.match(text, /hello/); + assert.match(text, / world/); +}); + +test("ensureStreamReadiness returns 504 when no useful content arrives before timeout", async () => { + const response = new Response( + streamFromChunks([": keepalive\n\n", `data: ${JSON.stringify({ type: "response.created" })}\n\n`], 20), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + + const result = await ensureStreamReadiness(response, { timeoutMs: 10 }); + assert.equal(result.ok, false); + assert.equal(result.response.status, 504); + assert.match(await result.response.text(), /STREAM_READINESS_TIMEOUT/); +}); + +test("ensureStreamReadiness returns 502 when stream ends without useful content", async () => { + const response = new Response(streamFromChunks([": keepalive\n\n"]), { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }); + + const result = await ensureStreamReadiness(response, { timeoutMs: 100 }); + assert.equal(result.ok, false); + assert.equal(result.response.status, 502); +}); diff --git a/tests/unit/t12-pricing-updates.test.ts b/tests/unit/t12-pricing-updates.test.ts index b47a399bc3..bf0f0cfe81 100644 --- a/tests/unit/t12-pricing-updates.test.ts +++ b/tests/unit/t12-pricing-updates.test.ts @@ -38,9 +38,13 @@ test("T12: pricing table includes MiniMax, GLM, Kimi and gpt-5.4 mini entries", test("T12: codex catalog includes GPT 5.5 entries", () => { const codexModels = new Map(REGISTRY.codex.models.map((m) => [m.id, m])); assert.ok(codexModels.has("gpt-5.5"), "missing codex/gpt-5.5"); + assert.ok(codexModels.has("gpt-5.5-medium"), "missing codex/gpt-5.5-medium"); assert.ok(codexModels.has("gpt-5.5-mini"), "missing codex/gpt-5.5-mini"); + assert.equal(codexModels.get("gpt-5.5")?.name, "GPT 5.5"); + assert.equal(codexModels.get("gpt-5.5-medium")?.name, "GPT 5.5 (Medium)"); assert.equal(codexModels.get("gpt-5.5")?.contextLength, 1050000); assert.equal(codexModels.get("gpt-5.5")?.supportsXHighEffort, true); + assert.equal(codexModels.get("gpt-5.5-medium")?.targetFormat, "openai-responses"); assert.equal(codexModels.get("gpt-5.5-xhigh")?.targetFormat, "openai-responses"); }); diff --git a/tests/unit/t20-t22-provider-headers.test.ts b/tests/unit/t20-t22-provider-headers.test.ts index 0b6be2972b..26b34d90e2 100644 --- a/tests/unit/t20-t22-provider-headers.test.ts +++ b/tests/unit/t20-t22-provider-headers.test.ts @@ -14,15 +14,15 @@ test("T20: antigravity config has updated User-Agent and sandbox fallback URL", assert.equal(antigravity.headers["User-Agent"], antigravityUserAgent()); }); -test("T20: gemini CLI fingerprint uses 0.31.0 and preserves darwin platform name", () => { - assert.equal(GEMINI_CLI_VERSION, "0.31.0"); +test("T20: gemini CLI fingerprint uses 0.39.1 and normalizes darwin to macos", () => { + assert.equal(GEMINI_CLI_VERSION, "0.39.1"); const descriptor = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: "darwin" }); try { assert.match( geminiCLIUserAgent("gemini-3-flash"), - /^GeminiCLI\/0\.31\.0\/gemini-3-flash \(darwin; / + /^GeminiCLI\/0\.39\.1\/gemini-3-flash \(macos; / ); } finally { if (descriptor) { @@ -41,9 +41,9 @@ test("T25: anthropic API-key config includes the full Anthropic beta header set" test("T22: github headers include updated editor/plugin versions and required fields", () => { const github = REGISTRY.github; - assert.equal(github.headers["editor-version"], "vscode/1.110.0"); - assert.equal(github.headers["editor-plugin-version"], "copilot-chat/0.38.0"); - assert.equal(github.headers["user-agent"], "GitHubCopilotChat/0.38.0"); + assert.equal(github.headers["editor-version"], "vscode/1.117.0"); + assert.equal(github.headers["editor-plugin-version"], "copilot-chat/0.45.1"); + assert.equal(github.headers["user-agent"], "GitHubCopilotChat/0.45.1"); assert.equal(github.headers["x-github-api-version"], "2025-04-01"); assert.equal(github.headers["x-vscode-user-agent-library-version"], "electron-fetch"); assert.equal(github.headers["X-Initiator"], "user"); @@ -59,6 +59,7 @@ test("T20: codex config advertises current client headers and auto-review model" const codex = REGISTRY.codex; assert.equal(codex.headers.Version, "0.125.0"); assert.equal(codex.headers["Openai-Beta"], "responses=experimental"); + assert.equal(codex.headers["X-Codex-Beta-Features"], "responses_websockets"); assert.equal(codex.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26100; x64)"); assert.ok(codex.models.some((model) => model.id === "codex-auto-review")); }); diff --git a/tests/unit/tool-schema-sanitizer.test.mjs b/tests/unit/tool-schema-sanitizer.test.mjs new file mode 100644 index 0000000000..a83965136d --- /dev/null +++ b/tests/unit/tool-schema-sanitizer.test.mjs @@ -0,0 +1,446 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +const { sanitizeOpenAITool, sanitizeOpenAITools } = + await import("../../open-sse/services/toolSchemaSanitizer.ts"); + +describe("toolSchemaSanitizer", () => { + describe("enum null filter (the actual fix)", () => { + it("strips null entries from enum (ForgeCode + Moonshot reproduction)", () => { + const tool = { + type: "function", + function: { + name: "fs_search", + parameters: { + type: "object", + properties: { + output_mode: { + type: "string", + enum: ["content", "files_with_matches", "count", null], + nullable: true, + }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.output_mode.enum, [ + "content", + "files_with_matches", + "count", + ]); + }); + + it("strips undefined entries from enum", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { mode: { type: "string", enum: ["a", undefined, "b"] } }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.enum, ["a", "b"]); + }); + + it("preserves enum without null/undefined", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { mode: { type: "string", enum: ["a", "b", "c"] } }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.enum, ["a", "b", "c"]); + }); + }); + + describe("required[] filter", () => { + it("drops required keys that are absent from properties", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { a: { type: "string" } }, + required: ["a", "b", "c"], + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.a, { type: "string" }); + assert.deepEqual(out.function.parameters.required, ["a"]); + }); + + it("filters non-string entries in required[]", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { a: { type: "string" } }, + required: ["a", null, 123, undefined], + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.required, ["a"]); + }); + }); + + describe("items normalization", () => { + it("collapses tuple items to first plain-object schema", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + list: { type: "array", items: [{ type: "string" }, { type: "number" }] }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.list.items, { + type: "string", + }); + }); + + it("falls back to empty schema when tuple has no plain-object entries", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + list: { type: "array", items: [null, "junk", 1] }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.list.items, {}); + }); + + it("preserves single-object items unchanged", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + list: { type: "array", items: { type: "string" } }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.list.items, { + type: "string", + }); + }); + }); + + describe("parameters shape normalization", () => { + it("creates empty object schema when parameters is null", () => { + const tool = { + type: "function", + function: { name: "x", parameters: null }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters, { type: "object", properties: {} }); + }); + + it("creates empty object schema when parameters is missing", () => { + const tool = { type: "function", function: { name: "x" } }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters, { type: "object", properties: {} }); + }); + + it("replaces non-object/non-boolean property values with empty schema", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { a: null, b: 1, c: "string-not-object" }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.a, {}); + assert.deepEqual(out.function.parameters.properties.b, {}); + assert.deepEqual(out.function.parameters.properties.c, {}); + }); + + it("preserves valid property schemas", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + a: { type: "string", description: "first" }, + b: { type: "integer", minimum: 0 }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.a, { + type: "string", + description: "first", + }); + assert.deepEqual(out.function.parameters.properties.b, { + type: "integer", + minimum: 0, + }); + }); + }); + + describe("anyOf / oneOf / allOf recursion", () => { + it("strips null from enum nested inside anyOf (Moonshot validates here)", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + mode: { + anyOf: [{ type: "string", enum: ["a", "b", null] }, { type: "null" }], + }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.anyOf[0].enum, ["a", "b"]); + }); + + it("strips null from enum nested inside oneOf", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + mode: { + oneOf: [{ type: "string", enum: ["a", null] }, { type: "integer" }], + }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.oneOf[0].enum, ["a"]); + }); + + it("strips null from enum nested inside allOf", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + mode: { + allOf: [{ type: "string", enum: ["a", null] }], + }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.allOf[0].enum, ["a"]); + }); + + it("replaces non-object entries inside anyOf/oneOf/allOf with empty schema", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + mode: { anyOf: [{ type: "string" }, null, "junk", 1] }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.mode.anyOf, [ + { type: "string" }, + {}, + {}, + {}, + ]); + }); + }); + + describe("additionalProperties recursion", () => { + it("strips null from enum nested inside additionalProperties (Moonshot validates here)", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + map: { + type: "object", + additionalProperties: { type: "string", enum: ["a", null] }, + }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.map.additionalProperties.enum, ["a"]); + }); + + it("preserves boolean additionalProperties", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { + map: { type: "object", additionalProperties: false }, + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.equal(out.function.parameters.properties.map.additionalProperties, false); + }); + }); + + describe("boolean property schemas (JSON Schema 2019)", () => { + it("preserves true / false property values rather than coercing to {}", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { allow_anything: true, deny_anything: false }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.equal(out.function.parameters.properties.allow_anything, true); + assert.equal(out.function.parameters.properties.deny_anything, false); + }); + + it("still coerces other non-object property values (null, string, number) to {}", () => { + const tool = { + type: "function", + function: { + name: "x", + parameters: { + type: "object", + properties: { a: null, b: "junk", c: 1 }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.function.parameters.properties.a, {}); + assert.deepEqual(out.function.parameters.properties.b, {}); + assert.deepEqual(out.function.parameters.properties.c, {}); + }); + }); + + describe("Responses API tool shape (top-level parameters)", () => { + it("strips null from enum in Responses-format tool (top-level parameters)", () => { + // /v1/responses tools have { type, name, parameters } at top level — no + // `function` wrapper. They reach chatCore in this shape before the + // request translator unwraps them, so the sanitizer must handle both. + const tool = { + type: "function", + name: "fs_search", + description: "search", + parameters: { + type: "object", + properties: { + output_mode: { + type: "string", + enum: ["a", "b", null], + }, + }, + }, + }; + const out = sanitizeOpenAITool(tool); + assert.equal(out.name, "fs_search"); + assert.deepEqual(out.parameters.properties.output_mode.enum, ["a", "b"]); + }); + + it("normalizes missing parameters in Responses-format tool", () => { + const tool = { type: "function", name: "x" }; + const out = sanitizeOpenAITool(tool); + assert.deepEqual(out.parameters, { type: "object", properties: {} }); + }); + + it("does not touch Responses-format tool without type=function", () => { + const tool = { type: "web_search", name: "websearch" }; + assert.deepEqual(sanitizeOpenAITool(tool), { + type: "web_search", + name: "websearch", + }); + }); + }); + + describe("non-function tools", () => { + it("leaves Responses API built-in tool types untouched", () => { + const tool = { type: "web_search" }; + assert.deepEqual(sanitizeOpenAITool(tool), { type: "web_search" }); + }); + }); + + describe("sanitizeOpenAITools", () => { + it("maps over an array", () => { + const tools = [ + { + type: "function", + function: { + name: "a", + parameters: { + type: "object", + properties: { m: { type: "string", enum: ["x", null] } }, + }, + }, + }, + { + type: "function", + function: { + name: "b", + parameters: { + type: "object", + properties: { n: { type: "string" } }, + }, + }, + }, + ]; + const out = sanitizeOpenAITools(tools); + assert.deepEqual(out[0].function.parameters.properties.m.enum, ["x"]); + assert.deepEqual(out[1].function.parameters.properties.n, { type: "string" }); + }); + }); +}); diff --git a/tests/unit/usage-analytics.test.ts b/tests/unit/usage-analytics.test.ts index 78e934b30b..8ff5f6662c 100644 --- a/tests/unit/usage-analytics.test.ts +++ b/tests/unit/usage-analytics.test.ts @@ -15,12 +15,8 @@ const usageStats = await import("../../src/lib/usage/usageStats.ts"); const callLogs = await import("../../src/lib/usage/callLogs.ts"); const { calculateCost } = await import("../../src/lib/usage/costCalculator.ts"); -function clearPendingRequests() { - const pending = usageHistory.getPendingRequests(); - for (const key of Object.keys(pending.byModel)) delete pending.byModel[key]; - for (const key of Object.keys(pending.byAccount)) delete pending.byAccount[key]; - for (const key of Object.keys(pending.details || {})) delete pending.details[key]; -} +// Use the official clearPendingRequests export instead of manual cleanup +const clearPendingRequests = usageHistory.clearPendingRequests; async function resetStorage() { core.resetDbInstance(); @@ -342,3 +338,35 @@ test("pending request metadata stores sanitized payload previews and clears afte usageHistory.trackPendingRequest("gpt-test", "openai", "conn-preview", false); assert.equal(pending.details["conn-preview"], undefined); }); + +test("clearPendingRequests resets all pending counts and details", () => { + // Simulate leaked pending counts (increment without matching decrement) + usageHistory.trackPendingRequest("model-a", "provider-x", "conn-1", true); + usageHistory.trackPendingRequest("model-a", "provider-x", "conn-1", true); + usageHistory.trackPendingRequest("model-b", "provider-y", "conn-2", true); + + const before = usageHistory.getPendingRequests(); + assert.equal(before.byModel["model-a (provider-x)"], 2); + assert.equal(before.byModel["model-b (provider-y)"], 1); + assert.ok(before.details["conn-1"]); + assert.ok(before.details["conn-2"]); + + // Clear all pending + usageHistory.clearPendingRequests(); + + const after = usageHistory.getPendingRequests(); + assert.equal(Object.keys(after.byModel).length, 0); + assert.equal(Object.keys(after.byAccount).length, 0); + assert.equal(Object.keys(after.details).length, 0); +}); + +test("clearPendingRequests allows fresh tracking after clearing", () => { + usageHistory.trackPendingRequest("model-c", "provider-z", "conn-3", true); + usageHistory.clearPendingRequests(); + + // Tracking should work normally after clearing + usageHistory.trackPendingRequest("model-d", "provider-w", "conn-4", true); + const pending = usageHistory.getPendingRequests(); + assert.equal(pending.byModel["model-d (provider-w)"], 1); + assert.ok(pending.details["conn-4"]); +}); diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index fd28dfc780..e49683675d 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -44,9 +44,9 @@ test("usage service covers GitHub free-plan parsing, auth denial and unsupported assert.equal(freeUsage.quotas.chat.remaining, 20); assert.equal(freeUsage.quotas.completions.remainingPercentage, 80); assert.equal(calls[0].headers.Authorization, "token gho-free"); - assert.equal(calls[0].headers["User-Agent"], "GitHubCopilotChat/0.38.0"); - assert.equal(calls[0].headers["Editor-Version"], "vscode/1.110.0"); - assert.equal(calls[0].headers["Editor-Plugin-Version"], "copilot-chat/0.38.0"); + assert.equal(calls[0].headers["User-Agent"], "GitHubCopilotChat/0.45.1"); + assert.equal(calls[0].headers["Editor-Version"], "vscode/1.117.0"); + assert.equal(calls[0].headers["Editor-Plugin-Version"], "copilot-chat/0.45.1"); assert.equal(calls[0].headers["X-GitHub-Api-Version"], "2025-04-01"); globalThis.fetch = async () => new Response("forbidden", { status: 403 }); @@ -317,7 +317,7 @@ test("usage service covers Antigravity quota parsing, exclusions and forbidden a assert.equal(usage.quotas["gemini-open"].total, 0); assert.equal(usage.quotas["gemini-open"].remainingPercentage, 100); const loadCodeAssistCall = calls.find((call) => call.url.includes("loadCodeAssist")); - assert.equal(loadCodeAssistCall?.init.headers["User-Agent"], "google-api-nodejs-client/9.15.1"); + assert.equal(loadCodeAssistCall?.init.headers["User-Agent"], "google-api-nodejs-client/10.3.0"); assert.equal( loadCodeAssistCall?.init.headers["X-Goog-Api-Client"], "google-cloud-sdk vscode_cloudshelleditor/0.1" diff --git a/tests/unit/visionBridgeDefaults.test.ts b/tests/unit/visionBridgeDefaults.test.ts index 2d631e05de..65042c8eb9 100644 --- a/tests/unit/visionBridgeDefaults.test.ts +++ b/tests/unit/visionBridgeDefaults.test.ts @@ -8,6 +8,7 @@ import { VISION_BRIDGE_DEFAULTS, VISION_BRIDGE_SETTINGS_KEYS, getVisionBridgeConfig, + isVisionBridgeForcedModel, type VisionBridgeSettings, } from "@/shared/constants/visionBridgeDefaults"; @@ -32,6 +33,12 @@ test("VISION_BRIDGE_SETTINGS_KEYS exports all expected keys", () => { ]); }); +test("isVisionBridgeForcedModel does not blanket-force GPT-family models", () => { + assert.strictEqual(isVisionBridgeForcedModel("gpt-5.5"), false); + assert.strictEqual(isVisionBridgeForcedModel("codex/gpt-5.5"), false); + assert.strictEqual(isVisionBridgeForcedModel("openai/gpt-4o-mini"), false); +}); + test("getVisionBridgeConfig returns defaults when no settings provided", () => { const config = getVisionBridgeConfig({});