@ishlabs/cli 0.24.0 → 0.24.1
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/dist/commands/study-run.js +3 -3
- package/dist/lib/billing.d.ts +30 -16
- package/dist/lib/billing.js +77 -27
- package/dist/lib/modality.d.ts +10 -1
- package/dist/lib/modality.js +21 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
|
14
14
|
import { output, formatSimulationPoll } from "../lib/output.js";
|
|
15
15
|
import { fetchStudyParticipants } from "../lib/study-participants.js";
|
|
16
16
|
import { streamStudyEvents } from "../lib/study-events.js";
|
|
17
|
-
import { isMediaModality, isChatModality, iterationHasContent, describeRequiredContentFlag, readChatMode, readParticipantPairConfig, summarizeRoleCriteria, } from "../lib/modality.js";
|
|
17
|
+
import { isMediaModality, isChatModality, iterationHasContent, describeRequiredContentFlag, readChatMode, readParticipantPairConfig, summarizeRoleCriteria, toModality, } from "../lib/modality.js";
|
|
18
18
|
// NOTE: local-sim modules are loaded via dynamic import at the `--local`
|
|
19
19
|
// branch below, NOT statically here. `local-sim/install.ts` deep-imports
|
|
20
20
|
// `playwright-core/lib/server/registry/index`, which is not exposed by
|
|
@@ -663,7 +663,7 @@ Examples:
|
|
|
663
663
|
: `CLI default — pass --max-interactions to override`;
|
|
664
664
|
log(` Max steps: ${stepsForMedia} (${source})`);
|
|
665
665
|
if (Number.isFinite(stepsForMedia)) {
|
|
666
|
-
const est = estimateMediaRun({ participantCount, maxInteractions: stepsForMedia });
|
|
666
|
+
const est = estimateMediaRun({ modality: toModality(modality), participantCount, maxInteractions: stepsForMedia });
|
|
667
667
|
log(` Credits (est): ≈ ${est.upper_bound} credit(s) upper bound — ${est.breakdown}`);
|
|
668
668
|
}
|
|
669
669
|
}
|
|
@@ -1021,7 +1021,7 @@ Examples:
|
|
|
1021
1021
|
const steps = resolveMaxInteractions(opts.maxInteractions, iteration.details);
|
|
1022
1022
|
if (!Number.isFinite(steps))
|
|
1023
1023
|
return null;
|
|
1024
|
-
return estimateMediaRun({ participantCount, maxInteractions: steps });
|
|
1024
|
+
return estimateMediaRun({ modality: toModality(modality), participantCount, maxInteractions: steps });
|
|
1025
1025
|
})();
|
|
1026
1026
|
if (!opts.wait) {
|
|
1027
1027
|
if (globals.json) {
|
package/dist/lib/billing.d.ts
CHANGED
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
* Credit cost estimators — mirror the backend's billing formulas so the CLI
|
|
3
3
|
* can surface a pre-dispatch estimate without a network round-trip.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Source of truth: `ish-backend/app/billing/rates.py` (`MODALITY_RATES`,
|
|
6
|
+
* `compute_step_cost`, `compute_chat_cost`, `ASK_PER_RESPONSE_CREDITS`,
|
|
7
|
+
* `PAIR_CHAT_SIDE_MULTIPLIER`). These numbers MUST match that module — when a
|
|
8
|
+
* multiplier changes there, update this file in the same commit, otherwise the
|
|
9
|
+
* CLI will mislead agents. The backend prices through one `price_run` dispatcher
|
|
10
|
+
* shared by `POST /billing/estimate` and the real preflight reservation, so the
|
|
11
|
+
* preview here is calibrated to the actual charge as long as the rates agree.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Each modality has its own per-step rate (interactive costs the most —
|
|
14
|
+
* screenshot + vision per step; text-only the least). The per-principal cost is
|
|
15
|
+
* `max(1, round(steps * per_step_credits))`, per participant for step-based
|
|
16
|
+
* modalities and per conversation (×2 in pair mode) for chat. Asks bill a flat
|
|
17
|
+
* credit per successful participant response (an upper bound — refusals/errors
|
|
18
|
+
* don't bill). Clients that need authoritative live rates can fetch them from
|
|
19
|
+
* `GET /billing/rates`; this offline mirror is for the pre-dispatch preview.
|
|
15
20
|
*/
|
|
21
|
+
import type { Modality } from "./modality.js";
|
|
16
22
|
export interface CreditEstimate {
|
|
17
23
|
/** Upper bound (no early termination). Never claims exactness. */
|
|
18
24
|
upper_bound: number;
|
|
@@ -23,16 +29,24 @@ export interface CreditEstimate {
|
|
|
23
29
|
/** Always "credits" today; reserved for future-proofing (e.g. millicredits). */
|
|
24
30
|
unit: "credits";
|
|
25
31
|
}
|
|
26
|
-
/** Mirror of `app/media/billing.py::media_credit_cost`. */
|
|
27
|
-
export declare function mediaCreditCost(steps: number): number;
|
|
28
|
-
/** Mirror of `app/chat/billing.py::chat_credit_cost`. */
|
|
29
|
-
export declare function chatCreditCost(turns: number): number;
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
33
|
+
* Per-principal step-based cost for one participant running `steps` interactions
|
|
34
|
+
* on a study of `modality`. Mirror of `app/billing/rates.py::compute_step_cost`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function stepCreditCost(modality: Modality, steps: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* Per-conversation chat cost. Mirror of `app/billing/rates.py::compute_chat_cost`
|
|
39
|
+
* — `max(1, round(turns * chat_rate))`, doubled in pair mode (both sides bill
|
|
40
|
+
* per turn).
|
|
41
|
+
*/
|
|
42
|
+
export declare function chatCreditCost(turns: number, isPair?: boolean): number;
|
|
43
|
+
/**
|
|
44
|
+
* Step-based run (interactive / text / image / video / audio / document):
|
|
45
|
+
* per-participant cost × participant count. The per-step rate is modality-
|
|
46
|
+
* specific (see `PER_STEP_CREDITS`).
|
|
34
47
|
*/
|
|
35
48
|
export declare function estimateMediaRun(args: {
|
|
49
|
+
modality: Modality;
|
|
36
50
|
participantCount: number;
|
|
37
51
|
maxInteractions: number;
|
|
38
52
|
}): CreditEstimate;
|
package/dist/lib/billing.js
CHANGED
|
@@ -2,41 +2,90 @@
|
|
|
2
2
|
* Credit cost estimators — mirror the backend's billing formulas so the CLI
|
|
3
3
|
* can surface a pre-dispatch estimate without a network round-trip.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Source of truth: `ish-backend/app/billing/rates.py` (`MODALITY_RATES`,
|
|
6
|
+
* `compute_step_cost`, `compute_chat_cost`, `ASK_PER_RESPONSE_CREDITS`,
|
|
7
|
+
* `PAIR_CHAT_SIDE_MULTIPLIER`). These numbers MUST match that module — when a
|
|
8
|
+
* multiplier changes there, update this file in the same commit, otherwise the
|
|
9
|
+
* CLI will mislead agents. The backend prices through one `price_run` dispatcher
|
|
10
|
+
* shared by `POST /billing/estimate` and the real preflight reservation, so the
|
|
11
|
+
* preview here is calibrated to the actual charge as long as the rates agree.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Each modality has its own per-step rate (interactive costs the most —
|
|
14
|
+
* screenshot + vision per step; text-only the least). The per-principal cost is
|
|
15
|
+
* `max(1, round(steps * per_step_credits))`, per participant for step-based
|
|
16
|
+
* modalities and per conversation (×2 in pair mode) for chat. Asks bill a flat
|
|
17
|
+
* credit per successful participant response (an upper bound — refusals/errors
|
|
18
|
+
* don't bill). Clients that need authoritative live rates can fetch them from
|
|
19
|
+
* `GET /billing/rates`; this offline mirror is for the pre-dispatch preview.
|
|
15
20
|
*/
|
|
16
|
-
/**
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Per-step credit multiplier per modality. Mirror of
|
|
23
|
+
* `app/billing/rates.py::MODALITY_RATES` (the `per_step_credits` field).
|
|
24
|
+
*/
|
|
25
|
+
const PER_STEP_CREDITS = {
|
|
26
|
+
interactive: 1.0,
|
|
27
|
+
video: 0.5,
|
|
28
|
+
audio: 0.3,
|
|
29
|
+
image: 0.2,
|
|
30
|
+
text: 0.1,
|
|
31
|
+
document: 0.3,
|
|
32
|
+
chat: 0.2,
|
|
33
|
+
};
|
|
34
|
+
/** Mirror of `app/billing/rates.py::ASK_PER_RESPONSE_CREDITS`. */
|
|
35
|
+
const ASK_PER_RESPONSE_CREDITS = 1;
|
|
36
|
+
/** Mirror of `app/billing/rates.py::PAIR_CHAT_SIDE_MULTIPLIER`. */
|
|
37
|
+
const PAIR_CHAT_SIDE_MULTIPLIER = 2;
|
|
38
|
+
/**
|
|
39
|
+
* Round half-to-even ("banker's rounding") to match Python's built-in
|
|
40
|
+
* `round()`, which the backend uses. `Math.round` rounds half *up*, so for
|
|
41
|
+
* costs that land on a .5 boundary (e.g. video at rate 0.5 with an odd step
|
|
42
|
+
* count: 5 × 0.5 = 2.5) it would over-quote by one credit vs the real charge.
|
|
43
|
+
* Replicating banker's rounding keeps the preview byte-for-byte with the
|
|
44
|
+
* backend's `compute_step_cost` / `compute_chat_cost`.
|
|
45
|
+
*/
|
|
46
|
+
function bankersRound(value) {
|
|
47
|
+
const floor = Math.floor(value);
|
|
48
|
+
const diff = value - floor;
|
|
49
|
+
if (diff < 0.5)
|
|
50
|
+
return floor;
|
|
51
|
+
if (diff > 0.5)
|
|
52
|
+
return floor + 1;
|
|
53
|
+
// Exactly .5 — round to the nearest even integer.
|
|
54
|
+
return floor % 2 === 0 ? floor : floor + 1;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Per-principal step-based cost for one participant running `steps` interactions
|
|
58
|
+
* on a study of `modality`. Mirror of `app/billing/rates.py::compute_step_cost`.
|
|
59
|
+
*/
|
|
60
|
+
export function stepCreditCost(modality, steps) {
|
|
18
61
|
if (!Number.isFinite(steps) || steps <= 0)
|
|
19
62
|
return 1;
|
|
20
|
-
return Math.max(1,
|
|
63
|
+
return Math.max(1, bankersRound(steps * PER_STEP_CREDITS[modality]));
|
|
21
64
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Per-conversation chat cost. Mirror of `app/billing/rates.py::compute_chat_cost`
|
|
67
|
+
* — `max(1, round(turns * chat_rate))`, doubled in pair mode (both sides bill
|
|
68
|
+
* per turn).
|
|
69
|
+
*/
|
|
70
|
+
export function chatCreditCost(turns, isPair = false) {
|
|
24
71
|
if (!Number.isFinite(turns) || turns <= 0)
|
|
25
|
-
return 1;
|
|
26
|
-
|
|
72
|
+
return isPair ? PAIR_CHAT_SIDE_MULTIPLIER : 1;
|
|
73
|
+
const perSide = Math.max(1, bankersRound(turns * PER_STEP_CREDITS.chat));
|
|
74
|
+
return isPair ? perSide * PAIR_CHAT_SIDE_MULTIPLIER : perSide;
|
|
27
75
|
}
|
|
28
76
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
77
|
+
* Step-based run (interactive / text / image / video / audio / document):
|
|
78
|
+
* per-participant cost × participant count. The per-step rate is modality-
|
|
79
|
+
* specific (see `PER_STEP_CREDITS`).
|
|
32
80
|
*/
|
|
33
81
|
export function estimateMediaRun(args) {
|
|
34
|
-
const perParticipant =
|
|
82
|
+
const perParticipant = stepCreditCost(args.modality, args.maxInteractions);
|
|
35
83
|
const total = Math.max(0, args.participantCount) * perParticipant;
|
|
84
|
+
const rate = PER_STEP_CREDITS[args.modality];
|
|
36
85
|
return {
|
|
37
86
|
upper_bound: total,
|
|
38
87
|
formula: "media_per_participant",
|
|
39
|
-
breakdown: `${args.participantCount} participant(s) × max(1, round(${args.maxInteractions} steps
|
|
88
|
+
breakdown: `${args.participantCount} participant(s) × max(1, round(${args.maxInteractions} steps × ${rate})) = ${args.participantCount} × ${perParticipant} = ${total}`,
|
|
40
89
|
unit: "credits",
|
|
41
90
|
};
|
|
42
91
|
}
|
|
@@ -47,18 +96,19 @@ export function estimateChatSolo(args) {
|
|
|
47
96
|
return {
|
|
48
97
|
upper_bound: total,
|
|
49
98
|
formula: "chat_solo",
|
|
50
|
-
breakdown: `${args.participantCount} participant(s) × max(1, round(${args.maxTurns} turns
|
|
99
|
+
breakdown: `${args.participantCount} participant(s) × max(1, round(${args.maxTurns} turns × ${PER_STEP_CREDITS.chat})) = ${args.participantCount} × ${perParticipant} = ${total}`,
|
|
51
100
|
unit: "credits",
|
|
52
101
|
};
|
|
53
102
|
}
|
|
54
103
|
/** Participant-pair chat: each turn bills both sides, so cost doubles. */
|
|
55
104
|
export function estimateChatPair(args) {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
105
|
+
const perConversation = chatCreditCost(args.maxTurns, true);
|
|
106
|
+
const perSide = perConversation / PAIR_CHAT_SIDE_MULTIPLIER;
|
|
107
|
+
const total = Math.max(0, args.conversationCount) * perConversation;
|
|
58
108
|
return {
|
|
59
109
|
upper_bound: total,
|
|
60
110
|
formula: "chat_pair",
|
|
61
|
-
breakdown: `${args.conversationCount} conv × max(1, round(${args.maxTurns} turns
|
|
111
|
+
breakdown: `${args.conversationCount} conv × max(1, round(${args.maxTurns} turns × ${PER_STEP_CREDITS.chat})) × ${PAIR_CHAT_SIDE_MULTIPLIER} sides = ${args.conversationCount} × ${perSide} × ${PAIR_CHAT_SIDE_MULTIPLIER} = ${total}`,
|
|
62
112
|
unit: "credits",
|
|
63
113
|
};
|
|
64
114
|
}
|
|
@@ -67,11 +117,11 @@ export function estimateChatPair(args) {
|
|
|
67
117
|
* for completed responses; the upper bound assumes everyone completes).
|
|
68
118
|
*/
|
|
69
119
|
export function estimateAskRound(args) {
|
|
70
|
-
const total = Math.max(0, args.participantCount);
|
|
120
|
+
const total = Math.max(0, args.participantCount) * ASK_PER_RESPONSE_CREDITS;
|
|
71
121
|
return {
|
|
72
122
|
upper_bound: total,
|
|
73
123
|
formula: "ask_per_response",
|
|
74
|
-
breakdown: `${args.participantCount} participant(s) ×
|
|
124
|
+
breakdown: `${args.participantCount} participant(s) × ${ASK_PER_RESPONSE_CREDITS} credit/response = ${total} (upper bound; only successful responses bill)`,
|
|
75
125
|
unit: "credits",
|
|
76
126
|
};
|
|
77
127
|
}
|
package/dist/lib/modality.d.ts
CHANGED
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* and the call sites (`study run`, `iteration update`, ...) pick up the
|
|
8
8
|
* change for free.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
/** All study modalities. Matches the backend `Modality` enum value set. */
|
|
11
|
+
export declare const MODALITIES: readonly ["interactive", "video", "audio", "text", "image", "document", "chat"];
|
|
12
|
+
export type Modality = typeof MODALITIES[number];
|
|
13
|
+
/**
|
|
14
|
+
* Coerce a raw study-modality string to a known `Modality`, defaulting to
|
|
15
|
+
* `"interactive"` for unknown/empty values — the same default the CLI uses
|
|
16
|
+
* elsewhere (`study.modality || "interactive"`) and the highest-cost rate, so
|
|
17
|
+
* an unrecognised modality errs toward over-estimating rather than under.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toModality(raw: string | undefined): Modality;
|
|
11
20
|
/** True for the media-bucket modalities that share batch + content semantics. */
|
|
12
21
|
export declare function isMediaModality(modality: string | undefined): boolean;
|
|
13
22
|
/** True for chat modality. Wrapped as a helper so callers don't sprinkle string literals. */
|
package/dist/lib/modality.js
CHANGED
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { normalizeEnumValue } from "./enums.js";
|
|
11
11
|
import { MEDIA_MODALITIES } from "./types.js";
|
|
12
|
+
/** All study modalities. Matches the backend `Modality` enum value set. */
|
|
13
|
+
export const MODALITIES = [
|
|
14
|
+
"interactive",
|
|
15
|
+
"video",
|
|
16
|
+
"audio",
|
|
17
|
+
"text",
|
|
18
|
+
"image",
|
|
19
|
+
"document",
|
|
20
|
+
"chat",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Coerce a raw study-modality string to a known `Modality`, defaulting to
|
|
24
|
+
* `"interactive"` for unknown/empty values — the same default the CLI uses
|
|
25
|
+
* elsewhere (`study.modality || "interactive"`) and the highest-cost rate, so
|
|
26
|
+
* an unrecognised modality errs toward over-estimating rather than under.
|
|
27
|
+
*/
|
|
28
|
+
export function toModality(raw) {
|
|
29
|
+
return MODALITIES.includes(raw ?? "")
|
|
30
|
+
? raw
|
|
31
|
+
: "interactive";
|
|
32
|
+
}
|
|
12
33
|
/** True for the media-bucket modalities that share batch + content semantics. */
|
|
13
34
|
export function isMediaModality(modality) {
|
|
14
35
|
return !!modality && MEDIA_MODALITIES.includes(modality);
|