fix: stop opencode-go quota lookup defaulting to Z.AI endpoint (#7022) (#7187)

* fix: stop opencode-go quota lookup defaulting to Z.AI endpoint (#7022)

getOpenCodeGoUsage() defaulted OPENCODE_GO_QUOTA_URL to
https://api.z.ai/api/monitor/usage/quota/limit, a Zhipu AI (Z.AI/GLM)
endpoint unrelated to opencode.ai. Whenever a connection had no
dashboard-scraping config (workspaceId/authCookie), the user's real
OpenCode Go API key was sent as a Bearer token to that third-party host
by default, with no operator opt-in.

Remove the hardcoded default: the quota-by-API-key fetch now only runs
when the operator explicitly sets OMNIROUTE_OPENCODE_GO_QUOTA_URL. With
it unset (the default), getOpenCodeGoUsage() returns a descriptive
message and makes zero outbound calls, since OpenCode Go has no public
quota API.

Also updates .env.example and both EN/zh-CN copies of
docs/reference/ENVIRONMENT.md to drop the stale Z.AI default value and
fix the stale open-sse/services/usage.ts source-file reference.

Regression test: tests/unit/opencode-go-quota-no-zai.test.ts (RED on
current code, GREEN after the fix).

* fix: align opencode-go-usage tests with opt-in quota URL contract (#7022)

The prior commit removed the hardcoded api.z.ai default from
OPENCODE_GO_QUOTA_URL, making the quota-by-API-key path opt-in via
OMNIROUTE_OPENCODE_GO_QUOTA_URL. Six pre-existing tests in
opencode-go-usage.test.ts still asserted the old default-fetch
behavior and the old Z.AI-specific error wording, so they broke.

Set OMNIROUTE_OPENCODE_GO_QUOTA_URL before the module import (the
value is read once at load time) to simulate an operator who opted
in, and update the two error-message assertions to the new generic
wording ("the configured OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint"
instead of "the Z.AI quota API"). Each test still verifies exactly
the same behavior it did before (invalid key, fetch failure, 200
with auth error in body, invalid JSON, quota shape) — only the
opt-in setup and message wording changed.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-14 21:45:23 -03:00
committed by GitHub
parent fce2bb67ad
commit 3df06e5552
7 changed files with 78 additions and 10 deletions

View File

@@ -505,7 +505,9 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
# OpenCode Go has no public quota API — this has no default and stays
# unset unless you explicitly opt in to a self-hosted/mirrored endpoint:
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings

View File

@@ -0,0 +1 @@
- fix(usage): stop opencode-go quota lookup from defaulting to an unrelated Z.AI endpoint (#7022)

View File

@@ -272,7 +272,7 @@ OmniRoute 提供两层防护:请求侧的注入扫描和响应侧的 PII 脱
| `OMNIROUTE_PUBLIC_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 用于组合异步回调 URL 的公共源。kie.ai 回调的最低优先级回退;也用作其他中继的通用公共 URL。 |
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | Usage 页面使用的 CrofAI 配额查询端点。可覆盖为中继/测试固定件。 |
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | Usage 页面使用的 OpenCode (zen/go) 配额查询端点。可覆盖为中继/测试固定件。 |
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。可覆盖为中继/测试固定件。 |
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(未设置)_ | `open-sse/services/opencodeOllamaUsage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。OpenCode Go 没有公开的配额 API因此没有默认值除非运维人员显式设置该变量选择接入自建/镜像端点,否则不会发起网络请求。 |
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | 配置了 workspace ID 和 auth Cookie 时用于配额抓取的 OpenCode Go Dashboard 基础 URL。可覆盖为中继/测试固定件。 |
| `OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go workspace ID。配置多个账户时推荐使用每个连接的 Dashboard 字段。 |
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID 环境变量的备选名,在较短的别名之前使用。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |

View File

@@ -280,7 +280,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. |
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | CrofAI quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | OpenCode (zen/go) quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(unset)_ | `open-sse/services/opencodeOllamaUsage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. OpenCode Go has no public quota API, so this has no default and the network call is skipped unless the operator opts in to a self-hosted/mirrored endpoint. |
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | OpenCode Go dashboard base URL used for quota scraping when a workspace ID and auth cookie are configured. Override for relays / test fixtures. |
| `OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID used for dashboard quota scraping. Prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go workspace ID env var used before the shorter alias. Prefer the per-connection Dashboard field when multiple accounts are configured. |

View File

@@ -13,8 +13,13 @@ type UsageQuota = {
currency?: string;
};
const OPENCODE_GO_QUOTA_URL =
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit";
// OpenCode Go does not expose a public quota API. There is no working
// opencode.ai endpoint to default to (see #7022) — the quota-by-API-key path
// below is opt-in only and activates exclusively when the operator sets
// OMNIROUTE_OPENCODE_GO_QUOTA_URL explicitly. Never hardcode a third-party
// host here (a previous default silently sent the user's API key to an
// unrelated Z.AI endpoint).
const OPENCODE_GO_QUOTA_URL = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL?.trim() || "";
const OPENCODE_GO_DASHBOARD_BASE_URL =
process.env.OMNIROUTE_OPENCODE_GO_DASHBOARD_URL ?? "https://opencode.ai/workspace";
const OPENCODE_GO_QUOTA_TOTALS = { session: 12, weekly: 30, mcp_monthly: 60 } as const;
@@ -335,6 +340,15 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
};
}
if (!OPENCODE_GO_QUOTA_URL) {
return {
message:
"OpenCode Go does not expose a public quota API. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping, " +
"or set OMNIROUTE_OPENCODE_GO_QUOTA_URL to opt in to an explicit quota endpoint.",
};
}
try {
const res = await fetch(OPENCODE_GO_QUOTA_URL, {
headers: {
@@ -348,7 +362,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
if (res.status === 401 || res.status === 403) {
return {
message:
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
};
}
@@ -374,7 +389,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
) {
return {
message:
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
};
}

View File

@@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { getOpenCodeGoUsage } from "../../open-sse/services/opencodeOllamaUsage.ts";
test("getOpenCodeGoUsage does not send the user's OpenCode Go API key to api.z.ai by default", async () => {
const originalFetch = globalThis.fetch;
const originalEnv = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL;
delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL;
let calledHost: string | null = null;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : input.toString();
calledHost = new URL(url).host;
throw new Error(`unexpected outbound fetch to ${url}`);
}) as typeof fetch;
try {
const result = await getOpenCodeGoUsage("sk-fake-opencode-go-key", undefined);
assert.notStrictEqual(calledHost, "api.z.ai");
assert.strictEqual(calledHost, null);
assert.ok(
typeof result.message === "string" && result.message.length > 0,
"expected a descriptive message when no quota URL is configured"
);
} finally {
globalThis.fetch = originalFetch;
if (originalEnv === undefined) delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL;
else process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = originalEnv;
}
});

View File

@@ -1,9 +1,25 @@
import test from "node:test";
import test, { after } from "node:test";
import assert from "node:assert/strict";
// The OpenCode Go quota-by-API-key path is opt-in only (see #7022 — there is no
// working default quota endpoint, so OMNIROUTE_OPENCODE_GO_QUOTA_URL must be set
// explicitly by the operator). The module reads this env var once at import time,
// so it has to be set BEFORE the dynamic import below for the opt-in tests in this
// file (which simulate an operator who configured the URL) to exercise the fetch path.
const ORIGINAL_OPENCODE_GO_QUOTA_URL = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL;
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
const usage = await import("../../open-sse/services/usage.ts");
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
after(() => {
if (ORIGINAL_OPENCODE_GO_QUOTA_URL === undefined) {
delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL;
} else {
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = ORIGINAL_OPENCODE_GO_QUOTA_URL;
}
});
test("USAGE_SUPPORTED_PROVIDERS includes opencode-go", () => {
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as string[]).includes("opencode-go"),
@@ -298,7 +314,8 @@ test("getUsageForProvider returns message for invalid OpenCode Go API keys", asy
})) as { message: string };
assert.equal(
result.message,
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
);
} finally {
@@ -342,7 +359,8 @@ test("getUsageForProvider returns message when OpenCode Go quota API returns 200
})) as { message: string };
assert.equal(
result.message,
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
);
} finally {