Repository navigation
Release v3.4.0 (Integration) - #861
Conversation
Tests cover: - PATCH debugMode=true/false - PATCH hiddenSidebarItems with array values - Combined updates with both fields
Tests cover: - Debug mode toggle on/off - Sidebar visibility toggle - Settings persistence after page reload
- Unit tests: fix async/await for getSettings, use direct db functions - E2E tests: remove conditional logic, use Playwright auto-waiting assertions
Add section documenting: - Debug Mode toggle (Settings → Advanced) - Sidebar Visibility toggle (Settings → General)
Only inject prompt_cache_key for providers that support prompt caching (Claude, Anthropic, ZAI, Qwen, DeepSeek). This fixes issue #848 where NVIDIA API rejected the parameter.
Previously, the cache control preservation logic only recognized a hardcoded list of providers (claude, anthropic, zai, qwen, deepseek). This caused OmniRoute to inject its own cache_control markers for Claude-protocol providers not in that list (bailian-coding-plan, glm, minimax, minimax-cn, etc.), overwriting the client's cache markers. The fix checks both: 1. Known caching providers list (existing behavior) 2. Whether targetFormat === 'claude' (all Claude-protocol providers) This ensures all Claude-compatible providers properly preserve client cache_control headers when appropriate (Claude Code client, deterministic routing, etc.). Also removes unused CacheStatsCard from settings/components (duplicate of the one in cache/ page). Fixes cache token calculation for GLM, Minimax, and other Claude-compatible providers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Claude passthrough path round-trips through OpenAI format (claude→openai→claude) for structural normalization. This strips cache_control markers from every content block since OpenAI format has no equivalent, causing ~42k cache creation tokens per request with zero cache reads. When preserveCacheControl is true (Claude Code client, "always" setting, or deterministic combo), skip the round-trip entirely and forward the body as-is. Claude Code sends well-formed Messages API payloads — the normalization was only needed for non-Code clients. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The first commit incorrectly deleted CacheStatsCard from settings/components/ as a "duplicate". It's the only copy — both settings/page.tsx and cache/page.tsx import from this location. Restored the i18n-ized version from main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parse XhYmZs format from antigravity error messages (e.g., 27h41m36s) - Dynamic retry-after threshold (60s default) instead of hardcoded 10s - Add parseRetryFromErrorText() in accountFallback.ts for body parsing - Fix 403 'verify your account' to trigger permanent deactivation - Add keyword matching for 'quota will reset', 'exhausted capacity' - Add unit tests for retry parsing and keyword matching Fixes #858 (Antigravity 429 handling) Fixes #832 (Qwen quota 429 - same underlying bug)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Code Review SummaryStatus: No New Issues | Recommendation: Merge Incremental ReviewOnly 6 files were modified since the previous review (commit
All changes are improvements. No new issues introduced. Previous Issues (Carried Forward)
Files Reviewed (6 files)
The changes improve token refresh compliance, Antigravity quota accuracy, 429 parsing universality, and dashboard UI responsiveness. No blocking issues. |
There was a problem hiding this comment.
Code Review
This pull request redesigns the request logging and retention system, transitioning to a unified artifact format that stores a SQLite index row and a JSON file per request. It introduces new APP_LOG_* environment variables, implements a migration path for legacy logs, and enhances error handling for rate limits and quota resets. The changes also include improved Chinese translations, new model additions, and expanded test coverage. Feedback was provided to correct a minor versioning inconsistency in a migration script's internal comment.
| @@ -0,0 +1,15 @@ | |||
| -- 013_unified_log_artifacts.sql | |||
There was a problem hiding this comment.
There's a minor inconsistency between the filename, which is 014_unified_log_artifacts.sql, and the comment inside the file, which states 013_unified_log_artifacts.sql. To improve clarity and maintainability, it's best to ensure these are aligned.
| -- 013_unified_log_artifacts.sql | |
| -- 014_unified_log_artifacts.sql |
There was a problem hiding this comment.
Pull request overview
Release/integration PR for v3.4.0 that consolidates recent fixes and contributions, centered around the new unified request logging system (breaking env var rename for request/app logging) and corresponding test updates.
Changes:
- Updates usage analytics/request-summary tests to pull recent request summaries from SQLite
call_logs(vs legacy file-based logs). - Adds new unit coverage around settings updates, legacy request-log archival migration, model-sync logging behavior, and log retention windows.
- Expands error classification tests to cover parsing retry/cooldown durations from provider error text and updates console log tests for
APP_LOG_*env var renames.
Reviewed changes
Copilot reviewed 55 out of 57 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/usage-analytics.test.mjs | Switches “recent logs” test to exercise SQLite-backed call logs. |
| tests/unit/settings-api.test.mjs | Adds unit tests for updateSettings/getSettings around debugMode and hiddenSidebarItems. |
| tests/unit/request-log-migration.test.mjs | Adds tests for archiving/removing legacy on-disk request logs into a zip and failure behavior. |
| tests/unit/model-sync-route.test.mjs | Adds tests ensuring model-sync logging only occurs when models change and uses the current channel label. |
| tests/unit/log-retention.test.mjs | Adds test verifying separate APP vs CALL retention windows in log cleanup. |
| tests/unit/error-classification.test.mjs | Adds tests for parsing retry durations from error text and triggering long cooldown behavior. |
| tests/unit/console-log-levels.test.mjs | Updates env var usage from legacy LOG_FILE_PATH to APP_LOG_FILE_PATH. |
| tests/unit/call-log-cap.test.mjs | Reworks call log tests toward per-request artifact storage and retention-based rotation. |
| tests/e2e/settings-toggles.spec.ts | E2E coverage for settings toggles (currently has selector/state assertion issues per review comments). |
| src/lib/db/migrations/014_unified_log_artifacts.sql | Migration for unified log artifacts (minor header comment mismatch per review comment). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function createRequestLogger( | ||
| _sourceFormat?: string, | ||
| _targetFormat?: string, | ||
| _model?: string | ||
| ): Promise<RequestLogger> { | ||
| const streamChunks = createEmptyStreamChunks(); | ||
| const payloads: RequestPipelinePayloads = { | ||
| streamChunks, | ||
| }; | ||
|
|
||
| return { | ||
| get sessionPath() { | ||
| return sessionPath; | ||
| }, | ||
| sessionPath: null, | ||
|
|
||
| // 1. Log client raw request (before all conversion steps) | ||
| logClientRawRequest(endpoint, body, headers = {}) { | ||
| writeJsonFile(sessionPath, "1_req_client.json", { | ||
| payloads.clientRawRequest = { | ||
| timestamp: new Date().toISOString(), | ||
| endpoint, | ||
| headers: maskSensitiveHeaders(headers), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 2. Log raw request from client (after initial conversion like responsesApi) | ||
| logRawRequest(body, headers = {}) { | ||
| writeJsonFile(sessionPath, "2_req_source.json", { | ||
| payloads.sourceRequest = { | ||
| timestamp: new Date().toISOString(), | ||
| headers: maskSensitiveHeaders(headers), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 3. Log OpenAI intermediate format (source → openai) | ||
| logOpenAIRequest(body) { | ||
| writeJsonFile(sessionPath, "3_req_openai.json", { | ||
| payloads.openaiRequest = { | ||
| timestamp: new Date().toISOString(), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 4. Log target format request (openai → target) | ||
| logTargetRequest(url, headers, body) { | ||
| writeJsonFile(sessionPath, "4_req_target.json", { | ||
| payloads.providerRequest = { | ||
| timestamp: new Date().toISOString(), | ||
| url, | ||
| headers: maskSensitiveHeaders(headers), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 5. Log provider response (for non-streaming or error) | ||
| logProviderResponse(status, statusText, headers, body) { | ||
| const filename = "5_res_provider.json"; | ||
| writeJsonFile(sessionPath, filename, { | ||
| payloads.providerResponse = { | ||
| timestamp: new Date().toISOString(), | ||
| status, | ||
| statusText, | ||
| headers: headers | ||
| ? typeof headers.entries === "function" | ||
| ? Object.fromEntries(headers.entries()) | ||
| : headers | ||
| : {}, | ||
| headers: maskSensitiveHeaders(headers), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 5. Append streaming chunk to provider response (async) | ||
| appendProviderChunk(chunk) { | ||
| if (!fs || !sessionPath) return; | ||
| const filePath = path.join(sessionPath, "5_res_provider.txt"); | ||
| fs.promises.appendFile(filePath, chunk).catch(() => {}); | ||
| if (typeof chunk === "string" && chunk.length > 0) { | ||
| streamChunks.provider.push(chunk); | ||
| } | ||
| }, | ||
|
|
||
| // 6. Append OpenAI intermediate chunks (async) | ||
| appendOpenAIChunk(chunk) { | ||
| if (!fs || !sessionPath) return; | ||
| const filePath = path.join(sessionPath, "6_res_openai.txt"); | ||
| fs.promises.appendFile(filePath, chunk).catch(() => {}); | ||
| if (typeof chunk === "string" && chunk.length > 0) { | ||
| streamChunks.openai.push(chunk); | ||
| } | ||
| }, | ||
|
|
||
| // 7. Log converted response to client (for non-streaming) | ||
| logConvertedResponse(body) { | ||
| writeJsonFile(sessionPath, "7_res_client.json", { | ||
| payloads.clientResponse = { | ||
| timestamp: new Date().toISOString(), | ||
| body, | ||
| }); | ||
| }; | ||
| }, | ||
|
|
||
| // 7b. Append streaming chunk to converted response (async) | ||
| appendConvertedChunk(chunk) { | ||
| if (!fs || !sessionPath) return; | ||
| const filePath = path.join(sessionPath, "7_res_client.txt"); | ||
| fs.promises.appendFile(filePath, chunk).catch(() => {}); | ||
| if (typeof chunk === "string" && chunk.length > 0) { | ||
| streamChunks.client.push(chunk); | ||
| } | ||
| }, |
There was a problem hiding this comment.
createRequestLogger() now buffers every streamed chunk into in-memory arrays (streamChunks.*.push(chunk)). Since chatCore creates a request logger for every request and stream.ts calls reqLogger.appendProviderChunk() for every chunk, this can accumulate unbounded memory on long/large streams even when pipeline capture is disabled (the payloads are later discarded). Consider returning a no-op logger unless pipeline capture is enabled, or gating/capping chunk buffering (e.g., max bytes/chunks) so streaming can’t OOM the process.
| await page.click("text=Advanced"); | ||
|
|
||
| const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); | ||
|
|
||
| await expect(debugToggle).toBeVisible({ timeout: 5000 }); | ||
|
|
||
| const initialState = await debugToggle.isChecked(); | ||
| await debugToggle.click(); | ||
| await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); | ||
| }); |
There was a problem hiding this comment.
These Playwright locators/assertions don’t match the actual Toggle implementation. The settings Toggle renders a button role="switch" with aria-label="Toggle" (no “debug” substring) and uses aria-checked, so locator('[aria-label*="debug"...]') will not find it and isChecked()/toBeChecked() won’t behave as intended. Additionally, the test always expects the toggle to be unchecked after clicking, regardless of the initial state. Update the test to target the switch reliably (e.g., by locating the row by its visible label text and then selecting its role=switch) and assert the state flips via aria-checked (or similar).
| test("Debug mode should persist after page reload", async ({ page }) => { | ||
| await page.goto("/dashboard/settings"); | ||
| await page.waitForLoadState("networkidle"); | ||
| await page.click("text=Advanced"); | ||
|
|
||
| const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); | ||
|
|
||
| await expect(debugToggle).toBeVisible({ timeout: 5000 }); | ||
|
|
||
| const wasChecked = await debugToggle.isChecked(); | ||
| await debugToggle.click(); | ||
| await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); | ||
| await page.reload(); | ||
| await page.waitForLoadState("networkidle"); | ||
| await page.click("text=Advanced"); | ||
| await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); | ||
| }); |
There was a problem hiding this comment.
Same issue as above for the persistence test: the locator likely won’t find the switch (no accessible name includes “debug”), and toBeChecked() is not appropriate for a role="switch" button. The assertion also hard-codes not.toBeChecked() after clicking without accounting for the prior state. Please rewrite this to (1) locate the correct switch and (2) assert aria-checked toggles and persists across reload (e.g., expect it to equal the flipped value after reload).
| import { describe, test } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { getSettings, updateSettings } from "../../src/lib/db/settings.ts"; | ||
|
|
||
| describe("Settings API - debugMode and hiddenSidebarItems", () => { | ||
| describe("debugMode", () => { | ||
| test("updateSettings with debugMode=true succeeds", async () => { | ||
| const result = await updateSettings({ debugMode: true }); | ||
| assert.ok(result, "updateSettings should return truthy result"); | ||
|
|
||
| const settings = await getSettings(); | ||
| assert.strictEqual(settings.debugMode, true, "debugMode should be true"); | ||
| }); |
There was a problem hiding this comment.
This test updates the real SQLite settings DB because it does not set process.env.DATA_DIR to a temp directory or reset the DB singleton (via core.resetDbInstance()). That can make the test suite order-dependent and can write into a developer’s actual OmniRoute data dir when running locally. Please isolate it like the other DB-backed unit tests: create a per-file temp DATA_DIR, reset/remove it in hooks, and reset the DB instance between tests.
| function resetDataDir() { | ||
| fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); | ||
| fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); | ||
| } | ||
|
|
||
| function seedLegacyLayout() { | ||
| fs.mkdirSync(path.join(LEGACY_LOGS_DIR, "session-a"), { recursive: true }); | ||
| fs.writeFileSync( | ||
| path.join(LEGACY_LOGS_DIR, "session-a", "1_req_client.json"), | ||
| JSON.stringify({ ok: true }, null, 2) | ||
| ); | ||
|
|
||
| fs.mkdirSync(path.join(LEGACY_CALL_LOGS_DIR, "2026-03-30"), { recursive: true }); | ||
| fs.writeFileSync( | ||
| path.join(LEGACY_CALL_LOGS_DIR, "2026-03-30", "123000_model_200.json"), | ||
| JSON.stringify({ ok: true }, null, 2) | ||
| ); | ||
|
|
||
| fs.writeFileSync(LEGACY_SUMMARY_FILE, "legacy summary\n"); | ||
| } | ||
|
|
||
| test.beforeEach(() => { | ||
| resetDataDir(); | ||
| }); | ||
|
|
||
| test.after(() => { | ||
| resetDataDir(); | ||
| }); |
There was a problem hiding this comment.
The temp data directory created via mkdtempSync is never removed at the end of the file: test.after() calls resetDataDir(), which recreates the directory instead of deleting it. This will leak directories under the OS temp folder across test runs. Consider changing the after hook to rmSync(TEST_DATA_DIR, { recursive: true, force: true }) (without re-creating), and optionally restore process.env.DATA_DIR if other tests rely on it.
| function resetStorage() { | ||
| core.resetDbInstance(); | ||
| fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); | ||
| fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); | ||
| } | ||
|
|
||
| test.beforeEach(() => { | ||
| resetStorage(); | ||
| }); | ||
|
|
||
| test.after(() => { | ||
| resetStorage(); | ||
| }); |
There was a problem hiding this comment.
Same temp-dir cleanup issue here: test.after() calls resetStorage(), which recreates TEST_DATA_DIR instead of deleting it, leaving an empty temp directory behind after the suite completes. Consider removing the directory in the after hook (and optionally restoring the original env vars) to avoid leaking temp artifacts.
| -- 013_unified_log_artifacts.sql | ||
| -- Switch request logging to unified single-file artifacts and prefixed settings. |
There was a problem hiding this comment.
The header comment appears to reference the wrong migration number (-- 013_unified_log_artifacts.sql) even though the filename is 014_unified_log_artifacts.sql. This can be confusing when debugging applied migrations; update the comment to match the actual migration ID.
…ling - Fix Claude OAuth token refresh to use form-urlencoded format (standard OAuth2) - Add anthropic-beta header required by Claude OAuth API - Switch Antigravity quota to use retrieveUserQuota API (same as Gemini CLI) - Parse quota reset time for all providers (not just Antigravity) - Add quota reset keywords to error classifier - Cap maximum retry time at 24 hours to prevent infinite wait Closes #836, #857, #858, #832
…rate-limit handling
…s via chunk parallelization (#784)
* test(settings): add unit tests for debugMode and hiddenSidebarItems Tests cover: - PATCH debugMode=true/false - PATCH hiddenSidebarItems with array values - Combined updates with both fields * test(e2e): add Playwright tests for settings toggles Tests cover: - Debug mode toggle on/off - Sidebar visibility toggle - Settings persistence after page reload * fix(tests): address code review issues - Unit tests: fix async/await for getSettings, use direct db functions - E2E tests: remove conditional logic, use Playwright auto-waiting assertions * feat(logging): unify request log retention and artifacts * docs: add dashboard settings toggles to CONTRIBUTING Add section documenting: - Debug Mode toggle (Settings → Advanced) - Sidebar Visibility toggle (Settings → General) * fix(cache): only inject prompt_cache_key for supported providers Only inject prompt_cache_key for providers that support prompt caching (Claude, Anthropic, ZAI, Qwen, DeepSeek). This fixes issue diegosouzapw#848 where NVIDIA API rejected the parameter. * fix(model-sync): log only channel-level model changes * feat(providers): add 4 free models to opencode-zen * feat(providers): add explicit contextLength for opencode-zen free models * feat(providers): add contextLength for all opencode-zen models * feat: Improve the Chinese translation * fix: preserve client cache_control for all Claude-protocol providers Previously, the cache control preservation logic only recognized a hardcoded list of providers (claude, anthropic, zai, qwen, deepseek). This caused OmniRoute to inject its own cache_control markers for Claude-protocol providers not in that list (bailian-coding-plan, glm, minimax, minimax-cn, etc.), overwriting the client's cache markers. The fix checks both: 1. Known caching providers list (existing behavior) 2. Whether targetFormat === 'claude' (all Claude-protocol providers) This ensures all Claude-compatible providers properly preserve client cache_control headers when appropriate (Claude Code client, deterministic routing, etc.). Also removes unused CacheStatsCard from settings/components (duplicate of the one in cache/ page). Fixes cache token calculation for GLM, Minimax, and other Claude-compatible providers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pure passthrough for Claude→Claude when cache_control preserved The Claude passthrough path round-trips through OpenAI format (claude→openai→claude) for structural normalization. This strips cache_control markers from every content block since OpenAI format has no equivalent, causing ~42k cache creation tokens per request with zero cache reads. When preserveCacheControl is true (Claude Code client, "always" setting, or deterministic combo), skip the round-trip entirely and forward the body as-is. Claude Code sends well-formed Messages API payloads — the normalization was only needed for non-Code clients. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore CacheStatsCard — was not a duplicate The first commit incorrectly deleted CacheStatsCard from settings/components/ as a "duplicate". It's the only copy — both settings/page.tsx and cache/page.tsx import from this location. Restored the i18n-ized version from main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(429): parse long quota reset times from error body - Parse XhYmZs format from antigravity error messages (e.g., 27h41m36s) - Dynamic retry-after threshold (60s default) instead of hardcoded 10s - Add parseRetryFromErrorText() in accountFallback.ts for body parsing - Fix 403 'verify your account' to trigger permanent deactivation - Add keyword matching for 'quota will reset', 'exhausted capacity' - Add unit tests for retry parsing and keyword matching Fixes diegosouzapw#858 (Antigravity 429 handling) Fixes diegosouzapw#832 (Qwen quota 429 - same underlying bug) * chore: bump version to v3.4.0-dev * fix(migrations): rename 013 to 014 to avoid collision with v3.3.11 * chore(docs): update CHANGELOG for v3.4.0 integrations * fix: Claude token refresh, Antigravity quota, and 429 rate-limit handling - Fix Claude OAuth token refresh to use form-urlencoded format (standard OAuth2) - Add anthropic-beta header required by Claude OAuth API - Switch Antigravity quota to use retrieveUserQuota API (same as Gemini CLI) - Parse quota reset time for all providers (not just Antigravity) - Add quota reset keywords to error classifier - Cap maximum retry time at 24 hours to prevent infinite wait Closes diegosouzapw#836, diegosouzapw#857, diegosouzapw#858, diegosouzapw#832 * fix(dashboard): resolve /dashboard/limits hanging UI with 70+ accounts via chunk parallelization (diegosouzapw#784) --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: R.D. <rogerproself@gmail.com> Co-authored-by: kang-heewon <heewon.dev@gmail.com> Co-authored-by: gmw <rorschach1167@qq.com> Co-authored-by: tombii <github@tombii.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
CodeQL js/incomplete-url-substring-sanitization, alerts #860 and #861: volcengineConsoleAutoLogin accepted any cookie whose `domain` merely *contained* "volcengine.com". That check is an authorization decision, not a string test. The console auto-login harvests `digest`, `AccountID`, `csrfToken` and `userInfo` out of the Playwright context and persists them as the operator's Volcengine credentials, so a cookie set by `volcengine.com.attacker.tld` — or `notvolcengine.com` — was captured and stored as a provider connection. Add `matchesCookieDomain()` (open-sse/utils/cookieDomain.ts): exact host or dot-boundary suffix, leading dots and case normalized on both sides, failing closed on an empty expected domain. Same shape as the existing `isAdobeCookieDomain` in adobeFireflyBrowserLogin.ts, which already got this right. While sweeping the class, inAppLoginService's cookie capture had the identical weakness — `c.domain.includes(domain.replace(/^\./, ""))` — with the identical consequence: a look-alike host's cookie stored as the operator's credential. CodeQL did not flag it because the expected domain comes from TOKEN_EXTRACTION_CONFIGS rather than a literal. Both callsites now share the helper. tests/unit/volcengine-cookie-domain-suffix.test.ts — 5 tests, red before the fix, covering the real domains, seven look-alikes, empty/missing input, and the config-supplied path. Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* test(settings): add unit tests for debugMode and hiddenSidebarItems Tests cover: - PATCH debugMode=true/false - PATCH hiddenSidebarItems with array values - Combined updates with both fields * test(e2e): add Playwright tests for settings toggles Tests cover: - Debug mode toggle on/off - Sidebar visibility toggle - Settings persistence after page reload * fix(tests): address code review issues - Unit tests: fix async/await for getSettings, use direct db functions - E2E tests: remove conditional logic, use Playwright auto-waiting assertions * feat(logging): unify request log retention and artifacts * docs: add dashboard settings toggles to CONTRIBUTING Add section documenting: - Debug Mode toggle (Settings → Advanced) - Sidebar Visibility toggle (Settings → General) * fix(cache): only inject prompt_cache_key for supported providers Only inject prompt_cache_key for providers that support prompt caching (Claude, Anthropic, ZAI, Qwen, DeepSeek). This fixes issue diegosouzapw#848 where NVIDIA API rejected the parameter. * fix(model-sync): log only channel-level model changes * feat(providers): add 4 free models to opencode-zen * feat(providers): add explicit contextLength for opencode-zen free models * feat(providers): add contextLength for all opencode-zen models * feat: Improve the Chinese translation * fix: preserve client cache_control for all Claude-protocol providers Previously, the cache control preservation logic only recognized a hardcoded list of providers (claude, anthropic, zai, qwen, deepseek). This caused OmniRoute to inject its own cache_control markers for Claude-protocol providers not in that list (bailian-coding-plan, glm, minimax, minimax-cn, etc.), overwriting the client's cache markers. The fix checks both: 1. Known caching providers list (existing behavior) 2. Whether targetFormat === 'claude' (all Claude-protocol providers) This ensures all Claude-compatible providers properly preserve client cache_control headers when appropriate (Claude Code client, deterministic routing, etc.). Also removes unused CacheStatsCard from settings/components (duplicate of the one in cache/ page). Fixes cache token calculation for GLM, Minimax, and other Claude-compatible providers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pure passthrough for Claude→Claude when cache_control preserved The Claude passthrough path round-trips through OpenAI format (claude→openai→claude) for structural normalization. This strips cache_control markers from every content block since OpenAI format has no equivalent, causing ~42k cache creation tokens per request with zero cache reads. When preserveCacheControl is true (Claude Code client, "always" setting, or deterministic combo), skip the round-trip entirely and forward the body as-is. Claude Code sends well-formed Messages API payloads — the normalization was only needed for non-Code clients. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore CacheStatsCard — was not a duplicate The first commit incorrectly deleted CacheStatsCard from settings/components/ as a "duplicate". It's the only copy — both settings/page.tsx and cache/page.tsx import from this location. Restored the i18n-ized version from main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(429): parse long quota reset times from error body - Parse XhYmZs format from antigravity error messages (e.g., 27h41m36s) - Dynamic retry-after threshold (60s default) instead of hardcoded 10s - Add parseRetryFromErrorText() in accountFallback.ts for body parsing - Fix 403 'verify your account' to trigger permanent deactivation - Add keyword matching for 'quota will reset', 'exhausted capacity' - Add unit tests for retry parsing and keyword matching Fixes diegosouzapw#858 (Antigravity 429 handling) Fixes diegosouzapw#832 (Qwen quota 429 - same underlying bug) * chore: bump version to v3.4.0-dev * fix(migrations): rename 013 to 014 to avoid collision with v3.3.11 * chore(docs): update CHANGELOG for v3.4.0 integrations * fix: Claude token refresh, Antigravity quota, and 429 rate-limit handling - Fix Claude OAuth token refresh to use form-urlencoded format (standard OAuth2) - Add anthropic-beta header required by Claude OAuth API - Switch Antigravity quota to use retrieveUserQuota API (same as Gemini CLI) - Parse quota reset time for all providers (not just Antigravity) - Add quota reset keywords to error classifier - Cap maximum retry time at 24 hours to prevent infinite wait Closes diegosouzapw#836, diegosouzapw#857, diegosouzapw#858, diegosouzapw#832 * fix(dashboard): resolve /dashboard/limits hanging UI with 70+ accounts via chunk parallelization (diegosouzapw#784) --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: R.D. <rogerproself@gmail.com> Co-authored-by: kang-heewon <heewon.dev@gmail.com> Co-authored-by: gmw <rorschach1167@qq.com> Co-authored-by: tombii <github@tombii.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
…uzapw#11429) CodeQL js/incomplete-url-substring-sanitization, alerts diegosouzapw#860 and diegosouzapw#861: volcengineConsoleAutoLogin accepted any cookie whose `domain` merely *contained* "volcengine.com". That check is an authorization decision, not a string test. The console auto-login harvests `digest`, `AccountID`, `csrfToken` and `userInfo` out of the Playwright context and persists them as the operator's Volcengine credentials, so a cookie set by `volcengine.com.attacker.tld` — or `notvolcengine.com` — was captured and stored as a provider connection. Add `matchesCookieDomain()` (open-sse/utils/cookieDomain.ts): exact host or dot-boundary suffix, leading dots and case normalized on both sides, failing closed on an empty expected domain. Same shape as the existing `isAdobeCookieDomain` in adobeFireflyBrowserLogin.ts, which already got this right. While sweeping the class, inAppLoginService's cookie capture had the identical weakness — `c.domain.includes(domain.replace(/^\./, ""))` — with the identical consequence: a look-alike host's cookie stored as the operator's credential. CodeQL did not flag it because the expected domain comes from TOKEN_EXTRACTION_CONFIGS rather than a literal. Both callsites now share the helper. tests/unit/volcengine-cookie-domain-suffix.test.ts — 5 tests, red before the fix, covering the real domains, seven look-alikes, empty/missing input, and the config-supplied path. Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Integrates all recent bug fixes and community contributions. Includes breaking changes to the request logging environment variables.