codecartographer-pi 0.25.0 → 0.26.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/.codecarto/broadside/SKILL.md +4 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +2 -2
- package/dist/core/broadside/client.d.ts +56 -0
- package/dist/core/broadside/client.js +200 -0
- package/dist/core/broadside/collect.d.ts +68 -0
- package/dist/core/broadside/collect.js +676 -0
- package/dist/core/broadside/constants.d.ts +51 -0
- package/dist/core/broadside/constants.js +74 -0
- package/dist/core/broadside/lenses.d.ts +31 -0
- package/dist/core/broadside/lenses.js +312 -0
- package/dist/core/broadside/models.d.ts +46 -0
- package/dist/core/broadside/models.js +321 -0
- package/dist/core/broadside/render.d.ts +20 -0
- package/dist/core/broadside/render.js +285 -0
- package/dist/core/broadside/repo.d.ts +58 -0
- package/dist/core/broadside/repo.js +592 -0
- package/dist/core/broadside/requests.d.ts +23 -0
- package/dist/core/broadside/requests.js +71 -0
- package/dist/core/broadside/results.d.ts +36 -0
- package/dist/core/broadside/results.js +163 -0
- package/dist/core/broadside/schemas.d.ts +2 -0
- package/dist/core/broadside/schemas.js +342 -0
- package/dist/core/broadside/state.d.ts +99 -0
- package/dist/core/broadside/state.js +384 -0
- package/dist/core/broadside/submit.d.ts +30 -0
- package/dist/core/broadside/submit.js +350 -0
- package/dist/core/broadside/types.d.ts +491 -0
- package/dist/core/broadside/types.js +107 -0
- package/dist/core/{broadside-verify.d.ts → broadside/verify.d.ts} +23 -2
- package/dist/core/{broadside-verify.js → broadside/verify.js} +43 -5
- package/dist/core/broadside.d.ts +14 -952
- package/dist/core/broadside.js +25 -3726
- package/dist/core/completion.js +91 -72
- package/dist/core/dashboard-writer.js +9 -1
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/library.d.ts +24 -1
- package/dist/core/library.js +46 -15
- package/dist/core/orchestrator-config.js +22 -8
- package/dist/core/status.d.ts +42 -23
- package/dist/core/status.js +163 -137
- package/dist/core/workspace.d.ts +2 -0
- package/dist/core/workspace.js +49 -25
- package/dist/core/yaml.js +9 -3
- package/dist/extensions/codecarto/auto-runner.js +41 -23
- package/dist/extensions/codecarto/index.js +9 -4
- package/dist/extensions/codecarto/phase-compaction.js +6 -2
- package/dist/mcp-server/server.js +15 -4
- package/package.json +1 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// runBroadsideSubmit — pricing, confirmation, incremental scope, one batch per lens — and the stored lens results collect reads back.
|
|
2
|
+
//
|
|
3
|
+
// Split out of core/broadside.ts (#339); the barrel there re-exports every
|
|
4
|
+
// name, so `core/index.ts` and the tests see one module as before.
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { BROADSIDE_DIR, BROADSIDE_LENS_IDS, BROADSIDE_MODEL } from "./constants.js";
|
|
10
|
+
import { BroadsideCancelledError } from "./types.js";
|
|
11
|
+
import { getLens } from "./lenses.js";
|
|
12
|
+
import { BROADSIDE_LANGUAGES, MANIFEST_CANDIDATES, changedFilesSince, collectRepoInfo, gatherSlices, gitDirty, gitHead } from "./repo.js";
|
|
13
|
+
import { buildBatchRequest, estimateCost } from "./requests.js";
|
|
14
|
+
import { broadsideDirFor, loadBroadsideConfig, loadBroadsideState, persistBroadsideRun } from "./state.js";
|
|
15
|
+
import { recordBatchEndpoints, resolveCatalogEntry } from "./models.js";
|
|
16
|
+
import { submitBatch } from "./client.js";
|
|
17
|
+
// ---------- run orchestration ----------
|
|
18
|
+
/** `<ISO timestamp with : and . as ->-<4 hex>`, unused by any recorded run or run directory. */
|
|
19
|
+
function uniqueRunId(taken, broadsideDir) {
|
|
20
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
21
|
+
for (;;) {
|
|
22
|
+
const candidate = `${stamp}-${randomBytes(2).toString("hex")}`;
|
|
23
|
+
if (!taken.includes(candidate) && !existsSync(join(broadsideDir, candidate)))
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
28
|
+
const lensIds = opts.lenses ?? BROADSIDE_LENS_IDS;
|
|
29
|
+
const broadsideDir = broadsideDirFor(cwd);
|
|
30
|
+
const model = opts.model ?? BROADSIDE_MODEL;
|
|
31
|
+
// Resolve a catalog entry per distinct model before anything is submitted:
|
|
32
|
+
// the guardrail must know real per-token rates, and every lens requires
|
|
33
|
+
// structured-output support that not all batch models offer. Lenses may run
|
|
34
|
+
// on different models (config `lens_models`), so each one is pre-flighted.
|
|
35
|
+
const config = await loadBroadsideConfig(broadsideDir);
|
|
36
|
+
const redact = config.redactSecrets;
|
|
37
|
+
const info = await collectRepoInfo(cwd, { redact });
|
|
38
|
+
// Before pricing, before the network, before any state write: a run on a
|
|
39
|
+
// language the lenses cannot scan used to submit empty batches and pay for
|
|
40
|
+
// them (#250).
|
|
41
|
+
if (info.language === "unknown") {
|
|
42
|
+
throw new Error(`Broad-Side could not tell what language this repository is: no ${MANIFEST_CANDIDATES.map(([candidate]) => candidate).join(", ")} ` +
|
|
43
|
+
`and no source files in a language the lenses can scan (${BROADSIDE_LANGUAGES.join(", ")}). Nothing was submitted.`);
|
|
44
|
+
}
|
|
45
|
+
if (info.sourceFileCount === 0) {
|
|
46
|
+
throw new Error(`Broad-Side found no ${info.language} source files to scan (detected from ${info.manifest?.path ?? "the file counts"}; ` +
|
|
47
|
+
`the lenses look for ${info.sourceExts.join(", ")}). Nothing was submitted.`);
|
|
48
|
+
}
|
|
49
|
+
const lensModels = { ...config.lensModels, ...opts.lensModels };
|
|
50
|
+
const modelForLens = (lensId) => lensModels[lensId] ?? model;
|
|
51
|
+
const resolved = new Map();
|
|
52
|
+
for (const candidate of new Set([model, ...lensIds.map(modelForLens)])) {
|
|
53
|
+
const catalog = await resolveCatalogEntry(broadsideDir, config, candidate, apiKey, opts.fetcher);
|
|
54
|
+
const entry = catalog.entry;
|
|
55
|
+
const supportsStructuredOutputs = entry.supportedParameters.length === 0 ||
|
|
56
|
+
entry.supportedParameters.some((p) => ["structured_outputs", "json_schema", "response_format", "structuredoutputs"].includes(p.toLowerCase()));
|
|
57
|
+
if (!supportsStructuredOutputs) {
|
|
58
|
+
throw new Error(`Batch model "${candidate}" does not advertise structured-output support ` +
|
|
59
|
+
`(supported_parameters: ${entry.supportedParameters.join(", ") || "unknown"}), but every ` +
|
|
60
|
+
"Broad-Side lens requires json_schema response_format. Choose another batch model " +
|
|
61
|
+
"(codecarto_broadside action 'models') or pass a pricing override only if you know it works.");
|
|
62
|
+
}
|
|
63
|
+
resolved.set(candidate, {
|
|
64
|
+
entry,
|
|
65
|
+
supportsStructuredOutputs,
|
|
66
|
+
pricing: { inputPerM: entry.inputPerM, outputPerM: entry.outputPerM, source: catalog.source },
|
|
67
|
+
// Respect the provider's completion ceiling: a request asking for more
|
|
68
|
+
// output than the model can produce fails the whole batch.
|
|
69
|
+
...(entry.maxCompletionTokens !== undefined && { outputCap: entry.maxCompletionTokens }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const pricing = resolved.get(model).pricing;
|
|
73
|
+
const outputCap = resolved.get(model).outputCap;
|
|
74
|
+
const defaultEntry = resolved.get(model).entry;
|
|
75
|
+
const limit = opts.maxCost ?? config.maxCost;
|
|
76
|
+
// Incremental re-scouting (#142): diff against the previous run's HEAD
|
|
77
|
+
// and scan only the modules whose files changed. Falls back to a full
|
|
78
|
+
// scan when there is no prior run, the tree is dirty, or the diff fails.
|
|
79
|
+
const sourceHead = await gitHead(cwd);
|
|
80
|
+
const sourceDirty = await gitDirty(cwd);
|
|
81
|
+
let baseHead = null;
|
|
82
|
+
let changed = null;
|
|
83
|
+
const incrementalOutcome = {
|
|
84
|
+
requested: opts.incremental === true,
|
|
85
|
+
applied: false,
|
|
86
|
+
baseHead: null,
|
|
87
|
+
};
|
|
88
|
+
if (opts.incremental) {
|
|
89
|
+
const state = await loadBroadsideState(broadsideDir);
|
|
90
|
+
// The baseline is the most recent run that recorded a HEAD — a
|
|
91
|
+
// submit-only run (never collected) is still a valid committed base.
|
|
92
|
+
const previous = [...state.runs].reverse().find((r) => r.sourceHead);
|
|
93
|
+
if (sourceDirty) {
|
|
94
|
+
incrementalOutcome.reason = "dirty-worktree";
|
|
95
|
+
}
|
|
96
|
+
else if (!previous?.sourceHead) {
|
|
97
|
+
incrementalOutcome.reason = "no-baseline";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
baseHead = previous.sourceHead;
|
|
101
|
+
changed = await changedFilesSince(cwd, baseHead);
|
|
102
|
+
incrementalOutcome.baseHead = baseHead;
|
|
103
|
+
if (changed)
|
|
104
|
+
incrementalOutcome.applied = true;
|
|
105
|
+
else
|
|
106
|
+
incrementalOutcome.reason = "diff-failed";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Slice offline first so the estimate covers every request we would send.
|
|
110
|
+
const slicesByLens = new Map();
|
|
111
|
+
// Why a lens ended up with nothing to submit, for the report (see below).
|
|
112
|
+
const skipReasons = new Map();
|
|
113
|
+
let estimatedInputTokens = 0;
|
|
114
|
+
let estimatedOutputTokens = 0;
|
|
115
|
+
let estimatedTotalCost = 0;
|
|
116
|
+
const perLensEstimate = [];
|
|
117
|
+
// What the redaction pass did across every lens's slices, for the run
|
|
118
|
+
// record and the report: a value in a file shared by two lenses counts
|
|
119
|
+
// once per lens it was sent in, files once each.
|
|
120
|
+
let redactedValues = info.redactedValues;
|
|
121
|
+
const redactedFiles = new Set();
|
|
122
|
+
for (const lensId of lensIds) {
|
|
123
|
+
const lens = getLens(lensId);
|
|
124
|
+
let slices = await gatherSlices(cwd, lens, info, { redact });
|
|
125
|
+
for (const slice of slices) {
|
|
126
|
+
redactedValues += slice.redactedValues ?? 0;
|
|
127
|
+
for (const file of slice.redactedFiles ?? [])
|
|
128
|
+
redactedFiles.add(file);
|
|
129
|
+
}
|
|
130
|
+
const matchedBeforeIncremental = slices.length;
|
|
131
|
+
if (changed) {
|
|
132
|
+
// Repo-info slices (empty files, e.g. architecture) always run;
|
|
133
|
+
// file-backed slices run only when one of their files changed.
|
|
134
|
+
slices = slices.filter((s) => s.files.length === 0 || s.files.some((f) => changed.has(f)));
|
|
135
|
+
}
|
|
136
|
+
slicesByLens.set(lensId, slices);
|
|
137
|
+
if (slices.length === 0) {
|
|
138
|
+
const globs = lens.globsFor(info).filter(Boolean);
|
|
139
|
+
const fallbackGlobs = lens.fallbackGlobsFor?.(info).filter(Boolean) ?? [];
|
|
140
|
+
skipReasons.set(lensId, globs.length === 0
|
|
141
|
+
? "the lens has no file patterns for this language"
|
|
142
|
+
: matchedBeforeIncremental > 0
|
|
143
|
+
? "incremental: none of this lens's files changed since the previous run"
|
|
144
|
+
: `no files matched ${globs.join(", ")}` +
|
|
145
|
+
(fallbackGlobs.length > 0 ? ` or the fallback ${fallbackGlobs.join(", ")}` : "") +
|
|
146
|
+
(lens.skipTestFiles ? " (test files excluded)" : ""));
|
|
147
|
+
}
|
|
148
|
+
const lensModel = modelForLens(lensId);
|
|
149
|
+
const { pricing: lensPricing, outputCap: lensOutputCap } = resolved.get(lensModel);
|
|
150
|
+
const maxTokens = lensOutputCap ? Math.min(lens.maxTokens, lensOutputCap) : lens.maxTokens;
|
|
151
|
+
const estimate = estimateCost(lens, slices, lensPricing, maxTokens, info);
|
|
152
|
+
estimatedInputTokens += estimate.inputTokens;
|
|
153
|
+
estimatedOutputTokens += estimate.outputTokens;
|
|
154
|
+
estimatedTotalCost += estimate.cost;
|
|
155
|
+
perLensEstimate.push({
|
|
156
|
+
lens,
|
|
157
|
+
cost: estimate.cost,
|
|
158
|
+
maxTokens,
|
|
159
|
+
lensModel,
|
|
160
|
+
lensPricing,
|
|
161
|
+
...(lensOutputCap !== undefined && { lensOutputCap }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const exceedsLimit = limit > 0 && estimatedTotalCost > limit;
|
|
165
|
+
if (opts.confirm) {
|
|
166
|
+
const approved = await opts.confirm({
|
|
167
|
+
model,
|
|
168
|
+
pricing,
|
|
169
|
+
lenses: perLensEstimate.map(({ lens, cost, maxTokens, lensModel, lensPricing }) => {
|
|
170
|
+
const fallback = (slicesByLens.get(lens.id) ?? []).find((slice) => slice.fallback)?.fallback;
|
|
171
|
+
return {
|
|
172
|
+
lensId: lens.id,
|
|
173
|
+
name: lens.name,
|
|
174
|
+
slices: (slicesByLens.get(lens.id) ?? []).length,
|
|
175
|
+
maxTokens,
|
|
176
|
+
cost,
|
|
177
|
+
model: lensModel,
|
|
178
|
+
pricing: lensPricing,
|
|
179
|
+
...(fallback && { fallback }),
|
|
180
|
+
};
|
|
181
|
+
}),
|
|
182
|
+
mixedModels: perLensEstimate.some(({ lensModel }) => lensModel !== model),
|
|
183
|
+
totalCost: estimatedTotalCost,
|
|
184
|
+
inputTokens: estimatedInputTokens,
|
|
185
|
+
outputTokens: estimatedOutputTokens,
|
|
186
|
+
maxCost: limit,
|
|
187
|
+
exceedsLimit,
|
|
188
|
+
baseHead,
|
|
189
|
+
sourceDirty,
|
|
190
|
+
incremental: incrementalOutcome,
|
|
191
|
+
...(outputCap !== undefined && { outputCap }),
|
|
192
|
+
});
|
|
193
|
+
if (!approved)
|
|
194
|
+
throw new BroadsideCancelledError();
|
|
195
|
+
}
|
|
196
|
+
else if (exceedsLimit && !opts.force) {
|
|
197
|
+
const breakdown = perLensEstimate
|
|
198
|
+
.map(({ lens, cost, lensModel }) => ` ${lens.name}: ~$${cost.toFixed(4)}${lensModel === model ? "" : ` (${lensModel})`}`)
|
|
199
|
+
.join("\n");
|
|
200
|
+
throw new Error(`Estimated Broad-Side cost ~$${estimatedTotalCost.toFixed(4)} exceeds the run limit ` +
|
|
201
|
+
`$${limit.toFixed(2)}. Nothing was submitted.\nBreakdown:\n${breakdown}\n` +
|
|
202
|
+
`Pass force: true to submit anyway, or raise max_cost in .codecarto/broadside/config.yaml.`);
|
|
203
|
+
}
|
|
204
|
+
// Read before anything is posted: a state.json that cannot be read refuses
|
|
205
|
+
// the run here (#233), while persistBroadsideRun below merges by run id.
|
|
206
|
+
const existing = await loadBroadsideState(broadsideDir);
|
|
207
|
+
// The timestamp alone collided when two submits landed in one millisecond
|
|
208
|
+
// (#367): the second run's record replaced the first's and both shared an
|
|
209
|
+
// output directory. A short random suffix, checked against the runs on
|
|
210
|
+
// record and the directories on disk, keeps every run its own.
|
|
211
|
+
const runId = uniqueRunId(existing.runs.map((candidate) => candidate.id), broadsideDir);
|
|
212
|
+
const run = {
|
|
213
|
+
id: runId,
|
|
214
|
+
createdAt: new Date().toISOString(),
|
|
215
|
+
model,
|
|
216
|
+
lenses: [...lensIds],
|
|
217
|
+
status: "in-flight",
|
|
218
|
+
outputDir: runId,
|
|
219
|
+
batches: {},
|
|
220
|
+
synthesis: { status: "pending" },
|
|
221
|
+
triage: { status: "pending" },
|
|
222
|
+
pricing,
|
|
223
|
+
maxCost: limit > 0 ? limit : undefined,
|
|
224
|
+
outputCap,
|
|
225
|
+
sourceHead,
|
|
226
|
+
sourceDirty,
|
|
227
|
+
baseHead,
|
|
228
|
+
snapshot: info.snapshot,
|
|
229
|
+
language: info.language,
|
|
230
|
+
redaction: {
|
|
231
|
+
enabled: redact,
|
|
232
|
+
values: redactedValues,
|
|
233
|
+
files: redactedFiles.size,
|
|
234
|
+
skippedFiles: info.secretFilesSkipped.length,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
await persistBroadsideRun(broadsideDir, run);
|
|
238
|
+
const requestsByCustomId = {};
|
|
239
|
+
const submissions = [];
|
|
240
|
+
// Submit from the estimate rather than recomputing: the user approved that
|
|
241
|
+
// breakdown, so the request that fires must be the one that was priced.
|
|
242
|
+
for (const priced of perLensEstimate) {
|
|
243
|
+
const { lens, maxTokens, lensModel, lensOutputCap } = priced;
|
|
244
|
+
const lensId = lens.id;
|
|
245
|
+
const slices = slicesByLens.get(lensId) ?? [];
|
|
246
|
+
const requests = slices.map((sl, i) => buildBatchRequest(lens, info, sl, i, slices.length, lensModel, maxTokens, config.reasoning ?? undefined));
|
|
247
|
+
for (const request of requests)
|
|
248
|
+
requestsByCustomId[request.custom_id] = request;
|
|
249
|
+
const fallback = slices.find((slice) => slice.fallback)?.fallback;
|
|
250
|
+
const entry = {
|
|
251
|
+
batchId: "",
|
|
252
|
+
requests: requests.length,
|
|
253
|
+
status: "submitting",
|
|
254
|
+
submittedAt: new Date().toISOString(),
|
|
255
|
+
estimatedCost: priced.cost,
|
|
256
|
+
// A scan of the fallback scope is recorded as such (#319).
|
|
257
|
+
...(fallback && { fallback }),
|
|
258
|
+
// Recorded per lens so collect's truncation retry re-submits against
|
|
259
|
+
// the model and ceiling this lens actually used, not the run default.
|
|
260
|
+
...(lensModel !== model && { model: lensModel }),
|
|
261
|
+
...(lensOutputCap !== undefined && { outputCap: lensOutputCap }),
|
|
262
|
+
};
|
|
263
|
+
run.batches[lensId] = entry;
|
|
264
|
+
if (requests.length === 0) {
|
|
265
|
+
// No files matched the lens's globs. That is a coverage gap to
|
|
266
|
+
// report, not a batch to submit — the API rejects empty batches.
|
|
267
|
+
// Name the globs: a JavaScript service whose server lives at
|
|
268
|
+
// src/server.js gets no security review (that lens reads server/**,
|
|
269
|
+
// **/auth*, **/middleware/**), and "skipped (0 request(s))" alone
|
|
270
|
+
// read as an empty repository rather than a lens that looked in
|
|
271
|
+
// the wrong place.
|
|
272
|
+
entry.status = "skipped";
|
|
273
|
+
const reason = skipReasons.get(lensId);
|
|
274
|
+
if (reason)
|
|
275
|
+
entry.reason = reason;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
submissions.push((async () => {
|
|
279
|
+
// A network-level throw (DNS, abort, TLS) must not strand the
|
|
280
|
+
// entry in "submitting" forever — allSettled would swallow the
|
|
281
|
+
// rejection and collect would never see a terminal status.
|
|
282
|
+
try {
|
|
283
|
+
const { batchId, status, error } = await submitBatch(requests, apiKey, opts.fetcher, lensModel);
|
|
284
|
+
entry.batchId = batchId;
|
|
285
|
+
entry.status = status;
|
|
286
|
+
if (error)
|
|
287
|
+
entry.error = error;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
entry.status = "rejected";
|
|
291
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
292
|
+
}
|
|
293
|
+
})());
|
|
294
|
+
}
|
|
295
|
+
await Promise.allSettled(submissions);
|
|
296
|
+
// A run with no batch behind it has nothing in flight. Every lens was
|
|
297
|
+
// skipped or refused, so no poll will ever complete it; leaving it
|
|
298
|
+
// "in-flight" had status listing a refused run above the completed ones
|
|
299
|
+
// with synthesis and triage "pending" forever.
|
|
300
|
+
if (!Object.values(run.batches).some((entry) => entry.batchId))
|
|
301
|
+
run.status = "failed";
|
|
302
|
+
await persistBroadsideRun(broadsideDir, run);
|
|
303
|
+
// What the provider just said about each model's batch endpoint outlives
|
|
304
|
+
// the run: the `models` action reads it back (#141).
|
|
305
|
+
await recordBatchEndpoints(broadsideDir, lensIds
|
|
306
|
+
.map((lensId) => run.batches[lensId])
|
|
307
|
+
.filter((entry) => Boolean(entry) && entry.status !== "skipped")
|
|
308
|
+
.map((entry) => ({ model: entry.model ?? model, batchId: entry.batchId, error: entry.error })));
|
|
309
|
+
// Persist the exact request bodies so collect can re-submit a truncated
|
|
310
|
+
// slice (bumped output cap) without re-walking the repo (#133). The run
|
|
311
|
+
// dir is created here rather than waiting for collect so a crash between
|
|
312
|
+
// submit and collect still leaves the retry input on disk.
|
|
313
|
+
const runDir = join(broadsideDir, runId);
|
|
314
|
+
await mkdir(runDir, { recursive: true });
|
|
315
|
+
await writeFile(join(runDir, "requests.json"), `${JSON.stringify(requestsByCustomId, null, "\t")}\n`, "utf8");
|
|
316
|
+
return {
|
|
317
|
+
runId,
|
|
318
|
+
outputDir: join(".codecarto", BROADSIDE_DIR, runId),
|
|
319
|
+
batches: run.batches,
|
|
320
|
+
estimatedTotalCost,
|
|
321
|
+
estimatedInputTokens,
|
|
322
|
+
estimatedOutputTokens,
|
|
323
|
+
pricing,
|
|
324
|
+
maxCost: limit > 0 ? limit : undefined,
|
|
325
|
+
// modelInfo describes the run's default model. Per-lens overrides are
|
|
326
|
+
// recorded on their own batch entries.
|
|
327
|
+
modelInfo: {
|
|
328
|
+
contextLength: defaultEntry.contextLength,
|
|
329
|
+
maxCompletionTokens: defaultEntry.maxCompletionTokens,
|
|
330
|
+
supportsStructuredOutputs: defaultEntry.supportedParameters.length === 0
|
|
331
|
+
? undefined
|
|
332
|
+
: resolved.get(model).supportsStructuredOutputs,
|
|
333
|
+
expirationDate: defaultEntry.expirationDate ?? null,
|
|
334
|
+
},
|
|
335
|
+
incremental: incrementalOutcome,
|
|
336
|
+
repo: {
|
|
337
|
+
language: info.language,
|
|
338
|
+
sourceFiles: info.sourceFileCount,
|
|
339
|
+
snapshot: info.snapshot,
|
|
340
|
+
sourceHead,
|
|
341
|
+
sourceDirty,
|
|
342
|
+
},
|
|
343
|
+
redaction: {
|
|
344
|
+
enabled: redact,
|
|
345
|
+
values: redactedValues,
|
|
346
|
+
files: redactedFiles.size,
|
|
347
|
+
skippedFiles: info.secretFilesSkipped,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|