@coseung2/opencodex 2.8.0-cs.7 → 2.8.0-cs.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DYcdEcsX.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-OY43ubAq.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coseung2/opencodex",
|
|
3
|
-
"version": "2.8.0-cs.
|
|
3
|
+
"version": "2.8.0-cs.9",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
package/src/providers/quota.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
|
3
3
|
import { resolveEnvValue } from "../config";
|
|
4
4
|
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
5
5
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
6
|
+
import { readUsageEntries } from "../usage/log";
|
|
6
7
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
7
8
|
import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
|
|
8
9
|
import type { KiroOAuthMetadata } from "../oauth/types";
|
|
@@ -20,16 +21,75 @@ const ACCOUNT_TOKEN_SKEW_MS = 60_000;
|
|
|
20
21
|
|
|
21
22
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
22
23
|
const REQUEST_TIMEOUT_MS = 8_000;
|
|
24
|
+
/** Kiro's getUsageLimits endpoint cold-starts slowly after idle periods (>8s seen); keep a
|
|
25
|
+
* dedicated, more generous bound so a flaky first hit does not drop the provider into the
|
|
26
|
+
* "no quota" bucket for the rest of the negative-cache window. */
|
|
27
|
+
const KIRO_QUOTA_TIMEOUT_MS = 20_000;
|
|
23
28
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
24
29
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
25
30
|
const KIRO_USAGE_LIMITS_PATH = "getUsageLimits";
|
|
26
31
|
/** Keep a failed probe's previous row at most this long before dropping it. */
|
|
27
32
|
const LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
|
|
28
33
|
|
|
34
|
+
const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
35
|
+
const OPENCODE_GO_COST_WINDOW_MS = 30 * 86_400_000;
|
|
36
|
+
const OPENCODE_GO_FIVE_HOUR_MS = 5 * 3_600_000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* opencode.go published per-model prices (USD per 1M tokens), from opencode.ai/docs/go.
|
|
40
|
+
* Over-cap token tiers (GPT 5.6 Luna >272K, Qwen3.7/3.6 Plus >256K) use the base tier;
|
|
41
|
+
* the estimate is intentionally approximate ("정확할 필요는 없고").
|
|
42
|
+
*/
|
|
43
|
+
const OPENCODE_GO_PRICES: Record<string, { input: number; output: number; cacheRead: number; cacheWrite: number }> = {
|
|
44
|
+
"grok-4.5": { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
|
|
45
|
+
"gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
|
|
46
|
+
"glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
47
|
+
"glm-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
48
|
+
"kimi-k3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
|
|
49
|
+
"kimi-k2.7-code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
|
50
|
+
"kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
|
51
|
+
"mimo-v2.5": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
52
|
+
"mimo-v2.5-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
53
|
+
"minimax-m3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
54
|
+
"minimax-m2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
|
55
|
+
"minimax-m2.5": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
|
56
|
+
"qwen3.8-max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
|
|
57
|
+
"qwen3.7-max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.125 },
|
|
58
|
+
"qwen3.7-plus": { input: 0.4, output: 1.6, cacheRead: 0.04, cacheWrite: 0.5 },
|
|
59
|
+
"qwen3.6-plus": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.625 },
|
|
60
|
+
"deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
61
|
+
"deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
62
|
+
"hy3": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** opencode.go published request limits per 5-hour window (1x tier), opencode.ai/docs/go. */
|
|
66
|
+
const OPENCODE_GO_FIVE_HOUR_LIMITS: Record<string, { label: string; limit: number }> = {
|
|
67
|
+
"grok-4.5": { label: "Grok 4.5", limit: 120 },
|
|
68
|
+
"gpt-5.6-luna": { label: "GPT 5.6 Luna", limit: 2_050 },
|
|
69
|
+
"glm-5.2": { label: "GLM-5.2", limit: 880 },
|
|
70
|
+
"glm-5.1": { label: "GLM-5.1", limit: 880 },
|
|
71
|
+
"kimi-k3": { label: "Kimi K3", limit: 110 },
|
|
72
|
+
"kimi-k2.7-code": { label: "Kimi K2.7 Code", limit: 1_350 },
|
|
73
|
+
"kimi-k2.6": { label: "Kimi K2.6", limit: 1_150 },
|
|
74
|
+
"mimo-v2.5": { label: "MiMo-V2.5", limit: 30_100 },
|
|
75
|
+
"mimo-v2.5-pro": { label: "MiMo-V2.5 Pro", limit: 3_250 },
|
|
76
|
+
"minimax-m3": { label: "MiniMax M3", limit: 3_200 },
|
|
77
|
+
"minimax-m2.7": { label: "MiniMax M2.7", limit: 3_400 },
|
|
78
|
+
"qwen3.8-max": { label: "Qwen3.8 Max", limit: 160 },
|
|
79
|
+
"qwen3.7-max": { label: "Qwen3.7 Max", limit: 340 },
|
|
80
|
+
"qwen3.7-plus": { label: "Qwen3.7 Plus", limit: 4_300 },
|
|
81
|
+
"qwen3.6-plus": { label: "Qwen3.6 Plus", limit: 3_300 },
|
|
82
|
+
"deepseek-v4-pro": { label: "DeepSeek V4 Pro", limit: 3_450 },
|
|
83
|
+
"deepseek-v4-flash": { label: "DeepSeek V4 Flash", limit: 31_650 },
|
|
84
|
+
"hy3": { label: "Hy3", limit: 4_300 },
|
|
85
|
+
};
|
|
86
|
+
|
|
29
87
|
export interface ProviderQuotaWindow {
|
|
30
88
|
label: string;
|
|
31
89
|
percent: number;
|
|
32
90
|
resetAt?: number;
|
|
91
|
+
/** Text value shown instead of a percent bar (e.g. an estimated cost). */
|
|
92
|
+
valueLabel?: string;
|
|
33
93
|
}
|
|
34
94
|
|
|
35
95
|
export interface ProviderQuota {
|
|
@@ -81,7 +141,8 @@ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is Provide
|
|
|
81
141
|
return typeof quota.fiveHourPercent === "number"
|
|
82
142
|
|| typeof quota.weeklyPercent === "number"
|
|
83
143
|
|| typeof quota.monthlyPercent === "number"
|
|
84
|
-
|| !!quota.customWindows?.some(window =>
|
|
144
|
+
|| !!quota.customWindows?.some(window =>
|
|
145
|
+
typeof window.percent === "number" || typeof window.valueLabel === "string");
|
|
85
146
|
}
|
|
86
147
|
|
|
87
148
|
function providerLabel(providerId: string): string {
|
|
@@ -118,6 +179,13 @@ function normalizePercent(value: unknown): number | undefined {
|
|
|
118
179
|
return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
|
|
119
180
|
}
|
|
120
181
|
|
|
182
|
+
/** Numeric value from a billing object ({ val: 123 }) or a bare number. */
|
|
183
|
+
function billingValue(value: unknown): number | undefined {
|
|
184
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
185
|
+
const record = asRecord(value);
|
|
186
|
+
return toFiniteNumber(record?.val);
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
122
190
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
123
191
|
}
|
|
@@ -177,14 +245,33 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
|
|
|
177
245
|
const body = asRecord(await response.json().catch(() => null));
|
|
178
246
|
const config = asRecord(body?.config);
|
|
179
247
|
const currentPeriod = asRecord(config?.currentPeriod);
|
|
248
|
+
// A weekly window is the meter contract; other shapes (spend-cap etc.) fail closed.
|
|
180
249
|
if (currentPeriod?.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
|
|
181
|
-
const creditUsagePercent = config?.creditUsagePercent;
|
|
182
|
-
if (typeof creditUsagePercent !== "number" || !Number.isFinite(creditUsagePercent) || creditUsagePercent < 0 || creditUsagePercent > 100) return null;
|
|
183
250
|
if (typeof currentPeriod.end !== "string" || !currentPeriod.end.trim()) return null;
|
|
184
251
|
const resetAt = normalizeResetAt(currentPeriod.end);
|
|
185
252
|
if (resetAt === undefined) return null;
|
|
253
|
+
|
|
254
|
+
let weeklyPercent: number | undefined;
|
|
255
|
+
if (typeof config?.creditUsagePercent === "number"
|
|
256
|
+
&& Number.isFinite(config.creditUsagePercent)
|
|
257
|
+
&& config.creditUsagePercent >= 0
|
|
258
|
+
&& config.creditUsagePercent <= 100) {
|
|
259
|
+
weeklyPercent = config.creditUsagePercent;
|
|
260
|
+
} else {
|
|
261
|
+
// Unified-billing shape exposes on-demand usage vs cap as { val } objects.
|
|
262
|
+
const cap = billingValue(config?.onDemandCap);
|
|
263
|
+
const used = billingValue(config?.onDemandUsed);
|
|
264
|
+
if (cap !== undefined && cap > 0 && used !== undefined) {
|
|
265
|
+
weeklyPercent = normalizePercent((used / cap) * 100);
|
|
266
|
+
} else if (used === 0) {
|
|
267
|
+
// A zero-usage unified account still owns the weekly meter; report 0% so the
|
|
268
|
+
// provider stays in the usage section instead of being reclassified as no-quota.
|
|
269
|
+
weeklyPercent = 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (weeklyPercent === undefined) return null;
|
|
186
273
|
const quota: ProviderQuota = {
|
|
187
|
-
weeklyPercent
|
|
274
|
+
weeklyPercent,
|
|
188
275
|
weeklyResetAt: resetAt,
|
|
189
276
|
updatedAt: Date.now(),
|
|
190
277
|
};
|
|
@@ -226,7 +313,7 @@ async function fetchKiroUsageQuota(
|
|
|
226
313
|
|
|
227
314
|
const response = await fetch(url, {
|
|
228
315
|
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
229
|
-
signal: AbortSignal.timeout(
|
|
316
|
+
signal: AbortSignal.timeout(KIRO_QUOTA_TIMEOUT_MS),
|
|
230
317
|
});
|
|
231
318
|
if (!response.ok) return null;
|
|
232
319
|
const body = asRecord(await response.json().catch(() => null));
|
|
@@ -702,6 +789,129 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
|
|
|
702
789
|
return quota ? report(provider, "kimi:usages", quota) : null;
|
|
703
790
|
}
|
|
704
791
|
|
|
792
|
+
function isCanonicalOpencodeGoBaseUrl(baseUrl: string | undefined): boolean {
|
|
793
|
+
return normalizedBaseUrl(baseUrl ?? "") === normalizedBaseUrl(OPENCODE_GO_BASE_URL);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
interface OpencodeGoUsageEstimate {
|
|
797
|
+
costUsd: number;
|
|
798
|
+
priced: boolean;
|
|
799
|
+
perKeyCostUsd: Map<string, { costUsd: number; priced: boolean }>;
|
|
800
|
+
windowCounts: Map<string, number>;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Read the local usage log once and estimate opencode.go usage: 30-day cost from the
|
|
805
|
+
* published per-model prices (cache-aware), and 5-hour request counts per model.
|
|
806
|
+
*
|
|
807
|
+
* Attribution: the pool's ACTIVE key serves every provider-bound request (loopback
|
|
808
|
+
* traffic carries no client key id), so all rows are charged to it. Rows written before
|
|
809
|
+
* a key switch stay with the currently active key — an intentional approximation.
|
|
810
|
+
*/
|
|
811
|
+
function estimateOpencodeGoUsage(name: string, config: OcxProviderConfig): OpencodeGoUsageEstimate | null {
|
|
812
|
+
if (!isCanonicalOpencodeGoBaseUrl(config.baseUrl)) return null;
|
|
813
|
+
const now = Date.now();
|
|
814
|
+
const fiveHourAgo = now - OPENCODE_GO_FIVE_HOUR_MS;
|
|
815
|
+
const monthAgo = now - OPENCODE_GO_COST_WINDOW_MS;
|
|
816
|
+
const pool = config.apiKeyPool ?? [];
|
|
817
|
+
const activeKey = resolveEnvValue(config.apiKey)?.trim() ?? config.apiKey;
|
|
818
|
+
const activeKeyId = activeKey ? pool.find(entry => entry.key === activeKey)?.id : undefined;
|
|
819
|
+
|
|
820
|
+
const estimate: OpencodeGoUsageEstimate = {
|
|
821
|
+
costUsd: 0,
|
|
822
|
+
priced: false,
|
|
823
|
+
perKeyCostUsd: new Map(pool.map(entry => [entry.id, { costUsd: 0, priced: false }])),
|
|
824
|
+
windowCounts: new Map(),
|
|
825
|
+
};
|
|
826
|
+
for (const entry of readUsageEntries()) {
|
|
827
|
+
if (entry.provider !== name || entry.status !== 200) continue;
|
|
828
|
+
const usage = entry.usage;
|
|
829
|
+
if (!usage || typeof usage.inputTokens !== "number") continue;
|
|
830
|
+
const timestamp = entry.timestamp ?? 0;
|
|
831
|
+
if (timestamp >= monthAgo) {
|
|
832
|
+
const price = OPENCODE_GO_PRICES[entry.model];
|
|
833
|
+
if (price) {
|
|
834
|
+
const cachedRead = usage.cachedInputTokens ?? 0;
|
|
835
|
+
const cachedWrite = usage.cacheCreationInputTokens ?? 0;
|
|
836
|
+
const uncached = Math.max(0, usage.inputTokens - cachedRead - cachedWrite);
|
|
837
|
+
const amount = (
|
|
838
|
+
uncached * price.input
|
|
839
|
+
+ cachedRead * price.cacheRead
|
|
840
|
+
+ cachedWrite * price.cacheWrite
|
|
841
|
+
+ usage.outputTokens * price.output
|
|
842
|
+
) / 1_000_000;
|
|
843
|
+
estimate.costUsd += amount;
|
|
844
|
+
estimate.priced = true;
|
|
845
|
+
if (activeKeyId) {
|
|
846
|
+
const bucket = estimate.perKeyCostUsd.get(activeKeyId) ?? { costUsd: 0, priced: false };
|
|
847
|
+
bucket.costUsd += amount;
|
|
848
|
+
bucket.priced = true;
|
|
849
|
+
estimate.perKeyCostUsd.set(activeKeyId, bucket);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (timestamp >= fiveHourAgo && OPENCODE_GO_FIVE_HOUR_LIMITS[entry.model]) {
|
|
854
|
+
estimate.windowCounts.set(entry.model, (estimate.windowCounts.get(entry.model) ?? 0) + 1);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return estimate;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* opencode.go has no per-key billing API (the console endpoint is session-authenticated),
|
|
862
|
+
* so the quota is estimated locally from the traffic we already proxy:
|
|
863
|
+
* - the 30-day estimated cost uses the published per-model prices with the cache split
|
|
864
|
+
* our own usage log records, which matches the console's per-request costs;
|
|
865
|
+
* - the 5-hour window rows use the published request limits (1x tier) as the quota.
|
|
866
|
+
*/
|
|
867
|
+
function fetchOpencodeGoQuota(name: string, config: OcxProviderConfig): ProviderQuotaReport | null {
|
|
868
|
+
const estimate = estimateOpencodeGoUsage(name, config);
|
|
869
|
+
if (!estimate) return null;
|
|
870
|
+
const now = Date.now();
|
|
871
|
+
|
|
872
|
+
const customWindows: ProviderQuotaWindow[] = [{
|
|
873
|
+
label: "추산 비용 · 30일",
|
|
874
|
+
percent: 0,
|
|
875
|
+
valueLabel: `~$${estimate.costUsd.toFixed(2)}`,
|
|
876
|
+
}];
|
|
877
|
+
for (const [model, count] of [...estimate.windowCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
878
|
+
const { label, limit } = OPENCODE_GO_FIVE_HOUR_LIMITS[model]!;
|
|
879
|
+
customWindows.push({
|
|
880
|
+
label: `5h · ${label}`,
|
|
881
|
+
percent: normalizePercent((count / limit) * 100) ?? 0,
|
|
882
|
+
resetAt: now + OPENCODE_GO_FIVE_HOUR_MS,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return report(name, "opencode-go:docs-estimate", {
|
|
886
|
+
customWindows,
|
|
887
|
+
updatedAt: now,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Per-key estimated-cost quota for every connected key of a canonical opencode.go
|
|
893
|
+
* provider. Each key gets a value row even at $0.00 so the notch pool shows all keys.
|
|
894
|
+
*/
|
|
895
|
+
export function opencodeGoKeyQuotaEstimates(config: OcxConfig, name: string): Record<string, ProviderQuota> | null {
|
|
896
|
+
const provider = config.providers[name];
|
|
897
|
+
if (!provider) return null;
|
|
898
|
+
const estimate = estimateOpencodeGoUsage(name, provider);
|
|
899
|
+
if (!estimate) return null;
|
|
900
|
+
const now = Date.now();
|
|
901
|
+
const out: Record<string, ProviderQuota> = {};
|
|
902
|
+
for (const [keyId, bucket] of estimate.perKeyCostUsd) {
|
|
903
|
+
out[keyId] = {
|
|
904
|
+
customWindows: [{
|
|
905
|
+
label: "추산 비용 · 30일",
|
|
906
|
+
percent: 0,
|
|
907
|
+
valueLabel: `~$${bucket.costUsd.toFixed(2)}`,
|
|
908
|
+
}],
|
|
909
|
+
updatedAt: now,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
|
|
705
915
|
/** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
|
|
706
916
|
async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
707
917
|
let accessToken: string;
|
|
@@ -982,6 +1192,11 @@ async function maybeFetchProviderQuota(
|
|
|
982
1192
|
if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name, forceRefresh);
|
|
983
1193
|
if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
|
|
984
1194
|
if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
|
|
1195
|
+
// opencode.go is a key-auth subscription: estimate usage locally from the traffic log.
|
|
1196
|
+
if (provider.authMode !== "oauth" && provider.authMode !== "forward"
|
|
1197
|
+
&& isCanonicalOpencodeGoBaseUrl(provider.baseUrl)) {
|
|
1198
|
+
return fetchOpencodeGoQuota(name, provider);
|
|
1199
|
+
}
|
|
985
1200
|
// Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
|
|
986
1201
|
// host and only for real key auth — forward/local modes carry no credential of ours.
|
|
987
1202
|
if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
|
|
@@ -417,7 +417,17 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
417
417
|
const name = (url.searchParams.get("name") ?? "").trim();
|
|
418
418
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
419
419
|
const { listProviderApiKeys } = await import("../../providers/api-keys");
|
|
420
|
-
|
|
420
|
+
const { opencodeGoKeyQuotaEstimates } = await import("../../providers/quota");
|
|
421
|
+
const result = listProviderApiKeys(config, name);
|
|
422
|
+
const keyQuotas = opencodeGoKeyQuotaEstimates(config, name);
|
|
423
|
+
if (!keyQuotas) return jsonResponse(result);
|
|
424
|
+
return jsonResponse({
|
|
425
|
+
...result,
|
|
426
|
+
keys: result.keys.map(key => ({
|
|
427
|
+
...key,
|
|
428
|
+
...(keyQuotas[key.id] ? { quota: keyQuotas[key.id] } : {}),
|
|
429
|
+
})),
|
|
430
|
+
});
|
|
421
431
|
}
|
|
422
432
|
if (url.pathname === "/api/providers/keys" && req.method === "POST") {
|
|
423
433
|
const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
|
|
Binary file
|