Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
981c1c1
test(settings): add unit tests for debugMode and hiddenSidebarItems
oyi77 Mar 30, 2026
8f5c9a3
test(e2e): add Playwright tests for settings toggles
oyi77 Mar 31, 2026
b98d698
fix(tests): address code review issues
oyi77 Mar 31, 2026
f8d4e1a
feat(logging): unify request log retention and artifacts
rdself Mar 31, 2026
ae3d2be
docs: add dashboard settings toggles to CONTRIBUTING
oyi77 Mar 31, 2026
ac37a44
fix(cache): only inject prompt_cache_key for supported providers
oyi77 Mar 31, 2026
715101c
fix(model-sync): log only channel-level model changes
rdself Mar 31, 2026
28c2fb9
feat(providers): add 4 free models to opencode-zen
kang-heewon Mar 31, 2026
1f0a584
feat(providers): add explicit contextLength for opencode-zen free models
kang-heewon Mar 31, 2026
641f84e
feat(providers): add contextLength for all opencode-zen models
kang-heewon Mar 31, 2026
2722847
feat: Improve the Chinese translation
Rorschachina Mar 31, 2026
75425ab
fix: preserve client cache_control for all Claude-protocol providers
tombii Mar 31, 2026
892830e
fix: pure passthrough for Claude→Claude when cache_control preserved
tombii Mar 31, 2026
ce28dcc
fix: restore CacheStatsCard — was not a duplicate
tombii Mar 31, 2026
a405f2e
fix(429): parse long quota reset times from error body
oyi77 Mar 31, 2026
3fa7828
Merge PR #859: fix(429): parse long quota reset times from error body
diegosouzapw Mar 31, 2026
f695559
Merge PR #856: fix: preserve client cache_control for all Claude-prot…
diegosouzapw Mar 31, 2026
df972b9
Merge PR #855: feat: Improve the Chinese translation
diegosouzapw Mar 31, 2026
d4ad9b3
Merge PR #854: feat(providers): add 4 free models to opencode-zen
diegosouzapw Mar 31, 2026
2a0c4d2
Merge PR #853: fix(model-sync): log only real channel model changes
diegosouzapw Mar 31, 2026
56ef849
Merge PR #850: test: add unit and E2E tests for settings toggles
diegosouzapw Mar 31, 2026
bcea92e
chore: bump version to v3.4.0-dev
diegosouzapw Mar 31, 2026
2f76552
Merge PR #851: feat(logging): unify request log retention and artifacts
diegosouzapw Mar 31, 2026
262e72d
fix(migrations): rename 013 to 014 to avoid collision with v3.3.11
diegosouzapw Mar 31, 2026
6de76ea
chore(docs): update CHANGELOG for v3.4.0 integrations
diegosouzapw Mar 31, 2026
c0f9b33
fix: Claude token refresh, Antigravity quota, and 429 rate-limit hand…
oyi77 Mar 31, 2026
e712883
Merge PR #862: fix: Claude token refresh, Antigravity quota, and 429 …
diegosouzapw Mar 31, 2026
0ad8576
fix(dashboard): resolve /dashboard/limits hanging UI with 70+ account…
diegosouzapw Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ vscode-extension/

# Compiled npm-package build artifact (not source, should not be in git)
/app

# IDEA
.idea/
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

## [Unreleased]

> [!WARNING]
> **BREAKING CHANGE: request logging, retention, and logging environment variables have been redesigned.**
> On the first startup after upgrading, OmniRoute archives legacy request logs from `DATA_DIR/logs/`, legacy `DATA_DIR/call_logs/`, and `DATA_DIR/log.txt` into `DATA_DIR/log_archives/*.zip`, then removes the deprecated layout and switches to the new unified artifact format under `DATA_DIR/call_logs/`.

### ✨ New Features

