mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(usage): support quota fetch for kimi-coding-apikey (#4435)
Thanks @janeza2! Rebased onto release/v3.8.32 and completed the end-to-end wiring on review: kimi-coding-apikey was missing from USAGE_SUPPORTED_PROVIDERS (dashboard 400) and USAGE_FETCHER_PROVIDERS (preflight) — added both + header-selection/wiring tests. Now fully routable.
This commit is contained in:
@@ -1511,6 +1511,7 @@ export const USAGE_FETCHER_PROVIDERS = [
|
||||
"kiro",
|
||||
"amazon-q",
|
||||
"kimi-coding",
|
||||
"kimi-coding-apikey",
|
||||
"qwen",
|
||||
"qoder",
|
||||
"glm",
|
||||
@@ -1573,6 +1574,8 @@ export async function getUsageForProvider(
|
||||
return await getVertexUsage(id || "", provider);
|
||||
case "kimi-coding":
|
||||
return await getKimiUsage(accessToken);
|
||||
case "kimi-coding-apikey":
|
||||
return await getKimiUsage(undefined, apiKey);
|
||||
case "qwen":
|
||||
return await getQwenUsage(accessToken, providerSpecificData);
|
||||
case "qoder":
|
||||
@@ -3186,7 +3189,7 @@ function getKimiPlanName(level: unknown): string {
|
||||
* Kimi Coding Usage - Fetch quota from Kimi API
|
||||
* Uses the official /v1/usages endpoint with custom X-Msh-* headers
|
||||
*/
|
||||
async function getKimiUsage(accessToken?: string) {
|
||||
async function getKimiUsage(accessToken?: string, apiKey?: string) {
|
||||
// Generate device info for headers (same as OAuth flow)
|
||||
const deviceId = "kimi-usage-" + Date.now();
|
||||
const platform = "omniroute";
|
||||
@@ -3194,16 +3197,28 @@ async function getKimiUsage(accessToken?: string) {
|
||||
const deviceModel =
|
||||
typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown";
|
||||
|
||||
try {
|
||||
const response = await fetch(KIMI_CONFIG.usageUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// API key auth takes precedence — Kimi's /usages endpoint accepts the same
|
||||
// API key used for /messages (verified live: responds with
|
||||
// authentication.method = METHOD_API_KEY). OAuth flow falls through to the
|
||||
// Bearer + device-headers shape used by Kimi Coding OAuth.
|
||||
const useApiKey = typeof apiKey === "string" && apiKey.length > 0;
|
||||
|
||||
const authHeaders: Record<string, string> = useApiKey
|
||||
? { "x-api-key": apiKey as string }
|
||||
: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Msh-Platform": platform,
|
||||
"X-Msh-Version": version,
|
||||
"X-Msh-Device-Model": deviceModel,
|
||||
"X-Msh-Device-Id": deviceId,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(KIMI_CONFIG.usageUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...authHeaders,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
|
||||
"xiaomi-mimo",
|
||||
"vertex",
|
||||
"vertex-partner",
|
||||
"kimi-coding-apikey",
|
||||
]);
|
||||
const DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES = 70;
|
||||
const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_run";
|
||||
|
||||
@@ -3212,6 +3212,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"claude",
|
||||
"cursor",
|
||||
"kimi-coding",
|
||||
"kimi-coding-apikey",
|
||||
"glm",
|
||||
"glm-cn",
|
||||
"zai",
|
||||
|
||||
80
tests/unit/kimi-coding-apikey-quota-4435.test.ts
Normal file
80
tests/unit/kimi-coding-apikey-quota-4435.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Regression for #4435 (port: quota fetch for kimi-coding-apikey).
|
||||
*
|
||||
* The new `kimi-coding-apikey` connection authenticates against Kimi's
|
||||
* /usages endpoint with an `x-api-key` header (the same key used for
|
||||
* /messages), while the OAuth `kimi-coding` connection keeps the
|
||||
* Bearer + X-Msh-* device-header shape. These tests pin that header
|
||||
* selection by routing through the exported `getUsageForProvider` switch
|
||||
* with a stubbed global fetch (no DB side effects on the kimi path).
|
||||
*
|
||||
* Also guards the wiring gap caught in review: `kimi-coding-apikey` must be
|
||||
* registered in USAGE_SUPPORTED_PROVIDERS (dashboard gate) and
|
||||
* USAGE_FETCHER_PROVIDERS (auto-routing preflight), otherwise the feature
|
||||
* is unreachable end-to-end.
|
||||
*/
|
||||
import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getUsageForProvider, USAGE_FETCHER_PROVIDERS } from "../../open-sse/services/usage.ts";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts";
|
||||
|
||||
const KIMI_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
||||
const realFetch = globalThis.fetch;
|
||||
let captured: Array<{ url: string; headers: Record<string, string> }> = [];
|
||||
|
||||
before(() => {
|
||||
globalThis.fetch = (async (url: unknown, init: { headers?: Record<string, string> } = {}) => {
|
||||
captured.push({ url: String(url), headers: { ...(init.headers ?? {}) } });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({ usage: { limit: "100", used: "10", remaining: "90" } }),
|
||||
} as unknown as Response;
|
||||
}) as typeof fetch;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
test("kimi-coding-apikey authenticates with x-api-key (not Bearer)", async () => {
|
||||
captured = [];
|
||||
await getUsageForProvider({
|
||||
id: "c-apikey",
|
||||
provider: "kimi-coding-apikey",
|
||||
apiKey: "sk-test-123",
|
||||
} as Parameters<typeof getUsageForProvider>[0]);
|
||||
|
||||
const call = captured.find((c) => c.url === KIMI_USAGE_URL);
|
||||
assert.ok(call, "kimi /usages endpoint should be called for kimi-coding-apikey");
|
||||
assert.equal(call.headers["x-api-key"], "sk-test-123");
|
||||
assert.equal(call.headers["Authorization"], undefined);
|
||||
});
|
||||
|
||||
test("kimi-coding (OAuth) keeps Bearer + device headers (not x-api-key)", async () => {
|
||||
captured = [];
|
||||
await getUsageForProvider({
|
||||
id: "c-oauth",
|
||||
provider: "kimi-coding",
|
||||
accessToken: "tok-abc",
|
||||
} as Parameters<typeof getUsageForProvider>[0]);
|
||||
|
||||
const call = captured.find((c) => c.url === KIMI_USAGE_URL);
|
||||
assert.ok(call, "kimi /usages endpoint should be called for kimi-coding");
|
||||
assert.equal(call.headers["Authorization"], "Bearer tok-abc");
|
||||
assert.ok(call.headers["X-Msh-Platform"], "OAuth path keeps the device headers");
|
||||
assert.equal(call.headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
test("kimi-coding-apikey is wired into both usage source-of-truth lists", () => {
|
||||
assert.ok(
|
||||
USAGE_SUPPORTED_PROVIDERS.includes("kimi-coding-apikey"),
|
||||
"must be in USAGE_SUPPORTED_PROVIDERS (else the dashboard gate returns 400)"
|
||||
);
|
||||
assert.ok(
|
||||
(USAGE_FETCHER_PROVIDERS as readonly string[]).includes("kimi-coding-apikey"),
|
||||
"must be in USAGE_FETCHER_PROVIDERS (else auto-routing preflight never registers a fetcher)"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user