diff --git a/changelog.d/features/9415-newapi-sub2api-aggregator-balance.md b/changelog.d/features/9415-newapi-sub2api-aggregator-balance.md index 91357cf987..e2317e3cb8 100644 --- a/changelog.d/features/9415-newapi-sub2api-aggregator-balance.md +++ b/changelog.d/features/9415-newapi-sub2api-aggregator-balance.md @@ -1 +1 @@ -- **sse:** New-API / One-API / Sub2API aggregator balance detection for compatible nodes — with the "Aggregator Gateway" toggle on, OmniRoute queries the aggregator's `/api/user/self` to read the account balance, shows it as a dashboard badge and lets quota-preflight routing skip exhausted accounts. Gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default off), with a `quotaPerUnit` override for aggregators that do not use the default 500000 units/$1 rate ([#9415](https://github.com/diegosouzapw/OmniRoute/issues/9415)) +- **feat(sse):** New-API/One-API/Sub2API aggregator balance detection for compatible provider nodes — when the "Aggregator Gateway" toggle is enabled, OmniRoute queries the aggregator's `/api/user/self` endpoint to detect the account balance; the dashboard shows a balance badge and quota-preflight routing skips exhausted accounts. Gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default: off), with a custom `quotaPerUnit` override for aggregators that use a different rate than the default 500000 units/$1 ([#9415](https://github.com/diegosouzapw/OmniRoute/issues/9415)) diff --git a/docs/ops/VM_DEPLOYMENT_GUIDE.md b/docs/ops/VM_DEPLOYMENT_GUIDE.md index c0e64c74e7..69e3ac4209 100644 --- a/docs/ops/VM_DEPLOYMENT_GUIDE.md +++ b/docs/ops/VM_DEPLOYMENT_GUIDE.md @@ -429,7 +429,5 @@ For deployments on small VPS instances (1 GB RAM or less): - **Disable background services** — set `OMNIROUTE_DISABLE_BACKGROUND_SERVICES=1` to skip scheduler, MCP server, and periodic maintenance tasks. See `docs/reference/ENVIRONMENT.md`. - **Use SQLite WAL mode** — enabled by default, reduces peak memory during concurrent reads. -- **Cap the V8 heap** — set `OMNIROUTE_MEMORY_MB` (e.g. `512`) so the runtime does not calibrate a ceiling larger than the VM. See `docs/reference/ENVIRONMENT.md`. -- **Limit concurrent heavy requests** — lower `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT` (default `1`); excess requests get a retryable `503` with `Retry-After` instead of competing for memory. - **Avoid `next build` on the VPS** — build locally and deploy the standalone output (`.next/standalone/`). - **Monitor with `top` / `free -m`** — OmniRoute typically uses 200-400 MB RSS at idle on a 1 GB VM. diff --git a/open-sse/services/accountSemaphore.ts b/open-sse/services/accountSemaphore.ts index 2d3a1f50b8..ec0f06f090 100644 --- a/open-sse/services/accountSemaphore.ts +++ b/open-sse/services/accountSemaphore.ts @@ -54,21 +54,8 @@ export function buildAccountSemaphoreKey({ return `${String(provider)}:${String(accountKey)}`; } -/** - * Effective positive cap, or null when the semaphore is bypassed (unset/<=0). - * - * Narrowing companion of {@link isBypassed}: that one returns a plain boolean, so - * TypeScript cannot narrow `number | null` to `number` in its else-branch (a - * `x is null | undefined` predicate would be unsound — 0 bypasses too). Callers - * that need the VALUE after the guard go through here instead of casting. - */ -function resolveActiveCap(maxConcurrency?: number | null): number | null { - if (maxConcurrency == null || maxConcurrency <= 0) return null; - return maxConcurrency; -} - function isBypassed(maxConcurrency?: number | null): boolean { - return resolveActiveCap(maxConcurrency) === null; + return maxConcurrency == null || maxConcurrency <= 0; } function createNoopReleaseFn(): () => void { @@ -205,8 +192,7 @@ export function acquire( maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, }: AcquireAccountSemaphoreOptions = {} ): Promise<() => void> { - const activeCap = resolveActiveCap(maxConcurrency); - if (activeCap === null) { + if (isBypassed(maxConcurrency)) { return Promise.resolve(createNoopReleaseFn()); } @@ -214,7 +200,9 @@ export function acquire( return Promise.reject(makeAbortError(signal)); } - const gate = ensureGate(semaphoreKey, activeCap); + // isBypassed() above already excluded null/<=0 — ensureGate requires a plain + // number, but a boolean-returning helper isn't a type predicate TS can narrow on. + const gate = ensureGate(semaphoreKey, maxConcurrency as number); clearCleanupTimer(gate); if (gate.running < gate.maxConcurrency && !isBlocked(gate)) { diff --git a/open-sse/services/antigravityProjectPersistence.ts b/open-sse/services/antigravityProjectPersistence.ts index 1421a8f480..e842492aae 100644 --- a/open-sse/services/antigravityProjectPersistence.ts +++ b/open-sse/services/antigravityProjectPersistence.ts @@ -13,16 +13,27 @@ import { persistDiscoveredAntigravityProjectId } from "./antigravityProjectPersi export { persistDiscoveredAntigravityProjectId }; /** - * Return only the Antigravity connections that already have a stored projectId. + * Prefer Antigravity connections with a discovered/stored `projectId` for + * reset-aware quota routing. * - * A connection whose projectId has been discovered and persisted can be used - * immediately; a connection without one would need to go through the Code Assist - * bootstrap first, which is handled by the calling strategy's fallback path. + * This is a preference, not a hard requirement: when no candidate has a stored + * projectId, retain the full pool rather than making freshly-added accounts unusable. */ -export function preferAntigravityConnectionsWithStoredProject( - connections: Array> -): Array> { - return connections.filter( - (conn) => conn != null && typeof conn.projectId === "string" && conn.projectId.trim().length > 0 - ); +function hasStoredProjectId(connection: Record): boolean { + if (typeof connection.projectId === "string" && connection.projectId.trim().length > 0) { + return true; + } + const providerSpecificData = connection.providerSpecificData; + if (providerSpecificData && typeof providerSpecificData === "object") { + const nested = (providerSpecificData as Record).projectId; + if (typeof nested === "string" && nested.trim().length > 0) return true; + } + return false; +} + +export function preferAntigravityConnectionsWithStoredProject>( + connections: T[] +): T[] { + const withStoredProject = connections.filter(hasStoredProjectId); + return withStoredProject.length > 0 ? withStoredProject : connections; } diff --git a/open-sse/services/combo/comboStructure.ts b/open-sse/services/combo/comboStructure.ts index deb72f012f..76ef372dc2 100644 --- a/open-sse/services/combo/comboStructure.ts +++ b/open-sse/services/combo/comboStructure.ts @@ -18,7 +18,6 @@ import { getHiddenModelsByProvider } from "../../../src/lib/db/models"; import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts"; import { getProviderByAlias, getProviderById } from "../../../src/shared/constants/providers.ts"; import { estimateTokens } from "../contextManager.ts"; -import { containsMediaKind } from "../../utils/mediaParts.ts"; import { getResolvedModelCapabilities } from "../modelCapabilities.ts"; import { parseModel, stripContextWindowSuffix } from "../model.ts"; import { dedupeTargetsByExecutionKey, isRecord } from "./comboData.ts"; @@ -138,9 +137,7 @@ function normalizeRuntimeStep( : {}), weight, label, - // `prompt` is a per-step pipeline input and only exists on a model step — - // #8894 widened the union with ComboProviderWildcardStep, which has no prompt. - prompt: (step.kind === "model" ? step.prompt : null) || null, + prompt: step.kind === "model" ? step.prompt || null : null, } satisfies ResolvedComboTarget; } @@ -484,15 +481,21 @@ function estimateRequestInputTokens(body: Record): number { return Object.keys(estimatePayload).length > 0 ? estimateTokens(estimatePayload) : 0; } -function valueContainsImagePart(value: unknown): boolean { - // Delegates to the unified media detector (open-sse/utils/mediaParts.ts) — - // single source of truth shared with the vision-bridge guardrail. The - // detector keeps this filter's legacy permissive matches (image-ish `type` - // in any casing, bare `image_url`/`input_image` keys, source.media_type - // image/*, bare data:image strings, recursion capped at depth 8) via - // "image_indicator" parts. containsMediaKind short-circuits on the first - // hit — this runs on every request, so no full-part collection here. - return containsMediaKind([{ content: [value] }], "image"); +function valueContainsImagePart(value: unknown, depth = 0): boolean { + if (depth > 8 || value === null || value === undefined) return false; + if (typeof value === "string") return value.startsWith("data:image/"); + if (Array.isArray(value)) return value.some((entry) => valueContainsImagePart(entry, depth + 1)); + if (!isRecord(value)) return false; + + const type = typeof value.type === "string" ? value.type.toLowerCase() : null; + if (type === "image" || type === "image_url" || type === "input_image") return true; + if ("image_url" in value || "input_image" in value) return true; + + const source = isRecord(value.source) ? value.source : null; + const mediaType = typeof source?.media_type === "string" ? source.media_type.toLowerCase() : ""; + if (mediaType.startsWith("image/")) return true; + + return Object.values(value).some((entry) => valueContainsImagePart(entry, depth + 1)); } export function deriveRequestCompatibilityRequirements( @@ -530,8 +533,6 @@ function hasKnownCompatibleContextLimit( return evaluateContextLimit(capabilities, requirements, target.modelStr) === true; } -const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output", "output_tokens"]); - /** * #8332: vision is a hard requirement, not a soft preference — a target whose vision * support is not confirmed can never succeed on an image_url request. Callers @@ -615,12 +616,10 @@ export type CompatFilterOptions = { failOpen?: boolean; }; - function hasHardCapabilityFailure(reasons: string[]): boolean { return reasons.some((reason) => HARD_COMPAT_REASONS.has(reason)); } - /** * Summarize a capability-filter exhaustion for a 400-class combo error (#8488). * Returns null when the empty pool is not attributable to hard requirements. @@ -724,9 +723,7 @@ export function filterTargetsByRequestCompatibility( if (compatible.length === targets.length) return targets; if (compatible.length === 0) { - const hardRejected = rejected.some((entry) => - entry.reasons.some((r) => HARD_COMPAT_REASONS.has(r)) - ); + const hardRejected = rejected.some((entry) => hasHardCapabilityFailure(entry.reasons)); const failOpen = options?.failOpen === true; log.debug?.( diff --git a/open-sse/services/combo/fusionPanel.ts b/open-sse/services/combo/fusionPanel.ts index a0f3d36d30..20540d5850 100644 --- a/open-sse/services/combo/fusionPanel.ts +++ b/open-sse/services/combo/fusionPanel.ts @@ -51,9 +51,9 @@ export function extractFusionPanelSpec( panel.push(step.comboName); return; } - // #8894 widened ComboStep with ComboProviderWildcardStep, which carries a - // modelPattern instead of a model. getComboModelString() already resolves any - // step shape (and returns null for the ones with no concrete model id). + // Provider-wildcard steps have no concrete model to dispatch — fusion is a + // fixed-size panel of literal models/combo-refs, not a wildcard-expanding + // strategy (see file header). Skip rather than push an undefined model. const modelStr = getComboModelString(step); if (modelStr) panel.push(modelStr); }); diff --git a/open-sse/services/combo/quotaStrategies.ts b/open-sse/services/combo/quotaStrategies.ts index 4234b29433..ad47e5d2df 100644 --- a/open-sse/services/combo/quotaStrategies.ts +++ b/open-sse/services/combo/quotaStrategies.ts @@ -89,9 +89,7 @@ async function getQuotaAwareConnectionsForTarget( ? (connections as Array>) : []; if (provider === "antigravity" || provider === "agy") { - activeConnections = preferAntigravityConnectionsWithStoredProject( - activeConnections - ) as Array>; + activeConnections = preferAntigravityConnectionsWithStoredProject(activeConnections); } if ( !resetAwareConnectionCache.has(provider) && diff --git a/open-sse/services/compression/engines/ccr/index.ts b/open-sse/services/compression/engines/ccr/index.ts index d0b5ba10e7..6d8d1e2f03 100644 --- a/open-sse/services/compression/engines/ccr/index.ts +++ b/open-sse/services/compression/engines/ccr/index.ts @@ -292,8 +292,10 @@ function rehydrateEntry(hash: string, principalId: string, now: number): CcrEntr // Re-admit through the same budgets a fresh store would face. If the block no longer // fits, it stays on disk and is served straight from the row instead of being cached. - const { principalId: owner, bytes } = entry; - if (enforcePrincipalBudget(owner, bytes) && enforceGlobalBudget(owner, bytes)) { + if ( + enforcePrincipalBudget(entry.principalId, entry.bytes) && + enforceGlobalBudget(entry.principalId, entry.bytes) + ) { const key = buildStoreKey(hash, principalId === ANON ? undefined : principalId); ccrStore.set(key, entry); ccrTotalBytes += entry.bytes; diff --git a/open-sse/services/firecrawlQuotaFetcher.ts b/open-sse/services/firecrawlQuotaFetcher.ts index 021ccedfc9..92a8bb0a87 100644 --- a/open-sse/services/firecrawlQuotaFetcher.ts +++ b/open-sse/services/firecrawlQuotaFetcher.ts @@ -110,8 +110,7 @@ export function getFirecrawlBaseUrl(connection?: Record): strin return envBase.replace(/\/+$/, ""); } const providerData = toRecord(connection?.providerSpecificData); - const connBase = - typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl; + const connBase = typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl; if (typeof connBase === "string" && connBase.trim() && !connBase.includes("api.firecrawl.dev")) { return connBase.trim().replace(/\/+$/, ""); } @@ -121,10 +120,6 @@ export function getFirecrawlBaseUrl(connection?: Record): strin export async function fetchFirecrawlQuota( connectionId: string, connection?: Record - // FirecrawlQuota, not the base QuotaInfo: every return here is a full credit - // breakdown (remainingCredits / planCredits / extraCreditsInferred / overPlan), - // and the narrower annotation made the custom-base literal below an excess- - // property error. FirecrawlQuota extends QuotaInfo, so callers are unaffected. ): Promise { const cached = quotaCache.get(connectionId); if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index d4b3f16030..62c884870b 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -1094,8 +1094,6 @@ "costsFreeTiersSubtitle": "Hạn mức token miễn phí hàng tháng", "freeProviderRankings": "Xếp hạng nhà cung cấp miễn phí", "freeProviderRankingsSubtitle": "Các nhà cung cấp miễn phí tốt nhất được xếp hạng theo điểm ELO của mô hình", - "radar": "Danh mục Radar", - "radarSubtitle": "Danh mục mô hình miễn phí được làm phong phú bởi cộng đồng", "costsQuotaShare": "Chia sẻ hạn mức", "costsPricing": "Định giá", "logsProxy": "Nhật ký proxy", @@ -1121,7 +1119,6 @@ "runtime": "Thời gian chạy", "consoleLogs": "Nhật ký bảng điều khiển", "logsTimeline": "Timeline", - "logsTimelineSubtitle": "Dòng thời gian yêu cầu trực quan", "globalRouting": "Định tuyến toàn cục", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1167,6 +1164,7 @@ "logsSubtitle": "Nhật ký ứng dụng", "logsProxySubtitle": "Nhật ký lưu lượng proxy", "consoleLogsSubtitle": "Đầu ra bảng điều khiển", + "logsTimelineSubtitle": "Dòng thời gian yêu cầu trực quan", "logsActivitySubtitle": "Nhật ký hoạt động người dùng", "healthSubtitle": "Kiểm tra tình trạng hệ thống", "costsPricingSubtitle": "Quy tắc định giá theo từng mô hình", @@ -1234,7 +1232,9 @@ "discovery": "Khám phá", "discoverySubtitle": "Quét các nhà cung cấp để tìm quyền truy cập miễn phí", "resilienceConnections": "Khả Năng Phục Hồi Kết Nối", - "resilienceConnectionsSubtitle": "Cooldown, cầu dao, trạng thái khóa" + "resilienceConnectionsSubtitle": "Cooldown, cầu dao, trạng thái khóa", + "radar": "Danh mục Radar", + "radarSubtitle": "Danh mục mô hình miễn phí được làm phong phú bởi cộng đồng" }, "webhooks": { "title": "Webhook", @@ -2979,7 +2979,15 @@ "copilotPasteInto": "Dán vào:", "copilotReloadInstruction": "Sau đó tải lại VS Code và nhập khóa API khi được yêu cầu.", "wireApiChatCompletions": "Chat Completions (/chat/completions)", - "wireApiResponses": "Responses API (/responses)" + "wireApiResponses": "Responses API (/responses)", + "ccDiscoveryInfoButton": "Cách bật khám phá mô hình trong Claude Code", + "ccDiscoveryInfoTooltip": "Công bố các mô hình không phải Claude dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Khi bật ở phạm vi toàn cục, số mục trong danh mục tăng gấp đôi với mọi client.", + "ccDiscoveryInfoLink": "Mở Feature Flags", + "ccOnboardingTitle": "settings.json cho việc khám phá mô hình qua gateway", + "ccOnboardingCopy": "Sao chép", + "ccOnboardingCopied": "Đã sao chép", + "ccOnboardingKeyPlaceholder": "", + "ccOnboardingWindowNote": "Claude Code mặc định coi mọi id mô hình mà nó không nhận ra là có cửa sổ ngữ cảnh 200K. Với mô hình có cửa sổ thực tế khác, hãy thêm CLAUDE_CODE_AUTO_COMPACT_WINDOW ngay bên dưới để việc nén ngữ cảnh tự động không kích hoạt quá sớm." }, "combos": { "title": "Combo", @@ -5073,8 +5081,6 @@ "noNewModelsAddedExisting": "Không có mô hình mới nào được thêm (tất cả đã tồn tại).", "importDoneCount": "✓ Hoàn tất! {count, plural, one {Đã nhập # mô hình.} other {Đã nhập # mô hình.}}", "unexpectedErrorOccurred": "Đã xảy ra lỗi không mong muốn", - "getApiKey": "Lấy khóa API", - "getApiKeyDescription": "Đăng ký hoặc tạo tài khoản để nhận khóa API", "connectionCountLabel": "{count, plural, one {# kết nối} other {# kết nối}}", "messagesPath": "messages", "responsesPath": "responses", @@ -5492,12 +5498,12 @@ "newApiUserIdPlaceholder": "vd. 12345", "newApiUserIdHint": "Giá trị tiêu đề New-Api-User của AgentRouter, dùng cùng với khóa API console để lấy số dư hạn mức.", "newApiAggregatorToggleLabel": "Cổng tổng hợp", - "newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.", - "newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.", - "newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.", - "newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị", - "newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.", - "featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API", + "newApiAggregatorToggleHint": "Bật tính năng phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến kiểm tra hạn mức trước sẽ bỏ qua các tài khoản đã hết hạn mức.", + "newApiAggregatorConsoleApiKeyHint": "Token truy cập hệ thống cho endpoint /api/user/self của bộ tổng hợp. Không phải là khóa API định tuyến.", + "newApiAggregatorUserIdHint": "Giá trị tiêu đề New-Api-User dùng để lấy số dư hạn mức của người dùng bộ tổng hợp.", + "newApiAggregatorQuotaPerUnitLabel": "Hạn mức trên mỗi đơn vị", + "newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API trên mỗi $1 (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn sử dụng tỷ lệ khác.", + "featureFlagNewApiAggregatorBalanceDescription": "Bật tính năng phát hiện số dư cho các node tương thích với bộ tổng hợp New-API / One-API / Sub2API", "cpaModeDisabledTitle": "Chế độ tương thích CLIProxyAPI đã bị tắt", "cpaModeEnabledTitle": "Chế độ tương thích CLIProxyAPI đã được bật", "customUserAgentHint": "Gợi ý User Agent tùy chỉnh", @@ -5613,7 +5619,7 @@ "tagGroupPlaceholder": "Nhập nhóm thẻ...", "testModel": "Kiểm tra mô hình", "testingModel": "Đang kiểm tra mô hình", - "modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm", + "modelTestQuotaTooltip": "Đã hết hạn mức — sẽ được đặt lại vào ngày mai hoặc cần nạp thêm", "toggleOffShort": "Tắt", "toggleOnShort": "Bật", "tokenExpiredBadge": "Nhãn token đã hết hạn", @@ -5783,7 +5789,6 @@ "onboardingProviderDescriptions": { "360ai": "Lấy khóa API tại ai.360.cn", "agentrouter": "Nhận 200 USD tín dụng miễn phí tại https://agentrouter.org/register — không cần thẻ tín dụng.", - "unorouter": "Tạo khóa API tại https://unorouter.ai, sau đó dán vào đây dưới dạng Bearer token.", "agnes": "Lấy khóa API tại agnes-ai.com", "aimlapi": "Gói miễn phí đã tạm dừng (2026) — AI/ML API hiện chỉ tính phí theo mức sử dụng (nạp tối thiểu 20 USD); không còn tín dụng miễn phí định kỳ.", "ai21": "10 USD tín dụng dùng thử khi đăng ký (có hiệu lực 3 tháng), không cần thẻ tín dụng", @@ -5980,7 +5985,8 @@ "kimi-coding": "Kết nối Kimi Coding bằng luồng OAuth hiện có.", "kiro": "Gói miễn phí: 50 tín dụng/tháng (khoảng 25–100 nghìn token). ⚠️ Điều khoản Kiro cấm sử dụng proxy/harness của bên thứ ba.", "codex": "Kết nối OpenAI Codex bằng luồng OAuth hiện có.", - "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có." + "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có.", + "unorouter": "Tạo khóa API tại https://unorouter.ai, sau đó dán vào đây dưới dạng Bearer token." }, "passthroughModelsDescription": "{provider} chấp nhận ID mô hình gốc của nhà cung cấp. Nhập từ /models hoặc thêm ID tùy chỉnh để định tuyến.", "bedrockModelsDescription": "Các mô hình Amazon Bedrock được giới hạn theo vùng AWS. Nhập từ /models hoặc thêm ID mô hình Bedrock được bật trong vùng đã chọn.", @@ -6032,7 +6038,9 @@ "kimiOfficialSupporterTooltip": "Kimi (Moonshot AI) là người bạn mã nguồn mở sáng lập của OmniRoute", "cheaperInferenceSupporterBadge": "Người bạn mã nguồn mở", "cheaperInferenceSupporterTooltip": "Cheaper Inference hỗ trợ OmniRoute với tư cách là người bạn mã nguồn mở", - "kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you" + "kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you", + "getApiKey": "Lấy khóa API", + "getApiKeyDescription": "Đăng ký hoặc tạo tài khoản để nhận khóa API" }, "settings": { "title": "Cài đặt", @@ -10798,8 +10806,6 @@ "sourceModel": "Mô hình nguồn (agent gốc)", "targetModel": "Mô hình mục tiêu (OmniRoute)", "noMappings": "Chưa cấu hình ánh xạ mô hình. Hãy chạy trình hướng dẫn thiết lập để tự động phát hiện mô hình.", - "noMappingsDesc": "Chưa cấu hình ánh xạ mô hình nào. Hãy thêm ánh xạ để định tuyến yêu cầu của agent qua OmniRoute.", - "addMapping": "Thêm ánh xạ", "selectModel": "Chọn…", "saveMappings": "Lưu ánh xạ", "setupWizard": "Trình hướng dẫn thiết lập", @@ -10871,7 +10877,9 @@ "goNow": "Đi ngay", "message": "MITM Proxy hiện nằm trong AgentBridge.", "title": "Trang này đã được di chuyển" - } + }, + "noMappingsDesc": "Chưa cấu hình ánh xạ mô hình nào. Hãy thêm ánh xạ để định tuyến yêu cầu của agent qua OmniRoute.", + "addMapping": "Thêm ánh xạ" }, "providerStats": { "unknownError": "Lỗi không xác định", @@ -11741,7 +11749,6 @@ "danger": "Nguy hiểm", "requiresRestart": "Cần khởi động lại", "source": "Nguồn", - "ccDiscoveryAliasesEnvWarning": "Đang bật qua biến môi trường (EXPOSE_CC_DISCOVERY_ALIASES) — giá trị này ghi đè mọi công tắc trong bảng điều khiển bên dưới.", "resetFlag": "Đặt lại {label} về mặc định", "reset": "Đặt lại", "loadFailed": "Không thể tải cờ tính năng", @@ -11898,7 +11905,8 @@ "SKILLS_SANDBOX_NETWORK_ENABLED": { "description": "Cho phép môi trường sandbox của kỹ năng truy cập mạng." } - } + }, + "ccDiscoveryAliasesEnvWarning": "Đang bật qua biến môi trường (EXPOSE_CC_DISCOVERY_ALIASES) — giá trị này ghi đè mọi công tắc trong bảng điều khiển bên dưới." }, "comboControl": { "title": "Trung tâm điều khiển combo", diff --git a/src/lib/db/migrations/120_interception_rules.sql b/src/lib/db/migrations/120_interception_rules.sql index d042a5f035..7e7e1593be 100644 --- a/src/lib/db/migrations/120_interception_rules.sql +++ b/src/lib/db/migrations/120_interception_rules.sql @@ -14,3 +14,4 @@ -- falls back to the existing native web-search-bypass defaults in webSearchFallback.ts). -- -- See: src/lib/db/interceptionRules.ts +SELECT 1;