Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(quota):** remaining console quota readings use the assigned connection egress path ([#16344](https://github.com/diegosouzapw/OmniRoute/pull/16344)) — thanks @maxmad64bis
21 changes: 9 additions & 12 deletions open-sse/services/grokQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ function writeRefreshedTokens(
async function discoverTokenEndpoint(
issuer: string,
signal: AbortSignal,
connectionId?: string | null
connectionId: string
): Promise<string> {
const discoveryUrl = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
if (!isAllowedXaiUrl(discoveryUrl)) {
throw new Error("invalid oidc issuer");
}
const res = await fetchWithConnectionProxy(connectionId ?? "", discoveryUrl, {
const res = await fetchWithConnectionProxy(connectionId, discoveryUrl, {
headers: { Accept: "application/json" },
signal,
});
Expand All @@ -229,7 +229,7 @@ async function discoverTokenEndpoint(
async function refreshAccessToken(
auth: ResolvedAuth,
signal: AbortSignal,
connectionId?: string | null
connectionId: string
): Promise<ResolvedAuth> {
if (!auth.refreshToken) {
throw new Error("auth expired — run `grok login`");
Expand All @@ -239,7 +239,7 @@ async function refreshAccessToken(
}

const tokenEndpoint = await discoverTokenEndpoint(auth.issuer, signal, connectionId);
const res = await fetchWithConnectionProxy(connectionId ?? "", tokenEndpoint, {
const res = await fetchWithConnectionProxy(connectionId, tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down Expand Up @@ -297,10 +297,7 @@ function needsRefresh(auth: ResolvedAuth): boolean {
return auth.expiresAtMs <= Date.now() + EXPIRY_SKEW_MS;
}

async function resolveAuth(
signal: AbortSignal,
connectionId?: string | null
): Promise<ResolvedAuth> {
async function resolveAuth(signal: AbortSignal, connectionId: string): Promise<ResolvedAuth> {
const file = readAuthFile();
if (!file) throw new Error("no grok auth — run `grok login`");
const auth = pickAuthEntry(file);
Expand Down Expand Up @@ -343,8 +340,8 @@ function resetLocalLabel(endIso?: string): string {
*/
export async function fetchGrokBillingWithToken(
token: string,
signal?: AbortSignal,
connectionId?: string | null
signal: AbortSignal | undefined,
connectionId: string
): Promise<GrokBillingSnapshot> {
const controller = signal ? null : new AbortController();
const timeout = controller ? setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) : null;
Expand All @@ -366,9 +363,9 @@ export function grokBillingSnapshotToQuotaInfo(snap: GrokBillingSnapshot): Quota
async function fetchBilling(
token: string,
signal: AbortSignal,
connectionId?: string | null
connectionId: string
): Promise<UsageSnapshot> {
const res = await fetchWithConnectionProxy(connectionId ?? "", BILLING_URL, {
const res = await fetchWithConnectionProxy(connectionId, BILLING_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Expand Down
4 changes: 3 additions & 1 deletion open-sse/services/jinaQuotaFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";

/**
* Jina's wallet-balance endpoint only accepts the key as the `api_key` QUERY
Expand Down Expand Up @@ -124,13 +125,14 @@ export async function fetchJinaQuota(
await throttleQuotaFetch();
// Credential-bearing URL (see module header) — keep it local, never log it.
const url = `${JINA_BALANCE_URL}?api_key=${encodeURIComponent(token)}`;
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Accept: "application/json",
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response) return null;

if (!response.ok) {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
Expand Down
6 changes: 5 additions & 1 deletion open-sse/services/llmgatewayQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";

const LLMGATEWAY_CONFIG = {
baseUrl: "https://api.llmgateway.io/v1",
Expand Down Expand Up @@ -225,18 +226,20 @@ export function buildLlmgatewayQuota(key: LlmgatewayKeyFields): LlmgatewayQuota
// ─── Core Fetcher ────────────────────────────────────────────────────────────

async function fetchJson(
connectionId: string,
url: string,
apiKey: string
): Promise<{ status: number; data: unknown } | null> {
try {
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;
if (!response.ok) return { status: response.status, data: null };
const data = await response.json();
return { status: response.status, data };
Expand Down Expand Up @@ -272,6 +275,7 @@ export async function fetchLlmgatewayQuota(
await throttleQuotaFetch();

const keyResult = await fetchJson(
connectionId,
`${LLMGATEWAY_CONFIG.baseUrl}${LLMGATEWAY_CONFIG.keyPath}`,
apiKey
);
Expand Down
7 changes: 5 additions & 2 deletions open-sse/services/lyceumQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";

const LYCEUM_CONFIG = {
creditsUrl: "https://api.lyceum.technology/api/v2/external/billing/credits",
Expand Down Expand Up @@ -153,18 +154,20 @@ export function buildLyceumQuota(fields: LyceumCreditsFields): LyceumQuota {
// ─── Core Fetcher ────────────────────────────────────────────────────────────

async function fetchJson(
connectionId: string,
url: string,
apiKey: string
): Promise<{ status: number; data: unknown } | null> {
try {
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;
if (!response.ok) return { status: response.status, data: null };
const data = await response.json();
return { status: response.status, data };
Expand Down Expand Up @@ -198,7 +201,7 @@ export async function fetchLyceumQuota(
try {
await throttleQuotaFetch();

const result = await fetchJson(LYCEUM_CONFIG.creditsUrl, apiKey);
const result = await fetchJson(connectionId, LYCEUM_CONFIG.creditsUrl, apiKey);
if (!result || result.status !== 200) {
quotaCache.delete(connectionId);
return null;
Expand Down
4 changes: 3 additions & 1 deletion open-sse/services/moonshotQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { toNumber } from "@/shared/utils/numeric";
import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";
import { isUrlHostOrSubdomain } from "../utils/urlHost.ts";
import {
isKimiCodingBaseUrl,
Expand Down Expand Up @@ -171,7 +172,7 @@ export async function fetchMoonshotQuota(

try {
await throttleQuotaFetch();
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Authorization: authHeader,
Expand All @@ -180,6 +181,7 @@ export async function fetchMoonshotQuota(
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;

if (response.status === 401 || response.status === 403) {
quotaCache.delete(connectionId);
Expand Down
4 changes: 3 additions & 1 deletion open-sse/services/newApiAggregatorQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import type { QuotaInfo } from "./quotaPreflight.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";
import { toNumber } from "@/shared/utils/numeric";

const SELF_PATH = "/api/user/self";
Expand Down Expand Up @@ -174,7 +175,7 @@ export async function fetchNewApiAggregatorQuota(
try {
await throttleQuotaFetch();

const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Authorization: `Bearer ${systemAccessToken}`,
Expand All @@ -184,6 +185,7 @@ export async function fetchNewApiAggregatorQuota(
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;

if (response.status === 401 || response.status === 403) {
quotaCache.delete(connectionId);
Expand Down
21 changes: 13 additions & 8 deletions open-sse/services/opencodeQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { registerQuotaFetcher, registerQuotaWindows, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";
import { buildOpencodeBackgroundHeaders } from "../utils/opencodeHeaders.ts";

const OPENCODE_QUOTA_URL =
Expand Down Expand Up @@ -41,6 +42,15 @@ interface CacheEntry {
// In-memory cache: connectionId → successful quota bound to its normalized API key
const quotaCache = new Map<string, CacheEntry>();

function normalizeApiKey(connection?: Record<string, unknown>): string {
return typeof connection?.apiKey === "string"
? connection.apiKey
.trim()
.replace(/^Bearer\s+/i, "")
.trim()
: "";
}

// One-time 404 warning per URL (avoids spamming on every request)
const _warned404Urls = new Set<string>();

Expand Down Expand Up @@ -160,13 +170,7 @@ export async function fetchOpencodeQuota(
connectionId: string,
connection?: Record<string, unknown>
): Promise<OpencodeTripleWindowQuota | null> {
const apiKey =
typeof connection?.apiKey === "string"
? connection.apiKey
.trim()
.replace(/^Bearer\s+/i, "")
.trim()
: "";
const apiKey = normalizeApiKey(connection);
if (!apiKey) return null;

const cached = quotaCache.get(connectionId);
Expand All @@ -176,7 +180,7 @@ export async function fetchOpencodeQuota(

try {
await throttleQuotaFetch();
const response = await fetch(OPENCODE_QUOTA_URL, {
const response = await fetchWithConnectionProxy(connectionId, OPENCODE_QUOTA_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Expand All @@ -190,6 +194,7 @@ export async function fetchOpencodeQuota(
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;

if (!response.ok) {
if (response.status === 404 && !_warned404Urls.has(OPENCODE_QUOTA_URL)) {
Expand Down
7 changes: 6 additions & 1 deletion open-sse/services/openrouterQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";
import {
getFreeWindowStatus,
isFreeVariantModel,
Expand Down Expand Up @@ -283,18 +284,20 @@ function checkFreeWindowExhausted(
// ─── Core Fetcher ────────────────────────────────────────────────────────────

async function fetchJson(
connectionId: string,
url: string,
apiKey: string
): Promise<{ status: number; data: unknown } | null> {
try {
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(8_000),
});
if (!response) return null;
if (!response.ok) return { status: response.status, data: null };
const data = await response.json();
return { status: response.status, data };
Expand Down Expand Up @@ -385,10 +388,12 @@ export async function fetchOpenrouterQuota(
await throttleQuotaFetch();

const keyResult = await fetchJson(
connectionId,
`${OPENROUTER_CONFIG.baseUrl}${OPENROUTER_CONFIG.keyPath}`,
apiKey
);
const creditsResult = await fetchJson(
connectionId,
`${OPENROUTER_CONFIG.baseUrl}${OPENROUTER_CONFIG.creditsPath}`,
apiKey
);
Expand Down
7 changes: 6 additions & 1 deletion open-sse/services/tavilyQuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";
import { isUrlHost } from "../utils/urlHost.ts";
import { toNumberOrNull } from "@/shared/utils/numeric";

Expand Down Expand Up @@ -180,14 +181,18 @@ export async function fetchTavilyQuota(
try {
await throttleQuotaFetch();

const response = await fetch(TAVILY_USAGE_URL, {
const response = await fetchWithConnectionProxy(connectionId, TAVILY_USAGE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response) {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
return null;
}

if (!response.ok) {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
Expand Down
7 changes: 6 additions & 1 deletion open-sse/services/v0QuotaFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { registerQuotaFetcher, registerQuotaWindows, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { fetchWithConnectionProxy } from "./connectionProxyFetch.ts";

const V0_CONFIG = {
baseUrl: "https://api.v0.dev",
Expand Down Expand Up @@ -115,17 +116,19 @@ interface WindowFetchResult {
* rate-limits (dailyOps) calls — the only difference is the response shape parser.
*/
async function fetchWindow(
connectionId: string,
url: string,
headers: Record<string, string>,
parse: (data: unknown) => { billingType: string | null; window: WindowFetchResult["window"] }
): Promise<WindowFetchResult> {
try {
await throttleQuotaFetch();
const response = await fetch(url, {
const response = await fetchWithConnectionProxy(connectionId, url, {
method: "GET",
headers,
signal: AbortSignal.timeout(8_000),
});
if (!response) return { window: null, billingType: null, invalidCredential: false };

if (response.status === 401 || response.status === 403) {
return { window: null, billingType: null, invalidCredential: true };
Expand Down Expand Up @@ -219,6 +222,7 @@ export async function fetchV0Quota(
let billingType = "unknown";

const billingResult = await fetchWindow(
connectionId,
`${V0_CONFIG.baseUrl}${V0_CONFIG.billingPath}`,
headers,
parseBillingResponse
Expand All @@ -229,6 +233,7 @@ export async function fetchV0Quota(
}

const rateLimitsResult = await fetchWindow(
connectionId,
`${V0_CONFIG.baseUrl}${V0_CONFIG.rateLimitsPath}`,
headers,
parseRateLimitsResponse
Expand Down
Loading
Loading