@coseung2/opencodex 2.8.0-cs.1 → 2.8.0-cs.10

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.
@@ -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,52 @@ 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
+ const OPENCODE_GO_WEEK_MS = 7 * 86_400_000;
38
+ const OPENCODE_GO_USAGE_PATH = "/usage";
39
+
40
+ /** opencode.go published request limits per window (1x tier), opencode.ai/docs/go. */
41
+ const OPENCODE_GO_LIMITS: Record<string, { label: string; fiveHour: number; weekly: number; monthly: number }> = {
42
+ "grok-4.5": { label: "Grok 4.5", fiveHour: 120, weekly: 300, monthly: 600 },
43
+ "gpt-5.6-luna": { label: "GPT 5.6 Luna", fiveHour: 2_050, weekly: 5_100, monthly: 10_250 },
44
+ "glm-5.2": { label: "GLM-5.2", fiveHour: 880, weekly: 2_150, monthly: 4_300 },
45
+ "glm-5.1": { label: "GLM-5.1", fiveHour: 880, weekly: 2_150, monthly: 4_300 },
46
+ "kimi-k3": { label: "Kimi K3", fiveHour: 110, weekly: 250, monthly: 490 },
47
+ "kimi-k2.7-code": { label: "Kimi K2.7 Code", fiveHour: 1_350, weekly: 3_380, monthly: 6_750 },
48
+ "kimi-k2.6": { label: "Kimi K2.6", fiveHour: 1_150, weekly: 2_880, monthly: 5_750 },
49
+ "mimo-v2.5": { label: "MiMo-V2.5", fiveHour: 30_100, weekly: 75_200, monthly: 150_400 },
50
+ "mimo-v2.5-pro": { label: "MiMo-V2.5 Pro", fiveHour: 3_250, weekly: 8_150, monthly: 16_300 },
51
+ "minimax-m3": { label: "MiniMax M3", fiveHour: 3_200, weekly: 8_000, monthly: 16_000 },
52
+ "minimax-m2.7": { label: "MiniMax M2.7", fiveHour: 3_400, weekly: 8_500, monthly: 17_000 },
53
+ "qwen3.8-max": { label: "Qwen3.8 Max", fiveHour: 160, weekly: 400, monthly: 810 },
54
+ "qwen3.7-max": { label: "Qwen3.7 Max", fiveHour: 340, weekly: 840, monthly: 1_690 },
55
+ "qwen3.7-plus": { label: "Qwen3.7 Plus", fiveHour: 4_300, weekly: 10_800, monthly: 21_600 },
56
+ "qwen3.6-plus": { label: "Qwen3.6 Plus", fiveHour: 3_300, weekly: 8_200, monthly: 16_300 },
57
+ "deepseek-v4-pro": { label: "DeepSeek V4 Pro", fiveHour: 3_450, weekly: 8_550, monthly: 17_150 },
58
+ "deepseek-v4-flash": { label: "DeepSeek V4 Flash", fiveHour: 31_650, weekly: 79_050, monthly: 158_150 },
59
+ "hy3": { label: "Hy3", fiveHour: 4_300, weekly: 10_750, monthly: 21_500 },
60
+ };
61
+
29
62
  export interface ProviderQuotaWindow {
30
63
  label: string;
31
64
  percent: number;
32
65
  resetAt?: number;
66
+ /** Text value shown instead of a percent bar (e.g. an estimated cost). */
67
+ valueLabel?: string;
68
+ /** Compact percent segments rendered as narrow bars on ONE row (label/percent per segment). */
69
+ segments?: { label: string; percent: number; resetAt?: number }[];
33
70
  }
34
71
 
35
72
  export interface ProviderQuota {
@@ -81,7 +118,8 @@ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is Provide
81
118
  return typeof quota.fiveHourPercent === "number"
82
119
  || typeof quota.weeklyPercent === "number"
83
120
  || typeof quota.monthlyPercent === "number"
84
- || !!quota.customWindows?.some(window => typeof window.percent === "number");
121
+ || !!quota.customWindows?.some(window =>
122
+ typeof window.percent === "number" || typeof window.valueLabel === "string");
85
123
  }
