humanish 0.113.0 → 0.115.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/CHANGELOG.md +41 -0
- package/CONTEXT.md +1 -0
- package/README.md +1 -1
- package/dist/actors/closing-report-limits.d.ts +11 -0
- package/dist/actors/closing-report-limits.js +13 -0
- package/dist/actors/closing-report.d.ts +68 -0
- package/dist/actors/closing-report.js +55 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
- package/dist/actors/codex/restricted-participant-policy.js +12 -40
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
- package/dist/actors/computer-use/loop/debrief.js +10 -46
- package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
- package/dist/actors/computer-use/loop/impressions.js +6 -1
- package/dist/actors/computer-use/loop.d.ts +0 -1
- package/dist/actors/computer-use/loop.js +3 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
- package/dist/actors/computer-use/openai-conversation.js +12 -36
- package/dist/actors/computer-use/openai-provider.js +11 -5
- package/dist/actors/contract.d.ts +6 -22
- package/dist/actors/contract.js +0 -13
- package/dist/analysis/admission.d.ts +47 -0
- package/dist/analysis/admission.js +65 -0
- package/dist/analysis/automatic-completion.d.ts +0 -6
- package/dist/analysis/automatic-completion.js +3 -3
- package/dist/analysis/automatic-config.d.ts +10 -2
- package/dist/analysis/automatic-config.js +29 -14
- package/dist/analysis/automatic.js +7 -0
- package/dist/analysis/execute.d.ts +24 -5
- package/dist/analysis/execute.js +61 -27
- package/dist/analysis/finding-lead.d.ts +27 -0
- package/dist/analysis/finding-lead.js +23 -0
- package/dist/analysis/image-tokens.d.ts +2 -0
- package/dist/analysis/image-tokens.js +1 -1
- package/dist/analysis/job.d.ts +9 -1
- package/dist/analysis/job.js +11 -2
- package/dist/analysis/service.js +8 -3
- package/dist/cli/commands/analyze.js +7 -2
- package/dist/cli/commands/notes.d.ts +3 -0
- package/dist/cli/commands/notes.js +145 -0
- package/dist/cli/commands/observe.js +1 -1
- package/dist/cli/commands/study-run.js +2 -1
- package/dist/cli/findings.js +11 -30
- package/dist/cli/io.d.ts +1 -0
- package/dist/cli/io.js +8 -12
- package/dist/cli/program.js +2 -0
- package/dist/cli/telemetry.js +10 -22
- package/dist/cli/update-check.js +6 -18
- package/dist/cli/user-config.d.ts +11 -0
- package/dist/cli/user-config.js +25 -0
- package/dist/comms/email-catch.js +7 -31
- package/dist/evidence/encoded-text.d.ts +17 -3
- package/dist/evidence/encoded-text.js +121 -46
- package/dist/evidence/redaction.d.ts +11 -0
- package/dist/evidence/redaction.js +21 -10
- package/dist/evidence/secret-scrub.d.ts +14 -1
- package/dist/evidence/secret-scrub.js +125 -28
- package/dist/feedback/draft.d.ts +19 -0
- package/dist/feedback/draft.js +62 -15
- package/dist/feedback/export-bundle.js +3 -3
- package/dist/feedback/export.js +50 -20
- package/dist/feedback/feedback.js +29 -2
- package/dist/keys/key-resolution.js +12 -17
- package/dist/observer/artifact.d.ts +5 -0
- package/dist/observer/artifact.js +18 -2
- package/dist/observer/http.d.ts +7 -0
- package/dist/observer/http.js +19 -1
- package/dist/observer/notes-route.d.ts +20 -0
- package/dist/observer/notes-route.js +120 -0
- package/dist/observer/pinned-files.d.ts +8 -1
- package/dist/observer/pinned-files.js +17 -3
- package/dist/observer/render.js +39 -14
- package/dist/observer/run-routes.d.ts +8 -2
- package/dist/observer/run-routes.js +32 -6
- package/dist/observer/serve.d.ts +3 -0
- package/dist/observer/serve.js +29 -3
- package/dist/observer-app.html +7 -6
- package/dist/run/contained-output.d.ts +8 -0
- package/dist/run/contained-output.js +27 -0
- package/dist/run/evidence-files.d.ts +7 -0
- package/dist/run/evidence-files.js +24 -14
- package/dist/run/note-files.d.ts +54 -0
- package/dist/run/note-files.js +212 -0
- package/dist/run/note-shape.d.ts +35 -0
- package/dist/run/note-shape.js +20 -0
- package/dist/run/notes.d.ts +34 -0
- package/dist/run/notes.js +192 -0
- package/dist/run/participant-caption.d.ts +9 -0
- package/dist/run/participant-caption.js +19 -0
- package/dist/run/participant-records.d.ts +2 -0
- package/dist/run/participant-records.js +17 -2
- package/dist/run/transient-comms-secrets.d.ts +6 -5
- package/dist/run/transient-comms-secrets.js +19 -24
- package/dist/study/doctor.js +1 -1
- package/dist/study/preflight-result.js +2 -1
- package/dist/study/summary.d.ts +4 -5
- package/dist/study/summary.js +6 -4
- package/dist/study/warnings.js +2 -1
- package/dist/tui-app.js +1 -1
- package/docs/architecture/actor-contract.md +3 -3
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/state-driven-executor.md +1 -1
- package/docs/contracts/errors.md +2 -2
- package/docs/contracts/run-bundle.md +61 -0
- package/docs/contracts/schemas.md +17 -14
- package/docs/contracts/study-analysis.md +35 -9
- package/docs/principles/invariants-and-defaults.md +2 -2
- package/docs/product/automatic-analysis.md +22 -16
- package/package.json +8 -7
- package/skills/humanish/SKILL.md +6 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closingReportSchema, impressionsReplySchema, strictOutputSchema, } from "../closing-report.js";
|
|
2
2
|
import { CarriedConversation, estimateTokens } from "./openai-context.js";
|
|
3
3
|
import { buildCallOutput, buildContinuationRequest, buildInitialRequest, hintItems, openingMessage, parseOpenAiResponse, } from "./openai-wire.js";
|
|
4
4
|
/**
|
|
@@ -6,44 +6,18 @@ import { buildCallOutput, buildContinuationRequest, buildInitialRequest, hintIte
|
|
|
6
6
|
* counts reasoning too, and leaves room for six impressions after the summary and friction reports.
|
|
7
7
|
*/
|
|
8
8
|
const CLOSING_OUTPUT_LIMIT = 3072;
|
|
9
|
-
const IMPRESSIONS_SCHEMA = {
|
|
10
|
-
type: "array",
|
|
11
|
-
items: {
|
|
12
|
-
type: "object",
|
|
13
|
-
additionalProperties: false,
|
|
14
|
-
required: ["kind", "text"],
|
|
15
|
-
properties: {
|
|
16
|
-
kind: { type: "string", enum: [...PARTICIPANT_IMPRESSION_KINDS] },
|
|
17
|
-
text: { type: "string" },
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
9
|
const CLOSING_FORMATS = {
|
|
22
10
|
report: {
|
|
23
11
|
type: "json_schema",
|
|
24
12
|
name: "participant_closing_report",
|
|
25
13
|
strict: true,
|
|
26
|
-
schema:
|
|
27
|
-
type: "object",
|
|
28
|
-
additionalProperties: false,
|
|
29
|
-
required: ["summary", "frictionReports", "impressions"],
|
|
30
|
-
properties: {
|
|
31
|
-
summary: { type: "string" },
|
|
32
|
-
frictionReports: { type: "array", items: { type: "string" } },
|
|
33
|
-
impressions: IMPRESSIONS_SCHEMA,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
14
|
+
schema: strictOutputSchema(closingReportSchema),
|
|
36
15
|
},
|
|
37
16
|
impressions: {
|
|
38
17
|
type: "json_schema",
|
|
39
18
|
name: "participant_impressions",
|
|
40
19
|
strict: true,
|
|
41
|
-
schema:
|
|
42
|
-
type: "object",
|
|
43
|
-
additionalProperties: false,
|
|
44
|
-
required: ["impressions"],
|
|
45
|
-
properties: { impressions: IMPRESSIONS_SCHEMA },
|
|
46
|
-
},
|
|
20
|
+
schema: strictOutputSchema(impressionsReplySchema),
|
|
47
21
|
},
|
|
48
22
|
};
|
|
49
23
|
export class OpenAiConversation {
|
|
@@ -73,7 +47,7 @@ export class OpenAiConversation {
|
|
|
73
47
|
const sent = this.newItems(req);
|
|
74
48
|
return {
|
|
75
49
|
body: () => closing === undefined ? this.turnBody(req, sent) : this.closingBody(req, sent, closing),
|
|
76
|
-
accept: (reply) => this.accept(reply, closing
|
|
50
|
+
accept: (reply) => this.accept(reply, closing, sent),
|
|
77
51
|
};
|
|
78
52
|
}
|
|
79
53
|
/**
|
|
@@ -171,10 +145,10 @@ export class OpenAiConversation {
|
|
|
171
145
|
accept(reply, closing, sent) {
|
|
172
146
|
// Recorded before the reply is accepted: accepting can cut the carried history, and the record
|
|
173
147
|
// counts what this request carried.
|
|
174
|
-
if (
|
|
175
|
-
this.evidence.requested(this.mode, this.carried, sent);
|
|
148
|
+
if (closing !== "report")
|
|
149
|
+
this.evidence.requested(this.mode, this.carried, sent, closing);
|
|
176
150
|
const parsed = parseOpenAiResponse(reply);
|
|
177
|
-
if (
|
|
151
|
+
if (closing === undefined && parsed.turn.interruption === "output_limit")
|
|
178
152
|
return parsed.turn;
|
|
179
153
|
if (parsed.turn.responseId !== undefined)
|
|
180
154
|
this.lastResponseId = parsed.turn.responseId;
|
|
@@ -206,10 +180,11 @@ class ConversationRecord {
|
|
|
206
180
|
// Counted like requests[], which includes replies set aside at their output limit.
|
|
207
181
|
this.switchedAtRequest = this.requests.length + 1;
|
|
208
182
|
}
|
|
209
|
-
/** A participant request went out with `sent` after `conversation`. */
|
|
210
|
-
requested(mode, conversation, sent) {
|
|
183
|
+
/** A participant turn or impressions request went out with `sent` after `conversation`. */
|
|
184
|
+
requested(mode, conversation, sent, kind) {
|
|
185
|
+
const marker = kind === undefined ? {} : { kind };
|
|
211
186
|
if (mode !== "explicit_context") {
|
|
212
|
-
this.requests.push({ mode: "threaded" });
|
|
187
|
+
this.requests.push({ mode: "threaded", ...marker });
|
|
213
188
|
return;
|
|
214
189
|
}
|
|
215
190
|
const carried = conversation.size();
|
|
@@ -217,6 +192,7 @@ class ConversationRecord {
|
|
|
217
192
|
this.summarizedTurns = carried.notedTurns;
|
|
218
193
|
this.requests.push({
|
|
219
194
|
mode: "explicit_context",
|
|
195
|
+
...marker,
|
|
220
196
|
carriedExchanges: carried.exchanges,
|
|
221
197
|
carriedScreenshots: carried.screenshots,
|
|
222
198
|
estimatedInputTokens: carried.estimatedTokens + estimateTokens(sent),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closingReportSchema, impressionsReplySchema } from "../closing-report.js";
|
|
2
2
|
import { ComputerUseAdmissionLimitError, isComputerUseAdmissionLimitError, } from "./admission-limit.js";
|
|
3
3
|
import { ComputerUsePromptRefusedError } from "./provider-error.js";
|
|
4
4
|
import { OpenAiConversation, } from "./openai-conversation.js";
|
|
@@ -469,10 +469,16 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
469
469
|
try {
|
|
470
470
|
const reply = JSON.parse(turn.message ?? "");
|
|
471
471
|
if (asRecord(raw).status === "completed") {
|
|
472
|
-
if (closing === "report"
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
472
|
+
if (closing === "report") {
|
|
473
|
+
const report = closingReportSchema.safeParse(reply);
|
|
474
|
+
if (report.success)
|
|
475
|
+
return { ...turn, closingReport: report.data };
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
const impressions = impressionsReplySchema.safeParse(reply);
|
|
479
|
+
if (impressions.success)
|
|
480
|
+
return { ...turn, impressions: impressions.data.impressions };
|
|
481
|
+
}
|
|
476
482
|
}
|
|
477
483
|
}
|
|
478
484
|
catch {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AffordanceUse } from "./affordance.js";
|
|
2
2
|
import type { ActorEstimatedCost } from "../run/pricing.js";
|
|
3
3
|
import type { TaskFunnel } from "../study/tasks.js";
|
|
4
|
+
import type { ParticipantClosingReport, ParticipantImpression } from "./closing-report.js";
|
|
4
5
|
import { type CuaProviderFailurePhase } from "./computer-use/provider-error.js";
|
|
5
6
|
export declare const ACTOR_TRACE_SCHEMA = "humanish.actor-trace.v1";
|
|
7
|
+
export type { ParticipantClosingReport, ParticipantImpression } from "./closing-report.js";
|
|
6
8
|
/**
|
|
7
9
|
* How a session ended, from the point of view of the study rather than the harness.
|
|
8
10
|
*
|
|
@@ -182,25 +184,6 @@ export interface ActorProviderRequest extends ProviderRequestReceipt {
|
|
|
182
184
|
export declare function validActorExecutionProfile(value: unknown): value is ActorExecutionProfile;
|
|
183
185
|
/** Closed per-attempt evidence; dollar amounts are never part of account usage. */
|
|
184
186
|
export declare function validActorProviderRequests(value: unknown): value is ActorProviderRequest[];
|
|
185
|
-
/** The participant's account, not an independently confirmed product diagnosis. */
|
|
186
|
-
export interface ParticipantClosingReport {
|
|
187
|
-
summary: string;
|
|
188
|
-
frictionReports: string[];
|
|
189
|
-
/** Absent when the provider did not ask for impressions; empty when the participant had none. */
|
|
190
|
-
impressions?: ParticipantImpression[];
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* What a participant can say about the product at the end of a session, beyond friction.
|
|
194
|
-
* `unlike_my_work` compares the screen with how the persona does the same task in its own work or
|
|
195
|
-
* life.
|
|
196
|
-
*/
|
|
197
|
-
export declare const PARTICIPANT_IMPRESSION_KINDS: readonly ["unclear", "unfinished", "untrustworthy", "liked", "missing", "unlike_my_work"];
|
|
198
|
-
type ParticipantImpressionKind = (typeof PARTICIPANT_IMPRESSION_KINDS)[number];
|
|
199
|
-
/** One first-person impression about something the participant saw in the session. */
|
|
200
|
-
export interface ParticipantImpression {
|
|
201
|
-
kind: ParticipantImpressionKind;
|
|
202
|
-
text: string;
|
|
203
|
-
}
|
|
204
187
|
/** An impression as the trace keeps it, with the message that quotes it. */
|
|
205
188
|
interface RecordedImpression extends ParticipantImpression {
|
|
206
189
|
/** The trace message carrying the impression, which the analysis quotes and cites. */
|
|
@@ -261,11 +244,13 @@ export interface ActorConversation {
|
|
|
261
244
|
*/
|
|
262
245
|
summarizedTurns: number;
|
|
263
246
|
/**
|
|
264
|
-
* Per participant request, in order: its mode and, in explicit_context,
|
|
265
|
-
* estimated input. `tokenUsage.turns` has each request's billed input.
|
|
247
|
+
* Per participant turn or impressions request, in order: its mode and, in explicit_context,
|
|
248
|
+
* what it carried and its estimated input. `tokenUsage.turns` has each request's billed input.
|
|
266
249
|
*/
|
|
267
250
|
requests: Array<{
|
|
268
251
|
mode: "threaded" | "explicit_context";
|
|
252
|
+
/** Present for the impressions-only request; absent for participant turns and older bundles. */
|
|
253
|
+
kind?: "impressions";
|
|
269
254
|
carriedExchanges?: number;
|
|
270
255
|
carriedScreenshots?: number;
|
|
271
256
|
estimatedInputTokens?: number;
|
|
@@ -397,4 +382,3 @@ export interface ActorSessionResult {
|
|
|
397
382
|
export declare const CODEX_APP_SERVER_CAPABILITIES: ActorCapabilities;
|
|
398
383
|
export declare const SCRIPTED_BROWSER_CAPABILITIES: ActorCapabilities;
|
|
399
384
|
export declare const TERMINAL_AGENT_CAPABILITIES: ActorCapabilities;
|
|
400
|
-
export {};
|
package/dist/actors/contract.js
CHANGED
|
@@ -168,19 +168,6 @@ export function validActorProviderRequests(value) {
|
|
|
168
168
|
usage.input));
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
|
-
/**
|
|
172
|
-
* What a participant can say about the product at the end of a session, beyond friction.
|
|
173
|
-
* `unlike_my_work` compares the screen with how the persona does the same task in its own work or
|
|
174
|
-
* life.
|
|
175
|
-
*/
|
|
176
|
-
export const PARTICIPANT_IMPRESSION_KINDS = [
|
|
177
|
-
"unclear",
|
|
178
|
-
"unfinished",
|
|
179
|
-
"untrustworthy",
|
|
180
|
-
"liked",
|
|
181
|
-
"missing",
|
|
182
|
-
"unlike_my_work",
|
|
183
|
-
];
|
|
184
171
|
export const CODEX_APP_SERVER_CAPABILITIES = {
|
|
185
172
|
headless: true,
|
|
186
173
|
structuredTrace: true,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ModelRate } from "../run/pricing.js";
|
|
2
|
+
/** Admission compares the expected cost times this margin with the cap. */
|
|
3
|
+
export declare const ADMISSION_MARGIN = 1.1;
|
|
4
|
+
export interface AnalysisRequestSize {
|
|
5
|
+
/** UTF-8 bytes of the instructions, the evidence packet and the result schema. */
|
|
6
|
+
textBytes: number;
|
|
7
|
+
imageTokens: number;
|
|
8
|
+
participants: number;
|
|
9
|
+
/** The request's output limit. The worst case spends all of it. */
|
|
10
|
+
outputAllowance: number;
|
|
11
|
+
}
|
|
12
|
+
export interface AnalysisCostEstimate {
|
|
13
|
+
inputTokens: number;
|
|
14
|
+
expectedOutputTokens: number;
|
|
15
|
+
expectedCostUsd: number;
|
|
16
|
+
worstCaseCostUsd: number;
|
|
17
|
+
/** The expected cost with the margin, never above the worst case: what admission compares. */
|
|
18
|
+
admittedCostUsd: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function estimateAnalysisCost(rate: ModelRate, size: AnalysisRequestSize): AnalysisCostEstimate;
|
|
21
|
+
/** The numbers a cost refusal reports. */
|
|
22
|
+
export interface RefusedAnalysisCost {
|
|
23
|
+
expectedCostUsd: number;
|
|
24
|
+
worstCaseCostUsd: number;
|
|
25
|
+
maxCostUsd: number;
|
|
26
|
+
}
|
|
27
|
+
/** A cost refusal in plain words, and the command that runs the analysis under a cap that admits it. */
|
|
28
|
+
export interface CostRefusal {
|
|
29
|
+
/** The costs, the cap and that no request was sent, ending in the lead-in to `command`. */
|
|
30
|
+
text: string;
|
|
31
|
+
/** `analyze` with `--max-cost` at the worst case rounded up to a whole dollar, at least 1. */
|
|
32
|
+
command: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Words a cost refusal. `runFlags` are the `analyze` flags that select the run, such as
|
|
36
|
+
* `--run <id>`. `cli` prefixes a humanish command: the CLI passes `cli` from
|
|
37
|
+
* src/cli/invocation.ts, and the Observer, which cannot read the install, a bare `humanish`.
|
|
38
|
+
* This module keeps only type imports, so the Observer can bundle it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function costRefusal(cost: RefusedAnalysisCost, runFlags: string, cli: (rest: string) => string): CostRefusal;
|
|
41
|
+
/** The admission's refused cost, when it refused the analysis for its cost. */
|
|
42
|
+
export declare function refusedCost(admission: {
|
|
43
|
+
error: string | null;
|
|
44
|
+
estimatedCostUsd: number | null;
|
|
45
|
+
worstCaseCostUsd: number | null;
|
|
46
|
+
maxCostUsd: number | null;
|
|
47
|
+
}): RefusedAnalysisCost | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The admission cost model for one study analysis request: the expected cost, the worst case, the
|
|
2
|
+
// figure admission compares with the cap, and the words and command a cost refusal gives.
|
|
3
|
+
/**
|
|
4
|
+
* UTF-8 bytes per input token for the instructions, evidence packet and schema. On 148 billed
|
|
5
|
+
* gpt-6-astra analyses the evidence packet ran 3.1 to 3.3 bytes per token and the instructions
|
|
6
|
+
* more, so 3 stays above every billed input. Chinese, Japanese and Korean text is 3 bytes per
|
|
7
|
+
* character in UTF-8, so it also counts at least a token per character.
|
|
8
|
+
*/
|
|
9
|
+
const BYTES_PER_INPUT_TOKEN = 3;
|
|
10
|
+
/** Request framing on top of the text and the images. */
|
|
11
|
+
const FRAMING_TOKENS = 2048;
|
|
12
|
+
/**
|
|
13
|
+
* The output an analysis is expected to write, reasoning included. The most any billed analysis
|
|
14
|
+
* used was 12,511 tokens for one participant and 16,722 for eight.
|
|
15
|
+
*/
|
|
16
|
+
const EXPECTED_OUTPUT_TOKENS = 12_000;
|
|
17
|
+
const EXPECTED_OUTPUT_TOKENS_PER_PARTICIPANT = 1_000;
|
|
18
|
+
/** Admission compares the expected cost times this margin with the cap. */
|
|
19
|
+
export const ADMISSION_MARGIN = 1.1;
|
|
20
|
+
/** Rounded up to the micro-dollar, so a small positive boundary never rounds into admission. */
|
|
21
|
+
const roundUp = (usd) => Math.ceil(usd * 1e6) / 1e6;
|
|
22
|
+
/** One request's cost. Input is priced at the highest input rate the model bills, cache writes
|
|
23
|
+
* included, and the long-context tier re-prices the whole request past its threshold. */
|
|
24
|
+
function requestCost(rate, inputTokens, outputTokens) {
|
|
25
|
+
const long = rate.longContext !== undefined && inputTokens > rate.longContext.thresholdInputTokens;
|
|
26
|
+
const inputRate = Math.max(rate.inputUsdPerToken, rate.cacheWriteUsdPerToken ?? 0, rate.cachedInputUsdPerToken ?? 0);
|
|
27
|
+
return roundUp(inputTokens * inputRate * (long ? rate.longContext.inputMultiplier : 1) +
|
|
28
|
+
outputTokens * rate.outputUsdPerToken * (long ? rate.longContext.outputMultiplier : 1));
|
|
29
|
+
}
|
|
30
|
+
export function estimateAnalysisCost(rate, size) {
|
|
31
|
+
const inputTokens = Math.ceil(size.textBytes / BYTES_PER_INPUT_TOKEN) + FRAMING_TOKENS + size.imageTokens;
|
|
32
|
+
const expectedOutputTokens = Math.min(size.outputAllowance, EXPECTED_OUTPUT_TOKENS + EXPECTED_OUTPUT_TOKENS_PER_PARTICIPANT * size.participants);
|
|
33
|
+
const expectedCostUsd = requestCost(rate, inputTokens, expectedOutputTokens);
|
|
34
|
+
const worstCaseCostUsd = requestCost(rate, inputTokens, size.outputAllowance);
|
|
35
|
+
return {
|
|
36
|
+
inputTokens,
|
|
37
|
+
expectedOutputTokens,
|
|
38
|
+
expectedCostUsd,
|
|
39
|
+
worstCaseCostUsd,
|
|
40
|
+
admittedCostUsd: Math.min(worstCaseCostUsd, roundUp(expectedCostUsd * ADMISSION_MARGIN)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Words a cost refusal. `runFlags` are the `analyze` flags that select the run, such as
|
|
45
|
+
* `--run <id>`. `cli` prefixes a humanish command: the CLI passes `cli` from
|
|
46
|
+
* src/cli/invocation.ts, and the Observer, which cannot read the install, a bare `humanish`.
|
|
47
|
+
* This module keeps only type imports, so the Observer can bundle it.
|
|
48
|
+
*/
|
|
49
|
+
export function costRefusal(cost, runFlags, cli) {
|
|
50
|
+
const margin = Math.round((ADMISSION_MARGIN - 1) * 100);
|
|
51
|
+
return {
|
|
52
|
+
text: `The expected cost is $${cost.expectedCostUsd.toFixed(2)} and the worst case is $${cost.worstCaseCostUsd.toFixed(2)}. With a ${margin}% margin the expected cost is over the $${cost.maxCostUsd} cap, so no request was sent. To run it, raise the cap:`,
|
|
53
|
+
command: cli(`analyze ${runFlags} --max-cost ${Math.max(1, Math.ceil(cost.worstCaseCostUsd))}`),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The admission's refused cost, when it refused the analysis for its cost. */
|
|
57
|
+
export function refusedCost(admission) {
|
|
58
|
+
const { error, estimatedCostUsd, worstCaseCostUsd, maxCostUsd } = admission;
|
|
59
|
+
return error === "analysis_budget_exceeded" &&
|
|
60
|
+
estimatedCostUsd !== null &&
|
|
61
|
+
worstCaseCostUsd !== null &&
|
|
62
|
+
maxCostUsd !== null
|
|
63
|
+
? { expectedCostUsd: estimatedCostUsd, worstCaseCostUsd, maxCostUsd }
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
@@ -35,10 +35,4 @@ export declare function completeAutomaticAnalysis<T extends {
|
|
|
35
35
|
/** The route's reason not to analyze this run; see AutomaticAnalysisDeps.refusal. */
|
|
36
36
|
refusal?: AutomaticAnalysisDeps["refusal"];
|
|
37
37
|
}): Promise<T & AutomaticAnalysisResult>;
|
|
38
|
-
/**
|
|
39
|
-
* True when an analysis the study never declared was refused because its conservative estimate is
|
|
40
|
-
* over the default cap. The author did not ask for it, so the refusal does not fail the run;
|
|
41
|
-
* `humanish analyze --max-cost` can still run it.
|
|
42
|
-
*/
|
|
43
|
-
export declare function defaultAnalysisOverBudget(result: AutomaticAnalysisResult): boolean;
|
|
44
38
|
export declare function automaticAnalysisSucceeded(result: AutomaticAnalysisResult): boolean;
|
|
@@ -85,11 +85,11 @@ export async function completeAutomaticAnalysis(result, finished, config, input,
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* True when an analysis the study never declared was refused because its
|
|
89
|
-
* over the default cap. The author did not ask for it, so the refusal does not fail the run;
|
|
88
|
+
* True when an analysis the study never declared was refused because its expected cost, with the
|
|
89
|
+
* admission margin, is over the default cap. The author did not ask for it, so the refusal does not fail the run;
|
|
90
90
|
* `humanish analyze --max-cost` can still run it.
|
|
91
91
|
*/
|
|
92
|
-
|
|
92
|
+
function defaultAnalysisOverBudget(result) {
|
|
93
93
|
const value = result.automaticAnalysis;
|
|
94
94
|
return (result.automaticAnalysisTrigger === "default" &&
|
|
95
95
|
value?.state === "skipped" &&
|
|
@@ -34,8 +34,16 @@ export interface AutomaticAnalysisBudget {
|
|
|
34
34
|
model: string;
|
|
35
35
|
maxCostUsd: number | null;
|
|
36
36
|
trigger: "default" | "explicit";
|
|
37
|
+
/** The participants the expected cost range is for. */
|
|
38
|
+
participants?: number;
|
|
39
|
+
/** The expected cost, from a run that keeps no evidence to one at the evidence limits. */
|
|
40
|
+
expectedCostUsd?: {
|
|
41
|
+
low: number;
|
|
42
|
+
high: number;
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
|
-
/** Metadata only: resolving the future live-run budget never reads keys or dispatches.
|
|
39
|
-
|
|
45
|
+
/** Metadata only: resolving the future live-run budget never reads keys or dispatches. With a
|
|
46
|
+
* participant count it also gives the expected cost range for that many participants. */
|
|
47
|
+
export declare function automaticAnalysisBudget(raw: unknown, route: StudyRoute, participants?: number): AutomaticAnalysisBudget | undefined;
|
|
40
48
|
export declare function formatAutomaticAnalysisBudget(budget: AutomaticAnalysisBudget): string;
|
|
41
49
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CODEX_ANALYSIS_MODEL, codexAnalysisIdentity } from "./codex-config.js";
|
|
2
|
-
import { DEFAULT_ANALYSIS_MAX_OUTPUT_TOKENS, MAX_ANALYSIS_OUTPUT_TOKENS, isSupportedAnalysisModel, } from "./execute.js";
|
|
2
|
+
import { DEFAULT_ANALYSIS_MAX_OUTPUT_TOKENS, MAX_ANALYSIS_OUTPUT_TOKENS, analysisCostRange, isSupportedAnalysisModel, } from "./execute.js";
|
|
3
|
+
import { ADMISSION_MARGIN } from "./admission.js";
|
|
4
|
+
import { plural } from "../run/text.js";
|
|
3
5
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
4
6
|
export const DEFAULT_ANALYSIS_TIMEOUT_MS = 600_000;
|
|
5
7
|
const MAX_ANALYSIS_TIMEOUT_MS = 600_000;
|
|
@@ -111,25 +113,38 @@ export function resolveAutomaticAnalysis(raw) {
|
|
|
111
113
|
...(value.maxOutputTokens === undefined ? { preferLargerOutput: true } : {}),
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
|
-
/** Metadata only: resolving the future live-run budget never reads keys or dispatches.
|
|
115
|
-
|
|
116
|
+
/** Metadata only: resolving the future live-run budget never reads keys or dispatches. With a
|
|
117
|
+
* participant count it also gives the expected cost range for that many participants. */
|
|
118
|
+
export function automaticAnalysisBudget(raw, route, participants) {
|
|
116
119
|
// The synthetic preview has no participants, so nothing is analyzed after it.
|
|
117
120
|
if (route === "preview")
|
|
118
121
|
return undefined;
|
|
119
122
|
const resolved = resolveAutomaticAnalysis(raw);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
if (!resolved.ok || !resolved.config)
|
|
124
|
+
return undefined;
|
|
125
|
+
const { config } = resolved;
|
|
126
|
+
const range = participants === undefined || config.provider === "codex"
|
|
127
|
+
? undefined
|
|
128
|
+
: analysisCostRange(resolved.preferLargerOutput
|
|
129
|
+
? { ...config, maxOutputTokens: MAX_ANALYSIS_OUTPUT_TOKENS }
|
|
130
|
+
: config, participants);
|
|
131
|
+
return {
|
|
132
|
+
...(config.provider === "codex"
|
|
133
|
+
? { provider: "codex", billing: "account-unknown" }
|
|
134
|
+
: {}),
|
|
135
|
+
model: config.model,
|
|
136
|
+
maxCostUsd: config.maxCostUsd,
|
|
137
|
+
trigger: raw === undefined ? "default" : "explicit",
|
|
138
|
+
...(range === undefined || participants === undefined
|
|
139
|
+
? {}
|
|
140
|
+
: { participants, expectedCostUsd: range }),
|
|
141
|
+
};
|
|
130
142
|
}
|
|
131
143
|
export function formatAutomaticAnalysisBudget(budget) {
|
|
132
144
|
if (budget.provider === "codex")
|
|
133
145
|
return `After live runs: Codex account analysis · ${budget.model} · separate restricted analyst with remote inference. Account limits apply; dollar cost and output-token ceiling are unknown. Set review.analysis: false to disable.`;
|
|
134
|
-
|
|
146
|
+
const range = budget.expectedCostUsd === undefined || budget.participants === undefined
|
|
147
|
+
? ""
|
|
148
|
+
: ` · expected $${budget.expectedCostUsd.low.toFixed(2)} to $${budget.expectedCostUsd.high.toFixed(2)} for ${plural(budget.participants, "participant")}, depending on how much evidence the run keeps`;
|
|
149
|
+
return `After live runs: ${budget.trigger} analysis · ${budget.model}${range} · refused before it starts if the expected cost plus a ${Math.round((ADMISSION_MARGIN - 1) * 100)}% margin is over $${budget.maxCostUsd}; this is not a billing cap. Set review.analysis: false to disable.`;
|
|
135
150
|
}
|
|
@@ -5,6 +5,7 @@ import { refreshObserver, analyzeRun, readCompletedAnalysisSource, resolveAnalys
|
|
|
5
5
|
import { preferLargerAnalysisOutput, ANALYSIS_PROMPT_VERSION } from "./execute.js";
|
|
6
6
|
import { captureEvidence } from "./evidence.js";
|
|
7
7
|
import { hashAnalysisValue } from "./validation.js";
|
|
8
|
+
import { refusedCost } from "./admission.js";
|
|
8
9
|
import { claimAutomaticAnalysis, readAutomaticAnalysisPrepared, requestAutomaticAnalysisCancellationPrepared, } from "./job.js";
|
|
9
10
|
import { ANALYSIS_ID_PATTERN } from "./types.js";
|
|
10
11
|
import { readAnalysisVersion } from "./store.js";
|
|
@@ -105,6 +106,11 @@ async function readAutomaticSource(cwd, prepared, configInput, deps, hasKey) {
|
|
|
105
106
|
return skipped("AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE");
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
/** The cost of an analysis admission refused for its cost, kept so review and Observer can say it. */
|
|
110
|
+
function withRefusedCost(result) {
|
|
111
|
+
const cost = result?.admission && refusedCost(result.admission);
|
|
112
|
+
return cost ? { admission: cost } : {};
|
|
113
|
+
}
|
|
108
114
|
/**
|
|
109
115
|
* Bind the job's terminal metadata to the exact safely published execution. Reads never promote a
|
|
110
116
|
* completed-looking sidecar without rechecking these bindings. Null when storage failed.
|
|
@@ -130,6 +136,7 @@ async function persistOutcome(job, prepared, outcome) {
|
|
|
130
136
|
receiptSha256: hashAnalysisValue(receipt),
|
|
131
137
|
}),
|
|
132
138
|
...(entry?.analysis ? { analysisSha256: hashAnalysisValue(entry.analysis) } : {}),
|
|
139
|
+
...withRefusedCost(outcome.result),
|
|
133
140
|
});
|
|
134
141
|
const persisted = await readAutomaticAnalysisPrepared(prepared);
|
|
135
142
|
return persisted?.state === "unknown"
|
|
@@ -9,9 +9,19 @@ export declare const MAX_ANALYSIS_OUTPUT_TOKENS = 32768;
|
|
|
9
9
|
export interface AnalysisAdmission {
|
|
10
10
|
allowed: boolean;
|
|
11
11
|
error: string | null;
|
|
12
|
+
/** The input tokens admission priced. */
|
|
12
13
|
inputTokenAllowance: number | null;
|
|
13
14
|
outputTokenAllowance: number | null;
|
|
15
|
+
/** The expected cost. Admission compares it, with a margin, with maxCostUsd. */
|
|
14
16
|
estimatedCostUsd: number | null;
|
|
17
|
+
/** The cost if the analyst spends its whole output allowance. */
|
|
18
|
+
worstCaseCostUsd: number | null;
|
|
19
|
+
/**
|
|
20
|
+
* The expected cost plus the margin, or the worst case when that is lower: the figure admission
|
|
21
|
+
* compares with maxCostUsd, so the smallest cap that admits this analysis.
|
|
22
|
+
*/
|
|
23
|
+
admittedCostUsd: number | null;
|
|
24
|
+
maxCostUsd: number | null;
|
|
15
25
|
ratesAsOf: string | null;
|
|
16
26
|
}
|
|
17
27
|
export interface AnalysisProgress {
|
|
@@ -23,15 +33,24 @@ export interface AnalysisProgress {
|
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
35
|
* Local admission estimate, not a provider-enforced billed-spend guarantee. No token-count API
|
|
26
|
-
* call, credential, or network access.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* assumptions.
|
|
36
|
+
* call, credential, or network access. admission.ts holds the cost model: text at a calibrated
|
|
37
|
+
* bytes-per-token ratio, the expected output, and the worst case that spends the whole output
|
|
38
|
+
* allowance. Each high-detail image is priced from its PNG size by the vision guide's patch
|
|
39
|
+
* formula (see image-tokens.ts), or at the 3,000-token ceiling when its size or the model's sizing
|
|
40
|
+
* is unknown. Unknown models/rates fail closed rather than inheriting those assumptions.
|
|
31
41
|
* Known-sensitive decoded text denies admission with analysis_input_sensitive or
|
|
32
42
|
* analysis_question_sensitive. Neither error includes rejected input values.
|
|
33
43
|
*/
|
|
34
44
|
export declare function estimateAnalysisAdmission(input: AnalysisInput, config: AnalysisConfig): AnalysisAdmission;
|
|
45
|
+
/**
|
|
46
|
+
* The expected cost of an analysis for this many participants before any evidence exists: from a
|
|
47
|
+
* packet with no evidence to one at the evidence limits with every capture at the image-token
|
|
48
|
+
* ceiling. Undefined for Codex and for a model without rates.
|
|
49
|
+
*/
|
|
50
|
+
export declare function analysisCostRange(config: AnalysisConfig, participants: number): {
|
|
51
|
+
low: number;
|
|
52
|
+
high: number;
|
|
53
|
+
} | undefined;
|
|
35
54
|
/** Only for an omitted output limit. Preserve the established allowance when
|
|
36
55
|
* more reasoning/report space would refuse a study its declared budget admits.
|
|
37
56
|
* This is one pre-dispatch choice, never a fallback request or a budget increase. */
|
package/dist/analysis/execute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validCodexAnalysisConfig } from "./codex-config.js";
|
|
2
|
-
import { highDetailImageTokens } from "./image-tokens.js";
|
|
2
|
+
import { HIGH_DETAIL_IMAGE_TOKEN_CEILING, highDetailImageTokens } from "./image-tokens.js";
|
|
3
|
+
import { estimateAnalysisCost } from "./admission.js";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { estimateActorCost, MODEL_RATES } from "../run/pricing.js";
|
|
5
6
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
@@ -128,11 +129,11 @@ function inputError(input) {
|
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
131
|
* Local admission estimate, not a provider-enforced billed-spend guarantee. No token-count API
|
|
131
|
-
* call, credential, or network access.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* assumptions.
|
|
132
|
+
* call, credential, or network access. admission.ts holds the cost model: text at a calibrated
|
|
133
|
+
* bytes-per-token ratio, the expected output, and the worst case that spends the whole output
|
|
134
|
+
* allowance. Each high-detail image is priced from its PNG size by the vision guide's patch
|
|
135
|
+
* formula (see image-tokens.ts), or at the 3,000-token ceiling when its size or the model's sizing
|
|
136
|
+
* is unknown. Unknown models/rates fail closed rather than inheriting those assumptions.
|
|
136
137
|
* Known-sensitive decoded text denies admission with analysis_input_sensitive or
|
|
137
138
|
* analysis_question_sensitive. Neither error includes rejected input values.
|
|
138
139
|
*/
|
|
@@ -143,6 +144,9 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
143
144
|
inputTokenAllowance: 0,
|
|
144
145
|
outputTokenAllowance: 0,
|
|
145
146
|
estimatedCostUsd: null,
|
|
147
|
+
worstCaseCostUsd: null,
|
|
148
|
+
admittedCostUsd: null,
|
|
149
|
+
maxCostUsd: null,
|
|
146
150
|
ratesAsOf: null,
|
|
147
151
|
});
|
|
148
152
|
if ((config.provider === "codex"
|
|
@@ -174,6 +178,9 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
174
178
|
inputTokenAllowance: null,
|
|
175
179
|
outputTokenAllowance: null,
|
|
176
180
|
estimatedCostUsd: null,
|
|
181
|
+
worstCaseCostUsd: null,
|
|
182
|
+
admittedCostUsd: null,
|
|
183
|
+
maxCostUsd: null,
|
|
177
184
|
ratesAsOf: null,
|
|
178
185
|
};
|
|
179
186
|
const rate = MODEL_RATES[config.model];
|
|
@@ -184,32 +191,57 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
184
191
|
!Number.isFinite(rate.outputUsdPerToken) ||
|
|
185
192
|
rate.outputUsdPerToken < 0)
|
|
186
193
|
return denied("analysis_rate_unknown");
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const estimate = inputTokenAllowance * inputRate * (long ? rate.longContext.inputMultiplier : 1) +
|
|
197
|
-
config.maxOutputTokens *
|
|
198
|
-
rate.outputUsdPerToken *
|
|
199
|
-
(long ? rate.longContext.outputMultiplier : 1);
|
|
200
|
-
// Round upward for admission; rounding a small positive boundary down could admit an overrun.
|
|
201
|
-
const estimatedCostUsd = Math.ceil(estimate * 1e6) / 1e6;
|
|
202
|
-
if (!Number.isFinite(estimatedCostUsd))
|
|
194
|
+
const cost = estimateAnalysisCost(rate, {
|
|
195
|
+
textBytes: Buffer.byteLength(instructions(config)) +
|
|
196
|
+
Buffer.byteLength(evidenceText(input)) +
|
|
197
|
+
Buffer.byteLength(JSON.stringify(analysisResultJsonSchema)),
|
|
198
|
+
imageTokens: input.images.reduce((sum, image) => sum + highDetailImageTokens(config.model, image.dataUrl), 0),
|
|
199
|
+
participants: input.participants.length,
|
|
200
|
+
outputAllowance: config.maxOutputTokens,
|
|
201
|
+
});
|
|
202
|
+
if (!Number.isFinite(cost.worstCaseCostUsd))
|
|
203
203
|
return denied("analysis_rate_unknown");
|
|
204
|
+
const allowed = cost.admittedCostUsd <= config.maxCostUsd;
|
|
204
205
|
return {
|
|
205
|
-
allowed
|
|
206
|
-
error:
|
|
207
|
-
inputTokenAllowance,
|
|
206
|
+
allowed,
|
|
207
|
+
error: allowed ? null : "analysis_budget_exceeded",
|
|
208
|
+
inputTokenAllowance: cost.inputTokens,
|
|
208
209
|
outputTokenAllowance: config.maxOutputTokens,
|
|
209
|
-
estimatedCostUsd,
|
|
210
|
+
estimatedCostUsd: cost.expectedCostUsd,
|
|
211
|
+
worstCaseCostUsd: cost.worstCaseCostUsd,
|
|
212
|
+
admittedCostUsd: cost.admittedCostUsd,
|
|
213
|
+
maxCostUsd: config.maxCostUsd,
|
|
210
214
|
ratesAsOf: rate.asOf,
|
|
211
215
|
};
|
|
212
216
|
}
|
|
217
|
+
/** Structure each evidence entry adds to the packet: ids, kind, timestamps and flags. The largest
|
|
218
|
+
* retained packet averaged 200 bytes. */
|
|
219
|
+
const ENTRY_STRUCTURE_BYTES = 256;
|
|
220
|
+
/**
|
|
221
|
+
* The expected cost of an analysis for this many participants before any evidence exists: from a
|
|
222
|
+
* packet with no evidence to one at the evidence limits with every capture at the image-token
|
|
223
|
+
* ceiling. Undefined for Codex and for a model without rates.
|
|
224
|
+
*/
|
|
225
|
+
export function analysisCostRange(config, participants) {
|
|
226
|
+
if (config.provider === "codex")
|
|
227
|
+
return undefined;
|
|
228
|
+
const rate = MODEL_RATES[config.model];
|
|
229
|
+
if (!rate || rate.placeholder)
|
|
230
|
+
return undefined;
|
|
231
|
+
const prompt = Buffer.byteLength(instructions(config)) +
|
|
232
|
+
Buffer.byteLength(JSON.stringify(analysisResultJsonSchema));
|
|
233
|
+
const size = {
|
|
234
|
+
participants: Math.min(participants, EVIDENCE_LIMITS.participants),
|
|
235
|
+
outputAllowance: config.maxOutputTokens,
|
|
236
|
+
};
|
|
237
|
+
const low = estimateAnalysisCost(rate, { ...size, textBytes: prompt, imageTokens: 0 });
|
|
238
|
+
const high = estimateAnalysisCost(rate, {
|
|
239
|
+
...size,
|
|
240
|
+
textBytes: prompt + EVIDENCE_LIMITS.textBytes + EVIDENCE_LIMITS.evidence * ENTRY_STRUCTURE_BYTES,
|
|
241
|
+
imageTokens: EVIDENCE_LIMITS.captures * HIGH_DETAIL_IMAGE_TOKEN_CEILING,
|
|
242
|
+
});
|
|
243
|
+
return { low: low.expectedCostUsd, high: high.expectedCostUsd };
|
|
244
|
+
}
|
|
213
245
|
/** Only for an omitted output limit. Preserve the established allowance when
|
|
214
246
|
* more reasoning/report space would refuse a study its declared budget admits.
|
|
215
247
|
* This is one pre-dispatch choice, never a fallback request or a budget increase. */
|
|
@@ -464,9 +496,11 @@ function settleCompletedOutput(artifact, input, config, admission, response, onR
|
|
|
464
496
|
artifact.result = checked.result;
|
|
465
497
|
artifact.status = input.coverage.complete ? "complete" : "partial";
|
|
466
498
|
artifact.error = null;
|
|
499
|
+
// A bill above the expected cost is a normal outcome. Only one above the worst case or the cap
|
|
500
|
+
// shows the estimate was wrong.
|
|
467
501
|
if (config.provider !== "codex" &&
|
|
468
502
|
((response.usage?.output ?? 0) > config.maxOutputTokens ||
|
|
469
|
-
(artifact.usage.estimatedCostUsd ?? 0) > (admission.
|
|
503
|
+
(artifact.usage.estimatedCostUsd ?? 0) > (admission.worstCaseCostUsd ?? config.maxCostUsd) ||
|
|
470
504
|
(artifact.usage.estimatedCostUsd ?? 0) > config.maxCostUsd)) {
|
|
471
505
|
artifact.status = "partial";
|
|
472
506
|
artifact.error = "analysis_admission_estimate_exceeded";
|