metergraph-cli 0.2.2 → 0.2.3
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 +49 -5
- package/bin/metergraph.js +1 -1
- package/package.json +1 -1
- package/src/agent-key.js +6 -3
- package/src/args.js +218 -22
- package/src/auth-oauth.js +58 -5
- package/src/cli.js +98 -10
- package/src/constants.js +41 -0
- package/src/experiments-contract.js +462 -0
- package/src/experiments-output.js +189 -0
- package/src/experiments.js +397 -0
- package/src/output.js +112 -1
- package/src/pricing-args.js +163 -0
- package/src/pricing-output.js +206 -0
- package/src/pricing-reference.js +195 -0
- package/src/pricing.js +307 -0
- package/src/read-contract.js +23 -3
- package/src/read.js +15 -1
- package/src/setup-deployment.js +7 -0
- package/src/setup-interactive.js +341 -0
- package/src/transport.js +5 -3
package/src/pricing.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { send } from "./transport.js";
|
|
2
|
+
import { parseJsonObject } from "./http.js";
|
|
3
|
+
import { validDate } from "./pricing-args.js";
|
|
4
|
+
import { runReferencePricing, RATE_KEYS } from "./pricing-reference.js";
|
|
5
|
+
|
|
6
|
+
const fail = (
|
|
7
|
+
outcome,
|
|
8
|
+
reason,
|
|
9
|
+
message = "Pricing could not be read. Check the reason and retry with supported inputs.",
|
|
10
|
+
) => ({ outcome, reason, message });
|
|
11
|
+
const text = (v) =>
|
|
12
|
+
typeof v === "string" &&
|
|
13
|
+
v.length > 0 &&
|
|
14
|
+
v.length <= 500 &&
|
|
15
|
+
!/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
16
|
+
const currency = (v) => typeof v === "string" && /^[A-Z]{3}$/.test(v);
|
|
17
|
+
const decimal = (v) => typeof v === "string" && /^(0|[1-9]\d{0,14})(\.\d{1,16})?$/.test(v);
|
|
18
|
+
const instant = (v) =>
|
|
19
|
+
typeof v === "string" &&
|
|
20
|
+
/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,6})?(?:Z|\+00:00)$/.test(v) &&
|
|
21
|
+
validDate(v.slice(0, 10)) &&
|
|
22
|
+
Number.isFinite(Date.parse(v));
|
|
23
|
+
const date = (v) => validDate(v) || instant(v);
|
|
24
|
+
const strings = (v) => Array.isArray(v) && v.length <= 10000 && v.every(text);
|
|
25
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
26
|
+
const source = (v) => {
|
|
27
|
+
if (!text(v)) return false;
|
|
28
|
+
try {
|
|
29
|
+
const u = new URL(v);
|
|
30
|
+
return u.protocol === "https:" && !u.username && !u.password;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const units = (v) => {
|
|
36
|
+
const [a, b = ""] = v.split(".");
|
|
37
|
+
return BigInt(a) * 10n ** 16n + BigInt(b.padEnd(16, "0"));
|
|
38
|
+
};
|
|
39
|
+
function price(p) {
|
|
40
|
+
return (
|
|
41
|
+
object(p) &&
|
|
42
|
+
["id", "model_id", "channel", "region"].every((k) => text(p[k])) &&
|
|
43
|
+
currency(p.currency) &&
|
|
44
|
+
date(p.effective_from) &&
|
|
45
|
+
(p.effective_to === null || (date(p.effective_to) && Date.parse(p.effective_to) > Date.parse(p.effective_from))) &&
|
|
46
|
+
source(p.source_url) &&
|
|
47
|
+
object(p.rules) &&
|
|
48
|
+
RATE_KEYS.every((k) => p[k] === null || decimal(p[k]))
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
export function validateModels(d) {
|
|
52
|
+
return (
|
|
53
|
+
object(d) &&
|
|
54
|
+
instant(d.as_of) &&
|
|
55
|
+
object(d.catalog) &&
|
|
56
|
+
text(d.catalog.version) &&
|
|
57
|
+
currency(d.catalog.currency) &&
|
|
58
|
+
validDate(d.catalog.pricing_verified_at) &&
|
|
59
|
+
["formats", "providers", "channels", "regions", "service_tiers"].every((k) => strings(d[k])) &&
|
|
60
|
+
Array.isArray(d.models) &&
|
|
61
|
+
d.models.length <= 10000 &&
|
|
62
|
+
new Set(d.models.map((m) => m?.model_id)).size === d.models.length &&
|
|
63
|
+
d.models.every(
|
|
64
|
+
(m) =>
|
|
65
|
+
object(m) &&
|
|
66
|
+
text(m.model_id) &&
|
|
67
|
+
text(m.publisher) &&
|
|
68
|
+
Array.isArray(m.aliases) &&
|
|
69
|
+
m.aliases.every((a) => object(a) && ["provider", "model", "channel"].every((k) => text(a[k]))) &&
|
|
70
|
+
Array.isArray(m.prices) &&
|
|
71
|
+
m.prices.every(
|
|
72
|
+
(p) =>
|
|
73
|
+
object(p) &&
|
|
74
|
+
text(p.channel) &&
|
|
75
|
+
text(p.region) &&
|
|
76
|
+
currency(p.currency) &&
|
|
77
|
+
date(p.effective_from) &&
|
|
78
|
+
strings(p.dimensions) &&
|
|
79
|
+
strings(p.service_tiers),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
export function validatePublicResult(d, action, q, now = Date.now()) {
|
|
85
|
+
if (!object(d) || !Array.isArray(d.results) || d.results.length !== 1) return false;
|
|
86
|
+
const r = d.results[0];
|
|
87
|
+
if (
|
|
88
|
+
!object(r) ||
|
|
89
|
+
!["priced", "partial", "unpriced"].includes(r.status) ||
|
|
90
|
+
!instant(r.effective_at) ||
|
|
91
|
+
!strings(r.missing_dimensions) ||
|
|
92
|
+
new Set(r.missing_dimensions).size !== r.missing_dimensions.length
|
|
93
|
+
)
|
|
94
|
+
return false;
|
|
95
|
+
if (
|
|
96
|
+
q.at ? Date.parse(r.effective_at) !== Date.parse(q.at) : Math.abs(Date.parse(r.effective_at) - now) > 15 * 60 * 1000
|
|
97
|
+
)
|
|
98
|
+
return false;
|
|
99
|
+
if (r.status === "priced" && r.missing_dimensions.length) return false;
|
|
100
|
+
if (r.status === "unpriced") {
|
|
101
|
+
if (r.model_id !== null || r.price !== null || r.currency !== null) return false;
|
|
102
|
+
} else if (
|
|
103
|
+
!price(r.price) ||
|
|
104
|
+
r.model_id !== r.price.model_id ||
|
|
105
|
+
r.currency !== r.price.currency ||
|
|
106
|
+
(q.channel && r.price.channel !== q.channel) ||
|
|
107
|
+
(q.region && ![q.region, "global", "*"].includes(r.price.region)) ||
|
|
108
|
+
Date.parse(r.price.effective_from) > Date.parse(r.effective_at) ||
|
|
109
|
+
(r.price.effective_to !== null && Date.parse(r.price.effective_to) <= Date.parse(r.effective_at))
|
|
110
|
+
)
|
|
111
|
+
return false;
|
|
112
|
+
if (action === "cost" || action === "calculate") {
|
|
113
|
+
if (r.status === "unpriced") return r.cost === null && r.components === null;
|
|
114
|
+
if (
|
|
115
|
+
!decimal(r.cost) ||
|
|
116
|
+
!object(r.components) ||
|
|
117
|
+
Object.keys(r.components).sort().join() !== "cache_read,cache_write,input,output,request" ||
|
|
118
|
+
!Object.values(r.components).every(decimal) ||
|
|
119
|
+
Object.values(r.components).reduce((n, v) => n + units(v), 0n) !== units(r.cost)
|
|
120
|
+
)
|
|
121
|
+
return false;
|
|
122
|
+
if (["input_tokens", "output_tokens"].some((k) => !Object.hasOwn(q, k) && !r.missing_dimensions.includes(k)))
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
export function pricingPayload(o) {
|
|
128
|
+
const q = o.query;
|
|
129
|
+
const context = Object.fromEntries(
|
|
130
|
+
["provider", "channel", "region"].filter((k) => q[k] !== undefined).map((k) => [k, q[k]]),
|
|
131
|
+
);
|
|
132
|
+
if (q.at) context.at = `${q.at}T00:00:00Z`;
|
|
133
|
+
if (q.batch) context.service_tier = "batch";
|
|
134
|
+
if (o.action === "lookup") return { model: q.model, context };
|
|
135
|
+
const usage = {};
|
|
136
|
+
const put = (key, value) => {
|
|
137
|
+
if (value !== undefined) usage[key] = value;
|
|
138
|
+
};
|
|
139
|
+
let trace;
|
|
140
|
+
if (o.traceFormat === "anthropic.messages") {
|
|
141
|
+
put("input_tokens", q.input_tokens);
|
|
142
|
+
put("output_tokens", q.output_tokens);
|
|
143
|
+
put("cache_read_input_tokens", q.cache_read_tokens);
|
|
144
|
+
put("cache_creation_input_tokens", q.cache_write_tokens);
|
|
145
|
+
if (q.cache_write_5m_tokens !== undefined || q.cache_write_1h_tokens !== undefined)
|
|
146
|
+
usage.cache_creation = {
|
|
147
|
+
...(q.cache_write_5m_tokens === undefined ? {} : { ephemeral_5m_input_tokens: q.cache_write_5m_tokens }),
|
|
148
|
+
...(q.cache_write_1h_tokens === undefined ? {} : { ephemeral_1h_input_tokens: q.cache_write_1h_tokens }),
|
|
149
|
+
};
|
|
150
|
+
trace = { model: q.model, usage };
|
|
151
|
+
} else if (o.traceFormat.startsWith("openai.")) {
|
|
152
|
+
const chat = o.traceFormat === "openai.chat_completions";
|
|
153
|
+
put(chat ? "prompt_tokens" : "input_tokens", q.input_tokens);
|
|
154
|
+
put(chat ? "completion_tokens" : "output_tokens", q.output_tokens);
|
|
155
|
+
if (q.cache_read_tokens !== undefined)
|
|
156
|
+
usage[chat ? "prompt_tokens_details" : "input_tokens_details"] = { cached_tokens: q.cache_read_tokens };
|
|
157
|
+
trace = { model: q.model, usage };
|
|
158
|
+
} else {
|
|
159
|
+
usage["gen_ai.request.model"] = q.model;
|
|
160
|
+
put("gen_ai.usage.input_tokens", q.input_tokens);
|
|
161
|
+
put("gen_ai.usage.output_tokens", q.output_tokens);
|
|
162
|
+
put("gen_ai.usage.cache_read.input_tokens", q.cache_read_tokens);
|
|
163
|
+
trace = { attributes: usage };
|
|
164
|
+
}
|
|
165
|
+
return { format: o.traceFormat, trace, context };
|
|
166
|
+
}
|
|
167
|
+
export async function runPricing(o) {
|
|
168
|
+
if (o.api === "reference") return runReferencePricing(o);
|
|
169
|
+
if (o.action === "history")
|
|
170
|
+
return fail(
|
|
171
|
+
"unsupported",
|
|
172
|
+
"pricing_history_unavailable",
|
|
173
|
+
"The public API documents no history endpoint. Historical charts remain available with --api reference and a loopback --url; no public history request was made.",
|
|
174
|
+
);
|
|
175
|
+
if (o.action === "market") return runPublicMarket(o);
|
|
176
|
+
const path =
|
|
177
|
+
o.action === "models"
|
|
178
|
+
? "/v1/pricing/models"
|
|
179
|
+
: o.action === "lookup"
|
|
180
|
+
? "/v1/pricing/lookup"
|
|
181
|
+
: "/v1/pricing/calculate";
|
|
182
|
+
const res = await send(o.origin, path, {
|
|
183
|
+
method: o.action === "models" ? "GET" : "POST",
|
|
184
|
+
json: o.action === "models" ? null : pricingPayload(o),
|
|
185
|
+
maxBytes: 1024 * 1024,
|
|
186
|
+
signal: AbortSignal.timeout(o.timeoutMs),
|
|
187
|
+
useGlobalAgent: true,
|
|
188
|
+
});
|
|
189
|
+
const failure = responseFailure(res);
|
|
190
|
+
if (failure) return failure;
|
|
191
|
+
const d = parseJsonObject(res);
|
|
192
|
+
if (o.action === "models") {
|
|
193
|
+
if (!validateModels(d)) return fail("verification_failed", "pricing_contract_mismatch");
|
|
194
|
+
const q = o.query;
|
|
195
|
+
const matches = d.models.filter(
|
|
196
|
+
(m) =>
|
|
197
|
+
(!q.model || m.model_id === q.model || m.aliases.some((a) => a.model === q.model)) &&
|
|
198
|
+
(!q.provider || m.aliases.some((a) => a.provider === q.provider && (!q.channel || a.channel === q.channel))) &&
|
|
199
|
+
(!q.channel || m.prices.some((p) => p.channel === q.channel && (!q.region || p.region === q.region))) &&
|
|
200
|
+
(!q.region || m.prices.some((p) => p.region === q.region && (!q.channel || p.channel === q.channel))),
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
outcome: "ok",
|
|
204
|
+
data: {
|
|
205
|
+
api: "public",
|
|
206
|
+
action: o.action,
|
|
207
|
+
request: q,
|
|
208
|
+
catalog: d.catalog,
|
|
209
|
+
as_of: d.as_of,
|
|
210
|
+
formats: d.formats,
|
|
211
|
+
providers: d.providers,
|
|
212
|
+
channels: d.channels,
|
|
213
|
+
regions: d.regions,
|
|
214
|
+
service_tiers: d.service_tiers,
|
|
215
|
+
result: {
|
|
216
|
+
models: matches.slice(0, q.limit),
|
|
217
|
+
matched: matches.length,
|
|
218
|
+
total_models: d.models.length,
|
|
219
|
+
truncated: matches.length > q.limit,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!validatePublicResult(d, o.action, o.query)) return fail("verification_failed", "pricing_contract_mismatch");
|
|
225
|
+
const r = d.results[0];
|
|
226
|
+
return {
|
|
227
|
+
outcome: "ok",
|
|
228
|
+
data: {
|
|
229
|
+
api: "public",
|
|
230
|
+
action: o.action,
|
|
231
|
+
request: o.query,
|
|
232
|
+
...(o.action === "lookup" ? {} : { trace_format: o.traceFormat }),
|
|
233
|
+
catalog: null,
|
|
234
|
+
result: r,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function responseFailure(res) {
|
|
240
|
+
if (res.kind === "error") return fail("connection_failed", res.reason);
|
|
241
|
+
if (res.status >= 300 && res.status < 400) return fail("redirect_rejected", "pricing_redirect_rejected");
|
|
242
|
+
if (res.status === 429) return fail("rate_limited", "pricing_rate_limited");
|
|
243
|
+
if (res.status >= 500) return fail("unhealthy", "pricing_service_unhealthy");
|
|
244
|
+
if ([401, 403, 404, 405].includes(res.status)) return fail("unsupported", "public_pricing_unavailable");
|
|
245
|
+
if (res.tooLarge) return fail("unsupported", "pricing_response_too_large");
|
|
246
|
+
if ([400, 422].includes(res.status)) return fail("invalid_input", "pricing_request_rejected");
|
|
247
|
+
if (res.status !== 200) return fail("unsupported", "pricing_response_unsupported");
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function runPublicMarket(o) {
|
|
252
|
+
if (o.query.at)
|
|
253
|
+
return fail(
|
|
254
|
+
"unsupported",
|
|
255
|
+
"market_date_unsupported",
|
|
256
|
+
"The public market board uses current discovery and lookups. Use lookup --at for one historical price; historical board coverage is unavailable.",
|
|
257
|
+
);
|
|
258
|
+
const signal = AbortSignal.timeout(o.timeoutMs);
|
|
259
|
+
const options = { signal, maxBytes: 1024 * 1024, useGlobalAgent: true };
|
|
260
|
+
const discovery = await send(o.origin, "/v1/pricing/models", options);
|
|
261
|
+
const error = responseFailure(discovery);
|
|
262
|
+
if (error) return error;
|
|
263
|
+
const d = parseJsonObject(discovery);
|
|
264
|
+
if (!validateModels(d)) return fail("verification_failed", "pricing_contract_mismatch");
|
|
265
|
+
const q = o.query;
|
|
266
|
+
const matches = d.models.filter((m) => m.prices.some((p) => p.channel === q.channel && p.region === q.region));
|
|
267
|
+
const selected = matches.slice(0, q.limit),
|
|
268
|
+
items = new Array(selected.length);
|
|
269
|
+
let next = 0,
|
|
270
|
+
failure = null;
|
|
271
|
+
const worker = async () => {
|
|
272
|
+
while (!failure && next < selected.length) {
|
|
273
|
+
const index = next++,
|
|
274
|
+
model = selected[index].model_id;
|
|
275
|
+
const request = { model, channel: q.channel, region: q.region };
|
|
276
|
+
const res = await send(o.origin, "/v1/pricing/lookup", {
|
|
277
|
+
...options,
|
|
278
|
+
method: "POST",
|
|
279
|
+
json: { model, context: { channel: q.channel, region: q.region } },
|
|
280
|
+
});
|
|
281
|
+
const error = responseFailure(res);
|
|
282
|
+
if (error) {
|
|
283
|
+
failure = error;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
const result = parseJsonObject(res);
|
|
287
|
+
if (!validatePublicResult(result, "lookup", request)) {
|
|
288
|
+
failure = fail("verification_failed", "pricing_contract_mismatch");
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
items[index] = { requested_model: model, ...result.results[0] };
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
await Promise.all(Array.from({ length: Math.min(3, selected.length) }, worker));
|
|
295
|
+
if (failure) return failure;
|
|
296
|
+
return {
|
|
297
|
+
outcome: "ok",
|
|
298
|
+
data: {
|
|
299
|
+
api: "public",
|
|
300
|
+
action: "market",
|
|
301
|
+
request: q,
|
|
302
|
+
catalog: d.catalog,
|
|
303
|
+
as_of: d.as_of,
|
|
304
|
+
result: { items, matched: matches.length, truncated: matches.length > items.length, history_available: false },
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
package/src/read-contract.js
CHANGED
|
@@ -16,8 +16,8 @@ import { traceLink } from "./trace-contract.js";
|
|
|
16
16
|
// { ok: true, value } or { ok: false, outcome, reason }
|
|
17
17
|
// with fixed tokens. notices is a Set that collects fixed notice tokens.
|
|
18
18
|
|
|
19
|
-
// Every capability name the contract defines,
|
|
20
|
-
//
|
|
19
|
+
// Every capability name the contract defines. Content, replay and start
|
|
20
|
+
// entries are never available to a Metadata grant.
|
|
21
21
|
export const CAPABILITY_NAMES = Object.freeze([
|
|
22
22
|
"workspace_context",
|
|
23
23
|
"capability_discovery",
|
|
@@ -30,11 +30,16 @@ export const CAPABILITY_NAMES = Object.freeze([
|
|
|
30
30
|
"reports",
|
|
31
31
|
"report_detail",
|
|
32
32
|
"report_evidence",
|
|
33
|
+
"experiments",
|
|
34
|
+
"experiment_estimate",
|
|
35
|
+
"experiment",
|
|
36
|
+
"experiment_cases",
|
|
37
|
+
"experiment_start",
|
|
33
38
|
"trace_content",
|
|
34
39
|
"trace_replay",
|
|
35
40
|
]);
|
|
36
41
|
const PRIVACY_CLASSES = Object.freeze(["metadata", "content", "replay"]);
|
|
37
|
-
const SCOPES = Object.freeze(["agent:metadata", "agent:read", "agent:replay"]);
|
|
42
|
+
const SCOPES = Object.freeze(["agent:metadata", "agent:read", "agent:replay", "agent:analysis-run"]);
|
|
38
43
|
|
|
39
44
|
const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/;
|
|
40
45
|
const IDENT = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
@@ -1547,3 +1552,18 @@ function checkFilters(body, rows, request) {
|
|
|
1547
1552
|
}
|
|
1548
1553
|
}
|
|
1549
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
// Shared with the experiments validators (experiments-contract.js), which
|
|
1557
|
+
// hold their documents to the same rules.
|
|
1558
|
+
export {
|
|
1559
|
+
Invalid,
|
|
1560
|
+
displayText,
|
|
1561
|
+
isAmount,
|
|
1562
|
+
isCount,
|
|
1563
|
+
isObject,
|
|
1564
|
+
isTimestamp,
|
|
1565
|
+
isToken,
|
|
1566
|
+
provenance,
|
|
1567
|
+
rowObject,
|
|
1568
|
+
run,
|
|
1569
|
+
};
|
package/src/read.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./read-contract.js";
|
|
20
20
|
import { compareReports, priorWindow, rankReport } from "./usage-aggregates.js";
|
|
21
21
|
import { deadline, failureOf, send, trapSignals } from "./transport.js";
|
|
22
|
+
import { experimentNextAction, experimentRead } from "./experiments.js";
|
|
22
23
|
import { agentKeyAction, classifyEnvironment, handoffPages } from "./environment.js";
|
|
23
24
|
|
|
24
25
|
// The read commands: status, context, capabilities, usage, routes and traces.
|
|
@@ -211,6 +212,16 @@ export async function runRead(options) {
|
|
|
211
212
|
return end("ok", null, context.value);
|
|
212
213
|
}
|
|
213
214
|
if (options.command === "capabilities") return end("ok", null, capabilities.value);
|
|
215
|
+
if (options.command === "experiments") {
|
|
216
|
+
const read = await experimentRead(session.session, options, ctx, documents, limit.signal, notices);
|
|
217
|
+
if (!read.ok) {
|
|
218
|
+
const settled = read.outcome === "refused" ? read : settle(read);
|
|
219
|
+
const report = read.refusal ? { refusal: read.refusal }
|
|
220
|
+
: read.retryAfter !== undefined ? { retry_after_seconds: read.retryAfter } : null;
|
|
221
|
+
return end(settled.outcome, settled.reason, report);
|
|
222
|
+
}
|
|
223
|
+
return end("ok", null, read.value);
|
|
224
|
+
}
|
|
214
225
|
|
|
215
226
|
const read = grouped ? GROUPED_USAGE : READS[options.command];
|
|
216
227
|
const entry = capabilities.value.agent[read.capability];
|
|
@@ -328,7 +339,7 @@ const INSUFFICIENT_SCOPE = /(?:^|[\s,])error\s*=\s*"?insufficient_scope"?(?:$|[\
|
|
|
328
339
|
// True when any string in value (keys included) contains one of the known
|
|
329
340
|
// credential values. An exact containment check against values this run
|
|
330
341
|
// holds, not a guess at what looks secret.
|
|
331
|
-
function holdsKnown(value, known) {
|
|
342
|
+
export function holdsKnown(value, known) {
|
|
332
343
|
if (known.length === 0) return false;
|
|
333
344
|
if (typeof value === "string") return known.some((secret) => value.includes(secret));
|
|
334
345
|
if (value === null || typeof value !== "object") return false;
|
|
@@ -403,6 +414,7 @@ function readData(ctx, report, notices, outcome, options, extra = {}) {
|
|
|
403
414
|
result: outcome === "ok" ? report : null,
|
|
404
415
|
retry_after_seconds: outcome === "rate_limited" || report?.retry_after_seconds !== undefined
|
|
405
416
|
? report?.retry_after_seconds ?? null : null,
|
|
417
|
+
...(options.command === "experiments" ? { refusal: outcome === "refused" ? report?.refusal ?? null : null } : {}),
|
|
406
418
|
notices: [...notices].sort(),
|
|
407
419
|
next_action: nextAction(outcome, ctx, options, extra),
|
|
408
420
|
};
|
|
@@ -460,6 +472,8 @@ function nextAction(outcome, ctx, options, { grouped = false, report = null } =
|
|
|
460
472
|
const reason = ctx.reason;
|
|
461
473
|
const capabilities = ctx.capabilities;
|
|
462
474
|
if (outcome === "rate_limited") return { kind: "retry_later", message: "Wait, then run the same command again." };
|
|
475
|
+
const experiment = experimentNextAction(outcome, ctx, options, report);
|
|
476
|
+
if (experiment !== undefined) return experiment;
|
|
463
477
|
if (reason === "usage_aggregates_unavailable" || (grouped && reason === "endpoint_unavailable")) {
|
|
464
478
|
// Offer the route-by-day read only when this grant actually has it.
|
|
465
479
|
if (capabilities?.agent?.usage?.available !== true) return null;
|
package/src/setup-deployment.js
CHANGED
|
@@ -64,6 +64,13 @@ const ACTION_GUIDE = Object.freeze({
|
|
|
64
64
|
retry: "Rerun the same command.",
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
// Each prerequisite a non-hosted route asserts, with its guidance, for a
|
|
68
|
+
// person confirming them at a terminal.
|
|
69
|
+
export function prerequisitesFor(deployment) {
|
|
70
|
+
const profile = EXPECTED_PROFILE[deployment];
|
|
71
|
+
return (PREREQUISITES[deployment] ?? []).map((name) => ({ name, guide: prerequisiteGuide(name, profile) }));
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
export function prerequisiteGuide(name, profile) {
|
|
68
75
|
if (name === "metadata_agent_credential") return METADATA_CREDENTIAL_GUIDE[profile] ?? null;
|
|
69
76
|
return PREREQUISITE_GUIDE[name] ?? null;
|