@slkiser/opencode-quota 3.11.2 → 4.0.1
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/README.md +92 -62
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +16 -7
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +11 -7
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/command-surfaces.d.ts +10 -0
- package/dist/lib/command-surfaces.d.ts.map +1 -0
- package/dist/lib/command-surfaces.js +9 -0
- package/dist/lib/command-surfaces.js.map +1 -0
- package/dist/lib/config-file-utils.d.ts +5 -0
- package/dist/lib/config-file-utils.d.ts.map +1 -1
- package/dist/lib/config-file-utils.js +20 -3
- package/dist/lib/config-file-utils.js.map +1 -1
- package/dist/lib/config.d.ts +5 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +124 -32
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +19 -12
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +462 -566
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +13 -0
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +55 -4
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/init-installer.d.ts +11 -1
- package/dist/lib/init-installer.d.ts.map +1 -1
- package/dist/lib/init-installer.js +524 -158
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/maintainer-announcements.d.ts.map +1 -1
- package/dist/lib/maintainer-announcements.js +7 -15
- package/dist/lib/maintainer-announcements.js.map +1 -1
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +15 -0
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-config-editor.d.ts +59 -0
- package/dist/lib/opencode-config-editor.d.ts.map +1 -0
- package/dist/lib/opencode-config-editor.js +320 -0
- package/dist/lib/opencode-config-editor.js.map +1 -0
- package/dist/lib/opencode-config-providers.d.ts +18 -0
- package/dist/lib/opencode-config-providers.d.ts.map +1 -1
- package/dist/lib/opencode-config-providers.js +75 -4
- package/dist/lib/opencode-config-providers.js.map +1 -1
- package/dist/lib/opencode-runtime-paths.d.ts.map +1 -1
- package/dist/lib/opencode-runtime-paths.js +10 -3
- package/dist/lib/opencode-runtime-paths.js.map +1 -1
- package/dist/lib/opencode-storage.d.ts +10 -0
- package/dist/lib/opencode-storage.d.ts.map +1 -1
- package/dist/lib/opencode-storage.js +75 -0
- package/dist/lib/opencode-storage.js.map +1 -1
- package/dist/lib/provider-add-command.d.ts +35 -0
- package/dist/lib/provider-add-command.d.ts.map +1 -0
- package/dist/lib/provider-add-command.js +212 -0
- package/dist/lib/provider-add-command.js.map +1 -0
- package/dist/lib/provider-add.d.ts +21 -0
- package/dist/lib/provider-add.d.ts.map +1 -0
- package/dist/lib/provider-add.js +219 -0
- package/dist/lib/provider-add.js.map +1 -0
- package/dist/lib/provider-metadata.d.ts +1 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +11 -5
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts +2 -0
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +94 -26
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +8 -4
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-export-types.d.ts +48 -27
- package/dist/lib/quota-export-types.d.ts.map +1 -1
- package/dist/lib/quota-export-types.js +0 -7
- package/dist/lib/quota-export-types.js.map +1 -1
- package/dist/lib/quota-export.d.ts.map +1 -1
- package/dist/lib/quota-export.js +63 -16
- package/dist/lib/quota-export.js.map +1 -1
- package/dist/lib/quota-providers-local.d.ts +55 -0
- package/dist/lib/quota-providers-local.d.ts.map +1 -0
- package/dist/lib/quota-providers-local.js +313 -0
- package/dist/lib/quota-providers-local.js.map +1 -0
- package/dist/lib/quota-providers-remote.d.ts +28 -0
- package/dist/lib/quota-providers-remote.d.ts.map +1 -0
- package/dist/lib/quota-providers-remote.js +682 -0
- package/dist/lib/quota-providers-remote.js.map +1 -0
- package/dist/lib/quota-providers.d.ts +158 -0
- package/dist/lib/quota-providers.d.ts.map +1 -0
- package/dist/lib/quota-providers.js +1167 -0
- package/dist/lib/quota-providers.js.map +1 -0
- package/dist/lib/quota-render-data.d.ts +1 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +35 -16
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts.map +1 -1
- package/dist/lib/quota-runtime-context.js +3 -4
- package/dist/lib/quota-runtime-context.js.map +1 -1
- package/dist/lib/quota-state.d.ts +5 -2
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +268 -51
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-status.d.ts +2 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +247 -69
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/qwen-local-quota.d.ts +12 -11
- package/dist/lib/qwen-local-quota.d.ts.map +1 -1
- package/dist/lib/qwen-local-quota.js +82 -87
- package/dist/lib/qwen-local-quota.js.map +1 -1
- package/dist/lib/scoped-update.d.ts.map +1 -1
- package/dist/lib/scoped-update.js +32 -22
- package/dist/lib/scoped-update.js.map +1 -1
- package/dist/lib/tui-runtime.d.ts +2 -0
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +33 -9
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/types.d.ts +51 -15
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +2 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +90 -111
- package/dist/plugin.js.map +1 -1
- package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
- package/dist/providers/alibaba-coding-plan.js +35 -4
- package/dist/providers/alibaba-coding-plan.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +13 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/chutes.d.ts.map +1 -1
- package/dist/providers/chutes.js +6 -0
- package/dist/providers/chutes.js.map +1 -1
- package/dist/providers/copilot.d.ts +2 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +143 -60
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +30 -1
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +13 -1
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/google-agy.d.ts.map +1 -1
- package/dist/providers/google-agy.js +7 -1
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/google-antigravity.d.ts.map +1 -1
- package/dist/providers/google-antigravity.js +10 -2
- package/dist/providers/google-antigravity.js.map +1 -1
- package/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/dist/providers/google-gemini-cli.js +8 -2
- package/dist/providers/google-gemini-cli.js.map +1 -1
- package/dist/providers/kimi-code.d.ts.map +1 -1
- package/dist/providers/kimi-code.js +6 -0
- package/dist/providers/kimi-code.js.map +1 -1
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +41 -15
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/nanogpt.d.ts.map +1 -1
- package/dist/providers/nanogpt.js +19 -1
- package/dist/providers/nanogpt.js.map +1 -1
- package/dist/providers/ollama-cloud.d.ts.map +1 -1
- package/dist/providers/ollama-cloud.js +12 -0
- package/dist/providers/ollama-cloud.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +6 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +9 -1
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/quota-providers.d.ts +6 -0
- package/dist/providers/quota-providers.d.ts.map +1 -0
- package/dist/providers/quota-providers.js +279 -0
- package/dist/providers/quota-providers.js.map +1 -0
- package/dist/providers/qwen-code.d.ts.map +1 -1
- package/dist/providers/qwen-code.js +29 -3
- package/dist/providers/qwen-code.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/result-helpers.d.ts +2 -1
- package/dist/providers/result-helpers.d.ts.map +1 -1
- package/dist/providers/result-helpers.js +6 -1
- package/dist/providers/result-helpers.js.map +1 -1
- package/dist/providers/synthetic.d.ts.map +1 -1
- package/dist/providers/synthetic.js +7 -1
- package/dist/providers/synthetic.js.map +1 -1
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +7 -1
- package/dist/providers/zai.js.map +1 -1
- package/dist/providers/zhipu.d.ts.map +1 -1
- package/dist/providers/zhipu.js +7 -1
- package/dist/providers/zhipu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +345 -41
- package/dist/tui.tsx +267 -18
- package/package.json +28 -21
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { createProviderApiKeyResolver, getApiKeyCheckedPaths, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js";
|
|
2
|
+
import { sanitizeSingleLineDisplayText } from "./display-sanitize.js";
|
|
3
|
+
import { JSON_V1_MAX_DISPLAY_CODE_POINTS, JSON_V1_MAX_NUMBER_MAGNITUDE, normalizeJsonV1Timestamp, } from "./quota-providers.js";
|
|
4
|
+
import { getAuthPaths, readAuthFile } from "./opencode-auth.js";
|
|
5
|
+
import { REQUEST_TIMEOUT_MS } from "./types.js";
|
|
6
|
+
export const QUOTA_PROVIDER_MAX_BODY_BYTES = 256 * 1024;
|
|
7
|
+
export const QUOTA_PROVIDER_MAX_REMOTE_ROWS = 100;
|
|
8
|
+
export const JSON_V1_MAX_RESPONSE_DEPTH = 32;
|
|
9
|
+
export const JSON_V1_MAX_CANDIDATES = 1_600;
|
|
10
|
+
export const JSON_V1_MAX_ENTRIES = 100;
|
|
11
|
+
export const JSON_V1_MAX_DETAILED_ERRORS = 16;
|
|
12
|
+
export const QUOTA_PROVIDER_CONCURRENCY = 4;
|
|
13
|
+
const RESULT_TYPES = new Set([
|
|
14
|
+
"quota",
|
|
15
|
+
"rate_limit",
|
|
16
|
+
"usage",
|
|
17
|
+
"spend",
|
|
18
|
+
"budget",
|
|
19
|
+
"balance",
|
|
20
|
+
"status",
|
|
21
|
+
]);
|
|
22
|
+
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function hasOnlyKeys(value, allowed) {
|
|
27
|
+
const allowedSet = new Set(allowed);
|
|
28
|
+
return Object.keys(value).every((key) => allowedSet.has(key));
|
|
29
|
+
}
|
|
30
|
+
function isIsoTimestamp(value) {
|
|
31
|
+
return (typeof value === "string" && ISO_TIMESTAMP_RE.test(value) && Number.isFinite(Date.parse(value)));
|
|
32
|
+
}
|
|
33
|
+
function boundedDisplayText(value, maxCodePoints) {
|
|
34
|
+
if (typeof value !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
const normalized = sanitizeSingleLineDisplayText(value);
|
|
37
|
+
const length = Array.from(normalized).length;
|
|
38
|
+
return length >= 1 && length <= maxCodePoints ? normalized : null;
|
|
39
|
+
}
|
|
40
|
+
function formatUsd(value) {
|
|
41
|
+
return `$${value.toFixed(2)}`;
|
|
42
|
+
}
|
|
43
|
+
function accountingMetadata(resultType, observedAtIso) {
|
|
44
|
+
return {
|
|
45
|
+
resultType,
|
|
46
|
+
acquisitionMethod: "remote_api",
|
|
47
|
+
ownership: "user_configured",
|
|
48
|
+
authority: "provider_reported",
|
|
49
|
+
...(observedAtIso ? { observedAtIso } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function resolveQuotaProviderApiKey(source) {
|
|
53
|
+
const resolver = createProviderApiKeyResolver({
|
|
54
|
+
envVars: source.apiKeyEnv ? [{ name: source.apiKeyEnv, source: "env" }] : [],
|
|
55
|
+
providerKeys: [source.providerId],
|
|
56
|
+
allowedEnvVars: source.apiKeyEnv ? [source.apiKeyEnv] : [],
|
|
57
|
+
configJsonSource: "opencode.json",
|
|
58
|
+
configJsoncSource: "opencode.jsonc",
|
|
59
|
+
getConfigCandidates: getGlobalOpencodeConfigCandidatePaths,
|
|
60
|
+
auth: {
|
|
61
|
+
readAuth: readAuthFile,
|
|
62
|
+
authKeys: [source.providerId],
|
|
63
|
+
authSource: "auth.json",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const resolved = await resolver.resolve();
|
|
67
|
+
return {
|
|
68
|
+
...(resolved ? { key: resolved.key } : {}),
|
|
69
|
+
source: resolved?.source ?? null,
|
|
70
|
+
checkedPaths: getApiKeyCheckedPaths({
|
|
71
|
+
envVarNames: source.apiKeyEnv ? [source.apiKeyEnv] : [],
|
|
72
|
+
getConfigCandidates: getGlobalOpencodeConfigCandidatePaths,
|
|
73
|
+
}),
|
|
74
|
+
authPaths: getAuthPaths(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function readBoundedJson(response) {
|
|
78
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
79
|
+
if (!contentType ||
|
|
80
|
+
(contentType !== "application/json" &&
|
|
81
|
+
!(contentType.startsWith("application/") && contentType.endsWith("+json")))) {
|
|
82
|
+
throw new Error("Expected a JSON response");
|
|
83
|
+
}
|
|
84
|
+
const contentLength = response.headers.get("content-length");
|
|
85
|
+
if (contentLength) {
|
|
86
|
+
const parsedLength = Number(contentLength);
|
|
87
|
+
if (Number.isFinite(parsedLength) && parsedLength > QUOTA_PROVIDER_MAX_BODY_BYTES) {
|
|
88
|
+
throw new Error("Response exceeded 262144 bytes");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let bytes;
|
|
92
|
+
if (response.body) {
|
|
93
|
+
const reader = response.body.getReader();
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let total = 0;
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const next = await reader.read();
|
|
99
|
+
if (next.done)
|
|
100
|
+
break;
|
|
101
|
+
total += next.value.byteLength;
|
|
102
|
+
if (total > QUOTA_PROVIDER_MAX_BODY_BYTES) {
|
|
103
|
+
await reader.cancel();
|
|
104
|
+
throw new Error("Response exceeded 262144 bytes");
|
|
105
|
+
}
|
|
106
|
+
chunks.push(next.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
reader.releaseLock();
|
|
111
|
+
}
|
|
112
|
+
bytes = new Uint8Array(total);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
bytes.set(chunk, offset);
|
|
116
|
+
offset += chunk.byteLength;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
121
|
+
if (buffer.byteLength > QUOTA_PROVIDER_MAX_BODY_BYTES) {
|
|
122
|
+
throw new Error("Response exceeded 262144 bytes");
|
|
123
|
+
}
|
|
124
|
+
bytes = buffer;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error("Invalid JSON response");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function parseQuotaV1(source, body) {
|
|
134
|
+
if (!isRecord(body) ||
|
|
135
|
+
!hasOnlyKeys(body, ["version", "entries"]) ||
|
|
136
|
+
body.version !== "quota-v1" ||
|
|
137
|
+
!Array.isArray(body.entries)) {
|
|
138
|
+
return { success: false, error: "Invalid quota-v1 response" };
|
|
139
|
+
}
|
|
140
|
+
if (body.entries.length < 1 || body.entries.length > QUOTA_PROVIDER_MAX_REMOTE_ROWS) {
|
|
141
|
+
return { success: false, error: "Invalid quota-v1 response" };
|
|
142
|
+
}
|
|
143
|
+
const entries = [];
|
|
144
|
+
for (const raw of body.entries) {
|
|
145
|
+
if (!isRecord(raw) ||
|
|
146
|
+
!hasOnlyKeys(raw, [
|
|
147
|
+
"kind",
|
|
148
|
+
"name",
|
|
149
|
+
"resultType",
|
|
150
|
+
"percentRemaining",
|
|
151
|
+
"value",
|
|
152
|
+
"label",
|
|
153
|
+
"right",
|
|
154
|
+
"resetTimeIso",
|
|
155
|
+
"observedAtIso",
|
|
156
|
+
])) {
|
|
157
|
+
return { success: false, error: "Invalid quota-v1 response" };
|
|
158
|
+
}
|
|
159
|
+
const name = boundedDisplayText(raw.name, 80);
|
|
160
|
+
const resultType = typeof raw.resultType === "string" && RESULT_TYPES.has(raw.resultType)
|
|
161
|
+
? raw.resultType
|
|
162
|
+
: null;
|
|
163
|
+
const label = raw.label === undefined ? undefined : boundedDisplayText(raw.label, 80);
|
|
164
|
+
const right = raw.right === undefined ? undefined : boundedDisplayText(raw.right, 160);
|
|
165
|
+
const resetTimeIso = raw.resetTimeIso === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: isIsoTimestamp(raw.resetTimeIso)
|
|
168
|
+
? raw.resetTimeIso
|
|
169
|
+
: null;
|
|
170
|
+
const observedAtIso = raw.observedAtIso === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: isIsoTimestamp(raw.observedAtIso)
|
|
173
|
+
? raw.observedAtIso
|
|
174
|
+
: null;
|
|
175
|
+
if (!name ||
|
|
176
|
+
!resultType ||
|
|
177
|
+
(raw.label !== undefined && label === null) ||
|
|
178
|
+
(raw.right !== undefined && right === null) ||
|
|
179
|
+
resetTimeIso === null ||
|
|
180
|
+
observedAtIso === null) {
|
|
181
|
+
return { success: false, error: "Invalid quota-v1 response" };
|
|
182
|
+
}
|
|
183
|
+
const common = {
|
|
184
|
+
accounting: accountingMetadata(resultType, observedAtIso),
|
|
185
|
+
name: `${source.label} ${name}`,
|
|
186
|
+
group: source.label,
|
|
187
|
+
...(label ? { label } : {}),
|
|
188
|
+
...(right ? { right } : {}),
|
|
189
|
+
...(resetTimeIso ? { resetTimeIso } : {}),
|
|
190
|
+
};
|
|
191
|
+
if (raw.kind === "percent" &&
|
|
192
|
+
typeof raw.percentRemaining === "number" &&
|
|
193
|
+
Number.isFinite(raw.percentRemaining) &&
|
|
194
|
+
raw.percentRemaining <= 100 &&
|
|
195
|
+
["quota", "rate_limit", "budget"].includes(resultType) &&
|
|
196
|
+
raw.value === undefined) {
|
|
197
|
+
entries.push({ ...common, kind: "percent", percentRemaining: raw.percentRemaining });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const value = boundedDisplayText(raw.value, 160);
|
|
201
|
+
if (raw.kind === "value" && value !== null && raw.percentRemaining === undefined) {
|
|
202
|
+
entries.push({ ...common, kind: "value", value });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
return { success: false, error: "Invalid quota-v1 response" };
|
|
206
|
+
}
|
|
207
|
+
return { success: true, entries };
|
|
208
|
+
}
|
|
209
|
+
function isJsonV1ResponseDepthValid(value) {
|
|
210
|
+
if (!value || typeof value !== "object")
|
|
211
|
+
return true;
|
|
212
|
+
const stack = [{ value, depth: 1 }];
|
|
213
|
+
while (stack.length > 0) {
|
|
214
|
+
const current = stack.pop();
|
|
215
|
+
if (current.depth > JSON_V1_MAX_RESPONSE_DEPTH)
|
|
216
|
+
return false;
|
|
217
|
+
const children = Array.isArray(current.value) ? current.value : Object.values(current.value);
|
|
218
|
+
for (const child of children) {
|
|
219
|
+
if (child && typeof child === "object") {
|
|
220
|
+
stack.push({ value: child, depth: current.depth + 1 });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function resolveJsonV1Path(root, path) {
|
|
227
|
+
let current = root;
|
|
228
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
229
|
+
if (!isRecord(current))
|
|
230
|
+
return { state: "wrong-type" };
|
|
231
|
+
const segment = path[index];
|
|
232
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment))
|
|
233
|
+
return { state: "missing" };
|
|
234
|
+
current = current[segment];
|
|
235
|
+
if (current === null)
|
|
236
|
+
return { state: "null" };
|
|
237
|
+
if (index < path.length - 1 && !isRecord(current))
|
|
238
|
+
return { state: "wrong-type" };
|
|
239
|
+
}
|
|
240
|
+
return { state: "value", value: current };
|
|
241
|
+
}
|
|
242
|
+
function jsonV1SourceValue(row, source) {
|
|
243
|
+
if ("literal" in source) {
|
|
244
|
+
return source.literal === null ? { state: "null" } : { state: "value", value: source.literal };
|
|
245
|
+
}
|
|
246
|
+
return resolveJsonV1Path(row, source.path);
|
|
247
|
+
}
|
|
248
|
+
function jsonV1ResolutionIssue(resolution) {
|
|
249
|
+
switch (resolution.state) {
|
|
250
|
+
case "missing":
|
|
251
|
+
return "was missing";
|
|
252
|
+
case "null":
|
|
253
|
+
return "was null";
|
|
254
|
+
case "wrong-type":
|
|
255
|
+
return "had wrong type";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function resolveJsonV1Number(row, source) {
|
|
259
|
+
const resolution = jsonV1SourceValue(row, source);
|
|
260
|
+
if (resolution.state !== "value") {
|
|
261
|
+
return { ok: false, issue: jsonV1ResolutionIssue(resolution) };
|
|
262
|
+
}
|
|
263
|
+
if (typeof resolution.value !== "number" || !Number.isFinite(resolution.value)) {
|
|
264
|
+
return { ok: false, issue: "had wrong type" };
|
|
265
|
+
}
|
|
266
|
+
if (Math.abs(resolution.value) > JSON_V1_MAX_NUMBER_MAGNITUDE) {
|
|
267
|
+
return { ok: false, issue: "exceeded the numeric magnitude limit" };
|
|
268
|
+
}
|
|
269
|
+
const value = "path" in source && source.divideBy !== undefined
|
|
270
|
+
? resolution.value / source.divideBy
|
|
271
|
+
: resolution.value;
|
|
272
|
+
return Number.isFinite(value)
|
|
273
|
+
? { ok: true, value }
|
|
274
|
+
: { ok: false, issue: "did not resolve to a finite number" };
|
|
275
|
+
}
|
|
276
|
+
function resolveJsonV1Text(row, source) {
|
|
277
|
+
const resolution = jsonV1SourceValue(row, source);
|
|
278
|
+
if (resolution.state !== "value") {
|
|
279
|
+
return { ok: false, issue: jsonV1ResolutionIssue(resolution) };
|
|
280
|
+
}
|
|
281
|
+
if (typeof resolution.value !== "string") {
|
|
282
|
+
return { ok: false, issue: "had wrong type" };
|
|
283
|
+
}
|
|
284
|
+
const normalized = boundedDisplayText(resolution.value, JSON_V1_MAX_DISPLAY_CODE_POINTS);
|
|
285
|
+
return normalized
|
|
286
|
+
? { ok: true, value: normalized }
|
|
287
|
+
: { ok: false, issue: "did not resolve to bounded display text" };
|
|
288
|
+
}
|
|
289
|
+
function resolveJsonV1TimestampSource(row, source) {
|
|
290
|
+
const resolution = jsonV1SourceValue(row, source);
|
|
291
|
+
if (resolution.state !== "value") {
|
|
292
|
+
return { ok: false, issue: jsonV1ResolutionIssue(resolution) };
|
|
293
|
+
}
|
|
294
|
+
const normalized = normalizeJsonV1Timestamp(resolution.value, source.encoding);
|
|
295
|
+
return normalized
|
|
296
|
+
? { ok: true, value: normalized }
|
|
297
|
+
: { ok: false, issue: "did not resolve to a valid timestamp" };
|
|
298
|
+
}
|
|
299
|
+
function jsonV1CandidateError(mappingIndex, field, issue, rowIndex) {
|
|
300
|
+
return `adapter.mappings[${mappingIndex}].${field} ${issue} at row ${rowIndex}`;
|
|
301
|
+
}
|
|
302
|
+
function formatJsonV1Value(value, mapping) {
|
|
303
|
+
const text = String(value);
|
|
304
|
+
if (!mapping.unit)
|
|
305
|
+
return text;
|
|
306
|
+
return mapping.unitPosition === "prefix" ? `${mapping.unit}${text}` : `${text} ${mapping.unit}`;
|
|
307
|
+
}
|
|
308
|
+
function formatJsonV1Pair(left, right, mapping) {
|
|
309
|
+
const pair = `${String(left)}/${String(right)}`;
|
|
310
|
+
if (!mapping.unit)
|
|
311
|
+
return pair;
|
|
312
|
+
return mapping.unitPosition === "prefix"
|
|
313
|
+
? `${mapping.unit}${String(left)}/${mapping.unit}${String(right)}`
|
|
314
|
+
: `${pair} ${mapping.unit}`;
|
|
315
|
+
}
|
|
316
|
+
function jsonV1DerivedPercentageIssue(value) {
|
|
317
|
+
if (!Number.isFinite(value))
|
|
318
|
+
return "produced a non-finite percentage";
|
|
319
|
+
if (Math.abs(value) > JSON_V1_MAX_NUMBER_MAGNITUDE) {
|
|
320
|
+
return "produced a percentage beyond the numeric magnitude limit";
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function mapJsonV1Candidate(source, row, rowIndex, mapping, mappingIndex) {
|
|
325
|
+
const fail = (field, issue) => ({
|
|
326
|
+
error: jsonV1CandidateError(mappingIndex, field, issue, rowIndex),
|
|
327
|
+
});
|
|
328
|
+
let resetTimeIso;
|
|
329
|
+
if (mapping.resetTime) {
|
|
330
|
+
const resolved = resolveJsonV1TimestampSource(row, mapping.resetTime);
|
|
331
|
+
if (!resolved.ok)
|
|
332
|
+
return fail("resetTime", resolved.issue);
|
|
333
|
+
resetTimeIso = resolved.value;
|
|
334
|
+
}
|
|
335
|
+
let observedAtIso;
|
|
336
|
+
if (mapping.observedTime) {
|
|
337
|
+
const resolved = resolveJsonV1TimestampSource(row, mapping.observedTime);
|
|
338
|
+
if (!resolved.ok)
|
|
339
|
+
return fail("observedTime", resolved.issue);
|
|
340
|
+
observedAtIso = resolved.value;
|
|
341
|
+
}
|
|
342
|
+
const common = {
|
|
343
|
+
accounting: accountingMetadata(mapping.resultType, observedAtIso),
|
|
344
|
+
name: `${source.label} ${mapping.name}`,
|
|
345
|
+
group: source.label,
|
|
346
|
+
...(mapping.label ? { label: mapping.label } : {}),
|
|
347
|
+
...(resetTimeIso ? { resetTimeIso } : {}),
|
|
348
|
+
};
|
|
349
|
+
switch (mapping.metric.type) {
|
|
350
|
+
case "percentage": {
|
|
351
|
+
const percentage = resolveJsonV1Number(row, mapping.metric.percentage);
|
|
352
|
+
if (!percentage.ok)
|
|
353
|
+
return fail("metric.percentage", percentage.issue);
|
|
354
|
+
if (mapping.metric.meaning === "remaining" && percentage.value > 100) {
|
|
355
|
+
return fail("metric.percentage", "must not exceed 100 for remaining meaning");
|
|
356
|
+
}
|
|
357
|
+
if (mapping.metric.meaning === "used" && percentage.value < 0) {
|
|
358
|
+
return fail("metric.percentage", "must be non-negative for used meaning");
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
entry: {
|
|
362
|
+
...common,
|
|
363
|
+
kind: "percent",
|
|
364
|
+
percentRemaining: mapping.metric.meaning === "remaining" ? percentage.value : 100 - percentage.value,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
case "used-limit": {
|
|
369
|
+
const used = resolveJsonV1Number(row, mapping.metric.used);
|
|
370
|
+
if (!used.ok)
|
|
371
|
+
return fail("metric.used", used.issue);
|
|
372
|
+
const limit = resolveJsonV1Number(row, mapping.metric.limit);
|
|
373
|
+
if (!limit.ok)
|
|
374
|
+
return fail("metric.limit", limit.issue);
|
|
375
|
+
if (used.value < 0)
|
|
376
|
+
return fail("metric.used", "must be non-negative");
|
|
377
|
+
if (limit.value <= 0)
|
|
378
|
+
return fail("metric.limit", "must be greater than zero");
|
|
379
|
+
const percentRemaining = ((limit.value - used.value) / limit.value) * 100;
|
|
380
|
+
const percentageIssue = jsonV1DerivedPercentageIssue(percentRemaining);
|
|
381
|
+
if (percentageIssue)
|
|
382
|
+
return fail("metric.limit", percentageIssue);
|
|
383
|
+
return {
|
|
384
|
+
entry: {
|
|
385
|
+
...common,
|
|
386
|
+
kind: "percent",
|
|
387
|
+
percentRemaining,
|
|
388
|
+
right: formatJsonV1Pair(used.value, limit.value, mapping),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
case "remaining-limit": {
|
|
393
|
+
const remaining = resolveJsonV1Number(row, mapping.metric.remaining);
|
|
394
|
+
if (!remaining.ok)
|
|
395
|
+
return fail("metric.remaining", remaining.issue);
|
|
396
|
+
const limit = resolveJsonV1Number(row, mapping.metric.limit);
|
|
397
|
+
if (!limit.ok)
|
|
398
|
+
return fail("metric.limit", limit.issue);
|
|
399
|
+
if (limit.value <= 0)
|
|
400
|
+
return fail("metric.limit", "must be greater than zero");
|
|
401
|
+
if (remaining.value > limit.value) {
|
|
402
|
+
return fail("metric.remaining", "must not exceed limit");
|
|
403
|
+
}
|
|
404
|
+
const percentRemaining = (remaining.value / limit.value) * 100;
|
|
405
|
+
const percentageIssue = jsonV1DerivedPercentageIssue(percentRemaining);
|
|
406
|
+
if (percentageIssue)
|
|
407
|
+
return fail("metric.limit", percentageIssue);
|
|
408
|
+
return {
|
|
409
|
+
entry: {
|
|
410
|
+
...common,
|
|
411
|
+
kind: "percent",
|
|
412
|
+
percentRemaining,
|
|
413
|
+
right: formatJsonV1Pair(remaining.value, limit.value, mapping),
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
case "spend-budget": {
|
|
418
|
+
const spend = resolveJsonV1Number(row, mapping.metric.spend);
|
|
419
|
+
if (!spend.ok)
|
|
420
|
+
return fail("metric.spend", spend.issue);
|
|
421
|
+
const budget = resolveJsonV1Number(row, mapping.metric.budget);
|
|
422
|
+
if (!budget.ok)
|
|
423
|
+
return fail("metric.budget", budget.issue);
|
|
424
|
+
if (spend.value < 0)
|
|
425
|
+
return fail("metric.spend", "must be non-negative");
|
|
426
|
+
if (budget.value <= 0)
|
|
427
|
+
return fail("metric.budget", "must be greater than zero");
|
|
428
|
+
const percentRemaining = ((budget.value - spend.value) / budget.value) * 100;
|
|
429
|
+
const percentageIssue = jsonV1DerivedPercentageIssue(percentRemaining);
|
|
430
|
+
if (percentageIssue)
|
|
431
|
+
return fail("metric.budget", percentageIssue);
|
|
432
|
+
return {
|
|
433
|
+
entry: {
|
|
434
|
+
...common,
|
|
435
|
+
kind: "percent",
|
|
436
|
+
percentRemaining,
|
|
437
|
+
right: formatJsonV1Pair(spend.value, budget.value, mapping),
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
case "remaining-budget": {
|
|
442
|
+
const remaining = resolveJsonV1Number(row, mapping.metric.remaining);
|
|
443
|
+
if (!remaining.ok)
|
|
444
|
+
return fail("metric.remaining", remaining.issue);
|
|
445
|
+
const budget = resolveJsonV1Number(row, mapping.metric.budget);
|
|
446
|
+
if (!budget.ok)
|
|
447
|
+
return fail("metric.budget", budget.issue);
|
|
448
|
+
if (budget.value <= 0)
|
|
449
|
+
return fail("metric.budget", "must be greater than zero");
|
|
450
|
+
if (remaining.value > budget.value) {
|
|
451
|
+
return fail("metric.remaining", "must not exceed budget");
|
|
452
|
+
}
|
|
453
|
+
const percentRemaining = (remaining.value / budget.value) * 100;
|
|
454
|
+
const percentageIssue = jsonV1DerivedPercentageIssue(percentRemaining);
|
|
455
|
+
if (percentageIssue)
|
|
456
|
+
return fail("metric.budget", percentageIssue);
|
|
457
|
+
return {
|
|
458
|
+
entry: {
|
|
459
|
+
...common,
|
|
460
|
+
kind: "percent",
|
|
461
|
+
percentRemaining,
|
|
462
|
+
right: formatJsonV1Pair(remaining.value, budget.value, mapping),
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
case "value": {
|
|
467
|
+
const value = resolveJsonV1Number(row, mapping.metric.value);
|
|
468
|
+
if (!value.ok)
|
|
469
|
+
return fail("metric.value", value.issue);
|
|
470
|
+
if (!["remaining", "balance"].includes(mapping.metric.valueType) && value.value < 0) {
|
|
471
|
+
return fail("metric.value", "must be non-negative");
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
entry: {
|
|
475
|
+
...common,
|
|
476
|
+
kind: "value",
|
|
477
|
+
value: formatJsonV1Value(value.value, mapping),
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
case "status": {
|
|
482
|
+
const value = resolveJsonV1Text(row, mapping.metric.value);
|
|
483
|
+
if (!value.ok)
|
|
484
|
+
return fail("metric.value", value.issue);
|
|
485
|
+
return { entry: { ...common, kind: "value", value: value.value } };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function parseJsonV1(source, body) {
|
|
490
|
+
const invalid = (message) => ({
|
|
491
|
+
success: false,
|
|
492
|
+
error: `Invalid json-v1 response: ${message}`,
|
|
493
|
+
});
|
|
494
|
+
if (!isJsonV1ResponseDepthValid(body)) {
|
|
495
|
+
return invalid(`nesting depth exceeded ${JSON_V1_MAX_RESPONSE_DEPTH}`);
|
|
496
|
+
}
|
|
497
|
+
let rows;
|
|
498
|
+
if (source.adapter.rowsPath) {
|
|
499
|
+
if (!isRecord(body))
|
|
500
|
+
return invalid("adapter.rowsPath requires an object root");
|
|
501
|
+
const resolved = resolveJsonV1Path(body, source.adapter.rowsPath);
|
|
502
|
+
if (resolved.state !== "value") {
|
|
503
|
+
return invalid(`adapter.rowsPath ${jsonV1ResolutionIssue(resolved)}`);
|
|
504
|
+
}
|
|
505
|
+
if (!Array.isArray(resolved.value)) {
|
|
506
|
+
return invalid("adapter.rowsPath did not resolve to an array");
|
|
507
|
+
}
|
|
508
|
+
rows = resolved.value;
|
|
509
|
+
}
|
|
510
|
+
else if (Array.isArray(body)) {
|
|
511
|
+
rows = body;
|
|
512
|
+
}
|
|
513
|
+
else if (isRecord(body)) {
|
|
514
|
+
rows = [body];
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
return invalid("expected an object or array root");
|
|
518
|
+
}
|
|
519
|
+
if (rows.length < 1 || rows.length > QUOTA_PROVIDER_MAX_REMOTE_ROWS) {
|
|
520
|
+
return invalid(`selected rows must contain 1-${QUOTA_PROVIDER_MAX_REMOTE_ROWS} elements`);
|
|
521
|
+
}
|
|
522
|
+
if (rows.length * source.adapter.mappings.length > JSON_V1_MAX_CANDIDATES) {
|
|
523
|
+
return invalid(`candidate count exceeded ${JSON_V1_MAX_CANDIDATES}`);
|
|
524
|
+
}
|
|
525
|
+
const entries = [];
|
|
526
|
+
const rowErrors = [];
|
|
527
|
+
let errorCount = 0;
|
|
528
|
+
const recordError = (error) => {
|
|
529
|
+
errorCount += 1;
|
|
530
|
+
if (rowErrors.length < JSON_V1_MAX_DETAILED_ERRORS)
|
|
531
|
+
rowErrors.push(error);
|
|
532
|
+
};
|
|
533
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
534
|
+
const row = rows[rowIndex];
|
|
535
|
+
for (let mappingIndex = 0; mappingIndex < source.adapter.mappings.length; mappingIndex += 1) {
|
|
536
|
+
if (!isRecord(row)) {
|
|
537
|
+
recordError(jsonV1CandidateError(mappingIndex, "metric", "requires an object row", rowIndex));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const mapped = mapJsonV1Candidate(source, row, rowIndex, source.adapter.mappings[mappingIndex], mappingIndex);
|
|
541
|
+
if ("error" in mapped) {
|
|
542
|
+
recordError(mapped.error);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
entries.push(mapped.entry);
|
|
546
|
+
if (entries.length > JSON_V1_MAX_ENTRIES) {
|
|
547
|
+
return invalid(`more than ${JSON_V1_MAX_ENTRIES} entries were produced`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (entries.length === 0) {
|
|
552
|
+
return invalid(rowErrors[0] ?? "no mappings produced entries");
|
|
553
|
+
}
|
|
554
|
+
if (errorCount > rowErrors.length) {
|
|
555
|
+
rowErrors.push("Additional json-v1 mapping errors omitted");
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
entries,
|
|
560
|
+
...(rowErrors.length > 0 ? { rowErrors } : {}),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function parseOpenRouterKeyV1(source, body) {
|
|
564
|
+
if (!isRecord(body) || !isRecord(body.data)) {
|
|
565
|
+
return { success: false, error: "Invalid openrouter-key-v1 response" };
|
|
566
|
+
}
|
|
567
|
+
const data = body.data;
|
|
568
|
+
const usage = data.usage;
|
|
569
|
+
const limit = data.limit;
|
|
570
|
+
const remaining = data.limit_remaining;
|
|
571
|
+
if (typeof usage !== "number" ||
|
|
572
|
+
!Number.isFinite(usage) ||
|
|
573
|
+
usage < 0 ||
|
|
574
|
+
(limit !== null && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0)) ||
|
|
575
|
+
(remaining !== undefined && (typeof remaining !== "number" || !Number.isFinite(remaining)))) {
|
|
576
|
+
return { success: false, error: "Invalid openrouter-key-v1 response" };
|
|
577
|
+
}
|
|
578
|
+
if (typeof limit === "number" && limit > 0) {
|
|
579
|
+
const remainingValue = typeof remaining === "number" ? remaining : limit - usage;
|
|
580
|
+
if (remainingValue > limit) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: "Invalid openrouter-key-v1 response",
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const percentRemaining = (remainingValue / limit) * 100;
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
entries: [
|
|
590
|
+
{
|
|
591
|
+
accounting: accountingMetadata("budget"),
|
|
592
|
+
kind: "percent",
|
|
593
|
+
name: `${source.label} budget`,
|
|
594
|
+
group: source.label,
|
|
595
|
+
label: "Budget:",
|
|
596
|
+
right: `${formatUsd(usage)}/${formatUsd(limit)}`,
|
|
597
|
+
percentRemaining,
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
success: true,
|
|
604
|
+
entries: [
|
|
605
|
+
{
|
|
606
|
+
accounting: accountingMetadata("spend"),
|
|
607
|
+
kind: "value",
|
|
608
|
+
name: `${source.label} spend`,
|
|
609
|
+
group: source.label,
|
|
610
|
+
label: "Spend:",
|
|
611
|
+
value: formatUsd(usage),
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
export async function fetchRemoteQuotaProvider(source, apiKey, requestTimeoutMs) {
|
|
617
|
+
const timeoutMs = requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
618
|
+
const controller = new AbortController();
|
|
619
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
620
|
+
try {
|
|
621
|
+
const response = await fetch(source.url, {
|
|
622
|
+
method: "GET",
|
|
623
|
+
redirect: "manual",
|
|
624
|
+
signal: controller.signal,
|
|
625
|
+
headers: {
|
|
626
|
+
Authorization: `Bearer ${apiKey}`,
|
|
627
|
+
Accept: "application/json",
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
if (response.redirected || (response.status >= 300 && response.status < 400)) {
|
|
631
|
+
return { success: false, error: "Redirect rejected" };
|
|
632
|
+
}
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
635
|
+
}
|
|
636
|
+
const body = await readBoundedJson(response);
|
|
637
|
+
switch (source.format) {
|
|
638
|
+
case "quota-v1":
|
|
639
|
+
return parseQuotaV1(source, body);
|
|
640
|
+
case "openrouter-key-v1":
|
|
641
|
+
return parseOpenRouterKeyV1(source, body);
|
|
642
|
+
case "json-v1":
|
|
643
|
+
return parseJsonV1(source, body);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
if (controller.signal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
error: `Request timeout after ${Math.round(timeoutMs / 1000)}s`,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (error instanceof Error &&
|
|
654
|
+
[
|
|
655
|
+
"Response exceeded 262144 bytes",
|
|
656
|
+
"Expected a JSON response",
|
|
657
|
+
"Invalid JSON response",
|
|
658
|
+
].includes(error.message)) {
|
|
659
|
+
return { success: false, error: error.message };
|
|
660
|
+
}
|
|
661
|
+
return { success: false, error: "Failed to read accounting data" };
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
clearTimeout(timeoutId);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
export async function mapWithConcurrency(values, concurrency, map) {
|
|
668
|
+
const results = new Array(values.length);
|
|
669
|
+
let nextIndex = 0;
|
|
670
|
+
async function worker() {
|
|
671
|
+
while (true) {
|
|
672
|
+
const index = nextIndex;
|
|
673
|
+
nextIndex += 1;
|
|
674
|
+
if (index >= values.length)
|
|
675
|
+
return;
|
|
676
|
+
results[index] = await map(values[index], index);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, concurrency), values.length) }, () => worker()));
|
|
680
|
+
return results;
|
|
681
|
+
}
|
|
682
|
+
//# sourceMappingURL=quota-providers-remote.js.map
|