- **Unified Request Log Artifacts:** Request logging now stores one SQLite index row plus one JSON artifact per request under `DATA_DIR/call_logs/`, with optional pipeline capture embedded in the same file.
- **Language:** Improved the Chinese translation (#855)
- **Opencode-Zen Models:** Added 4 free models to opencode-zen registry (#854)
- **Tests:** Added unit and E2E tests for settings toggles and bug fixes (#850)

### 🐛 Bug Fixes

- **429 Quota Parsing:** Parsed long quota reset times from error bodies to honor correct backoffs and prevent rate-limited account bans (#859)
- **Prompt Caching:** Preserved client `cache_control` headers for all Claude-protocol providers (like Minimax, GLM, and Bailian), correctly recognizing caching support (#856)
- **Model Sync Logs:** Reduced log spam by recording `sync-models` only when the channel actually modifies the list (#853)

### ⚠️ Breaking Changes

- **Request Log Layout:** Removed the old multi-file `DATA_DIR/logs/` request log sessions and the `DATA_DIR/log.txt` summary file. New requests are written as single JSON artifacts in `DATA_DIR/call_logs/YYYY-MM-DD/`.
- **Logging Environment Variables:** Replaced `LOG_*`, `ENABLE_REQUEST_LOGS`, `CALL_LOGS_MAX`, `CALL_LOG_PAYLOAD_MODE`, and `PROXY_LOG_MAX_ENTRIES` with the new `APP_LOG_*` and `CALL_LOG_RETENTION_DAYS` configuration model.
- **Pipeline Toggle Setting:** Replaced the legacy `detailed_logs_enabled` setting with `call_log_pipeline_enabled`. New pipeline details are embedded inside the request artifact instead of being stored as separate `request_detail_logs` records.

### 🛠️ Maintenance

- **Legacy Request Log Upgrade Backup:** Upgrades now archive old `data/logs/`, legacy `data/call_logs/`, and `data/log.txt` layouts into `DATA_DIR/log_archives/*.zip` before removing the deprecated structure.
- **Streaming Usage Persistence:** Streaming requests now write a single `usage_history` row on completion instead of emitting a duplicate in-progress usage row with empty status metadata.

---

## [3.3.11] - 2026-03-31
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ Key variables for development:
| `INITIAL_PASSWORD` | `123456` | First login password |
| `ENABLE_REQUEST_LOGS` | `false` | Enable debug request logs |

### Dashboard Settings

The dashboard provides UI toggles for features that can also be configured via environment variables:

| Setting Location | Toggle | Description |
| ------------------- | ------------------ | ------------------------------ |
| Settings → Advanced | Debug Mode | Enable debug request logs (UI) |
| Settings → General | Sidebar Visibility | Show/hide sidebar sections |

These settings are stored in the database and persist across restarts, overriding env var defaults when set.

### Running Locally

```bash
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ _Your universal API proxy — one endpoint, 67+ providers, zero downtime. Now wi

---

## Breaking Change: Unified Logging Upgrade

> [!WARNING]
> **This release changes both the on-disk request log layout and the logging environment variables.**
>
> If you are upgrading an existing instance:
>
> - Request logs now live in `DATA_DIR/call_logs/YYYY-MM-DD/` as **one JSON artifact per request**.
> - The old `DATA_DIR/logs/` session folders and `DATA_DIR/log.txt` summary file are removed.
> - On the first startup after upgrading, OmniRoute creates a safety backup at `DATA_DIR/log_archives/*.zip` before removing the deprecated request log layout.
> - Legacy logging env vars such as `LOG_TO_FILE`, `LOG_FILE_PATH`, `LOG_MAX_FILE_SIZE`, `LOG_RETENTION_DAYS`, `LOG_LEVEL`, `LOG_FORMAT`, `ENABLE_REQUEST_LOGS`, `CALL_LOGS_MAX`, `CALL_LOG_PAYLOAD_MODE`, and `PROXY_LOG_MAX_ENTRIES` are no longer supported.
> - Use the new env model instead:
> - `APP_LOG_TO_FILE`
> - `APP_LOG_FILE_PATH`
> - `APP_LOG_MAX_FILE_SIZE`
> - `APP_LOG_RETENTION_DAYS`
> - `APP_LOG_LEVEL`
> - `APP_LOG_FORMAT`
> - `CALL_LOG_RETENTION_DAYS`
>
> For release details and upgrade notes, see the [CHANGELOG](CHANGELOG.md).

---

## 🆕 What's New in v3.0.0

> **Upgrading from v2.9.5?** — See the [full CHANGELOG](CHANGELOG.md#300--2026-03-22-release-candidate--not-yet-merged-to-main) for all changes.
Expand Down Expand Up @@ -1811,7 +1835,9 @@ opencode

**No request logs**

- Set `ENABLE_REQUEST_LOGS=true` in `.env`
- Request artifacts are written to `DATA_DIR/call_logs/` as one JSON file per request
- Enable pipeline capture from Dashboard → Logs → Request Logs if you need detailed per-stage payloads
- Set `APP_LOG_TO_FILE=true` if you also want application console logs in `logs/application/app.log`

**Connection test shows "Invalid" for OpenAI-compatible providers**

Expand Down
10 changes: 7 additions & 3 deletions open-sse/config/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,9 +591,13 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authPrefix: "Bearer",
defaultContextLength: 200000,
models: [
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" },
{ id: "big-pickle", name: "Big Pickle" },
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 },
{ id: "big-pickle", name: "Big Pickle", contextLength: 200000 },
{ id: "gpt-5-nano", name: "GPT 5 Nano", contextLength: 400000 },
{ id: "mimo-v2-omni-free", name: "MiMo V2 Omni Free", contextLength: 262144 },
{ id: "mimo-v2-pro-free", name: "MiMo V2 Pro Free", contextLength: 1048576 },
{ id: "nemotron-3-super-free", name: "Nemotron 3 Super Free", contextLength: 1000000 },
{ id: "qwen3.6-plus-free", name: "Qwen 3.6 Plus Free", contextLength: 1048576 },
],
},

Expand Down
42 changes: 37 additions & 5 deletions open-sse/executors/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import crypto from "crypto";
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";

const MAX_RETRY_AFTER_MS = 10000;
const MAX_RETRY_AFTER_MS = 60_000;
const LONG_RETRY_THRESHOLD_MS = 60_000;

/**
* Strip provider prefixes (e.g. "antigravity/model" → "model").
Expand Down Expand Up @@ -224,12 +225,15 @@ export class AntigravityExecutor extends BaseExecutor {
signal,
});

// Parse retry time for 429/503 responses
let retryMs = null;

if (
response.status === HTTP_STATUS.RATE_LIMITED ||
response.status === HTTP_STATUS.SERVICE_UNAVAILABLE
) {
// Try to get retry time from headers first
let retryMs = this.parseRetryHeaders(response.headers);
retryMs = this.parseRetryHeaders(response.headers);

// If no retry time in headers, try to parse from error message body
if (!retryMs) {
Expand All @@ -243,12 +247,13 @@ export class AntigravityExecutor extends BaseExecutor {
}
}

if (retryMs && retryMs <= MAX_RETRY_AFTER_MS) {
if (retryMs && retryMs <= LONG_RETRY_THRESHOLD_MS) {
const effectiveRetryMs = Math.min(retryMs, MAX_RETRY_AFTER_MS);
log?.debug?.(
"RETRY",
`${response.status} with Retry-After: ${Math.ceil(retryMs / 1000)}s, waiting...`
`${response.status} with Retry-After: ${Math.ceil(effectiveRetryMs / 1000)}s, waiting...`
);
await new Promise((resolve) => setTimeout(resolve, retryMs));
await new Promise((resolve) => setTimeout(resolve, effectiveRetryMs));
urlIndex--;
continue;
}
Expand Down Expand Up @@ -291,6 +296,33 @@ export class AntigravityExecutor extends BaseExecutor {
continue;
}

// If we have a 429 with long retry time, embed it in response body
if (
response.status === HTTP_STATUS.RATE_LIMITED &&
retryMs &&
retryMs > LONG_RETRY_THRESHOLD_MS
) {
try {
const respBody = await response.clone().text();
let obj;
try {
obj = JSON.parse(respBody);
} catch {
obj = {};
}
obj.retryAfterMs = retryMs;
const modifiedBody = JSON.stringify(obj);
const modifiedResponse = new Response(modifiedBody, {
status: response.status,
headers: response.headers,
});
return { response: modifiedResponse, url, headers, transformedBody };
} catch (err) {
log?.warn?.("RETRY", `Failed to embed retryAfterMs: ${err}`);
// Fall back to original response
}
}

return { response, url, headers, transformedBody };
} catch (error) {
lastError = error;
Expand Down
88 changes: 61 additions & 27 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { HTTP_STATUS, PROVIDER_MAX_TOKENS } from "../config/constants.ts";
import { classifyProviderError, PROVIDER_ERROR_TYPES } from "../services/errorClassifier.ts";
import { updateProviderConnection } from "@/lib/db/providers";
import { isDetailedLoggingEnabled, saveRequestDetailLog } from "@/lib/db/detailedLogs";
import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs";
import { logAuditEvent } from "@/lib/compliance";
import { handleBypassRequest } from "../utils/bypassHandler.ts";
import {
Expand All @@ -47,7 +47,10 @@ import {
} from "@/lib/localDb";
import { getExecutor } from "../executors/index.ts";
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
import { shouldPreserveCacheControl } from "../utils/cacheControlPolicy.ts";
import {
shouldPreserveCacheControl,
providerSupportsCaching,
} from "../utils/cacheControlPolicy.ts";
import { getCacheMetrics } from "@/lib/db/settings.ts";

import {
Expand Down Expand Up @@ -533,6 +536,24 @@ export async function handleChatCore({
claudeCacheUsageMeta?: Record<string, unknown>;
}) => {
const callLogId = generateRequestId();
const pipelinePayloads = detailedLoggingEnabled ? reqLogger?.getPipelinePayloads?.() : null;

if (pipelinePayloads) {
if (providerResponse !== undefined) {
pipelinePayloads.providerResponse = providerResponse as Record<string, unknown>;
}
if (clientResponse !== undefined) {
pipelinePayloads.clientResponse = clientResponse as Record<string, unknown>;
}
if (error) {
pipelinePayloads.error = {
...(typeof pipelinePayloads.error === "object" && pipelinePayloads.error
? (pipelinePayloads.error as Record<string, unknown>)
: {}),
message: error,
};
}
}

saveCallLog({
id: callLogId,
Expand Down Expand Up @@ -565,31 +586,8 @@ export async function handleChatCore({
apiKeyId: apiKeyInfo?.id || null,
apiKeyName: apiKeyInfo?.name || null,
noLog: noLogEnabled,
pipelinePayloads,
}).catch(() => {});

if (!detailedLoggingEnabled) {
return;
}

try {
saveRequestDetailLog({
call_log_id: callLogId,
client_request: clientRawRequest?.body ?? body,
translated_request: providerRequest ?? null,
provider_response: providerResponse ?? null,
client_response: clientResponse ?? null,
provider,
model,
source_format: sourceFormat,
target_format: targetFormat,
duration_ms: Date.now() - startTime,
api_key_id: apiKeyInfo?.id || null,
no_log: noLogEnabled,
});
} catch (err) {
const errMessage = err instanceof Error ? err.message : String(err);
log?.debug?.("DETAIL_LOG", `Failed to save detailed log: ${errMessage}`);
}
};

// Primary path: merge client model id + alias target so config on either key applies; resolved
Expand Down Expand Up @@ -697,6 +695,7 @@ export async function handleChatCore({
isCombo,
comboStrategy,
targetProvider: provider,
targetFormat,
settings: { alwaysPreserveClientCache: cacheControlMode },
});

Expand All @@ -711,6 +710,15 @@ export async function handleChatCore({
if (nativeCodexPassthrough) {
translatedBody = { ...body, _nativeCodexPassthrough: true };
log?.debug?.("FORMAT", "native codex passthrough enabled");
} else if (isClaudePassthrough && preserveCacheControl) {
// Pure passthrough: when preserveCacheControl is true, forward the body
// as-is without any normalization. The OpenAI round-trip would strip
// cache_control markers; even prepareClaudeRequest can alter structure.
// Claude Code sends well-formed Messages API payloads — trust it.
translatedBody = { ...body };
translatedBody._disableToolPrefix = true;

log?.debug?.("FORMAT", "claude passthrough with cache_control preservation");
} else if (isClaudePassthrough) {
// Claude OAuth expects the same Claude Code prompt + structural normalization
// as the OpenAI-compatible chat path. Round-trip through OpenAI to reuse the
Expand Down Expand Up @@ -955,9 +963,10 @@ export async function handleChatCore({
? translatedBody
: { ...translatedBody, model: modelToCall };

// Inject prompt_cache_key for OpenAI providers if not already set
// Inject prompt_cache_key only for providers that support it
if (
targetFormat === FORMATS.OPENAI &&
providerSupportsCaching(provider) &&
!bodyToSend.prompt_cache_key &&
Array.isArray(bodyToSend.messages) &&
!["nvidia", "codex", "xai"].includes(provider)
Expand Down Expand Up @@ -1190,6 +1199,31 @@ export async function handleChatCore({
console.warn(
`[provider] Node ${connectionId} banned (${statusCode}) — disabling permanently`
);
} else if (errorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED) {
await updateProviderConnection(connectionId, {
isActive: false,
testStatus: "deactivated",
lastErrorType: errorType,
lastError: message,
errorCode: statusCode,
});
console.warn(
`[provider] Node ${connectionId} account deactivated (${statusCode}) — disabling permanently`
);
} else if (errorType === PROVIDER_ERROR_TYPES.RATE_LIMITED) {
const rateLimitedUntil = new Date(Date.now() + retryAfterMs).toISOString();
await updateProviderConnection(connectionId, {
rateLimitedUntil: rateLimitedUntil,
testStatus: "credits_exhausted",
lastErrorType: errorType,
lastError: message,
errorCode: statusCode,
healthCheckInterval: null,
lastHealthCheckAt: null,
});
console.warn(
`[provider] Node ${connectionId} rate limited (${statusCode}) - Next available at ${rateLimitedUntil}`
);
} else if (errorType === PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED) {
await updateProviderConnection(connectionId, {
testStatus: "credits_exhausted",
Expand Down
Loading
Loading