Skip to content

Release v3.4.0 (Integration) - #861

Merged
diegosouzapw merged 28 commits into
mainfrom
release/v3.4.0
Mar 31, 2026
Merged

diegosouzapw merged 28 commits into
mainfrom
release/v3.4.0

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Integrates all recent bug fixes and community contributions. Includes breaking changes to the request logging environment variables.

oyi77 and others added 25 commits March 31, 2026 05:54
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)
Copilot AI review requested due to automatic review settings March 31, 2026 12:18
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@kilo-code-bot

kilo-code-bot Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No New Issues | Recommendation: Merge

Incremental Review

Only 6 files were modified since the previous review (commit 6de76ea):

File Change Assessment
CHANGELOG.md +3 lines documenting fixes ✅
open-sse/services/accountFallback.ts Added "quota will reset" keywords for error classification ✅
open-sse/services/tokenRefresh.ts Fixed Claude OAuth token refresh to use form-urlencoded (OAuth2 spec compliant) ✅
open-sse/services/usage.ts Switched Antigravity from deprecated fetchAvailableModels to retrieveUserQuota API ✅
open-sse/utils/error.ts Universalized 429 retry parsing and added 24-hour cap to prevent infinite waits ✅
src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx Fixed UI freezing by loading table immediately after connections load ✅

All changes are improvements. No new issues introduced.

Previous Issues (Carried Forward)

File Line Issue
open-sse/executors/antigravity.ts 195 Missing radix parameter in parseInt (not in this diff - carries forward)

Files Reviewed (6 files)

  • CHANGELOG.md
  • open-sse/services/accountFallback.ts
  • open-sse/services/tokenRefresh.ts
  • open-sse/services/usage.ts
  • open-sse/utils/error.ts
  • src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx

The changes improve token refresh compliance, Antigravity quota accuracy, 429 parsing universality, and dashboard UI responsiveness. No blocking issues.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
-- 013_unified_log_artifacts.sql
-- 014_unified_log_artifacts.sql

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +133 to 212
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);
}
},

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +16
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 });
});

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +50
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 });
});

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +13
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");
});

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +44
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();
});

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +27
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();
});

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +2
-- 013_unified_log_artifacts.sql
-- Switch request logging to unified single-file artifacts and prefixed settings.

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
oyi77 and others added 3 commits March 31, 2026 19:30
…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
@diegosouzapw
diegosouzapw merged commit 70a4d38 into main Mar 31, 2026
5 checks passed
@diegosouzapw
diegosouzapw deleted the release/v3.4.0 branch April 1, 2026 11:51
@diegosouzapw
diegosouzapw restored the release/v3.4.0 branch April 1, 2026 11:51
@diegosouzapw
diegosouzapw deleted the release/v3.4.0 branch April 1, 2026 12:02
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
* 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>
diegosouzapw added a commit that referenced this pull request Aug 24, 2026
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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
* 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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants