mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
## Thinking Path
> - Paperclip coordinates agent work through Runner.
> - Scheduled Product E2E campaigns need measured cost and time
forecasts.
> - The saved forecast samples came from October 21.
> - The October 27 qualifications retain final usage for all 19 model
requests.
> - The Claude measurements are hosted; the Codex measurements are
local.
> - This change refreshes the samples and keeps measurement hosts
separate.
> - Operators can inspect the latest evidence without treating it as
current qualification or an invoice.
## Linked Issues or Issue Description
Refs #17127 and #17946.
**What happened?**
The scheduled forecast still uses the older pilot amounts. It reports no
hosted measurement and does not record the measurement host per sample.
**What did you expect to happen?**
The forecast must retain the latest complete model receipts and their
source hashes. It must keep local and hosted timing cohorts separate. A
changed definition must remain unqualified.
**Steps to reproduce**
Prepare the daily or weekly forecast from the default branch. Inspect
its saved measurement date, totals, and hosted fields.
## What Changed
- Refresh four samples from two intact request ledgers and four retained
Product results.
- Record measurement hosts, final request counts, and original campaign
outcomes.
- Add historical hosted totals. Current hosted totals still require
matching evidence for every selected cell.
- Keep hosts in separate per-cell cohorts. Preserve definition drift and
unknown slow-tail estimates.
- Document the model-cost scope and timing limits.
## Verification
- Forecast and scheduled-preflight tests: 33 passed.
- The extraction replays both hash-checked ledgers, requires final
receipts, and checks the retained pricecards. The four samples cover 19
final requests.
- Daily historical model subtotal: $0.428650100; summed attempt time:
107.286 seconds.
- Weekly historical model subtotal: $0.793676500; summed attempt time:
158.093 seconds. The Codex samples are local.
- Full Product support suite at the initial head: 4,510 passed, seven
failed, three skipped. A clean 6b9e5f3a baseline with the same installed
dependencies reproduces the same seven failures: two Copilot image
assertions, four workflow-selection shell assertions, and one database
startup test.
- Product typecheck also fails on that clean baseline because the shared
installed Runner workspace exports are stale. The fresh CI build remains
the typecheck gate.
- No provider request, new live campaign, budget change, or schedule
activation is part of this PR.
## Risks
- These are successful-attempt measurements, not a prediction of failure
frequency or a campaign invoice.
- The feedback source fingerprint changed after the samples.
Current-compatible totals remain unknown; no historical fingerprint is
rewritten.
- Hosted worker cases passed, but their original workflow failed during
report dependency setup. That outcome stays visible.
- Hosted detection deliberately accepts only the measured GitHub Ubuntu
environment. Other host labels remain unqualified until declared with
their own evidence.
- The weekly selection still has no complete hosted timing measurement.
Infrastructure, storage, and observability charges stay separate from
model spend.
## Model Used
OpenAI Codex, GPT-6 based, with tool execution and extended reasoning.
The exact serving model ID and context window are not exposed in this
session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
104 lines
6.2 KiB
TypeScript
104 lines
6.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { centsToUsd, unitsToCents, usdToUnits } from "../../packages/shared/src/money.js";
|
|
import measurements from "./scheduled-measurements.json" with { type: "json" };
|
|
import type { MatrixExecution } from "./types.js";
|
|
import type { EvalOperationsManifest } from "./operations.js";
|
|
|
|
type Sample = (typeof measurements.samples)[number];
|
|
const cohortKey = (sample: Sample) => JSON.stringify([sample.definitionHash, sample.model, sample.environmentId,
|
|
sample.runtimeMode, sample.route, sample.priceVersion, sample.measurementHost]);
|
|
const usd = (units: bigint) => centsToUsd(unitsToCents(units));
|
|
const average = (values: bigint[]) => values.length
|
|
? (values.reduce((sum, value) => sum + value, 0n) + BigInt(Math.floor(values.length / 2))) / BigInt(values.length) : null;
|
|
|
|
function statistics(values: bigint[]) {
|
|
const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
const middle = Math.floor(sorted.length / 2);
|
|
return {
|
|
count: sorted.length,
|
|
mean: average(sorted),
|
|
median: sorted.length ? average(sorted.length % 2 ? [sorted[middle]!] : [sorted[middle - 1]!, sorted[middle]!]) : null,
|
|
p95: sorted.length >= 20 ? sorted[Math.ceil(sorted.length * 0.95) - 1]! : null,
|
|
};
|
|
}
|
|
|
|
function summarize(samples: Sample[]) {
|
|
const cost = statistics(samples.map(sample => usdToUnits(sample.knownModelUsd)));
|
|
const time = statistics(samples.map(sample => BigInt(sample.attemptDurationMs)));
|
|
return {
|
|
samples: cost.count,
|
|
modelUsd: { mean: cost.mean === null ? null : usd(cost.mean), median: cost.median === null ? null : usd(cost.median),
|
|
p95: cost.p95 === null ? null : usd(cost.p95) },
|
|
attemptMs: { mean: time.mean === null ? null : Number(time.mean), median: time.median === null ? null : Number(time.median),
|
|
p95: time.p95 === null ? null : Number(time.p95) },
|
|
};
|
|
}
|
|
|
|
function summarizeCohort(samples: Sample[]) {
|
|
return summarize(new Set(samples.map(cohortKey)).size === 1 ? samples : []);
|
|
}
|
|
|
|
function total(rows: ReturnType<typeof summarize>[]) {
|
|
return {
|
|
modelUsd: rows.length && rows.every(row => row.modelUsd.mean !== null)
|
|
? usd(rows.reduce((sum, row) => sum + usdToUnits(row.modelUsd.mean!), 0n)) : null,
|
|
summedAttemptMs: rows.length && rows.every(row => row.attemptMs.mean !== null)
|
|
? rows.reduce((sum, row) => sum + row.attemptMs.mean!, 0) : null,
|
|
};
|
|
}
|
|
|
|
export function scheduledForecast(
|
|
executions: MatrixExecution[], models: EvalOperationsManifest["models"], snapshot = measurements,
|
|
) {
|
|
const seen = new Set<string>();
|
|
for (const sample of snapshot.samples) {
|
|
if (seen.has(sample.attemptId)) throw new Error("Duplicate forecast attempt");
|
|
seen.add(sample.attemptId);
|
|
if (typeof sample.measurementHost !== "string" || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(sample.measurementHost) || !Number.isSafeInteger(sample.finalModelRequests) || sample.finalModelRequests < 1
|
|
|| !Number.isSafeInteger(sample.attemptDurationMs) || sample.attemptDurationMs < 0 || usdToUnits(sample.knownModelUsd) < 0n
|
|
|| !/^[0-9a-f]{40}$/.test(sample.sourceRevision)
|
|
|| [sample.definitionHash, sample.resultSha256, sample.ledgerSha256, sample.ledgerFinalHash].some(hash => !/^[0-9a-f]{64}$/.test(hash))) {
|
|
throw new Error("Invalid forecast measurement or provenance");
|
|
}
|
|
}
|
|
if (new Set(executions.map(execution => execution.id)).size !== executions.length) throw new Error("Duplicate forecast execution");
|
|
const cells = executions.map(execution => {
|
|
const retained = snapshot.samples.filter(sample => sample.executionId === execution.id);
|
|
const cohorts = [...new Set(retained.map(cohortKey))].map(key => ({
|
|
key, ...summarize(retained.filter(sample => cohortKey(sample) === key)),
|
|
}));
|
|
const samples = retained.map(sample => {
|
|
const mismatches = [
|
|
sample.definitionHash === execution.suiteDefinitionHash ? null : "definition",
|
|
sample.model === execution.profile.model ? null : "model",
|
|
sample.environmentId === execution.environment.id ? null : "environment",
|
|
sample.runtimeMode === execution.profile.expectedRuntimeMode ? null : "runtime",
|
|
sample.route === "braintrust" ? null : "route",
|
|
models.some(model => model.price.model === sample.model && model.price.version === sample.priceVersion) ? null : "pricing",
|
|
].filter(value => value !== null);
|
|
return { ...sample, mismatches };
|
|
});
|
|
return { executionId: execution.id, currentDefinitionHash: execution.suiteDefinitionHash,
|
|
historicalSuccessful: summarize(cohorts.length === 1 ? retained : []), historicalCohorts: cohorts,
|
|
compatibleSuccessful: summarizeCohort(samples.filter(sample => sample.mismatches.length === 0)),
|
|
historicalHosted: summarizeCohort(retained.filter(sample => sample.measurementHost === "github-hosted-ubuntu-latest")),
|
|
compatibleHosted: summarizeCohort(samples.filter(sample => sample.mismatches.length === 0
|
|
&& sample.measurementHost === "github-hosted-ubuntu-latest")), samples };
|
|
});
|
|
const hosted = total(cells.map(cell => cell.compatibleHosted));
|
|
return {
|
|
measurementSnapshotSha256: createHash("sha256").update(JSON.stringify(snapshot)).digest("hex"),
|
|
measuredHost: snapshot.host, costStatus: snapshot.costStatus, sampleScope: snapshot.sampleScope,
|
|
historicalSuccessfulSelection: total(cells.map(cell => cell.historicalSuccessful)),
|
|
historicalHostedSelection: total(cells.map(cell => cell.historicalHosted)),
|
|
compatibleSuccessfulSelection: total(cells.map(cell => cell.compatibleSuccessful)),
|
|
hostedModelUsd: hosted.modelUsd, hostedAttemptMs: hosted.summedAttemptMs, selectionP95: null,
|
|
cells, limitations: [...snapshot.limitations,
|
|
"Compatibility compares definition, model, environment, runtime, route and price version; measurement hosts form separate cohorts and source revisions remain explicit",
|
|
"Hosted totals require compatible GitHub-hosted measurements for every selected cell",
|
|
"Per-cell P95 uses nearest rank only with at least 20 samples; it is descriptive, not a confidence bound",
|
|
"Summed attempt time is serial attempt work, not campaign wall time, summed agent time or billed runner time",
|
|
"Historical successful samples do not estimate failure frequency; every failure remains in campaign accounting"],
|
|
};
|
|
}
|