llm-cli-gateway 2.16.0 → 2.17.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/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +28 -0
- package/dist/acp/client.js +5 -0
- package/dist/acp/flight-redaction.d.ts +2 -0
- package/dist/acp/flight-redaction.js +2 -0
- package/dist/acp/runtime.d.ts +1 -0
- package/dist/acp/runtime.js +20 -0
- package/dist/acp/types.d.ts +52 -0
- package/dist/acp/types.js +8 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +1 -0
- package/dist/async-job-manager.d.ts +16 -0
- package/dist/async-job-manager.js +70 -22
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/transforms/ansi.js +12 -2
- package/dist/config.d.ts +31 -0
- package/dist/config.js +142 -7
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +5 -0
- package/dist/executor.js +11 -1
- package/dist/flight-recorder.d.ts +10 -0
- package/dist/flight-recorder.js +68 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +41 -1
- package/dist/index.js +658 -30
- package/dist/job-store.d.ts +10 -2
- package/dist/job-store.js +154 -43
- package/dist/lcr-priors.d.ts +60 -0
- package/dist/lcr-priors.js +190 -0
- package/dist/lcr-router-env.d.ts +20 -0
- package/dist/lcr-router-env.js +133 -0
- package/dist/lcr-telemetry.d.ts +2 -0
- package/dist/lcr-telemetry.js +17 -0
- package/dist/least-cost-router.d.ts +86 -0
- package/dist/least-cost-router.js +296 -0
- package/dist/least-cost-types.d.ts +34 -0
- package/dist/least-cost-types.js +1 -0
- package/dist/migrate-sessions.js +1 -1
- package/dist/migrate.js +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/postgres-job-store-worker.js +56 -13
- package/dist/pricing.d.ts +17 -0
- package/dist/pricing.js +167 -0
- package/dist/provider-definitions.d.ts +4 -0
- package/dist/provider-definitions.js +28 -0
- package/dist/request-helpers.d.ts +2 -2
- package/dist/resources.d.ts +37 -2
- package/dist/resources.js +96 -1
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +1 -1
- package/dist/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.js +1 -1
- package/dist/validation-receipt.js +1 -1
- package/dist/validation-tools.d.ts +6 -0
- package/dist/validation-tools.js +174 -54
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -4
- package/setup/status.schema.json +68 -0
package/dist/validation-tools.js
CHANGED
|
@@ -3,8 +3,62 @@ import { CLI_TYPES } from "./session-manager.js";
|
|
|
3
3
|
import { getAvailableCliInfo } from "./model-registry.js";
|
|
4
4
|
import { apiProviderCatalogEntry } from "./api-request.js";
|
|
5
5
|
import { getRequestContext, principalCanAccess, resolveOwnerPrincipal } from "./request-context.js";
|
|
6
|
+
import { PerformanceMetrics } from "./metrics.js";
|
|
7
|
+
import { loadLeastCostConfig } from "./config.js";
|
|
8
|
+
import { buildRouterEnv, toRouterConfig } from "./lcr-router-env.js";
|
|
9
|
+
import { selectCandidate, selectCheapestPerTier, } from "./least-cost-router.js";
|
|
6
10
|
import { currentCaller, eagerMintFromJobId, eagerMintFromValidationId, resolveValidationReceipt, } from "./validation-receipt.js";
|
|
7
11
|
import { collectValidationJobResult, startJudgeSynthesis, startValidationRun, } from "./validation-orchestrator.js";
|
|
12
|
+
const selectSchema = z
|
|
13
|
+
.enum(["cheapest", "cheapest_per_tier"])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Optional least-cost routing: fill the provider target(s) from the LCR selector instead of the explicit list. 'cheapest' picks the single cheapest eligible provider; 'cheapest_per_tier' picks the cheapest in each quality tier. Requires [least_cost].enabled=true; fails closed (no default-list fallback) when disabled or nothing is eligible.");
|
|
16
|
+
function selectorFailureMessage(decision) {
|
|
17
|
+
const reasons = decision.rejected.map(r => `${r.candidate.provider}/${r.candidate.model}: ${r.reason}`);
|
|
18
|
+
const detail = reasons.length > 0 ? ` Rejections: ${reasons.join("; ")}.` : "";
|
|
19
|
+
return `least-cost routing found no eligible provider (${decision.error ?? "NoEligibleCandidate"}); no fallback to the default provider list (fail closed).${detail}`;
|
|
20
|
+
}
|
|
21
|
+
function resolveSelectedProviders(ctx, prompt, mode, singleProvider) {
|
|
22
|
+
if (!ctx.leastCost.enabled) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: "select requires least-cost routing; set [least_cost].enabled=true in ~/.llm-cli-gateway/config.toml (no fallback to the default provider list).",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const env = buildRouterEnv({
|
|
29
|
+
performanceMetrics: ctx.performanceMetrics,
|
|
30
|
+
limiterSnapshot: ctx.asyncJobManager.getLimiterSnapshot(),
|
|
31
|
+
apiProviders: ctx.apiProviders,
|
|
32
|
+
preferCatalogPrice: ctx.leastCost.preferCatalogPrice,
|
|
33
|
+
flightRecorder: ctx.flightRecorder,
|
|
34
|
+
priorsScope: ctx.leastCost.priorsScope,
|
|
35
|
+
ownerPrincipal: resolveOwnerPrincipal(getRequestContext()),
|
|
36
|
+
});
|
|
37
|
+
const config = toRouterConfig(ctx.leastCost);
|
|
38
|
+
const req = { prompt };
|
|
39
|
+
if (singleProvider || mode === "cheapest") {
|
|
40
|
+
const decision = selectCandidate(req, env, config);
|
|
41
|
+
if (!decision.chosen)
|
|
42
|
+
return { ok: false, error: selectorFailureMessage(decision) };
|
|
43
|
+
return { ok: true, providers: [decision.chosen.provider] };
|
|
44
|
+
}
|
|
45
|
+
const decisions = selectCheapestPerTier(req, env, config);
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const providers = [];
|
|
48
|
+
for (const decision of decisions) {
|
|
49
|
+
if (!decision.chosen || seen.has(decision.chosen.provider))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(decision.chosen.provider);
|
|
52
|
+
providers.push(decision.chosen.provider);
|
|
53
|
+
}
|
|
54
|
+
if (providers.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: "least-cost routing found no eligible provider for select='cheapest_per_tier' (fail closed).",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, providers };
|
|
61
|
+
}
|
|
8
62
|
export function buildValidationSchemas(deps) {
|
|
9
63
|
const apiNames = (deps.apiProviders ?? []).map(p => p.name);
|
|
10
64
|
const allowed = [...CLI_TYPES, ...apiNames];
|
|
@@ -57,6 +111,13 @@ function findHumanReadableReport(value) {
|
|
|
57
111
|
}
|
|
58
112
|
export function registerValidationTools(server, deps) {
|
|
59
113
|
const { providerSchema, providerListSchema, normalizedProviderResultSchema } = buildValidationSchemas(deps);
|
|
114
|
+
const selectionContext = {
|
|
115
|
+
asyncJobManager: deps.asyncJobManager,
|
|
116
|
+
apiProviders: deps.apiProviders ?? [],
|
|
117
|
+
leastCost: deps.leastCost ?? loadLeastCostConfig(),
|
|
118
|
+
performanceMetrics: deps.performanceMetrics ?? new PerformanceMetrics(),
|
|
119
|
+
flightRecorder: deps.flightRecorder,
|
|
120
|
+
};
|
|
60
121
|
server.tool("validate_with_models", "Ask two or more provider CLIs to independently validate a question. Starts validation jobs — poll with job_status, collect with job_result (not llm_job_*).", {
|
|
61
122
|
question: z.string().min(1).describe("Question or content to validate."),
|
|
62
123
|
models: providerListSchema.describe("Providers to ask. Defaults to Claude and Codex."),
|
|
@@ -67,45 +128,71 @@ export function registerValidationTools(server, deps) {
|
|
|
67
128
|
judgeModel: providerSchema
|
|
68
129
|
.optional()
|
|
69
130
|
.describe("Optional provider to run an explicit judge synthesis job."),
|
|
131
|
+
select: selectSchema,
|
|
70
132
|
}, {
|
|
71
133
|
title: "Multi-model validation",
|
|
72
134
|
readOnlyHint: false,
|
|
73
135
|
destructiveHint: true,
|
|
74
136
|
idempotentHint: false,
|
|
75
137
|
openWorldHint: true,
|
|
76
|
-
}, async ({ question, models, focus, judgeModel }) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
138
|
+
}, async ({ question, models, focus, judgeModel, select }) => {
|
|
139
|
+
let providers = models;
|
|
140
|
+
if (select) {
|
|
141
|
+
const resolved = resolveSelectedProviders(selectionContext, question, select, false);
|
|
142
|
+
if (!resolved.ok) {
|
|
143
|
+
return textResponse({
|
|
144
|
+
success: false,
|
|
145
|
+
tool: "validate_with_models",
|
|
146
|
+
error: resolved.error,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
providers = resolved.providers;
|
|
150
|
+
}
|
|
151
|
+
return textResponse({
|
|
152
|
+
success: true,
|
|
153
|
+
tool: "validate_with_models",
|
|
154
|
+
readMostly: true,
|
|
155
|
+
report: startValidationRun(deps, {
|
|
156
|
+
intent: "validate",
|
|
157
|
+
question,
|
|
158
|
+
providers,
|
|
159
|
+
focus,
|
|
160
|
+
judgeProvider: judgeModel,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
});
|
|
88
164
|
server.tool("second_opinion", "Ask one provider CLI to review an answer (starts a validation job; poll job_status, collect job_result).", {
|
|
89
165
|
answer: z.string().min(1).describe("Answer to review."),
|
|
90
166
|
question: z.string().optional().describe("Original question, if available."),
|
|
91
167
|
model: providerSchema.default("codex").describe("Provider to ask for the second opinion."),
|
|
168
|
+
select: selectSchema,
|
|
92
169
|
}, {
|
|
93
170
|
title: "Second opinion",
|
|
94
171
|
readOnlyHint: false,
|
|
95
172
|
destructiveHint: true,
|
|
96
173
|
idempotentHint: false,
|
|
97
174
|
openWorldHint: true,
|
|
98
|
-
}, async ({ answer, question, model }) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
175
|
+
}, async ({ answer, question, model, select }) => {
|
|
176
|
+
let providers = [model];
|
|
177
|
+
if (select) {
|
|
178
|
+
const resolved = resolveSelectedProviders(selectionContext, answer, select, true);
|
|
179
|
+
if (!resolved.ok) {
|
|
180
|
+
return textResponse({ success: false, tool: "second_opinion", error: resolved.error });
|
|
181
|
+
}
|
|
182
|
+
providers = resolved.providers;
|
|
183
|
+
}
|
|
184
|
+
return textResponse({
|
|
185
|
+
success: true,
|
|
186
|
+
tool: "second_opinion",
|
|
187
|
+
readMostly: true,
|
|
188
|
+
report: startValidationRun(deps, {
|
|
189
|
+
intent: "second_opinion",
|
|
190
|
+
question,
|
|
191
|
+
content: answer,
|
|
192
|
+
providers,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
109
196
|
server.tool("compare_answers", "Summarize agreement/differences between caller-provided answers LOCALLY — does not call any provider.", {
|
|
110
197
|
question: z.string().min(1).describe("Question the answers respond to."),
|
|
111
198
|
answers: z.array(z.string().min(1)).min(2).describe("Two or more answers to compare."),
|
|
@@ -134,61 +221,94 @@ export function registerValidationTools(server, deps) {
|
|
|
134
221
|
.default("normal")
|
|
135
222
|
.describe("How aggressively to review."),
|
|
136
223
|
models: providerListSchema.describe("Providers to ask for adversarial review."),
|
|
224
|
+
select: selectSchema,
|
|
137
225
|
}, {
|
|
138
226
|
title: "Red-team review",
|
|
139
227
|
readOnlyHint: false,
|
|
140
228
|
destructiveHint: true,
|
|
141
229
|
idempotentHint: false,
|
|
142
230
|
openWorldHint: true,
|
|
143
|
-
}, async ({ content, riskLevel, models }) =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
providers
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
231
|
+
}, async ({ content, riskLevel, models, select }) => {
|
|
232
|
+
let providers = models;
|
|
233
|
+
if (select) {
|
|
234
|
+
const resolved = resolveSelectedProviders(selectionContext, content, select, false);
|
|
235
|
+
if (!resolved.ok) {
|
|
236
|
+
return textResponse({ success: false, tool: "red_team_review", error: resolved.error });
|
|
237
|
+
}
|
|
238
|
+
providers = resolved.providers;
|
|
239
|
+
}
|
|
240
|
+
return textResponse({
|
|
241
|
+
success: true,
|
|
242
|
+
tool: "red_team_review",
|
|
243
|
+
readMostly: true,
|
|
244
|
+
report: startValidationRun(deps, {
|
|
245
|
+
intent: "red_team",
|
|
246
|
+
content,
|
|
247
|
+
providers,
|
|
248
|
+
riskLevel,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
});
|
|
154
252
|
server.tool("consensus_check", "Ask provider CLIs whether they agree or disagree with a claim (starts validation jobs).", {
|
|
155
253
|
claim: z.string().min(1).describe("Claim to check across providers."),
|
|
156
254
|
models: providerListSchema.describe("Providers to ask for agreement or disagreement."),
|
|
255
|
+
select: selectSchema,
|
|
157
256
|
}, {
|
|
158
257
|
title: "Consensus check",
|
|
159
258
|
readOnlyHint: false,
|
|
160
259
|
destructiveHint: true,
|
|
161
260
|
idempotentHint: false,
|
|
162
261
|
openWorldHint: true,
|
|
163
|
-
}, async ({ claim, models }) =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
providers
|
|
171
|
-
}
|
|
172
|
-
|
|
262
|
+
}, async ({ claim, models, select }) => {
|
|
263
|
+
let providers = models;
|
|
264
|
+
if (select) {
|
|
265
|
+
const resolved = resolveSelectedProviders(selectionContext, claim, select, false);
|
|
266
|
+
if (!resolved.ok) {
|
|
267
|
+
return textResponse({ success: false, tool: "consensus_check", error: resolved.error });
|
|
268
|
+
}
|
|
269
|
+
providers = resolved.providers;
|
|
270
|
+
}
|
|
271
|
+
return textResponse({
|
|
272
|
+
success: true,
|
|
273
|
+
tool: "consensus_check",
|
|
274
|
+
readMostly: true,
|
|
275
|
+
report: startValidationRun(deps, {
|
|
276
|
+
intent: "consensus",
|
|
277
|
+
content: claim,
|
|
278
|
+
providers,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
});
|
|
173
282
|
server.tool("ask_model", "Ask one provider CLI a question through the simplified validation surface (starts a validation job).", {
|
|
174
283
|
question: z.string().min(1).describe("Question for one provider."),
|
|
175
284
|
model: providerSchema.default("claude").describe("Provider to ask."),
|
|
285
|
+
select: selectSchema,
|
|
176
286
|
}, {
|
|
177
287
|
title: "Ask one model",
|
|
178
288
|
readOnlyHint: false,
|
|
179
289
|
destructiveHint: true,
|
|
180
290
|
idempotentHint: false,
|
|
181
291
|
openWorldHint: true,
|
|
182
|
-
}, async ({ question, model }) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
providers
|
|
190
|
-
}
|
|
191
|
-
|
|
292
|
+
}, async ({ question, model, select }) => {
|
|
293
|
+
let providers = [model];
|
|
294
|
+
if (select) {
|
|
295
|
+
const resolved = resolveSelectedProviders(selectionContext, question, select, true);
|
|
296
|
+
if (!resolved.ok) {
|
|
297
|
+
return textResponse({ success: false, tool: "ask_model", error: resolved.error });
|
|
298
|
+
}
|
|
299
|
+
providers = resolved.providers;
|
|
300
|
+
}
|
|
301
|
+
return textResponse({
|
|
302
|
+
success: true,
|
|
303
|
+
tool: "ask_model",
|
|
304
|
+
readMostly: true,
|
|
305
|
+
report: startValidationRun(deps, {
|
|
306
|
+
intent: "ask_model",
|
|
307
|
+
question,
|
|
308
|
+
providers,
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
311
|
+
});
|
|
192
312
|
server.tool("synthesize_validation", "Run an explicit judge model over already-collected validation results to produce a synthesis.", {
|
|
193
313
|
question: z.string().min(1).describe("Original request that was validated."),
|
|
194
314
|
providerResults: z
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "llm-cli-gateway",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.17.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "Secure local control plane for AI coding agents across supported MCP clients, with workspace-scoped remote access, approval gates, durable jobs, sessions, and audit receipts.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
".agents/skills/secure-orchestration/SKILL.md",
|
|
61
61
|
".agents/skills/implement-review-fix/SKILL.md",
|
|
62
62
|
".agents/skills/retrospective-walk/SKILL.md",
|
|
63
|
-
".agents/skills/public-demo-session/SKILL.md"
|
|
63
|
+
".agents/skills/public-demo-session/SKILL.md",
|
|
64
|
+
".agents/skills/least-cost-routing/SKILL.md"
|
|
64
65
|
],
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -88,8 +89,12 @@
|
|
|
88
89
|
"site:generate": "node scripts/generate-site-discovery.mjs",
|
|
89
90
|
"site:generate:check": "node scripts/generate-site-discovery.mjs --check",
|
|
90
91
|
"site:validate": "node scripts/validate-site-discovery.mjs",
|
|
91
|
-
"
|
|
92
|
-
"
|
|
92
|
+
"supply-chain:scan": "node scripts/supply-chain/dep-drift-scan.mjs --advisory",
|
|
93
|
+
"supply-chain:scan:check": "node scripts/supply-chain/dep-drift-scan.mjs --frozen",
|
|
94
|
+
"supply-chain:seed": "node scripts/supply-chain/dep-drift-scan.mjs --seed",
|
|
95
|
+
"test:supply-chain": "vitest run scripts/supply-chain/dep-drift-scan.test.mjs",
|
|
96
|
+
"lint": "eslint src --ignore-pattern 'src/__tests__/**'",
|
|
97
|
+
"lint:fix": "eslint src --ignore-pattern 'src/__tests__/**' --fix",
|
|
93
98
|
"format": "prettier --write 'src/**/*.ts'",
|
|
94
99
|
"format:check": "prettier --check 'src/**/*.ts'",
|
|
95
100
|
"security:audit": "bash scripts/release-security-audit.sh",
|
package/setup/status.schema.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"client_config",
|
|
19
19
|
"cache_awareness",
|
|
20
20
|
"provider_capabilities",
|
|
21
|
+
"least_cost",
|
|
21
22
|
"upstream",
|
|
22
23
|
"next_actions"
|
|
23
24
|
],
|
|
@@ -480,6 +481,73 @@
|
|
|
480
481
|
},
|
|
481
482
|
"additionalProperties": false
|
|
482
483
|
},
|
|
484
|
+
"least_cost": {
|
|
485
|
+
"type": "object",
|
|
486
|
+
"description": "LCR phase_3: least-cost-routing eligibility summary. Economics-only and secret-free (model names, pricing families, price provenance, tiers, telemetry tiers, price-table staleness, anonymized calibration buckets). ALWAYS present; compact (empty arrays) when routing is disabled.",
|
|
487
|
+
"required": ["enabled", "pricing", "providers", "untieredModels", "calibrationQuality"],
|
|
488
|
+
"properties": {
|
|
489
|
+
"enabled": { "type": "boolean" },
|
|
490
|
+
"pricing": {
|
|
491
|
+
"type": "object",
|
|
492
|
+
"required": ["tableAsOf", "apiCatalogAsOf", "staleDays"],
|
|
493
|
+
"properties": {
|
|
494
|
+
"tableAsOf": { "type": "string" },
|
|
495
|
+
"apiCatalogAsOf": { "type": "string" },
|
|
496
|
+
"staleDays": { "type": "integer", "minimum": 0 }
|
|
497
|
+
},
|
|
498
|
+
"additionalProperties": false
|
|
499
|
+
},
|
|
500
|
+
"providers": {
|
|
501
|
+
"type": "array",
|
|
502
|
+
"items": {
|
|
503
|
+
"type": "object",
|
|
504
|
+
"required": ["provider", "telemetryTier", "candidates"],
|
|
505
|
+
"properties": {
|
|
506
|
+
"provider": { "type": "string" },
|
|
507
|
+
"telemetryTier": { "enum": ["T1", "T2", "T3", "T4"] },
|
|
508
|
+
"candidates": {
|
|
509
|
+
"type": "array",
|
|
510
|
+
"items": {
|
|
511
|
+
"type": "object",
|
|
512
|
+
"required": ["model", "family", "priced", "priceSource", "tier"],
|
|
513
|
+
"properties": {
|
|
514
|
+
"model": { "type": "string" },
|
|
515
|
+
"family": { "type": "string" },
|
|
516
|
+
"priced": { "type": "boolean" },
|
|
517
|
+
"priceSource": { "enum": ["table", "api-catalog", "unknown"] },
|
|
518
|
+
"tier": {
|
|
519
|
+
"type": ["string", "null"],
|
|
520
|
+
"enum": ["economy", "standard", "frontier", null]
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
"additionalProperties": false
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
"additionalProperties": false
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
"untieredModels": {
|
|
531
|
+
"type": "array",
|
|
532
|
+
"items": { "type": "string" }
|
|
533
|
+
},
|
|
534
|
+
"calibrationQuality": {
|
|
535
|
+
"type": "array",
|
|
536
|
+
"items": {
|
|
537
|
+
"type": "object",
|
|
538
|
+
"required": ["bucket", "k", "samples", "confidence"],
|
|
539
|
+
"properties": {
|
|
540
|
+
"bucket": { "type": "string" },
|
|
541
|
+
"k": { "type": "number" },
|
|
542
|
+
"samples": { "type": "integer", "minimum": 0 },
|
|
543
|
+
"confidence": { "enum": ["high", "medium", "low"] }
|
|
544
|
+
},
|
|
545
|
+
"additionalProperties": false
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
"additionalProperties": false
|
|
550
|
+
},
|
|
483
551
|
"api_providers": {
|
|
484
552
|
"type": "object",
|
|
485
553
|
"description": "Slice 6: health of enabled [providers.<name>] (kind:api) providers. OMITTED entirely when none are enabled (dormant byte-identical).",
|