@slkiser/opencode-quota 4.0.0 → 4.1.0
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 +37 -25
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +8 -0
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/anthropic.d.ts.map +1 -1
- package/dist/lib/anthropic.js +78 -72
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/api-key-resolver.d.ts.map +1 -1
- package/dist/lib/api-key-resolver.js +14 -24
- package/dist/lib/api-key-resolver.js.map +1 -1
- package/dist/lib/atomic-json.d.ts +3 -1
- package/dist/lib/atomic-json.d.ts.map +1 -1
- package/dist/lib/atomic-json.js +23 -9
- package/dist/lib/atomic-json.js.map +1 -1
- package/dist/lib/chutes.d.ts.map +1 -1
- package/dist/lib/chutes.js +28 -23
- package/dist/lib/chutes.js.map +1 -1
- package/dist/lib/cli-show.d.ts +9 -0
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +34 -5
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/cli-status.d.ts +8 -0
- package/dist/lib/cli-status.d.ts.map +1 -0
- package/dist/lib/cli-status.js +125 -0
- package/dist/lib/cli-status.js.map +1 -0
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +47 -12
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +11 -5
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +300 -52
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/deepseek.d.ts.map +1 -1
- package/dist/lib/deepseek.js +22 -17
- package/dist/lib/deepseek.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +8 -0
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +14 -1
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/format-utils.d.ts +7 -0
- package/dist/lib/format-utils.d.ts.map +1 -1
- package/dist/lib/format-utils.js +18 -0
- package/dist/lib/format-utils.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +22 -7
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/google-agy.d.ts.map +1 -1
- package/dist/lib/google-agy.js +203 -92
- package/dist/lib/google-agy.js.map +1 -1
- package/dist/lib/google-gemini-cli.d.ts.map +1 -1
- package/dist/lib/google-gemini-cli.js +53 -41
- package/dist/lib/google-gemini-cli.js.map +1 -1
- package/dist/lib/google-token-cache.d.ts.map +1 -1
- package/dist/lib/google-token-cache.js +87 -34
- package/dist/lib/google-token-cache.js.map +1 -1
- package/dist/lib/google.d.ts.map +1 -1
- package/dist/lib/google.js +57 -39
- package/dist/lib/google.js.map +1 -1
- package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
- package/dist/lib/grouped-entry-normalization.js +7 -1
- package/dist/lib/grouped-entry-normalization.js.map +1 -1
- package/dist/lib/http.d.ts +12 -7
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +28 -15
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/init-installer.d.ts +2 -1
- package/dist/lib/init-installer.d.ts.map +1 -1
- package/dist/lib/init-installer.js +29 -2
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/kimi.d.ts.map +1 -1
- package/dist/lib/kimi.js +20 -15
- package/dist/lib/kimi.js.map +1 -1
- package/dist/lib/maintainer-announcements.d.ts.map +1 -1
- package/dist/lib/maintainer-announcements.js +10 -13
- package/dist/lib/maintainer-announcements.js.map +1 -1
- package/dist/lib/mimo-config.d.ts +29 -0
- package/dist/lib/mimo-config.d.ts.map +1 -0
- package/dist/lib/mimo-config.js +147 -0
- package/dist/lib/mimo-config.js.map +1 -0
- package/dist/lib/mimo.d.ts +35 -0
- package/dist/lib/mimo.d.ts.map +1 -0
- package/dist/lib/mimo.js +236 -0
- package/dist/lib/mimo.js.map +1 -0
- package/dist/lib/modelsdev-pricing.d.ts.map +1 -1
- package/dist/lib/modelsdev-pricing.js +39 -27
- package/dist/lib/modelsdev-pricing.js.map +1 -1
- package/dist/lib/nanogpt.d.ts.map +1 -1
- package/dist/lib/nanogpt.js +40 -30
- package/dist/lib/nanogpt.js.map +1 -1
- package/dist/lib/ollama-cloud.d.ts +1 -1
- package/dist/lib/ollama-cloud.d.ts.map +1 -1
- package/dist/lib/ollama-cloud.js +73 -68
- package/dist/lib/ollama-cloud.js.map +1 -1
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +65 -45
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-config-editor.d.ts +6 -0
- package/dist/lib/opencode-config-editor.d.ts.map +1 -1
- package/dist/lib/opencode-config-editor.js +47 -1
- package/dist/lib/opencode-config-editor.js.map +1 -1
- package/dist/lib/opencode-config-providers.d.ts.map +1 -1
- package/dist/lib/opencode-config-providers.js +13 -16
- package/dist/lib/opencode-config-providers.js.map +1 -1
- package/dist/lib/opencode-config-read.d.ts +23 -0
- package/dist/lib/opencode-config-read.d.ts.map +1 -0
- package/dist/lib/opencode-config-read.js +30 -0
- package/dist/lib/opencode-config-read.js.map +1 -0
- package/dist/lib/opencode-go.d.ts.map +1 -1
- package/dist/lib/opencode-go.js +44 -39
- package/dist/lib/opencode-go.js.map +1 -1
- package/dist/lib/opencode-zen-config.d.ts +34 -0
- package/dist/lib/opencode-zen-config.d.ts.map +1 -0
- package/dist/lib/opencode-zen-config.js +103 -0
- package/dist/lib/opencode-zen-config.js.map +1 -0
- package/dist/lib/opencode-zen.d.ts +27 -0
- package/dist/lib/opencode-zen.d.ts.map +1 -0
- package/dist/lib/opencode-zen.js +143 -0
- package/dist/lib/opencode-zen.js.map +1 -0
- package/dist/lib/provider-add-command.d.ts.map +1 -1
- package/dist/lib/provider-add-command.js +21 -2
- package/dist/lib/provider-add-command.js.map +1 -1
- package/dist/lib/provider-add-json-v1-questionnaire.d.ts +32 -0
- package/dist/lib/provider-add-json-v1-questionnaire.d.ts.map +1 -0
- package/dist/lib/provider-add-json-v1-questionnaire.js +336 -0
- package/dist/lib/provider-add-json-v1-questionnaire.js.map +1 -0
- package/dist/lib/provider-add.d.ts.map +1 -1
- package/dist/lib/provider-add.js +11 -2
- package/dist/lib/provider-add.js.map +1 -1
- package/dist/lib/provider-availability.d.ts +2 -2
- package/dist/lib/provider-availability.d.ts.map +1 -1
- package/dist/lib/provider-availability.js +2 -3
- package/dist/lib/provider-availability.js.map +1 -1
- package/dist/lib/provider-metadata.d.ts +289 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +321 -227
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +12 -16
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts +59 -2
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +71 -30
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-entry-display.d.ts +2 -0
- package/dist/lib/quota-entry-display.d.ts.map +1 -1
- package/dist/lib/quota-entry-display.js +27 -13
- package/dist/lib/quota-entry-display.js.map +1 -1
- package/dist/lib/quota-providers-remote.d.ts +6 -1
- package/dist/lib/quota-providers-remote.d.ts.map +1 -1
- package/dist/lib/quota-providers-remote.js +376 -12
- package/dist/lib/quota-providers-remote.js.map +1 -1
- package/dist/lib/quota-providers.d.ts +84 -3
- package/dist/lib/quota-providers.d.ts.map +1 -1
- package/dist/lib/quota-providers.js +561 -24
- package/dist/lib/quota-providers.js.map +1 -1
- package/dist/lib/quota-render-data.d.ts +4 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +101 -121
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts +9 -1
- package/dist/lib/quota-runtime-context.d.ts.map +1 -1
- package/dist/lib/quota-runtime-context.js +5 -0
- package/dist/lib/quota-runtime-context.js.map +1 -1
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +26 -2
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-stats.d.ts +1 -0
- package/dist/lib/quota-stats.d.ts.map +1 -1
- package/dist/lib/quota-stats.js +14 -4
- package/dist/lib/quota-stats.js.map +1 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +96 -1
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/runtime-provider-ids.d.ts +4 -0
- package/dist/lib/runtime-provider-ids.d.ts.map +1 -0
- package/dist/lib/runtime-provider-ids.js +10 -0
- package/dist/lib/runtime-provider-ids.js.map +1 -0
- package/dist/lib/session-tokens.d.ts +2 -0
- package/dist/lib/session-tokens.d.ts.map +1 -1
- package/dist/lib/session-tokens.js +4 -2
- package/dist/lib/session-tokens.js.map +1 -1
- package/dist/lib/synthetic.d.ts.map +1 -1
- package/dist/lib/synthetic.js +37 -32
- package/dist/lib/synthetic.js.map +1 -1
- package/dist/lib/toast-format-grouped.d.ts +1 -0
- package/dist/lib/toast-format-grouped.d.ts.map +1 -1
- package/dist/lib/toast-format-grouped.js +33 -29
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/lib/tui-refresh-lifecycle.d.ts +17 -0
- package/dist/lib/tui-refresh-lifecycle.d.ts.map +1 -0
- package/dist/lib/tui-refresh-lifecycle.js +83 -0
- package/dist/lib/tui-refresh-lifecycle.js.map +1 -0
- package/dist/lib/tui-runtime.d.ts +0 -5
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +1 -6
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/tui-sidebar-format.d.ts +1 -1
- package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
- package/dist/lib/tui-sidebar-format.js +1 -0
- package/dist/lib/tui-sidebar-format.js.map +1 -1
- package/dist/lib/types.d.ts +61 -7
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/xai.d.ts +39 -0
- package/dist/lib/xai.d.ts.map +1 -0
- package/dist/lib/xai.js +153 -0
- package/dist/lib/xai.js.map +1 -0
- package/dist/lib/zai.d.ts.map +1 -1
- package/dist/lib/zai.js +62 -56
- package/dist/lib/zai.js.map +1 -1
- package/dist/lib/zhipu.d.ts.map +1 -1
- package/dist/lib/zhipu.js +55 -50
- package/dist/lib/zhipu.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +36 -157
- package/dist/plugin.js.map +1 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +24 -3
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/glm-coding-plan-provider.d.ts +35 -0
- package/dist/providers/glm-coding-plan-provider.d.ts.map +1 -0
- package/dist/providers/glm-coding-plan-provider.js +40 -0
- package/dist/providers/glm-coding-plan-provider.js.map +1 -0
- package/dist/providers/google-agy.d.ts.map +1 -1
- package/dist/providers/google-agy.js +60 -36
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/mimo.d.ts +3 -0
- package/dist/providers/mimo.d.ts.map +1 -0
- package/dist/providers/mimo.js +120 -0
- package/dist/providers/mimo.js.map +1 -0
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +60 -37
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/opencode-zen.d.ts +3 -0
- package/dist/providers/opencode-zen.d.ts.map +1 -0
- package/dist/providers/opencode-zen.js +94 -0
- package/dist/providers/opencode-zen.js.map +1 -0
- package/dist/providers/quota-providers.d.ts.map +1 -1
- package/dist/providers/quota-providers.js +5 -3
- package/dist/providers/quota-providers.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +6 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/xai.d.ts +6 -0
- package/dist/providers/xai.d.ts.map +1 -0
- package/dist/providers/xai.js +51 -0
- package/dist/providers/xai.js.map +1 -0
- package/dist/providers/zai.d.ts +1 -7
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +9 -45
- package/dist/providers/zai.js.map +1 -1
- package/dist/providers/zhipu.d.ts +1 -7
- package/dist/providers/zhipu.d.ts.map +1 -1
- package/dist/providers/zhipu.js +9 -45
- package/dist/providers/zhipu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +47 -136
- package/dist/tui.tsx +62 -179
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { getQuotaProviderShape } from "./provider-metadata.js";
|
|
3
3
|
import { lookupCost } from "./modelsdev-pricing.js";
|
|
4
4
|
import { resolvePricingKey } from "./quota-stats.js";
|
|
5
|
-
export const QUOTA_PROVIDER_REMOTE_FORMATS = ["
|
|
5
|
+
export const QUOTA_PROVIDER_REMOTE_FORMATS = ["quota-v1", "openrouter-key-v1", "json-v1"];
|
|
6
6
|
export const QUOTA_PROVIDER_MODES = ["remote-api", "local-estimate"];
|
|
7
7
|
export const QUOTA_PROVIDER_WINDOW_TYPES = ["utc-day", "rolling"];
|
|
8
8
|
export const QUOTA_PROVIDERS_AGGREGATE_ID = "quota-providers";
|
|
@@ -46,7 +46,7 @@ export function selectEligibleQuotaProviderDefinitions(params) {
|
|
|
46
46
|
(definition.modelIds === undefined || definition.modelIds.includes(identity.modelId)));
|
|
47
47
|
}
|
|
48
48
|
const BASE_FIELDS = ["id", "providerId", "label", "mode", "modelIds"];
|
|
49
|
-
const REMOTE_FIELDS = [...BASE_FIELDS, "url", "format", "apiKeyEnv"];
|
|
49
|
+
const REMOTE_FIELDS = [...BASE_FIELDS, "url", "format", "apiKeyEnv", "adapter"];
|
|
50
50
|
const LOCAL_FIELDS = [...BASE_FIELDS, "windows", "pricingModelMap"];
|
|
51
51
|
const WINDOW_FIELDS = [
|
|
52
52
|
"id",
|
|
@@ -60,6 +60,7 @@ const ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
60
60
|
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
61
61
|
const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
62
62
|
const ASCII_OR_UNICODE_CONTROL_PATTERN = /\p{Cc}/u;
|
|
63
|
+
const JSON_V1_STATIC_DISPLAY_FORBIDDEN_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
63
64
|
const URL_WHITESPACE_OR_CONTROL_PATTERN = /[\s\p{Cc}]/u;
|
|
64
65
|
const MODEL_FORBIDDEN_PATTERN = /[\s\p{Cc}#?]/u;
|
|
65
66
|
const MAX_ID_LENGTH = 64;
|
|
@@ -70,6 +71,35 @@ const MAX_WINDOWS = 16;
|
|
|
70
71
|
const MAX_ROLLING_MINUTES = 366 * 24 * 60;
|
|
71
72
|
const MAX_SAFE_REQUEST_LIMIT = 1_000_000_000;
|
|
72
73
|
const MAX_USD_BUDGET = 1_000_000_000;
|
|
74
|
+
export const JSON_V1_MAX_MAPPINGS = 16;
|
|
75
|
+
export const JSON_V1_MAX_ADAPTER_DEPTH = 8;
|
|
76
|
+
export const JSON_V1_MAX_ADAPTER_OBJECTS = 128;
|
|
77
|
+
export const JSON_V1_MAX_ADAPTER_PROPERTIES = 384;
|
|
78
|
+
export const JSON_V1_MAX_ADAPTER_ARRAY_ELEMENTS = 640;
|
|
79
|
+
export const JSON_V1_MAX_PATH_SEGMENTS = 8;
|
|
80
|
+
export const JSON_V1_MAX_PATH_SEGMENT_CODE_POINTS = 64;
|
|
81
|
+
export const JSON_V1_MAX_STATIC_NAME_CODE_POINTS = 80;
|
|
82
|
+
export const JSON_V1_MAX_STATIC_UNIT_CODE_POINTS = 32;
|
|
83
|
+
export const JSON_V1_MAX_DISPLAY_CODE_POINTS = 160;
|
|
84
|
+
export const JSON_V1_MAX_NUMBER_MAGNITUDE = 1e15;
|
|
85
|
+
const JSON_V1_FORBIDDEN_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
86
|
+
const JSON_V1_DIVISORS = new Set([100, 1_000, 1_000_000]);
|
|
87
|
+
const JSON_V1_TIMESTAMP_ENCODINGS = new Set([
|
|
88
|
+
"iso-8601",
|
|
89
|
+
"unix-seconds",
|
|
90
|
+
"unix-milliseconds",
|
|
91
|
+
]);
|
|
92
|
+
const JSON_V1_RESULT_TYPES = new Set([
|
|
93
|
+
"quota",
|
|
94
|
+
"rate_limit",
|
|
95
|
+
"usage",
|
|
96
|
+
"spend",
|
|
97
|
+
"budget",
|
|
98
|
+
"balance",
|
|
99
|
+
"status",
|
|
100
|
+
]);
|
|
101
|
+
const JSON_V1_MIN_TIMESTAMP_MS = 0;
|
|
102
|
+
const JSON_V1_MAX_TIMESTAMP_MS = 253_402_300_799_999;
|
|
73
103
|
function isPlainObject(value) {
|
|
74
104
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
105
|
}
|
|
@@ -151,10 +181,16 @@ function normalizeRemoteUrl(value) {
|
|
|
151
181
|
}
|
|
152
182
|
function cloneDefinition(definition) {
|
|
153
183
|
if (definition.mode === "remote-api") {
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
return definition.format === "json-v1"
|
|
185
|
+
? {
|
|
186
|
+
...definition,
|
|
187
|
+
...(definition.modelIds ? { modelIds: [...definition.modelIds] } : {}),
|
|
188
|
+
adapter: structuredClone(definition.adapter),
|
|
189
|
+
}
|
|
190
|
+
: {
|
|
191
|
+
...definition,
|
|
192
|
+
...(definition.modelIds ? { modelIds: [...definition.modelIds] } : {}),
|
|
193
|
+
};
|
|
158
194
|
}
|
|
159
195
|
return {
|
|
160
196
|
...definition,
|
|
@@ -163,6 +199,487 @@ function cloneDefinition(definition) {
|
|
|
163
199
|
...(definition.pricingModelMap ? { pricingModelMap: { ...definition.pricingModelMap } } : {}),
|
|
164
200
|
};
|
|
165
201
|
}
|
|
202
|
+
function adapterIssue(issues, key, message) {
|
|
203
|
+
issues.push({ key, message });
|
|
204
|
+
}
|
|
205
|
+
function validateKnownAdapterFields(value, allowed, key, issues) {
|
|
206
|
+
if (Object.keys(value).some((field) => !allowed.includes(field))) {
|
|
207
|
+
adapterIssue(issues, `${key}.*`, `unknown field; allowed fields are ${allowed.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function preflightJsonV1Adapter(value, key, issues) {
|
|
211
|
+
const stack = [{ value, depth: 1 }];
|
|
212
|
+
const seen = new WeakSet();
|
|
213
|
+
let objects = 0;
|
|
214
|
+
let properties = 0;
|
|
215
|
+
let arrayElements = 0;
|
|
216
|
+
while (stack.length > 0) {
|
|
217
|
+
const current = stack.pop();
|
|
218
|
+
if (!current.value || typeof current.value !== "object")
|
|
219
|
+
continue;
|
|
220
|
+
if (seen.has(current.value)) {
|
|
221
|
+
adapterIssue(issues, key, "expected a bounded JSON object tree");
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
seen.add(current.value);
|
|
225
|
+
if (current.depth > JSON_V1_MAX_ADAPTER_DEPTH) {
|
|
226
|
+
adapterIssue(issues, key, `must not exceed nesting depth ${JSON_V1_MAX_ADAPTER_DEPTH}`);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(current.value)) {
|
|
230
|
+
arrayElements += current.value.length;
|
|
231
|
+
if (arrayElements > JSON_V1_MAX_ADAPTER_ARRAY_ELEMENTS) {
|
|
232
|
+
adapterIssue(issues, key, `must not exceed ${JSON_V1_MAX_ADAPTER_ARRAY_ELEMENTS} total array elements`);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
for (let index = current.value.length - 1; index >= 0; index -= 1) {
|
|
236
|
+
stack.push({ value: current.value[index], depth: current.depth + 1 });
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
objects += 1;
|
|
241
|
+
const keys = Object.keys(current.value);
|
|
242
|
+
properties += keys.length;
|
|
243
|
+
if (objects > JSON_V1_MAX_ADAPTER_OBJECTS) {
|
|
244
|
+
adapterIssue(issues, key, `must not exceed ${JSON_V1_MAX_ADAPTER_OBJECTS} objects`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (properties > JSON_V1_MAX_ADAPTER_PROPERTIES) {
|
|
248
|
+
adapterIssue(issues, key, `must not exceed ${JSON_V1_MAX_ADAPTER_PROPERTIES} object properties`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const record = current.value;
|
|
252
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
253
|
+
stack.push({ value: record[keys[index]], depth: current.depth + 1 });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
function normalizeJsonV1StaticText(value, maxCodePoints) {
|
|
259
|
+
if (typeof value !== "string" || JSON_V1_STATIC_DISPLAY_FORBIDDEN_PATTERN.test(value))
|
|
260
|
+
return null;
|
|
261
|
+
const normalized = value.trim();
|
|
262
|
+
const length = Array.from(normalized).length;
|
|
263
|
+
return length >= 1 && length <= maxCodePoints ? normalized : null;
|
|
264
|
+
}
|
|
265
|
+
function validateJsonV1Path(value, key, issues) {
|
|
266
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > JSON_V1_MAX_PATH_SEGMENTS) {
|
|
267
|
+
adapterIssue(issues, key, `expected an array containing 1-${JSON_V1_MAX_PATH_SEGMENTS} path segments`);
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
const path = [];
|
|
271
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
272
|
+
const segment = value[index];
|
|
273
|
+
if (typeof segment !== "string" ||
|
|
274
|
+
Array.from(segment).length < 1 ||
|
|
275
|
+
Array.from(segment).length > JSON_V1_MAX_PATH_SEGMENT_CODE_POINTS ||
|
|
276
|
+
ASCII_OR_UNICODE_CONTROL_PATTERN.test(segment) ||
|
|
277
|
+
JSON_V1_FORBIDDEN_PATH_SEGMENTS.has(segment)) {
|
|
278
|
+
adapterIssue(issues, `${key}[${index}]`, "expected a safe 1-64 code point property segment");
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
path.push(segment);
|
|
282
|
+
}
|
|
283
|
+
return path.length === value.length ? path : undefined;
|
|
284
|
+
}
|
|
285
|
+
const JSON_V1_ISO_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|([+-])(\d{2}):(\d{2}))$/;
|
|
286
|
+
function daysInMonth(year, month) {
|
|
287
|
+
if (month === 2) {
|
|
288
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
289
|
+
}
|
|
290
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
291
|
+
}
|
|
292
|
+
export function normalizeJsonV1Timestamp(value, encoding) {
|
|
293
|
+
let milliseconds;
|
|
294
|
+
if (encoding === "iso-8601") {
|
|
295
|
+
if (typeof value !== "string")
|
|
296
|
+
return null;
|
|
297
|
+
const match = JSON_V1_ISO_RE.exec(value);
|
|
298
|
+
if (!match)
|
|
299
|
+
return null;
|
|
300
|
+
const year = Number(match[1]);
|
|
301
|
+
const month = Number(match[2]);
|
|
302
|
+
const day = Number(match[3]);
|
|
303
|
+
const hour = Number(match[4]);
|
|
304
|
+
const minute = Number(match[5]);
|
|
305
|
+
const second = Number(match[6]);
|
|
306
|
+
const fraction = (match[7] ?? "").padEnd(3, "0");
|
|
307
|
+
const offsetHour = match[8] === "Z" ? 0 : Number(match[10]);
|
|
308
|
+
const offsetMinute = match[8] === "Z" ? 0 : Number(match[11]);
|
|
309
|
+
if (year < 1970 ||
|
|
310
|
+
month < 1 ||
|
|
311
|
+
month > 12 ||
|
|
312
|
+
day < 1 ||
|
|
313
|
+
day > daysInMonth(year, month) ||
|
|
314
|
+
hour > 23 ||
|
|
315
|
+
minute > 59 ||
|
|
316
|
+
second > 59 ||
|
|
317
|
+
offsetHour > 14 ||
|
|
318
|
+
offsetMinute > 59 ||
|
|
319
|
+
(offsetHour === 14 && offsetMinute !== 0)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const offset = match[8] === "Z"
|
|
323
|
+
? 0
|
|
324
|
+
: (match[9] === "+" ? 1 : -1) * (offsetHour * 60 + offsetMinute) * 60_000;
|
|
325
|
+
milliseconds = Date.UTC(year, month - 1, day, hour, minute, second, Number(fraction)) - offset;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
milliseconds = encoding === "unix-seconds" ? value * 1_000 : value;
|
|
332
|
+
}
|
|
333
|
+
if (!Number.isSafeInteger(milliseconds) ||
|
|
334
|
+
milliseconds < JSON_V1_MIN_TIMESTAMP_MS ||
|
|
335
|
+
milliseconds > JSON_V1_MAX_TIMESTAMP_MS) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return new Date(milliseconds).toISOString();
|
|
339
|
+
}
|
|
340
|
+
function validateJsonV1NumberSource(value, key, issues) {
|
|
341
|
+
if (!isPlainObject(value)) {
|
|
342
|
+
adapterIssue(issues, key, "expected a path or literal number source object");
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
const hasPath = hasOwnKey(value, "path");
|
|
346
|
+
const hasLiteral = hasOwnKey(value, "literal");
|
|
347
|
+
if (hasPath === hasLiteral) {
|
|
348
|
+
adapterIssue(issues, key, "expected exactly one of path or literal");
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
if (hasPath) {
|
|
352
|
+
validateKnownAdapterFields(value, ["path", "divideBy"], key, issues);
|
|
353
|
+
const path = validateJsonV1Path(value.path, `${key}.path`, issues);
|
|
354
|
+
if (hasOwnKey(value, "divideBy") &&
|
|
355
|
+
(typeof value.divideBy !== "number" || !JSON_V1_DIVISORS.has(value.divideBy))) {
|
|
356
|
+
adapterIssue(issues, `${key}.divideBy`, "expected 100, 1000, or 1000000");
|
|
357
|
+
}
|
|
358
|
+
if (path &&
|
|
359
|
+
(!hasOwnKey(value, "divideBy") ||
|
|
360
|
+
(typeof value.divideBy === "number" && JSON_V1_DIVISORS.has(value.divideBy)))) {
|
|
361
|
+
return {
|
|
362
|
+
path,
|
|
363
|
+
...(value.divideBy !== undefined
|
|
364
|
+
? { divideBy: value.divideBy }
|
|
365
|
+
: {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
validateKnownAdapterFields(value, ["literal"], key, issues);
|
|
371
|
+
if (typeof value.literal !== "number" ||
|
|
372
|
+
!Number.isFinite(value.literal) ||
|
|
373
|
+
Math.abs(value.literal) > JSON_V1_MAX_NUMBER_MAGNITUDE) {
|
|
374
|
+
adapterIssue(issues, `${key}.literal`, `expected a finite number with absolute magnitude at most ${JSON_V1_MAX_NUMBER_MAGNITUDE}`);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
return { literal: value.literal };
|
|
378
|
+
}
|
|
379
|
+
function validateJsonV1TextSource(value, key, issues) {
|
|
380
|
+
if (!isPlainObject(value)) {
|
|
381
|
+
adapterIssue(issues, key, "expected a path or literal text source object");
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const hasPath = hasOwnKey(value, "path");
|
|
385
|
+
const hasLiteral = hasOwnKey(value, "literal");
|
|
386
|
+
if (hasPath === hasLiteral) {
|
|
387
|
+
adapterIssue(issues, key, "expected exactly one of path or literal");
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
validateKnownAdapterFields(value, hasPath ? ["path"] : ["literal"], key, issues);
|
|
391
|
+
if (hasPath) {
|
|
392
|
+
const path = validateJsonV1Path(value.path, `${key}.path`, issues);
|
|
393
|
+
return path ? { path } : undefined;
|
|
394
|
+
}
|
|
395
|
+
const literal = normalizeJsonV1StaticText(value.literal, JSON_V1_MAX_DISPLAY_CODE_POINTS);
|
|
396
|
+
if (!literal) {
|
|
397
|
+
adapterIssue(issues, `${key}.literal`, `expected 1-${JSON_V1_MAX_DISPLAY_CODE_POINTS} printable Unicode code points after trimming`);
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
return { literal };
|
|
401
|
+
}
|
|
402
|
+
function validateJsonV1TimestampSource(value, key, issues) {
|
|
403
|
+
if (!isPlainObject(value)) {
|
|
404
|
+
adapterIssue(issues, key, "expected a timestamp source object");
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
const hasPath = hasOwnKey(value, "path");
|
|
408
|
+
const hasLiteral = hasOwnKey(value, "literal");
|
|
409
|
+
if (hasPath === hasLiteral) {
|
|
410
|
+
adapterIssue(issues, key, "expected exactly one of path or literal");
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
validateKnownAdapterFields(value, hasPath ? ["path", "encoding"] : ["literal", "encoding"], key, issues);
|
|
414
|
+
const encoding = value.encoding;
|
|
415
|
+
if (typeof encoding !== "string" ||
|
|
416
|
+
!JSON_V1_TIMESTAMP_ENCODINGS.has(encoding)) {
|
|
417
|
+
adapterIssue(issues, `${key}.encoding`, 'expected "iso-8601", "unix-seconds", or "unix-milliseconds"');
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
const typedEncoding = encoding;
|
|
421
|
+
if (hasPath) {
|
|
422
|
+
const path = validateJsonV1Path(value.path, `${key}.path`, issues);
|
|
423
|
+
return path ? { path, encoding: typedEncoding } : undefined;
|
|
424
|
+
}
|
|
425
|
+
if (normalizeJsonV1Timestamp(value.literal, typedEncoding) === null) {
|
|
426
|
+
adapterIssue(issues, `${key}.literal`, "expected a valid timestamp for the selected encoding");
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
return { literal: value.literal, encoding: typedEncoding };
|
|
430
|
+
}
|
|
431
|
+
function literalNumber(source) {
|
|
432
|
+
return source && "literal" in source ? source.literal : undefined;
|
|
433
|
+
}
|
|
434
|
+
function requireLiteralNumber(source, key, issues, rule) {
|
|
435
|
+
const value = literalNumber(source);
|
|
436
|
+
if (value === undefined)
|
|
437
|
+
return;
|
|
438
|
+
if ((rule === "positive" && value <= 0) || (rule === "non-negative" && value < 0)) {
|
|
439
|
+
adapterIssue(issues, `${key}.literal`, rule === "positive"
|
|
440
|
+
? "expected a number greater than zero"
|
|
441
|
+
: "expected a non-negative number");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function validateJsonV1Metric(value, resultType, key, issues) {
|
|
445
|
+
if (!isPlainObject(value)) {
|
|
446
|
+
adapterIssue(issues, key, "expected a tagged metric object");
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
if (typeof value.type !== "string") {
|
|
450
|
+
adapterIssue(issues, `${key}.type`, "field is required");
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
const issueStart = issues.length;
|
|
454
|
+
switch (value.type) {
|
|
455
|
+
case "percentage": {
|
|
456
|
+
validateKnownAdapterFields(value, ["type", "percentage", "meaning"], key, issues);
|
|
457
|
+
const percentage = validateJsonV1NumberSource(value.percentage, `${key}.percentage`, issues);
|
|
458
|
+
if (value.meaning !== "remaining" && value.meaning !== "used") {
|
|
459
|
+
adapterIssue(issues, `${key}.meaning`, 'expected "remaining" or "used"');
|
|
460
|
+
}
|
|
461
|
+
if (resultType && !["quota", "rate_limit", "budget"].includes(resultType)) {
|
|
462
|
+
adapterIssue(issues, `${key}.type`, "percentage is incompatible with resultType");
|
|
463
|
+
}
|
|
464
|
+
const literal = literalNumber(percentage);
|
|
465
|
+
if (literal !== undefined &&
|
|
466
|
+
((value.meaning === "remaining" && literal > 100) ||
|
|
467
|
+
(value.meaning === "used" && literal < 0))) {
|
|
468
|
+
adapterIssue(issues, `${key}.percentage.literal`, value.meaning === "remaining"
|
|
469
|
+
? "remaining percentage must not exceed 100"
|
|
470
|
+
: "used percentage must be non-negative");
|
|
471
|
+
}
|
|
472
|
+
return issueStart === issues.length && percentage
|
|
473
|
+
? { type: "percentage", percentage, meaning: value.meaning }
|
|
474
|
+
: undefined;
|
|
475
|
+
}
|
|
476
|
+
case "used-limit": {
|
|
477
|
+
validateKnownAdapterFields(value, ["type", "used", "limit"], key, issues);
|
|
478
|
+
const used = validateJsonV1NumberSource(value.used, `${key}.used`, issues);
|
|
479
|
+
const limit = validateJsonV1NumberSource(value.limit, `${key}.limit`, issues);
|
|
480
|
+
requireLiteralNumber(used, `${key}.used`, issues, "non-negative");
|
|
481
|
+
requireLiteralNumber(limit, `${key}.limit`, issues, "positive");
|
|
482
|
+
if (resultType && !["quota", "rate_limit"].includes(resultType)) {
|
|
483
|
+
adapterIssue(issues, `${key}.type`, "used-limit is incompatible with resultType");
|
|
484
|
+
}
|
|
485
|
+
return issueStart === issues.length && used && limit
|
|
486
|
+
? { type: "used-limit", used, limit }
|
|
487
|
+
: undefined;
|
|
488
|
+
}
|
|
489
|
+
case "remaining-limit": {
|
|
490
|
+
validateKnownAdapterFields(value, ["type", "remaining", "limit"], key, issues);
|
|
491
|
+
const remaining = validateJsonV1NumberSource(value.remaining, `${key}.remaining`, issues);
|
|
492
|
+
const limit = validateJsonV1NumberSource(value.limit, `${key}.limit`, issues);
|
|
493
|
+
requireLiteralNumber(limit, `${key}.limit`, issues, "positive");
|
|
494
|
+
const remainingLiteral = literalNumber(remaining);
|
|
495
|
+
const limitLiteral = literalNumber(limit);
|
|
496
|
+
if (remainingLiteral !== undefined &&
|
|
497
|
+
limitLiteral !== undefined &&
|
|
498
|
+
remainingLiteral > limitLiteral) {
|
|
499
|
+
adapterIssue(issues, `${key}.remaining.literal`, "remaining must not exceed limit");
|
|
500
|
+
}
|
|
501
|
+
if (resultType && !["quota", "rate_limit"].includes(resultType)) {
|
|
502
|
+
adapterIssue(issues, `${key}.type`, "remaining-limit is incompatible with resultType");
|
|
503
|
+
}
|
|
504
|
+
return issueStart === issues.length && remaining && limit
|
|
505
|
+
? { type: "remaining-limit", remaining, limit }
|
|
506
|
+
: undefined;
|
|
507
|
+
}
|
|
508
|
+
case "spend-budget": {
|
|
509
|
+
validateKnownAdapterFields(value, ["type", "spend", "budget"], key, issues);
|
|
510
|
+
const spend = validateJsonV1NumberSource(value.spend, `${key}.spend`, issues);
|
|
511
|
+
const budget = validateJsonV1NumberSource(value.budget, `${key}.budget`, issues);
|
|
512
|
+
requireLiteralNumber(spend, `${key}.spend`, issues, "non-negative");
|
|
513
|
+
requireLiteralNumber(budget, `${key}.budget`, issues, "positive");
|
|
514
|
+
if (resultType && resultType !== "budget") {
|
|
515
|
+
adapterIssue(issues, `${key}.type`, "spend-budget is incompatible with resultType");
|
|
516
|
+
}
|
|
517
|
+
return issueStart === issues.length && spend && budget
|
|
518
|
+
? { type: "spend-budget", spend, budget }
|
|
519
|
+
: undefined;
|
|
520
|
+
}
|
|
521
|
+
case "remaining-budget": {
|
|
522
|
+
validateKnownAdapterFields(value, ["type", "remaining", "budget"], key, issues);
|
|
523
|
+
const remaining = validateJsonV1NumberSource(value.remaining, `${key}.remaining`, issues);
|
|
524
|
+
const budget = validateJsonV1NumberSource(value.budget, `${key}.budget`, issues);
|
|
525
|
+
requireLiteralNumber(budget, `${key}.budget`, issues, "positive");
|
|
526
|
+
const remainingLiteral = literalNumber(remaining);
|
|
527
|
+
const budgetLiteral = literalNumber(budget);
|
|
528
|
+
if (remainingLiteral !== undefined &&
|
|
529
|
+
budgetLiteral !== undefined &&
|
|
530
|
+
remainingLiteral > budgetLiteral) {
|
|
531
|
+
adapterIssue(issues, `${key}.remaining.literal`, "remaining must not exceed budget");
|
|
532
|
+
}
|
|
533
|
+
if (resultType && resultType !== "budget") {
|
|
534
|
+
adapterIssue(issues, `${key}.type`, "remaining-budget is incompatible with resultType");
|
|
535
|
+
}
|
|
536
|
+
return issueStart === issues.length && remaining && budget
|
|
537
|
+
? { type: "remaining-budget", remaining, budget }
|
|
538
|
+
: undefined;
|
|
539
|
+
}
|
|
540
|
+
case "value": {
|
|
541
|
+
validateKnownAdapterFields(value, ["type", "valueType", "value"], key, issues);
|
|
542
|
+
const source = validateJsonV1NumberSource(value.value, `${key}.value`, issues);
|
|
543
|
+
const allowed = {
|
|
544
|
+
used: ["quota", "rate_limit", "usage"],
|
|
545
|
+
limit: ["quota", "rate_limit"],
|
|
546
|
+
remaining: ["quota", "rate_limit"],
|
|
547
|
+
balance: ["balance"],
|
|
548
|
+
spend: ["spend"],
|
|
549
|
+
budget: ["budget"],
|
|
550
|
+
};
|
|
551
|
+
if (typeof value.valueType !== "string" || !hasOwnKey(allowed, value.valueType)) {
|
|
552
|
+
adapterIssue(issues, `${key}.valueType`, 'expected "used", "limit", "remaining", "balance", "spend", or "budget"');
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
if (!["remaining", "balance"].includes(value.valueType)) {
|
|
556
|
+
requireLiteralNumber(source, `${key}.value`, issues, "non-negative");
|
|
557
|
+
}
|
|
558
|
+
if (resultType && !allowed[value.valueType].includes(resultType)) {
|
|
559
|
+
adapterIssue(issues, `${key}.valueType`, "valueType is incompatible with resultType");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return issueStart === issues.length && source
|
|
563
|
+
? {
|
|
564
|
+
type: "value",
|
|
565
|
+
valueType: value.valueType,
|
|
566
|
+
value: source,
|
|
567
|
+
}
|
|
568
|
+
: undefined;
|
|
569
|
+
}
|
|
570
|
+
case "status": {
|
|
571
|
+
validateKnownAdapterFields(value, ["type", "value"], key, issues);
|
|
572
|
+
const source = validateJsonV1TextSource(value.value, `${key}.value`, issues);
|
|
573
|
+
if (resultType && resultType !== "status") {
|
|
574
|
+
adapterIssue(issues, `${key}.type`, "status is incompatible with resultType");
|
|
575
|
+
}
|
|
576
|
+
return issueStart === issues.length && source ? { type: "status", value: source } : undefined;
|
|
577
|
+
}
|
|
578
|
+
default:
|
|
579
|
+
adapterIssue(issues, `${key}.type`, "expected a supported tagged metric type");
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function validateJsonV1Mapping(value, key, providerLabel, issues) {
|
|
584
|
+
if (!isPlainObject(value)) {
|
|
585
|
+
adapterIssue(issues, key, "expected an object");
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
const issueStart = issues.length;
|
|
589
|
+
validateKnownAdapterFields(value, ["resultType", "name", "label", "unit", "unitPosition", "resetTime", "observedTime", "metric"], key, issues);
|
|
590
|
+
for (const field of ["resultType", "name", "metric"]) {
|
|
591
|
+
if (!hasOwnKey(value, field))
|
|
592
|
+
adapterIssue(issues, `${key}.${field}`, "field is required");
|
|
593
|
+
}
|
|
594
|
+
const resultType = typeof value.resultType === "string" &&
|
|
595
|
+
JSON_V1_RESULT_TYPES.has(value.resultType)
|
|
596
|
+
? value.resultType
|
|
597
|
+
: undefined;
|
|
598
|
+
if (hasOwnKey(value, "resultType") && !resultType) {
|
|
599
|
+
adapterIssue(issues, `${key}.resultType`, "expected a supported accounting result type");
|
|
600
|
+
}
|
|
601
|
+
const name = normalizeJsonV1StaticText(value.name, JSON_V1_MAX_STATIC_NAME_CODE_POINTS);
|
|
602
|
+
if (hasOwnKey(value, "name") && !name) {
|
|
603
|
+
adapterIssue(issues, `${key}.name`, `expected 1-${JSON_V1_MAX_STATIC_NAME_CODE_POINTS} printable Unicode code points after trimming`);
|
|
604
|
+
}
|
|
605
|
+
else if (name &&
|
|
606
|
+
Array.from(`${providerLabel} ${name}`).length > JSON_V1_MAX_DISPLAY_CODE_POINTS) {
|
|
607
|
+
adapterIssue(issues, `${key}.name`, `provider-prefixed name must not exceed ${JSON_V1_MAX_DISPLAY_CODE_POINTS} code points`);
|
|
608
|
+
}
|
|
609
|
+
const label = value.label === undefined
|
|
610
|
+
? undefined
|
|
611
|
+
: normalizeJsonV1StaticText(value.label, JSON_V1_MAX_STATIC_NAME_CODE_POINTS);
|
|
612
|
+
if (value.label !== undefined && !label) {
|
|
613
|
+
adapterIssue(issues, `${key}.label`, `expected 1-${JSON_V1_MAX_STATIC_NAME_CODE_POINTS} printable Unicode code points after trimming`);
|
|
614
|
+
}
|
|
615
|
+
const unit = value.unit === undefined
|
|
616
|
+
? undefined
|
|
617
|
+
: normalizeJsonV1StaticText(value.unit, JSON_V1_MAX_STATIC_UNIT_CODE_POINTS);
|
|
618
|
+
if (value.unit !== undefined && !unit) {
|
|
619
|
+
adapterIssue(issues, `${key}.unit`, `expected 1-${JSON_V1_MAX_STATIC_UNIT_CODE_POINTS} printable Unicode code points after trimming`);
|
|
620
|
+
}
|
|
621
|
+
const unitPosition = value.unitPosition === "prefix" || value.unitPosition === "suffix"
|
|
622
|
+
? value.unitPosition
|
|
623
|
+
: undefined;
|
|
624
|
+
if (value.unitPosition !== undefined && !unitPosition) {
|
|
625
|
+
adapterIssue(issues, `${key}.unitPosition`, 'expected "prefix" or "suffix"');
|
|
626
|
+
}
|
|
627
|
+
if ((unit === undefined) !== (unitPosition === undefined)) {
|
|
628
|
+
adapterIssue(issues, `${key}.unitPosition`, "unit and unitPosition must be configured together");
|
|
629
|
+
}
|
|
630
|
+
const resetTime = value.resetTime === undefined
|
|
631
|
+
? undefined
|
|
632
|
+
: validateJsonV1TimestampSource(value.resetTime, `${key}.resetTime`, issues);
|
|
633
|
+
const observedTime = value.observedTime === undefined
|
|
634
|
+
? undefined
|
|
635
|
+
: validateJsonV1TimestampSource(value.observedTime, `${key}.observedTime`, issues);
|
|
636
|
+
const metric = validateJsonV1Metric(value.metric, resultType, `${key}.metric`, issues);
|
|
637
|
+
if (metric && (metric.type === "percentage" || metric.type === "status") && unit !== undefined) {
|
|
638
|
+
adapterIssue(issues, `${key}.unit`, "units are not allowed for percentage or status metrics");
|
|
639
|
+
}
|
|
640
|
+
if (issueStart !== issues.length || !resultType || !name || !metric)
|
|
641
|
+
return undefined;
|
|
642
|
+
return {
|
|
643
|
+
resultType,
|
|
644
|
+
name,
|
|
645
|
+
...(label ? { label } : {}),
|
|
646
|
+
...(unit ? { unit, unitPosition: unitPosition } : {}),
|
|
647
|
+
...(resetTime ? { resetTime } : {}),
|
|
648
|
+
...(observedTime ? { observedTime } : {}),
|
|
649
|
+
metric,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function validateJsonV1Adapter(value, key, providerLabel, issues) {
|
|
653
|
+
if (!isPlainObject(value)) {
|
|
654
|
+
adapterIssue(issues, key, "expected an object");
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
if (!preflightJsonV1Adapter(value, key, issues))
|
|
658
|
+
return undefined;
|
|
659
|
+
const issueStart = issues.length;
|
|
660
|
+
validateKnownAdapterFields(value, ["rowsPath", "mappings"], key, issues);
|
|
661
|
+
const rowsPath = value.rowsPath === undefined
|
|
662
|
+
? undefined
|
|
663
|
+
: validateJsonV1Path(value.rowsPath, `${key}.rowsPath`, issues);
|
|
664
|
+
if (!hasOwnKey(value, "mappings")) {
|
|
665
|
+
adapterIssue(issues, `${key}.mappings`, "field is required");
|
|
666
|
+
}
|
|
667
|
+
if (!Array.isArray(value.mappings) ||
|
|
668
|
+
value.mappings.length < 1 ||
|
|
669
|
+
value.mappings.length > JSON_V1_MAX_MAPPINGS) {
|
|
670
|
+
adapterIssue(issues, `${key}.mappings`, `expected an array containing 1-${JSON_V1_MAX_MAPPINGS} mappings`);
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
const mappings = [];
|
|
674
|
+
for (let index = 0; index < value.mappings.length; index += 1) {
|
|
675
|
+
const mapping = validateJsonV1Mapping(value.mappings[index], `${key}.mappings[${index}]`, providerLabel, issues);
|
|
676
|
+
if (mapping)
|
|
677
|
+
mappings.push(mapping);
|
|
678
|
+
}
|
|
679
|
+
return issueStart === issues.length && mappings.length === value.mappings.length
|
|
680
|
+
? { ...(rowsPath ? { rowsPath } : {}), mappings }
|
|
681
|
+
: undefined;
|
|
682
|
+
}
|
|
166
683
|
function validateModelIds(params) {
|
|
167
684
|
if (!hasOwnKey(params.raw, "modelIds"))
|
|
168
685
|
return undefined;
|
|
@@ -515,13 +1032,29 @@ export function validateQuotaProviders(value) {
|
|
|
515
1032
|
if (normalizedUrl.message) {
|
|
516
1033
|
issues.push({ key: `${providerKey}.url`, message: normalizedUrl.message });
|
|
517
1034
|
}
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1035
|
+
const rawFormat = raw.format;
|
|
1036
|
+
const format = rawFormat === "accounting-v1" ? "quota-v1" : rawFormat;
|
|
1037
|
+
const formatValid = typeof format === "string" &&
|
|
1038
|
+
QUOTA_PROVIDER_REMOTE_FORMATS.includes(format);
|
|
1039
|
+
if (hasOwnKey(raw, "format") && !formatValid) {
|
|
522
1040
|
issues.push({
|
|
523
1041
|
key: `${providerKey}.format`,
|
|
524
|
-
message: 'expected "
|
|
1042
|
+
message: 'expected "quota-v1", "openrouter-key-v1", or "json-v1"',
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
let adapter;
|
|
1046
|
+
if (format === "json-v1") {
|
|
1047
|
+
if (!hasOwnKey(raw, "adapter")) {
|
|
1048
|
+
issues.push({ key: `${providerKey}.adapter`, message: "field is required" });
|
|
1049
|
+
}
|
|
1050
|
+
else if (label) {
|
|
1051
|
+
adapter = validateJsonV1Adapter(raw.adapter, `${providerKey}.adapter`, label, issues);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
else if (hasOwnKey(raw, "adapter")) {
|
|
1055
|
+
issues.push({
|
|
1056
|
+
key: `${providerKey}.adapter`,
|
|
1057
|
+
message: "allowed only for json-v1 format",
|
|
525
1058
|
});
|
|
526
1059
|
}
|
|
527
1060
|
const apiKeyEnv = raw.apiKeyEnv;
|
|
@@ -534,7 +1067,7 @@ export function validateQuotaProviders(value) {
|
|
|
534
1067
|
message: "expected at most 128 characters matching ^[A-Z_][A-Z0-9_]*$",
|
|
535
1068
|
});
|
|
536
1069
|
}
|
|
537
|
-
if (providerId && normalizedUrl.value &&
|
|
1070
|
+
if (providerId && normalizedUrl.value && formatValid) {
|
|
538
1071
|
const identity = JSON.stringify([providerId, normalizedUrl.value, format, apiKeyEnv ?? ""]);
|
|
539
1072
|
const first = requestIdentities.get(identity);
|
|
540
1073
|
if (first !== undefined) {
|
|
@@ -552,20 +1085,24 @@ export function validateQuotaProviders(value) {
|
|
|
552
1085
|
providerId &&
|
|
553
1086
|
label &&
|
|
554
1087
|
normalizedUrl.value &&
|
|
555
|
-
|
|
556
|
-
|
|
1088
|
+
formatValid) {
|
|
1089
|
+
const common = {
|
|
1090
|
+
id,
|
|
1091
|
+
providerId,
|
|
1092
|
+
label,
|
|
1093
|
+
mode: "remote-api",
|
|
1094
|
+
url: normalizedUrl.value,
|
|
1095
|
+
...(apiKeyEnv !== undefined ? { apiKeyEnv: apiKeyEnv } : {}),
|
|
1096
|
+
...(modelIds ? { modelIds } : {}),
|
|
1097
|
+
};
|
|
557
1098
|
definitions.push({
|
|
558
1099
|
index,
|
|
559
|
-
config:
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
format: format,
|
|
566
|
-
...(apiKeyEnv !== undefined ? { apiKeyEnv: apiKeyEnv } : {}),
|
|
567
|
-
...(modelIds ? { modelIds } : {}),
|
|
568
|
-
},
|
|
1100
|
+
config: format === "json-v1"
|
|
1101
|
+
? { ...common, format, adapter: adapter }
|
|
1102
|
+
: {
|
|
1103
|
+
...common,
|
|
1104
|
+
format: format,
|
|
1105
|
+
},
|
|
569
1106
|
});
|
|
570
1107
|
}
|
|
571
1108
|
}
|