@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,1167 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { getQuotaProviderShape } from "./provider-metadata.js";
|
|
3
|
+
import { lookupCost } from "./modelsdev-pricing.js";
|
|
4
|
+
import { resolvePricingKey } from "./quota-stats.js";
|
|
5
|
+
export const QUOTA_PROVIDER_REMOTE_FORMATS = ["quota-v1", "openrouter-key-v1", "json-v1"];
|
|
6
|
+
export const QUOTA_PROVIDER_MODES = ["remote-api", "local-estimate"];
|
|
7
|
+
export const QUOTA_PROVIDER_WINDOW_TYPES = ["utc-day", "rolling"];
|
|
8
|
+
export const QUOTA_PROVIDERS_AGGREGATE_ID = "quota-providers";
|
|
9
|
+
export const MAINTAINED_LOCAL_ESTIMATE_IDS = ["qwen-code", "alibaba-coding-plan"];
|
|
10
|
+
export function isMaintainedQuotaProviderTuning(definition) {
|
|
11
|
+
return MAINTAINED_LOCAL_ESTIMATE_IDS.includes(definition.id);
|
|
12
|
+
}
|
|
13
|
+
export function customQuotaProviderDefinitions(definitions) {
|
|
14
|
+
return definitions.filter((definition) => !isMaintainedQuotaProviderTuning(definition));
|
|
15
|
+
}
|
|
16
|
+
export function resolveQuotaProviderSessionModelIdentity(params) {
|
|
17
|
+
if (params.currentProviderID) {
|
|
18
|
+
return params.currentModel.length > 0
|
|
19
|
+
? { providerId: params.currentProviderID, modelId: params.currentModel }
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
const slashIndex = params.currentModel.indexOf("/");
|
|
23
|
+
if (slashIndex <= 0 || slashIndex === params.currentModel.length - 1)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
providerId: params.currentModel.slice(0, slashIndex),
|
|
27
|
+
modelId: params.currentModel.slice(slashIndex + 1),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function selectEligibleQuotaProviderDefinitions(params) {
|
|
31
|
+
const runtimeEligible = customQuotaProviderDefinitions(params.definitions).filter((definition) => params.availableProviderIds.has(definition.providerId));
|
|
32
|
+
if (!params.onlyCurrentModel)
|
|
33
|
+
return runtimeEligible;
|
|
34
|
+
if (!params.currentModel) {
|
|
35
|
+
if (!params.currentProviderID)
|
|
36
|
+
return [];
|
|
37
|
+
return runtimeEligible.filter((definition) => definition.providerId === params.currentProviderID && definition.modelIds === undefined);
|
|
38
|
+
}
|
|
39
|
+
const identity = resolveQuotaProviderSessionModelIdentity({
|
|
40
|
+
currentModel: params.currentModel,
|
|
41
|
+
currentProviderID: params.currentProviderID,
|
|
42
|
+
});
|
|
43
|
+
if (!identity)
|
|
44
|
+
return [];
|
|
45
|
+
return runtimeEligible.filter((definition) => definition.providerId === identity.providerId &&
|
|
46
|
+
(definition.modelIds === undefined || definition.modelIds.includes(identity.modelId)));
|
|
47
|
+
}
|
|
48
|
+
const BASE_FIELDS = ["id", "providerId", "label", "mode", "modelIds"];
|
|
49
|
+
const REMOTE_FIELDS = [...BASE_FIELDS, "url", "format", "apiKeyEnv", "adapter"];
|
|
50
|
+
const LOCAL_FIELDS = [...BASE_FIELDS, "windows", "pricingModelMap"];
|
|
51
|
+
const WINDOW_FIELDS = [
|
|
52
|
+
"id",
|
|
53
|
+
"label",
|
|
54
|
+
"type",
|
|
55
|
+
"durationMinutes",
|
|
56
|
+
"requestLimit",
|
|
57
|
+
"usdBudget",
|
|
58
|
+
];
|
|
59
|
+
const ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
60
|
+
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
61
|
+
const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
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;
|
|
64
|
+
const URL_WHITESPACE_OR_CONTROL_PATTERN = /[\s\p{Cc}]/u;
|
|
65
|
+
const MODEL_FORBIDDEN_PATTERN = /[\s\p{Cc}#?]/u;
|
|
66
|
+
const MAX_ID_LENGTH = 64;
|
|
67
|
+
const MAX_PROVIDER_ID_LENGTH = 64;
|
|
68
|
+
const MAX_LABEL_CODE_POINTS = 80;
|
|
69
|
+
const MAX_ENV_NAME_LENGTH = 128;
|
|
70
|
+
const MAX_WINDOWS = 16;
|
|
71
|
+
const MAX_ROLLING_MINUTES = 366 * 24 * 60;
|
|
72
|
+
const MAX_SAFE_REQUEST_LIMIT = 1_000_000_000;
|
|
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;
|
|
103
|
+
function isPlainObject(value) {
|
|
104
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
function hasOwnKey(value, key) {
|
|
107
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
108
|
+
}
|
|
109
|
+
function isValidId(value) {
|
|
110
|
+
return (typeof value === "string" &&
|
|
111
|
+
value.length >= 1 &&
|
|
112
|
+
value.length <= MAX_ID_LENGTH &&
|
|
113
|
+
ID_PATTERN.test(value));
|
|
114
|
+
}
|
|
115
|
+
function isValidProviderId(value) {
|
|
116
|
+
return (typeof value === "string" &&
|
|
117
|
+
value.length >= 1 &&
|
|
118
|
+
value.length <= MAX_PROVIDER_ID_LENGTH &&
|
|
119
|
+
PROVIDER_ID_PATTERN.test(value));
|
|
120
|
+
}
|
|
121
|
+
function isPositiveInteger(value, max) {
|
|
122
|
+
return Number.isInteger(value) && Number(value) > 0 && Number(value) <= max;
|
|
123
|
+
}
|
|
124
|
+
function isPositiveFinite(value, max) {
|
|
125
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max;
|
|
126
|
+
}
|
|
127
|
+
function normalizeLabel(value, fallback) {
|
|
128
|
+
if (value === undefined)
|
|
129
|
+
return fallback ?? null;
|
|
130
|
+
if (typeof value !== "string")
|
|
131
|
+
return null;
|
|
132
|
+
const normalized = value.trim();
|
|
133
|
+
const length = Array.from(normalized).length;
|
|
134
|
+
if (length < 1 ||
|
|
135
|
+
length > MAX_LABEL_CODE_POINTS ||
|
|
136
|
+
ASCII_OR_UNICODE_CONTROL_PATTERN.test(value)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
141
|
+
function isLoopbackHostname(hostname) {
|
|
142
|
+
const normalized = hostname.toLowerCase();
|
|
143
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost"))
|
|
144
|
+
return true;
|
|
145
|
+
if (normalized === "::1" || normalized === "[::1]")
|
|
146
|
+
return true;
|
|
147
|
+
if (isIP(normalized) === 4) {
|
|
148
|
+
const first = Number(normalized.split(".", 1)[0]);
|
|
149
|
+
return first === 127;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function normalizeRemoteUrl(value) {
|
|
154
|
+
if (typeof value !== "string" ||
|
|
155
|
+
value.length === 0 ||
|
|
156
|
+
URL_WHITESPACE_OR_CONTROL_PATTERN.test(value)) {
|
|
157
|
+
return {
|
|
158
|
+
message: "expected an absolute HTTPS URL without whitespace or control characters",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = new URL(value);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { message: "expected an absolute HTTPS URL" };
|
|
167
|
+
}
|
|
168
|
+
if (!parsed.hostname)
|
|
169
|
+
return { message: "URL must contain a host" };
|
|
170
|
+
if (parsed.username || parsed.password)
|
|
171
|
+
return { message: "URL must not contain credentials" };
|
|
172
|
+
if (parsed.search)
|
|
173
|
+
return { message: "URL must not contain a query string" };
|
|
174
|
+
if (parsed.hash)
|
|
175
|
+
return { message: "URL must not contain a fragment" };
|
|
176
|
+
if (parsed.protocol !== "https:" &&
|
|
177
|
+
!(parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname))) {
|
|
178
|
+
return { message: "HTTPS is required except for loopback HTTP endpoints" };
|
|
179
|
+
}
|
|
180
|
+
return { value: parsed.toString() };
|
|
181
|
+
}
|
|
182
|
+
function cloneDefinition(definition) {
|
|
183
|
+
if (definition.mode === "remote-api") {
|
|
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
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...definition,
|
|
197
|
+
...(definition.modelIds ? { modelIds: [...definition.modelIds] } : {}),
|
|
198
|
+
windows: definition.windows.map((window) => ({ ...window })),
|
|
199
|
+
...(definition.pricingModelMap ? { pricingModelMap: { ...definition.pricingModelMap } } : {}),
|
|
200
|
+
};
|
|
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
|
+
}
|
|
683
|
+
function validateModelIds(params) {
|
|
684
|
+
if (!hasOwnKey(params.raw, "modelIds"))
|
|
685
|
+
return undefined;
|
|
686
|
+
const rawModelIds = params.raw.modelIds;
|
|
687
|
+
if (!Array.isArray(rawModelIds) || rawModelIds.length === 0) {
|
|
688
|
+
params.issues.push({
|
|
689
|
+
key: `${params.providerKey}.modelIds`,
|
|
690
|
+
message: "expected a non-empty array of exact OpenCode model ids",
|
|
691
|
+
});
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
const modelIds = [];
|
|
695
|
+
const seen = new Map();
|
|
696
|
+
for (let index = 0; index < rawModelIds.length; index += 1) {
|
|
697
|
+
const modelId = rawModelIds[index];
|
|
698
|
+
const key = `${params.providerKey}.modelIds[${index}]`;
|
|
699
|
+
if (typeof modelId !== "string" ||
|
|
700
|
+
modelId.length === 0 ||
|
|
701
|
+
MODEL_FORBIDDEN_PATTERN.test(modelId)) {
|
|
702
|
+
params.issues.push({
|
|
703
|
+
key,
|
|
704
|
+
message: "expected an exact model id without whitespace, control characters, #, or ?",
|
|
705
|
+
});
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const first = seen.get(modelId);
|
|
709
|
+
if (first !== undefined) {
|
|
710
|
+
params.issues.push({
|
|
711
|
+
key,
|
|
712
|
+
message: `duplicates ${params.providerKey}.modelIds[${first}]`,
|
|
713
|
+
});
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
seen.set(modelId, index);
|
|
717
|
+
modelIds.push(modelId);
|
|
718
|
+
}
|
|
719
|
+
return modelIds;
|
|
720
|
+
}
|
|
721
|
+
function validateWindows(raw, providerKey, issues) {
|
|
722
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_WINDOWS) {
|
|
723
|
+
issues.push({
|
|
724
|
+
key: `${providerKey}.windows`,
|
|
725
|
+
message: `expected an array containing 1-${MAX_WINDOWS} windows`,
|
|
726
|
+
});
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
const windows = [];
|
|
730
|
+
const ids = new Map();
|
|
731
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
732
|
+
const item = raw[index];
|
|
733
|
+
const windowKey = `${providerKey}.windows[${index}]`;
|
|
734
|
+
if (!isPlainObject(item)) {
|
|
735
|
+
issues.push({ key: windowKey, message: "expected an object" });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
for (const field of Object.keys(item).filter((field) => !WINDOW_FIELDS.includes(field))) {
|
|
739
|
+
issues.push({
|
|
740
|
+
key: `${windowKey}.${field}`,
|
|
741
|
+
message: "unknown field; allowed fields are id, label, type, durationMinutes, requestLimit, usdBudget",
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
for (const field of ["id", "type", "requestLimit"]) {
|
|
745
|
+
if (!hasOwnKey(item, field)) {
|
|
746
|
+
issues.push({ key: `${windowKey}.${field}`, message: "field is required" });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const id = item.id;
|
|
750
|
+
if (hasOwnKey(item, "id") && !isValidId(id)) {
|
|
751
|
+
issues.push({
|
|
752
|
+
key: `${windowKey}.id`,
|
|
753
|
+
message: "expected 1-64 ASCII kebab-case characters",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
else if (isValidId(id)) {
|
|
757
|
+
const first = ids.get(id);
|
|
758
|
+
if (first !== undefined) {
|
|
759
|
+
issues.push({
|
|
760
|
+
key: `${windowKey}.id`,
|
|
761
|
+
message: `duplicates ${providerKey}.windows[${first}].id`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
ids.set(id, index);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const label = normalizeLabel(item.label, isValidId(id) ? id : undefined);
|
|
769
|
+
if (hasOwnKey(item, "label") && label === null) {
|
|
770
|
+
issues.push({
|
|
771
|
+
key: `${windowKey}.label`,
|
|
772
|
+
message: "expected 1-80 printable Unicode code points after trimming",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const type = item.type;
|
|
776
|
+
if (hasOwnKey(item, "type") &&
|
|
777
|
+
(typeof type !== "string" ||
|
|
778
|
+
!QUOTA_PROVIDER_WINDOW_TYPES.includes(type))) {
|
|
779
|
+
issues.push({
|
|
780
|
+
key: `${windowKey}.type`,
|
|
781
|
+
message: 'expected "utc-day" or "rolling"',
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
let durationMinutes;
|
|
785
|
+
if (type === "rolling") {
|
|
786
|
+
if (!isPositiveInteger(item.durationMinutes, MAX_ROLLING_MINUTES)) {
|
|
787
|
+
issues.push({
|
|
788
|
+
key: `${windowKey}.durationMinutes`,
|
|
789
|
+
message: `expected an integer from 1 to ${MAX_ROLLING_MINUTES}`,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
durationMinutes = item.durationMinutes;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else if (hasOwnKey(item, "durationMinutes")) {
|
|
797
|
+
issues.push({
|
|
798
|
+
key: `${windowKey}.durationMinutes`,
|
|
799
|
+
message: "allowed only for rolling windows",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (!isPositiveInteger(item.requestLimit, MAX_SAFE_REQUEST_LIMIT)) {
|
|
803
|
+
issues.push({
|
|
804
|
+
key: `${windowKey}.requestLimit`,
|
|
805
|
+
message: `expected an integer from 1 to ${MAX_SAFE_REQUEST_LIMIT}`,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
if (hasOwnKey(item, "usdBudget") && !isPositiveFinite(item.usdBudget, MAX_USD_BUDGET)) {
|
|
809
|
+
issues.push({
|
|
810
|
+
key: `${windowKey}.usdBudget`,
|
|
811
|
+
message: `expected a number greater than 0 and at most ${MAX_USD_BUDGET}`,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (isValidId(id) &&
|
|
815
|
+
label !== null &&
|
|
816
|
+
(type === "utc-day" || type === "rolling") &&
|
|
817
|
+
(type !== "rolling" || durationMinutes !== undefined) &&
|
|
818
|
+
isPositiveInteger(item.requestLimit, MAX_SAFE_REQUEST_LIMIT) &&
|
|
819
|
+
(!hasOwnKey(item, "usdBudget") || isPositiveFinite(item.usdBudget, MAX_USD_BUDGET))) {
|
|
820
|
+
windows.push({
|
|
821
|
+
id,
|
|
822
|
+
label,
|
|
823
|
+
type,
|
|
824
|
+
...(durationMinutes !== undefined ? { durationMinutes } : {}),
|
|
825
|
+
requestLimit: item.requestLimit,
|
|
826
|
+
...(item.usdBudget !== undefined ? { usdBudget: item.usdBudget } : {}),
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return windows.length === raw.length ? windows : undefined;
|
|
831
|
+
}
|
|
832
|
+
function validateMaintainedWindows(id, windows, providerKey, issues) {
|
|
833
|
+
if (!windows || !MAINTAINED_LOCAL_ESTIMATE_IDS.includes(id))
|
|
834
|
+
return;
|
|
835
|
+
const expected = id === "qwen-code"
|
|
836
|
+
? [
|
|
837
|
+
{ id: "daily", type: "utc-day" },
|
|
838
|
+
{ id: "rpm", type: "rolling", durationMinutes: 1 },
|
|
839
|
+
]
|
|
840
|
+
: [
|
|
841
|
+
{ id: "five-hour", type: "rolling", durationMinutes: 300 },
|
|
842
|
+
{ id: "weekly", type: "rolling", durationMinutes: 10_080 },
|
|
843
|
+
{ id: "monthly", type: "rolling", durationMinutes: 43_200 },
|
|
844
|
+
];
|
|
845
|
+
if (windows.length !== expected.length ||
|
|
846
|
+
expected.some((item, index) => {
|
|
847
|
+
const window = windows[index];
|
|
848
|
+
return (!window ||
|
|
849
|
+
window.id !== item.id ||
|
|
850
|
+
window.type !== item.type ||
|
|
851
|
+
window.durationMinutes !== item.durationMinutes ||
|
|
852
|
+
window.usdBudget !== undefined);
|
|
853
|
+
})) {
|
|
854
|
+
issues.push({
|
|
855
|
+
key: `${providerKey}.windows`,
|
|
856
|
+
message: id === "qwen-code"
|
|
857
|
+
? "qwen-code tuning requires ordered daily (utc-day) and rpm (1-minute rolling) request windows"
|
|
858
|
+
: "alibaba-coding-plan tuning requires ordered five-hour, weekly, and monthly rolling request windows",
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function validatePricingModelMap(raw, providerKey, providerId, issues) {
|
|
863
|
+
if (raw === undefined)
|
|
864
|
+
return undefined;
|
|
865
|
+
if (!isPlainObject(raw) || Object.keys(raw).length === 0) {
|
|
866
|
+
issues.push({
|
|
867
|
+
key: `${providerKey}.pricingModelMap`,
|
|
868
|
+
message: "expected a non-empty object of exact source model ids to models.dev provider/model ids",
|
|
869
|
+
});
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
const result = {};
|
|
873
|
+
for (const sourceModelId of Object.keys(raw).sort()) {
|
|
874
|
+
const key = `${providerKey}.pricingModelMap.${sourceModelId}`;
|
|
875
|
+
const target = raw[sourceModelId];
|
|
876
|
+
if (sourceModelId.length === 0 || MODEL_FORBIDDEN_PATTERN.test(sourceModelId)) {
|
|
877
|
+
issues.push({
|
|
878
|
+
key,
|
|
879
|
+
message: "source key must be an exact model id without whitespace or control characters",
|
|
880
|
+
});
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (typeof target !== "string") {
|
|
884
|
+
issues.push({ key, message: "expected a models.dev provider/model string" });
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const automatic = resolvePricingKey({ providerID: providerId, modelID: sourceModelId });
|
|
888
|
+
if (automatic.ok) {
|
|
889
|
+
issues.push({
|
|
890
|
+
key,
|
|
891
|
+
message: `automatic models.dev matching already resolves to ${automatic.key.provider}/${automatic.key.model}`,
|
|
892
|
+
});
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const slash = target.indexOf("/");
|
|
896
|
+
const targetProvider = slash > 0 ? target.slice(0, slash) : "";
|
|
897
|
+
const targetModel = slash > 0 ? target.slice(slash + 1) : "";
|
|
898
|
+
if (!targetProvider || !targetModel || !lookupCost(targetProvider, targetModel)) {
|
|
899
|
+
issues.push({
|
|
900
|
+
key,
|
|
901
|
+
message: "target must be an exact priced models.dev provider/model id",
|
|
902
|
+
});
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
result[sourceModelId] = target;
|
|
906
|
+
}
|
|
907
|
+
return Object.keys(result).length === Object.keys(raw).length ? result : undefined;
|
|
908
|
+
}
|
|
909
|
+
/** Validate and normalize the complete global-only quotaProviders array atomically. */
|
|
910
|
+
export function validateQuotaProviders(value) {
|
|
911
|
+
if (!Array.isArray(value)) {
|
|
912
|
+
return { issues: [{ key: "quotaProviders", message: "expected an array" }] };
|
|
913
|
+
}
|
|
914
|
+
const issues = [];
|
|
915
|
+
const definitions = [];
|
|
916
|
+
const ids = new Map();
|
|
917
|
+
const requestIdentities = new Map();
|
|
918
|
+
const coverage = [];
|
|
919
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
920
|
+
const raw = value[index];
|
|
921
|
+
const providerKey = `quotaProviders[${index}]`;
|
|
922
|
+
if (!isPlainObject(raw)) {
|
|
923
|
+
issues.push({ key: providerKey, message: "expected an object" });
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const issueStart = issues.length;
|
|
927
|
+
const mode = raw.mode;
|
|
928
|
+
const allowedFields = mode === "local-estimate"
|
|
929
|
+
? LOCAL_FIELDS
|
|
930
|
+
: mode === "remote-api"
|
|
931
|
+
? REMOTE_FIELDS
|
|
932
|
+
: [...new Set([...REMOTE_FIELDS, ...LOCAL_FIELDS])];
|
|
933
|
+
for (const field of Object.keys(raw)
|
|
934
|
+
.filter((field) => !allowedFields.includes(field))
|
|
935
|
+
.sort()) {
|
|
936
|
+
issues.push({
|
|
937
|
+
key: `${providerKey}.${field}`,
|
|
938
|
+
message: `unknown field for ${typeof mode === "string" ? mode : "quota provider"} mode`,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
for (const field of ["id", "mode"]) {
|
|
942
|
+
if (!hasOwnKey(raw, field)) {
|
|
943
|
+
issues.push({ key: `${providerKey}.${field}`, message: "field is required" });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const id = raw.id;
|
|
947
|
+
const idValid = isValidId(id);
|
|
948
|
+
if (hasOwnKey(raw, "id") && !idValid) {
|
|
949
|
+
issues.push({
|
|
950
|
+
key: `${providerKey}.id`,
|
|
951
|
+
message: "expected 1-64 ASCII kebab-case characters",
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
else if (idValid) {
|
|
955
|
+
if (id === QUOTA_PROVIDERS_AGGREGATE_ID) {
|
|
956
|
+
issues.push({
|
|
957
|
+
key: `${providerKey}.id`,
|
|
958
|
+
message: `"${QUOTA_PROVIDERS_AGGREGATE_ID}" is reserved for the aggregate provider`,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const builtIn = getQuotaProviderShape(id);
|
|
962
|
+
if (builtIn && !MAINTAINED_LOCAL_ESTIMATE_IDS.includes(id)) {
|
|
963
|
+
issues.push({
|
|
964
|
+
key: `${providerKey}.id`,
|
|
965
|
+
message: `collides with built-in quota provider id "${id}"`,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
const first = ids.get(id);
|
|
969
|
+
if (first !== undefined) {
|
|
970
|
+
issues.push({
|
|
971
|
+
key: `${providerKey}.id`,
|
|
972
|
+
message: `duplicates quotaProviders[${first}].id`,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
ids.set(id, index);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
let providerId;
|
|
980
|
+
if (!hasOwnKey(raw, "providerId")) {
|
|
981
|
+
if (idValid)
|
|
982
|
+
providerId = id;
|
|
983
|
+
}
|
|
984
|
+
else if (!isValidProviderId(raw.providerId)) {
|
|
985
|
+
issues.push({
|
|
986
|
+
key: `${providerKey}.providerId`,
|
|
987
|
+
message: "expected 1-64 lowercase provider-id characters",
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
else if (raw.providerId === id) {
|
|
991
|
+
issues.push({
|
|
992
|
+
key: `${providerKey}.providerId`,
|
|
993
|
+
message: "omit providerId when it is the same as id",
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
providerId = raw.providerId;
|
|
998
|
+
}
|
|
999
|
+
const label = normalizeLabel(raw.label, idValid ? id : undefined);
|
|
1000
|
+
if (hasOwnKey(raw, "label") && label === null) {
|
|
1001
|
+
issues.push({
|
|
1002
|
+
key: `${providerKey}.label`,
|
|
1003
|
+
message: "expected 1-80 printable Unicode code points after trimming",
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof mode !== "string" || !QUOTA_PROVIDER_MODES.includes(mode)) {
|
|
1007
|
+
issues.push({
|
|
1008
|
+
key: `${providerKey}.mode`,
|
|
1009
|
+
message: 'expected "remote-api" or "local-estimate"',
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
const modelIds = validateModelIds({ raw, providerKey, providerId, issues });
|
|
1013
|
+
const modelIdsValid = !hasOwnKey(raw, "modelIds") ||
|
|
1014
|
+
(Array.isArray(raw.modelIds) && modelIds?.length === raw.modelIds.length);
|
|
1015
|
+
if (idValid &&
|
|
1016
|
+
MAINTAINED_LOCAL_ESTIMATE_IDS.includes(id) &&
|
|
1017
|
+
mode !== "local-estimate") {
|
|
1018
|
+
issues.push({
|
|
1019
|
+
key: `${providerKey}.mode`,
|
|
1020
|
+
message: `${id} tuning must use local-estimate mode`,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (mode === "remote-api") {
|
|
1024
|
+
for (const field of ["url", "format"]) {
|
|
1025
|
+
if (!hasOwnKey(raw, field)) {
|
|
1026
|
+
issues.push({ key: `${providerKey}.${field}`, message: "field is required" });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const normalizedUrl = hasOwnKey(raw, "url")
|
|
1030
|
+
? normalizeRemoteUrl(raw.url)
|
|
1031
|
+
: { value: undefined };
|
|
1032
|
+
if (normalizedUrl.message) {
|
|
1033
|
+
issues.push({ key: `${providerKey}.url`, message: normalizedUrl.message });
|
|
1034
|
+
}
|
|
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) {
|
|
1040
|
+
issues.push({
|
|
1041
|
+
key: `${providerKey}.format`,
|
|
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",
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
const apiKeyEnv = raw.apiKeyEnv;
|
|
1061
|
+
if (hasOwnKey(raw, "apiKeyEnv") &&
|
|
1062
|
+
(typeof apiKeyEnv !== "string" ||
|
|
1063
|
+
apiKeyEnv.length > MAX_ENV_NAME_LENGTH ||
|
|
1064
|
+
!ENV_NAME_PATTERN.test(apiKeyEnv))) {
|
|
1065
|
+
issues.push({
|
|
1066
|
+
key: `${providerKey}.apiKeyEnv`,
|
|
1067
|
+
message: "expected at most 128 characters matching ^[A-Z_][A-Z0-9_]*$",
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (providerId && normalizedUrl.value && formatValid) {
|
|
1071
|
+
const identity = JSON.stringify([providerId, normalizedUrl.value, format, apiKeyEnv ?? ""]);
|
|
1072
|
+
const first = requestIdentities.get(identity);
|
|
1073
|
+
if (first !== undefined) {
|
|
1074
|
+
issues.push({
|
|
1075
|
+
key: `${providerKey}.url`,
|
|
1076
|
+
message: `duplicates request identity from quotaProviders[${first}]`,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
requestIdentities.set(identity, index);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (issueStart === issues.length &&
|
|
1084
|
+
idValid &&
|
|
1085
|
+
providerId &&
|
|
1086
|
+
label &&
|
|
1087
|
+
normalizedUrl.value &&
|
|
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
|
+
};
|
|
1098
|
+
definitions.push({
|
|
1099
|
+
index,
|
|
1100
|
+
config: format === "json-v1"
|
|
1101
|
+
? { ...common, format, adapter: adapter }
|
|
1102
|
+
: {
|
|
1103
|
+
...common,
|
|
1104
|
+
format: format,
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
else if (mode === "local-estimate") {
|
|
1110
|
+
if (!hasOwnKey(raw, "windows")) {
|
|
1111
|
+
issues.push({ key: `${providerKey}.windows`, message: "field is required" });
|
|
1112
|
+
}
|
|
1113
|
+
const windows = validateWindows(raw.windows, providerKey, issues);
|
|
1114
|
+
if (idValid)
|
|
1115
|
+
validateMaintainedWindows(id, windows, providerKey, issues);
|
|
1116
|
+
const pricingModelMap = providerId && hasOwnKey(raw, "pricingModelMap")
|
|
1117
|
+
? validatePricingModelMap(raw.pricingModelMap, providerKey, providerId, issues)
|
|
1118
|
+
: undefined;
|
|
1119
|
+
if (issueStart === issues.length && idValid && providerId && label && windows) {
|
|
1120
|
+
definitions.push({
|
|
1121
|
+
index,
|
|
1122
|
+
config: {
|
|
1123
|
+
id,
|
|
1124
|
+
providerId,
|
|
1125
|
+
label,
|
|
1126
|
+
mode,
|
|
1127
|
+
windows,
|
|
1128
|
+
...(modelIds ? { modelIds } : {}),
|
|
1129
|
+
...(pricingModelMap ? { pricingModelMap } : {}),
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (providerId && modelIdsValid) {
|
|
1135
|
+
coverage.push({ index, providerId, ...(modelIds ? { modelIds } : {}) });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
for (let index = 0; index < coverage.length; index += 1) {
|
|
1139
|
+
const current = coverage[index];
|
|
1140
|
+
for (let earlierIndex = 0; earlierIndex < index; earlierIndex += 1) {
|
|
1141
|
+
const earlier = coverage[earlierIndex];
|
|
1142
|
+
if (current.providerId !== earlier.providerId)
|
|
1143
|
+
continue;
|
|
1144
|
+
const overlap = current.modelIds === undefined ||
|
|
1145
|
+
earlier.modelIds === undefined ||
|
|
1146
|
+
current.modelIds.some((modelId) => earlier.modelIds.includes(modelId));
|
|
1147
|
+
if (!overlap)
|
|
1148
|
+
continue;
|
|
1149
|
+
issues.push({
|
|
1150
|
+
key: current.modelIds
|
|
1151
|
+
? `quotaProviders[${current.index}].modelIds`
|
|
1152
|
+
: `quotaProviders[${current.index}].providerId`,
|
|
1153
|
+
message: `provider/model coverage overlaps quotaProviders[${earlier.index}] for providerId "${current.providerId}"`,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (issues.length > 0)
|
|
1158
|
+
return { issues };
|
|
1159
|
+
return { value: definitions.map((item) => cloneDefinition(item.config)), issues: [] };
|
|
1160
|
+
}
|
|
1161
|
+
export function cloneQuotaProviders(definitions) {
|
|
1162
|
+
return definitions.map(cloneDefinition);
|
|
1163
|
+
}
|
|
1164
|
+
export function findQuotaProviderDefinition(definitions, id) {
|
|
1165
|
+
return definitions.find((definition) => definition.id === id);
|
|
1166
|
+
}
|
|
1167
|
+
//# sourceMappingURL=quota-providers.js.map
|