86
124
 
87
125
  function providerLabel(providerId: string): string {
@@ -118,6 +156,13 @@ function normalizePercent(value: unknown): number | undefined {
118
156
  return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
119
157
  }
120
158
 
159
+ /** Numeric value from a billing object ({ val: 123 }) or a bare number. */
160
+ function billingValue(value: unknown): number | undefined {
161
+ if (typeof value === "number" && Number.isFinite(value)) return value;
162
+ const record = asRecord(value);
163
+ return toFiniteNumber(record?.val);
164
+ }
165
+
121
166
  function asRecord(value: unknown): Record<string, unknown> | null {
122
167
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
123
168
  }
@@ -157,11 +202,6 @@ async function fetchChatGptForwardQuota(
157
202
  return quota ? report(provider, "chatgpt:wham", quota) : null;
158
203
  }
159
204
 
160
- function centsValue(value: unknown): number | undefined {
161
- const rec = asRecord(value);
162
- return rec ? toFiniteNumber(rec.val) : undefined;
163
- }
164
-
165
205
  async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
166
206
  let accessToken: string;
167
207
  try {
@@ -169,25 +209,50 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
169
209
  } catch {
170
210
  return null;
171
211
  }
172
- const response = await fetch("https://cli-chat-proxy.grok.com/v1/billing", {
173
- headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
212
+ const response = await fetch("https://cli-chat-proxy.grok.com/v1/billing?format=credits", {
213
+ headers: {
214
+ Accept: "application/json",
215
+ Authorization: `Bearer ${accessToken}`,
216
+ "X-XAI-Token-Auth": "xai-grok-cli",
217
+ "x-grok-client-mode": "cli",
218
+ },
174
219
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
175
220
  });
176
221
  if (!response.ok) return null;
177
222
  const body = asRecord(await response.json().catch(() => null));
178
223
  const config = asRecord(body?.config);
179
- if (!config) return null;
180
- const limitCents = centsValue(config.monthlyLimit);
181
- const usedCents = centsValue(config.used);
182
- if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
183
- const percent = normalizePercent((usedCents / limitCents) * 100);
184
- if (percent === undefined) return null;
224
+ const currentPeriod = asRecord(config?.currentPeriod);
225
+ // A weekly window is the meter contract; other shapes (spend-cap etc.) fail closed.
226
+ if (currentPeriod?.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
227
+ if (typeof currentPeriod.end !== "string" || !currentPeriod.end.trim()) return null;
228
+ const resetAt = normalizeResetAt(currentPeriod.end);
229
+ if (resetAt === undefined) return null;
230
+
231
+ let weeklyPercent: number | undefined;
232
+ if (typeof config?.creditUsagePercent === "number"
233
+ && Number.isFinite(config.creditUsagePercent)
234
+ && config.creditUsagePercent >= 0
235
+ && config.creditUsagePercent <= 100) {
236
+ weeklyPercent = config.creditUsagePercent;
237
+ } else {
238
+ // Unified-billing shape exposes on-demand usage vs cap as { val } objects.
239
+ const cap = billingValue(config?.onDemandCap);
240
+ const used = billingValue(config?.onDemandUsed);
241
+ if (cap !== undefined && cap > 0 && used !== undefined) {
242
+ weeklyPercent = normalizePercent((used / cap) * 100);
243
+ } else if (used === 0) {
244
+ // A zero-usage unified account still owns the weekly meter; report 0% so the
245
+ // provider stays in the usage section instead of being reclassified as no-quota.
246
+ weeklyPercent = 0;
247
+ }
248
+ }
249
+ if (weeklyPercent === undefined) return null;
185
250
  const quota: ProviderQuota = {
186
- monthlyPercent: percent,
187
- monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
251
+ weeklyPercent,
252
+ weeklyResetAt: resetAt,
188
253
  updatedAt: Date.now(),
189
254
  };
190
- return report(provider, "xai:grok-billing", quota);
255
+ return report(provider, "xai:grok-credits", quota);
191
256
  }
192
257
 
193
258
  function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
@@ -225,7 +290,7 @@ async function fetchKiroUsageQuota(
225
290
 
226
291
  const response = await fetch(url, {
227
292
  headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
228
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
293
+ signal: AbortSignal.timeout(KIRO_QUOTA_TIMEOUT_MS),
229
294
  });
230
295
  if (!response.ok) return null;
231
296
  const body = asRecord(await response.json().catch(() => null));
@@ -701,6 +766,230 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
701
766
  return quota ? report(provider, "kimi:usages", quota) : null;
702
767
  }
703
768
 
769
+ function isCanonicalOpencodeGoBaseUrl(baseUrl: string | undefined): boolean {
770
+ return normalizedBaseUrl(baseUrl ?? "") === normalizedBaseUrl(OPENCODE_GO_BASE_URL);
771
+ }
772
+
773
+ interface OpencodeGoUsageEstimate {
774
+ /** model -> request count in each window (status 200). */
775
+ fiveHourCounts: Map<string, number>;
776
+ weeklyCounts: Map<string, number>;
777
+ monthlyCounts: Map<string, number>;
778
+ /** key id -> 30-day request count (all traffic attributed to the active key). */
779
+ perKeyMonthlyCounts: Map<string, number>;
780
+ }
781
+
782
+ /**
783
+ * Read the local usage log once and count opencode.go requests per model per window.
784
+ * The console's allocation percent is request-count based (each model has published
785
+ * 5h/week/month request limits on opencode.ai/docs/go), so we compare like with like.
786
+ *
787
+ * Attribution: the pool's ACTIVE key serves every provider-bound request (loopback
788
+ * traffic carries no client key id), so all rows are charged to it. Rows written before
789
+ * a key switch stay with the currently active key — an intentional approximation.
790
+ */
791
+ function estimateOpencodeGoUsage(name: string, config: OcxProviderConfig): OpencodeGoUsageEstimate | null {
792
+ if (!isCanonicalOpencodeGoBaseUrl(config.baseUrl)) return null;
793
+ const now = Date.now();
794
+ const fiveHourAgo = now - OPENCODE_GO_FIVE_HOUR_MS;
795
+ const monthAgo = now - OPENCODE_GO_COST_WINDOW_MS;
796
+ const pool = config.apiKeyPool ?? [];
797
+ const activeKey = resolveEnvValue(config.apiKey)?.trim() ?? config.apiKey;
798
+ const activeKeyId = activeKey ? pool.find(entry => entry.key === activeKey)?.id : undefined;
799
+
800
+ const estimate: OpencodeGoUsageEstimate = {
801
+ fiveHourCounts: new Map(),
802
+ weeklyCounts: new Map(),
803
+ monthlyCounts: new Map(),
804
+ perKeyMonthlyCounts: new Map(pool.map(entry => [entry.id, 0])),
805
+ };
806
+ for (const entry of readUsageEntries()) {
807
+ if (entry.provider !== name || entry.status !== 200) continue;
808
+ const timestamp = entry.timestamp ?? 0;
809
+ if (!OPENCODE_GO_LIMITS[entry.model]) continue;
810
+ if (timestamp >= fiveHourAgo) {
811
+ estimate.fiveHourCounts.set(entry.model, (estimate.fiveHourCounts.get(entry.model) ?? 0) + 1);
812
+ }
813
+ if (timestamp >= now - OPENCODE_GO_WEEK_MS) {
814
+ estimate.weeklyCounts.set(entry.model, (estimate.weeklyCounts.get(entry.model) ?? 0) + 1);
815
+ }
816
+ if (timestamp >= monthAgo && activeKeyId) {
817
+ estimate.monthlyCounts.set(entry.model, (estimate.monthlyCounts.get(entry.model) ?? 0) + 1);
818
+ estimate.perKeyMonthlyCounts.set(activeKeyId, (estimate.perKeyMonthlyCounts.get(activeKeyId) ?? 0) + 1);
819
+ }
820
+ }
821
+ return estimate;
822
+ }
823
+
824
+ interface OpencodeGoUsageWindow {
825
+ percent?: number;
826
+ resetAt?: number;
827
+ }
828
+
829
+ /**
830
+ * Real allocation windows from the opencode.go usage endpoint (key-scoped).
831
+ * The console shows exactly these percents and reset times, so this replaces the
832
+ * local estimate whenever the endpoint answers.
833
+ */
834
+ async function fetchOpencodeGoUsageApi(apiKey: string | undefined): Promise<{
835
+ fiveHour: OpencodeGoUsageWindow;
836
+ weekly: OpencodeGoUsageWindow;
837
+ monthly: OpencodeGoUsageWindow;
838
+ } | null> {
839
+ if (!apiKey?.trim()) return null;
840
+ const response = await fetch(`${OPENCODE_GO_BASE_URL}${OPENCODE_GO_USAGE_PATH}`, {
841
+ headers: {
842
+ Accept: "application/json",
843
+ Authorization: `Bearer ${apiKey.trim()}`,
844
+ },
845
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
846
+ });
847
+ if (!response.ok) return null;
848
+ const body = asRecord(await response.json().catch(() => null));
849
+ const usage = asRecord(body?.usage);
850
+ if (!usage) return null;
851
+ const parse = (raw: unknown): OpencodeGoUsageWindow => {
852
+ const window = asRecord(raw);
853
+ const percent = normalizePercent(toFiniteNumber(window?.percent));
854
+ const resetAt = normalizeResetAt(window?.resetsAt);
855
+ return {
856
+ ...(percent !== undefined ? { percent } : {}),
857
+ ...(resetAt !== undefined ? { resetAt } : {}),
858
+ };
859
+ };
860
+ const fiveHour = parse(usage.rolling);
861
+ const weekly = parse(usage.weekly);
862
+ const monthly = parse(usage.monthly);
863
+ if (fiveHour.percent === undefined && weekly.percent === undefined && monthly.percent === undefined) {
864
+ return null;
865
+ }
866
+ return { fiveHour, weekly, monthly };
867
+ }
868
+
869
+ function opencodeGoSegments(api: NonNullable<Awaited<ReturnType<typeof fetchOpencodeGoUsageApi>>>): NonNullable<ProviderQuotaWindow["segments"]> {
870
+ const segments: NonNullable<ProviderQuotaWindow["segments"]> = [];
871
+ const push = (label: string, window: OpencodeGoUsageWindow) => {
872
+ if (window.percent === undefined) return;
873
+ segments.push({
874
+ label,
875
+ percent: window.percent,
876
+ ...(window.resetAt !== undefined ? { resetAt: window.resetAt } : {}),
877
+ });
878
+ };
879
+ push("5h", api.fiveHour);
880
+ push("Weekly", api.weekly);
881
+ push("Monthly", api.monthly);
882
+ return segments;
883
+ }
884
+
885
+ /**
886
+ * opencode.go allocation: prefer the key-scoped usage endpoint (exact console
887
+ * percents + real reset times); fall back to the local request-count estimate
888
+ * against published limits when the endpoint is unreachable or rejects.
889
+ */
890
+ async function fetchOpencodeGoQuota(name: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
891
+ const activeKey = resolveEnvValue(config.apiKey)?.trim() ?? config.apiKey;
892
+ const api = await fetchOpencodeGoUsageApi(activeKey).catch(() => null);
893
+ if (api) {
894
+ const segments = opencodeGoSegments(api);
895
+ if (segments.length > 0) {
896
+ return report(name, "opencode-go:usage-api", {
897
+ customWindows: [{
898
+ // Row label intentionally empty: the segments carry their own labels.
899
+ label: "",
900
+ percent: 0,
901
+ segments,
902
+ }],
903
+ updatedAt: Date.now(),
904
+ });
905
+ }
906
+ }
907
+
908
+ // Fallback: dominant model's local request counts against published limits.
909
+ const estimate = estimateOpencodeGoUsage(name, config);
910
+ if (!estimate) return null;
911
+ const dominant = [...estimate.monthlyCounts.entries()]
912
+ .sort((a, b) => b[1] - a[1] || (estimate.weeklyCounts.get(b[0]) ?? 0) - (estimate.weeklyCounts.get(a[0]) ?? 0))[0]?.[0];
913
+ if (!dominant) return null;
914
+ const { fiveHour, weekly, monthly } = OPENCODE_GO_LIMITS[dominant]!;
915
+ const now = Date.now();
916
+ return report(name, "opencode-go:docs-estimate", {
917
+ customWindows: [{
918
+ label: "",
919
+ percent: 0,
920
+ segments: [
921
+ {
922
+ label: "5h",
923
+ percent: normalizePercent(((estimate.fiveHourCounts.get(dominant) ?? 0) / fiveHour) * 100) ?? 0,
924
+ resetAt: now + OPENCODE_GO_FIVE_HOUR_MS,
925
+ },
926
+ {
927
+ label: "Weekly",
928
+ percent: normalizePercent(((estimate.weeklyCounts.get(dominant) ?? 0) / weekly) * 100) ?? 0,
929
+ resetAt: now + OPENCODE_GO_WEEK_MS,
930
+ },
931
+ {
932
+ label: "Monthly",
933
+ percent: normalizePercent(((estimate.monthlyCounts.get(dominant) ?? 0) / monthly) * 100) ?? 0,
934
+ resetAt: now + OPENCODE_GO_COST_WINDOW_MS,
935
+ },
936
+ ],
937
+ }],
938
+ updatedAt: now,
939
+ });
940
+ }
941
+
942
+ /**
943
+ * Per-key monthly-allocation percent for every connected key: the usage endpoint
944
+ * answers per key, so each pool key reports its own real monthly percent. Keys the
945
+ * endpoint rejects fall back to the local 30-day request estimate.
946
+ */
947
+ export async function opencodeGoKeyQuotaEstimates(config: OcxConfig, name: string): Promise<Record<string, ProviderQuota> | null> {
948
+ const provider = config.providers[name];
949
+ if (!provider) return null;
950
+ const now = Date.now();
951
+ const out: Record<string, ProviderQuota> = {};
952
+ const pool = provider.apiKeyPool ?? [];
953
+ if (pool.length > 0) {
954
+ const results = await Promise.all(pool.map(async entry => {
955
+ const api = await fetchOpencodeGoUsageApi(entry.key).catch(() => null);
956
+ return [entry.id, api?.monthly] as const;
957
+ }));
958
+ for (const [keyId, monthly] of results) {
959
+ if (monthly?.percent === undefined) continue;
960
+ out[keyId] = {
961
+ customWindows: [{
962
+ label: "월간 할당",
963
+ percent: monthly.percent,
964
+ ...(monthly.resetAt !== undefined ? { resetAt: monthly.resetAt } : {}),
965
+ }],
966
+ updatedAt: now,
967
+ };
968
+ }
969
+ }
970
+ // Fallback for keys the endpoint did not answer.
971
+ const estimate = estimateOpencodeGoUsage(name, provider);
972
+ if (estimate) {
973
+ const dominant = [...estimate.monthlyCounts.entries()]
974
+ .sort((a, b) => b[1] - a[1])[0]?.[0];
975
+ if (dominant) {
976
+ const monthlyLimit = OPENCODE_GO_LIMITS[dominant]!.monthly;
977
+ for (const [keyId, count] of estimate.perKeyMonthlyCounts) {
978
+ if (out[keyId]) continue;
979
+ out[keyId] = {
980
+ customWindows: [{
981
+ label: "월간 할당",
982
+ percent: normalizePercent((count / monthlyLimit) * 100) ?? 0,
983
+ resetAt: now + OPENCODE_GO_COST_WINDOW_MS,
984
+ }],
985
+ updatedAt: now,
986
+ };
987
+ }
988
+ }
989
+ }
990
+ return Object.keys(out).length > 0 ? out : null;
991
+ }
992
+
704
993
  /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
705
994
  async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
706
995
  let accessToken: string;
@@ -981,6 +1270,11 @@ async function maybeFetchProviderQuota(
981
1270
  if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name, forceRefresh);
982
1271
  if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
983
1272
  if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
1273
+ // opencode.go is a key-auth subscription: estimate usage locally from the traffic log.
1274
+ if (provider.authMode !== "oauth" && provider.authMode !== "forward"
1275
+ && isCanonicalOpencodeGoBaseUrl(provider.baseUrl)) {
1276
+ return fetchOpencodeGoQuota(name, provider);
1277
+ }
984
1278
  // Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
985
1279
  // host and only for real key auth — forward/local modes carry no credential of ours.
986
1280
  if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
@@ -728,6 +728,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
728
728
  baseUrl: "https://runtime.us-east-1.kiro.dev",
729
729
  authKind: "oauth",
730
730
  oauthId: "kiro",
731
+ dashboardPreset: true,
731
732
  note: "Import-first: reuses your installed and signed-in Kiro CLI session (requires `kiro-cli login`). Add account logs `kiro-cli` out, switches it through a fresh browser login, stores the account by profile ARN, and restores the previous CLI session on cancellation or failure. Experimental third-party harness — see Kiro ToS.",
732
733
  models: KIRO_MODELS,
733
734
  defaultModel: "kiro-auto",
@@ -267,6 +267,7 @@ const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "
267
267
 
268
268
  export function parseRequest(body: unknown): OcxParsedRequest {
269
269
  const replayedInputPrefixLength = previousResponseReplayPrefixLength(body);
270
+ let replayedMessagePrefixLength = 0;
270
271
  const parsed = responsesRequestSchema.safeParse(body);
271
272
  if (!parsed.success) {
272
273
  throw new Error(`responses parse error: ${parsed.error.message}`);
@@ -305,6 +306,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
305
306
  messages.push({ role: "user", content: data.input, timestamp: now });
306
307
  } else if (data.input) {
307
308
  for (let inputIndex = 0; inputIndex < data.input.length; inputIndex++) {
309
+ if (inputIndex === replayedInputPrefixLength) replayedMessagePrefixLength = messages.length;
308
310
  const item = data.input[inputIndex];
309
311
  const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
310
312
 
@@ -571,6 +573,9 @@ export function parseRequest(body: unknown): OcxParsedRequest {
571
573
  });
572
574
  }
573
575
  }
576
+ if (replayedInputPrefixLength > 0 && replayedInputPrefixLength >= data.input.length) {
577
+ replayedMessagePrefixLength = messages.length;
578
+ }
574
579
  }
575
580
 
576
581
  const declaredTools = buildTools(data.tools as unknown[] | undefined) ?? [];
@@ -637,7 +642,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
637
642
  stream: data.stream === true,
638
643
  options,
639
644
  _rawBody: body,
640
- ...(replayedInputPrefixLength > 0 ? { _replayPrefixLen: replayedInputPrefixLength } : {}),
645
+ ...(replayedInputPrefixLength > 0 ? {
646
+ _replayPrefixLen: replayedInputPrefixLength,
647
+ _replayMessagePrefixLen: replayedMessagePrefixLength,
648
+ } : {}),
641
649
  ...(webSearch ? { _webSearch: webSearch } : {}),
642
650
  ...(imageGen ? { _imageGeneration: imageGen } : {}),
643
651
  ...(structuredOutput ? { _structuredOutput: true } : {}),
@@ -1,5 +1,6 @@
1
1
  import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { isDeepStrictEqual } from "node:util";
3
4
  import { atomicWriteFileAsync, getConfigDir } from "../config";
4
5
  import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
5
6
  import type { OcxProviderContinuationState } from "../types";
@@ -584,6 +585,14 @@ function inputItems(input: unknown): unknown[] {
584
585
  return [input];
585
586
  }
586
587
 
588
+ function startsWithReplayPrefix(input: unknown[], prefix: unknown[]): boolean {
589
+ if (prefix.length === 0 || input.length < prefix.length) return false;
590
+ for (let index = 0; index < prefix.length; index++) {
591
+ if (!isDeepStrictEqual(input[index], prefix[index])) return false;
592
+ }
593
+ return true;
594
+ }
595
+
587
596
  function pruneResponses(at = now()): void {
588
597
  for (const [id, state] of states) {
589
598
  if (at - state.createdAt > RESPONSE_TTL_MS) deleteEntry(id);
@@ -710,11 +719,19 @@ export function expandPreviousResponseInput(body: unknown): unknown {
710
719
  replayFailures.set(request, materialized.failure);
711
720
  return body;
712
721
  }
722
+ const previousItems = materialized.state.items;
723
+ const currentItems = inputItems(request.input);
724
+ // Some Responses clients send the complete prior input while still supplying
725
+ // previous_response_id. Treat an exact stored prefix as already expanded instead of prepending
726
+ // it a second time; keep the provenance marker so downstream adapters can distinguish replayed
727
+ // history from this turn's new input.
713
728
  const expanded = {
714
729
  ...request,
715
- input: [...materialized.state.items, ...inputItems(request.input)],
730
+ input: startsWithReplayPrefix(currentItems, previousItems)
731
+ ? [...currentItems]
732
+ : [...previousItems, ...currentItems],
716
733
  };
717
- replayedInputPrefixLengths.set(expanded, materialized.state.items.length);
734
+ replayedInputPrefixLengths.set(expanded, previousItems.length);
718
735
  return expanded;
719
736
  }
720
737
 
@@ -34,6 +34,8 @@ export interface ActiveTurnLease extends AdmissionLease {
34
34
  const activeTurns = new Map<AbortController, ActiveTurnLease>();
35
35
  const admittedTurns = new Set<ActiveTurnLease>();
36
36
  const knownTurnControllers = new WeakSet<AbortController>();
37
+ export type ActiveTurnsIdleListener = () => void;
38
+ const activeTurnsIdleListeners = new Set<ActiveTurnsIdleListener>();
37
39
  let turnReleaseMisses = 0;
38
40
  let draining = false;
39
41
  let recyclingForExit = false;
@@ -41,6 +43,26 @@ let _serverRef: ReturnType<typeof Bun.serve> | undefined;
41
43
 
42
44
  export function setServerRef(server: ReturnType<typeof Bun.serve> | undefined): void { _serverRef = server; }
43
45
  export function setDraining(value: boolean): void { draining = value; }
46
+ /**
47
+ * Subscribe to the synchronous transition where the final admitted turn is
48
+ * released. The callback runs after the lease and admission gate are settled,
49
+ * so an observer can perform an idle-only action without racing a request on
50
+ * the event loop. Returns an unsubscribe function for bounded observers.
51
+ */
52
+ export function onActiveTurnsIdle(listener: ActiveTurnsIdleListener): () => void {
53
+ activeTurnsIdleListeners.add(listener);
54
+ return () => { activeTurnsIdleListeners.delete(listener); };
55
+ }
56
+ function notifyActiveTurnsIdle(): void {
57
+ if (admittedTurns.size !== 0) return;
58
+ for (const listener of [...activeTurnsIdleListeners]) {
59
+ try {
60
+ listener();
61
+ } catch {
62
+ // An idle observer must never break turn release or request teardown.
63
+ }
64
+ }
65
+ }
44
66
  export function tryAdmitTurn(): ActiveTurnLease | null {
45
67
  const gateLease = turnGate.tryAcquire();
46
68
  if (!gateLease) return null;
@@ -68,6 +90,7 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
68
90
  }
69
91
  controllers.clear();
70
92
  gateLease.release();
93
+ notifyActiveTurnsIdle();
71
94
  },
72
95
  };
73
96
  admittedTurns.add(lease);
@@ -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
- return jsonResponse(listProviderApiKeys(config, name));
420
+ const { opencodeGoKeyQuotaEstimates } = await import("../../providers/quota");
421
+ const result = listProviderApiKeys(config, name);
422
+ const keyQuotas = await 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 };