@wiro-ai/wiro-mcp 1.4.0 → 1.5.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/README.md +50 -17
- package/dist/client.d.ts +25 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +129 -29
- package/dist/client.js.map +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +24 -17
- package/dist/instructions.js.map +1 -1
- package/dist/playbooks.d.ts.map +1 -1
- package/dist/playbooks.js +61 -48
- package/dist/playbooks.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +4 -3
- package/dist/prompts.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/apply-agent-skills.d.ts.map +1 -1
- package/dist/tools/apply-agent-skills.js +400 -88
- package/dist/tools/apply-agent-skills.js.map +1 -1
- package/dist/tools/bind-agent-project.d.ts +4 -0
- package/dist/tools/bind-agent-project.d.ts.map +1 -0
- package/dist/tools/bind-agent-project.js +403 -0
- package/dist/tools/bind-agent-project.js.map +1 -0
- package/dist/tools/cancel-agent-message.d.ts.map +1 -1
- package/dist/tools/cancel-agent-message.js +6 -3
- package/dist/tools/cancel-agent-message.js.map +1 -1
- package/dist/tools/cancel-task.js +3 -3
- package/dist/tools/cancel-task.js.map +1 -1
- package/dist/tools/clone-workflow.d.ts.map +1 -1
- package/dist/tools/clone-workflow.js +5 -4
- package/dist/tools/clone-workflow.js.map +1 -1
- package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
- package/dist/tools/connect-agent-oauth.js +144 -44
- package/dist/tools/connect-agent-oauth.js.map +1 -1
- package/dist/tools/control-agent.d.ts.map +1 -1
- package/dist/tools/control-agent.js +133 -45
- package/dist/tools/control-agent.js.map +1 -1
- package/dist/tools/create-workflow.d.ts +9 -6
- package/dist/tools/create-workflow.d.ts.map +1 -1
- package/dist/tools/create-workflow.js +13 -13
- package/dist/tools/create-workflow.js.map +1 -1
- package/dist/tools/delete-agent-session.d.ts.map +1 -1
- package/dist/tools/delete-agent-session.js +20 -14
- package/dist/tools/delete-agent-session.js.map +1 -1
- package/dist/tools/delete-agent.d.ts.map +1 -1
- package/dist/tools/delete-agent.js +14 -10
- package/dist/tools/delete-agent.js.map +1 -1
- package/dist/tools/delete-workflow.d.ts.map +1 -1
- package/dist/tools/delete-workflow.js +6 -5
- package/dist/tools/delete-workflow.js.map +1 -1
- package/dist/tools/deploy-agent.d.ts.map +1 -1
- package/dist/tools/deploy-agent.js +215 -47
- package/dist/tools/deploy-agent.js.map +1 -1
- package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
- package/dist/tools/disconnect-agent-oauth.js +32 -12
- package/dist/tools/disconnect-agent-oauth.js.map +1 -1
- package/dist/tools/get-account.d.ts.map +1 -1
- package/dist/tools/get-account.js +157 -49
- package/dist/tools/get-account.js.map +1 -1
- package/dist/tools/get-agent-activity.d.ts.map +1 -1
- package/dist/tools/get-agent-activity.js +186 -36
- package/dist/tools/get-agent-activity.js.map +1 -1
- package/dist/tools/get-agent-conversation.d.ts.map +1 -1
- package/dist/tools/get-agent-conversation.js +65 -39
- package/dist/tools/get-agent-conversation.js.map +1 -1
- package/dist/tools/get-agent-message.d.ts.map +1 -1
- package/dist/tools/get-agent-message.js +6 -4
- package/dist/tools/get-agent-message.js.map +1 -1
- package/dist/tools/get-agent-template.d.ts.map +1 -1
- package/dist/tools/get-agent-template.js +33 -17
- package/dist/tools/get-agent-template.js.map +1 -1
- package/dist/tools/get-model-schema.d.ts.map +1 -1
- package/dist/tools/get-model-schema.js +7 -5
- package/dist/tools/get-model-schema.js.map +1 -1
- package/dist/tools/get-my-agent.d.ts.map +1 -1
- package/dist/tools/get-my-agent.js +54 -11
- package/dist/tools/get-my-agent.js.map +1 -1
- package/dist/tools/get-playbook.d.ts.map +1 -1
- package/dist/tools/get-playbook.js +3 -2
- package/dist/tools/get-playbook.js.map +1 -1
- package/dist/tools/get-task-price.d.ts +1 -1
- package/dist/tools/get-task-price.d.ts.map +1 -1
- package/dist/tools/get-task-price.js +29 -8
- package/dist/tools/get-task-price.js.map +1 -1
- package/dist/tools/get-task.d.ts.map +1 -1
- package/dist/tools/get-task.js +25 -9
- package/dist/tools/get-task.js.map +1 -1
- package/dist/tools/get-workflow.d.ts.map +1 -1
- package/dist/tools/get-workflow.js +46 -21
- package/dist/tools/get-workflow.js.map +1 -1
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/kill-task.js +3 -3
- package/dist/tools/kill-task.js.map +1 -1
- package/dist/tools/list-agent-skills.d.ts.map +1 -1
- package/dist/tools/list-agent-skills.js +5 -4
- package/dist/tools/list-agent-skills.js.map +1 -1
- package/dist/tools/list-agent-templates.js +4 -4
- package/dist/tools/list-agent-templates.js.map +1 -1
- package/dist/tools/list-my-agents.d.ts.map +1 -1
- package/dist/tools/list-my-agents.js +14 -8
- package/dist/tools/list-my-agents.js.map +1 -1
- package/dist/tools/list-projects.d.ts +4 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +141 -0
- package/dist/tools/list-projects.js.map +1 -0
- package/dist/tools/list-studio-results.js +3 -3
- package/dist/tools/list-studio-results.js.map +1 -1
- package/dist/tools/list-tasks.js +3 -3
- package/dist/tools/list-tasks.js.map +1 -1
- package/dist/tools/list-workflow-runs.d.ts.map +1 -1
- package/dist/tools/list-workflow-runs.js +6 -4
- package/dist/tools/list-workflow-runs.js.map +1 -1
- package/dist/tools/list-workflows.d.ts.map +1 -1
- package/dist/tools/list-workflows.js +7 -6
- package/dist/tools/list-workflows.js.map +1 -1
- package/dist/tools/manage-agent-billing.d.ts.map +1 -1
- package/dist/tools/manage-agent-billing.js +259 -87
- package/dist/tools/manage-agent-billing.js.map +1 -1
- package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
- package/dist/tools/manage-agent-custom-skill.js +91 -21
- package/dist/tools/manage-agent-custom-skill.js.map +1 -1
- package/dist/tools/recommend-model.js +3 -3
- package/dist/tools/recommend-model.js.map +1 -1
- package/dist/tools/resume-workflow-run.d.ts.map +1 -1
- package/dist/tools/resume-workflow-run.js +3 -2
- package/dist/tools/resume-workflow-run.js.map +1 -1
- package/dist/tools/run-model.js +5 -5
- package/dist/tools/run-model.js.map +1 -1
- package/dist/tools/run-workflow.d.ts.map +1 -1
- package/dist/tools/run-workflow.js +3 -2
- package/dist/tools/run-workflow.js.map +1 -1
- package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
- package/dist/tools/save-workflow-from-agent.js +22 -20
- package/dist/tools/save-workflow-from-agent.js.map +1 -1
- package/dist/tools/schemas.d.ts +150 -127
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +76 -3
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/search-docs.js +3 -3
- package/dist/tools/search-docs.js.map +1 -1
- package/dist/tools/search-models.d.ts.map +1 -1
- package/dist/tools/search-models.js +7 -4
- package/dist/tools/search-models.js.map +1 -1
- package/dist/tools/send-agent-message.d.ts.map +1 -1
- package/dist/tools/send-agent-message.js +32 -11
- package/dist/tools/send-agent-message.js.map +1 -1
- package/dist/tools/set-agent-credentials.d.ts.map +1 -1
- package/dist/tools/set-agent-credentials.js +60 -59
- package/dist/tools/set-agent-credentials.js.map +1 -1
- package/dist/tools/update-agent.d.ts.map +1 -1
- package/dist/tools/update-agent.js +59 -21
- package/dist/tools/update-agent.js.map +1 -1
- package/dist/tools/update-workflow.d.ts +205 -6
- package/dist/tools/update-workflow.d.ts.map +1 -1
- package/dist/tools/update-workflow.js +20 -7
- package/dist/tools/update-workflow.js.map +1 -1
- package/dist/tools/upload-file.d.ts.map +1 -1
- package/dist/tools/upload-file.js +13 -9
- package/dist/tools/upload-file.js.map +1 -1
- package/dist/tools/wait-for-task.d.ts.map +1 -1
- package/dist/tools/wait-for-task.js +23 -5
- package/dist/tools/wait-for-task.js.map +1 -1
- package/dist/tools/wait-for-workflow-run.d.ts +18 -18
- package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
- package/dist/tools/wait-for-workflow-run.js +7 -6
- package/dist/tools/wait-for-workflow-run.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/agent-catalog.d.ts +168 -139
- package/dist/utils/agent-catalog.d.ts.map +1 -1
- package/dist/utils/agent-catalog.js +34 -14
- package/dist/utils/agent-catalog.js.map +1 -1
- package/dist/utils/agent-config.d.ts +111 -8
- package/dist/utils/agent-config.d.ts.map +1 -1
- package/dist/utils/agent-config.js +290 -40
- package/dist/utils/agent-config.js.map +1 -1
- package/dist/utils/agent-message.d.ts +41 -22
- package/dist/utils/agent-message.d.ts.map +1 -1
- package/dist/utils/agent-message.js +94 -31
- package/dist/utils/agent-message.js.map +1 -1
- package/dist/utils/agent-targets.d.ts +8 -4
- package/dist/utils/agent-targets.d.ts.map +1 -1
- package/dist/utils/agent-targets.js +51 -35
- package/dist/utils/agent-targets.js.map +1 -1
- package/dist/utils/agent-turns.d.ts +31 -24
- package/dist/utils/agent-turns.d.ts.map +1 -1
- package/dist/utils/agent-turns.js +154 -35
- package/dist/utils/agent-turns.js.map +1 -1
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.d.ts.map +1 -1
- package/dist/utils/api.js +22 -1
- package/dist/utils/api.js.map +1 -1
- package/dist/utils/format.d.ts +27 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +429 -82
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/json-schema-dialect.d.ts +8 -1
- package/dist/utils/json-schema-dialect.d.ts.map +1 -1
- package/dist/utils/json-schema-dialect.js +27 -1
- package/dist/utils/json-schema-dialect.js.map +1 -1
- package/dist/utils/media.d.ts +8 -0
- package/dist/utils/media.d.ts.map +1 -1
- package/dist/utils/media.js +29 -0
- package/dist/utils/media.js.map +1 -1
- package/dist/utils/poll.d.ts +5 -2
- package/dist/utils/poll.d.ts.map +1 -1
- package/dist/utils/poll.js +10 -9
- package/dist/utils/poll.js.map +1 -1
- package/dist/utils/quoted-text.d.ts +28 -0
- package/dist/utils/quoted-text.d.ts.map +1 -0
- package/dist/utils/quoted-text.js +64 -0
- package/dist/utils/quoted-text.js.map +1 -0
- package/dist/utils/structured.d.ts +2 -0
- package/dist/utils/structured.d.ts.map +1 -1
- package/dist/utils/structured.js +15 -2
- package/dist/utils/structured.js.map +1 -1
- package/dist/utils/studio-runs.d.ts +4 -4
- package/dist/utils/studio-runs.d.ts.map +1 -1
- package/dist/utils/studio-runs.js +11 -8
- package/dist/utils/studio-runs.js.map +1 -1
- package/dist/utils/user-agent.d.ts +376 -241
- package/dist/utils/user-agent.d.ts.map +1 -1
- package/dist/utils/user-agent.js +302 -89
- package/dist/utils/user-agent.js.map +1 -1
- package/dist/utils/wiro-project.d.ts +45 -0
- package/dist/utils/wiro-project.d.ts.map +1 -0
- package/dist/utils/wiro-project.js +66 -0
- package/dist/utils/wiro-project.js.map +1 -0
- package/dist/utils/workflow-agent.d.ts +4 -4
- package/dist/utils/workflow-save.d.ts +23 -5
- package/dist/utils/workflow-save.d.ts.map +1 -1
- package/dist/utils/workflow-save.js +130 -38
- package/dist/utils/workflow-save.js.map +1 -1
- package/dist/utils/workflow.d.ts +79 -61
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +149 -19
- package/dist/utils/workflow.js.map +1 -1
- package/dist/utils/workspace.d.ts +64 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +173 -0
- package/dist/utils/workspace.js.map +1 -0
- package/package.json +6 -4
package/dist/utils/format.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isPlainObject } from './api.js';
|
|
1
2
|
import { isToolCallSegment, normalizeTaskRawContent, } from './task-segments.js';
|
|
3
|
+
import { MODEL_ANSWER_LABEL, MODEL_THINKING_LABEL, quotedBlock, RUN_ERROR_LABEL, RUN_RESPONSE_LABEL, } from './quoted-text.js';
|
|
2
4
|
// The value Wiro accepts for an option, and a label to show next to it. Wiro
|
|
3
5
|
// checks an option's value, or its id when it has no value.
|
|
4
6
|
export function toParameterOption(option) {
|
|
@@ -125,13 +127,13 @@ export function formatTaskResult(task) {
|
|
|
125
127
|
lines.push('');
|
|
126
128
|
lines.push('### Response');
|
|
127
129
|
lines.push('');
|
|
128
|
-
lines.push(task.debugoutput);
|
|
130
|
+
lines.push(...quotedBlock(RUN_RESPONSE_LABEL, task.debugoutput));
|
|
129
131
|
}
|
|
130
132
|
if (task.debugerror) {
|
|
131
133
|
lines.push('');
|
|
132
134
|
lines.push('### Error');
|
|
133
135
|
lines.push('');
|
|
134
|
-
lines.push(task.debugerror);
|
|
136
|
+
lines.push(...quotedBlock(RUN_ERROR_LABEL, task.debugerror));
|
|
135
137
|
}
|
|
136
138
|
const displayOutputs = (task.outputs ?? []).filter(output => {
|
|
137
139
|
const projectionState = projectionStates.get(output);
|
|
@@ -146,21 +148,15 @@ export function formatTaskResult(task) {
|
|
|
146
148
|
const thinking = asStringArray(output.content.thinking);
|
|
147
149
|
const answers = asStringArray(output.content.answer);
|
|
148
150
|
if (thinking.length > 0) {
|
|
149
|
-
lines.push('
|
|
150
|
-
for (const t of thinking) {
|
|
151
|
-
lines.push(t);
|
|
152
|
-
}
|
|
151
|
+
lines.push(...quotedBlock(MODEL_THINKING_LABEL, thinking.join('\n')));
|
|
153
152
|
lines.push('');
|
|
154
153
|
}
|
|
155
154
|
if (answers.length > 0) {
|
|
156
|
-
lines.push('
|
|
157
|
-
for (const a of answers) {
|
|
158
|
-
lines.push(a);
|
|
159
|
-
}
|
|
155
|
+
lines.push(...quotedBlock(MODEL_ANSWER_LABEL, answers.join('\n')));
|
|
160
156
|
}
|
|
161
157
|
else if (typeof output.content.raw === 'string'
|
|
162
158
|
&& output.content.raw) {
|
|
163
|
-
lines.push(output.content.raw);
|
|
159
|
+
lines.push(...quotedBlock(MODEL_ANSWER_LABEL, output.content.raw));
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
162
|
else if (output.url) {
|
|
@@ -201,8 +197,7 @@ function asStringArray(value) {
|
|
|
201
197
|
function appendTaskSegments(lines, segments) {
|
|
202
198
|
for (const segment of segments) {
|
|
203
199
|
if (!isToolCallSegment(segment)) {
|
|
204
|
-
lines.push(segment.type === 'thinking' ?
|
|
205
|
-
lines.push(segment.text);
|
|
200
|
+
lines.push(...quotedBlock(segment.type === 'thinking' ? MODEL_THINKING_LABEL : MODEL_ANSWER_LABEL, segment.text));
|
|
206
201
|
lines.push('');
|
|
207
202
|
continue;
|
|
208
203
|
}
|
|
@@ -233,6 +228,14 @@ export function formatTaskSubmitted(taskid, tasktoken) {
|
|
|
233
228
|
+ 'Use `wait_for_task` with this token to wait for the result. '
|
|
234
229
|
+ 'Do not submit the model again for the same request.';
|
|
235
230
|
}
|
|
231
|
+
// What get_task, wait_for_task and get_task_price say when Wiro finds no task
|
|
232
|
+
// with that ID or token in the key's workspace.
|
|
233
|
+
export const TASK_NOT_FOUND = 'Task not found in this workspace. '
|
|
234
|
+
+ 'Check the task ID or token, or find the task with list_tasks.';
|
|
235
|
+
// What they say when `taskid` is not a number, such as a task token passed as
|
|
236
|
+
// the ID: no task can have that ID, so Wiro is not asked.
|
|
237
|
+
export const TASK_ID_NOT_NUMERIC = '`taskid` is the numeric Task ID that run_model and list_tasks show. '
|
|
238
|
+
+ 'Pass a Task Token as `tasktoken` instead.';
|
|
236
239
|
export function formatTaskPending(params) {
|
|
237
240
|
const lines = ['## Task Still Running', ''];
|
|
238
241
|
if (params.taskid)
|
|
@@ -402,6 +405,9 @@ export function formatModelList(models, header) {
|
|
|
402
405
|
const pricing = formatPricing(model);
|
|
403
406
|
if (pricing)
|
|
404
407
|
lines.push(`- **Pricing:** ${pricing}`);
|
|
408
|
+
const discount = discountPercent(model);
|
|
409
|
+
if (discount !== null)
|
|
410
|
+
lines.push(`- **Your discount:** ${discount}% off the part of a charge paid from deposits`);
|
|
405
411
|
lines.push('');
|
|
406
412
|
}
|
|
407
413
|
return lines.join('\n');
|
|
@@ -422,6 +428,11 @@ export function formatModelSchema(model) {
|
|
|
422
428
|
lines.push(pricingSection);
|
|
423
429
|
lines.push('');
|
|
424
430
|
}
|
|
431
|
+
const discount = discountPercent(model);
|
|
432
|
+
if (discount !== null) {
|
|
433
|
+
lines.push(`**Your discount:** ${discount}% off the part of a charge paid from deposits. Charges use coupon credit and store revenue first, at the list price.`);
|
|
434
|
+
lines.push('');
|
|
435
|
+
}
|
|
425
436
|
if (model.parameters && model.parameters.length > 0) {
|
|
426
437
|
lines.push('### Parameters');
|
|
427
438
|
lines.push('');
|
|
@@ -473,121 +484,457 @@ export function formatModelSchema(model) {
|
|
|
473
484
|
lines.push('```');
|
|
474
485
|
return lines.join('\n');
|
|
475
486
|
}
|
|
487
|
+
// The units of the ten price methods Wiro bills by. A row with any other method
|
|
488
|
+
// has no fixed price: its run is charged for the time it takes. A live model's
|
|
489
|
+
// price is its rates (see livePrice), so its unit is never shown.
|
|
476
490
|
const PRICE_METHOD_LABELS = {
|
|
477
491
|
cpr: 'per request',
|
|
478
|
-
cps: 'per second',
|
|
492
|
+
cps: 'per second of run time',
|
|
479
493
|
cpo: 'per output',
|
|
480
|
-
cpt: 'per token',
|
|
481
|
-
'cp-pixel': '
|
|
482
|
-
'cp-audiosecondslength': 'per audio
|
|
483
|
-
'cp-promptlength': 'per character',
|
|
494
|
+
cpt: 'per input and output token',
|
|
495
|
+
'cp-pixel': 'by image size',
|
|
496
|
+
'cp-audiosecondslength': 'per second of audio in inputAudio',
|
|
497
|
+
'cp-promptlength': 'per prompt character',
|
|
484
498
|
'cp-outputVideoLength': 'per video second',
|
|
485
499
|
'cp-realtimeturn': 'per turn',
|
|
486
|
-
'cp-readoutput': '
|
|
500
|
+
'cp-readoutput': 'set by the run when it ends',
|
|
501
|
+
};
|
|
502
|
+
// What a unit price counts, where its unit alone leaves it open.
|
|
503
|
+
const PRICE_METHOD_RULES = {
|
|
504
|
+
cpt: 'Input and output tokens count together, and generated audio counts as output tokens, so the total is known only when the run ends.',
|
|
505
|
+
'cp-promptlength': 'Every character of the `prompt` value counts, including the brackets, keys and quotes of a JSON prompt.',
|
|
487
506
|
};
|
|
507
|
+
const NO_FIXED_PRICE = 'No fixed price: the charge follows the time the run takes and is set when it ends';
|
|
508
|
+
function parseDynamicPrice(dynamicprice) {
|
|
509
|
+
if (!dynamicprice)
|
|
510
|
+
return null;
|
|
511
|
+
try {
|
|
512
|
+
const parsed = typeof dynamicprice === 'string' ? JSON.parse(dynamicprice) : dynamicprice;
|
|
513
|
+
return Array.isArray(parsed) && parsed.length > 0 && parsed.every(row => isPlainObject(row)) ? parsed : null;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Whether a price list is empty, which Wiro bills by run time like a model without one. */
|
|
520
|
+
function hasEmptyPriceList(dynamicprice) {
|
|
521
|
+
if (Array.isArray(dynamicprice))
|
|
522
|
+
return dynamicprice.length === 0;
|
|
523
|
+
if (typeof dynamicprice !== 'string')
|
|
524
|
+
return false;
|
|
525
|
+
try {
|
|
526
|
+
const parsed = JSON.parse(dynamicprice);
|
|
527
|
+
return Array.isArray(parsed) && parsed.length === 0;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** A price row's note on one line, or undefined when it has none. */
|
|
534
|
+
function priceNote(row) {
|
|
535
|
+
const note = typeof row.note === 'string' ? row.note.replace(/\s+/g, ' ').trim() : '';
|
|
536
|
+
return note || undefined;
|
|
537
|
+
}
|
|
538
|
+
function isBilledMethod(row) {
|
|
539
|
+
return Object.prototype.hasOwnProperty.call(PRICE_METHOD_LABELS, row.priceMethod);
|
|
540
|
+
}
|
|
541
|
+
const isLive = (row) => row.priceMethod === 'cp-realtimeturn';
|
|
542
|
+
const isMetered = (row) => row.priceMethod === 'cp-readoutput';
|
|
543
|
+
const isPixel = (row) => row.priceMethod === 'cp-pixel';
|
|
544
|
+
// A live model is charged turn by turn while its session runs, at the rates in
|
|
545
|
+
// its note, so a row price is not what a session costs. One per-turn row makes
|
|
546
|
+
// the whole list live.
|
|
547
|
+
function livePrice(rows) {
|
|
548
|
+
if (!rows.some(isLive))
|
|
549
|
+
return null;
|
|
550
|
+
const note = rows.map(row => priceNote(row)).find(Boolean);
|
|
551
|
+
return note
|
|
552
|
+
? `Billed turn by turn during the live session. Rates: ${note}`
|
|
553
|
+
: 'Billed turn by turn during the live session. No fixed price per run.';
|
|
554
|
+
}
|
|
555
|
+
// A cp-readoutput run reports what it cost when it ends, from what it used,
|
|
556
|
+
// such as tokens. The row's note gives the rates, and a price above 0 is a
|
|
557
|
+
// listed estimate.
|
|
558
|
+
function meteredEstimate(row) {
|
|
559
|
+
const price = usdAmount(row.price);
|
|
560
|
+
return price !== null && price > 0 ? price : undefined;
|
|
561
|
+
}
|
|
562
|
+
function meteredPrice(row) {
|
|
563
|
+
const estimate = meteredEstimate(row);
|
|
564
|
+
const head = estimate === undefined
|
|
565
|
+
? 'Set by the run when it ends'
|
|
566
|
+
: `Set by the run when it ends (listed estimate $${formatPrice(estimate)})`;
|
|
567
|
+
const note = priceNote(row);
|
|
568
|
+
return note ? `${head}: ${note}` : `${head}. The finished task shows the cost.`;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The estimated cost of one run that a price list gives: the listed estimate of
|
|
572
|
+
* a price set when the run ends, else null, since the price is in `pricing`.
|
|
573
|
+
* Undefined for a model without a price list.
|
|
574
|
+
*/
|
|
575
|
+
export function listedEstimate(model) {
|
|
576
|
+
const rows = parseDynamicPrice(model.dynamicprice);
|
|
577
|
+
if (!rows)
|
|
578
|
+
return undefined;
|
|
579
|
+
if (!rows.every(isMetered))
|
|
580
|
+
return null;
|
|
581
|
+
return rows.map(meteredEstimate).find(estimate => estimate !== undefined) ?? null;
|
|
582
|
+
}
|
|
583
|
+
/** "$a", or "$a to $b" from the lowest amount to the highest; undefined when there is none. */
|
|
584
|
+
function amountRange(amounts) {
|
|
585
|
+
if (amounts.length === 0)
|
|
586
|
+
return undefined;
|
|
587
|
+
const low = Math.min(...amounts);
|
|
588
|
+
const high = Math.max(...amounts);
|
|
589
|
+
return low === high ? `$${formatPrice(low)}` : `$${formatPrice(low)} to $${formatPrice(high)}`;
|
|
590
|
+
}
|
|
591
|
+
function positiveAmount(value) {
|
|
592
|
+
const amount = usdAmount(value);
|
|
593
|
+
return amount !== null && amount > 0 ? amount : 0;
|
|
594
|
+
}
|
|
595
|
+
function quantityOf(value) {
|
|
596
|
+
const match = /^QUANTITY:(\d+)$/.exec(String(value));
|
|
597
|
+
return match ? Number(match[1]) : null;
|
|
598
|
+
}
|
|
599
|
+
// cp-pixel counts pixels in blocks of 1024 x 1024, and part of a block counts
|
|
600
|
+
// as a whole one. `price` covers the first block of the first output image and
|
|
601
|
+
// `priceExtra` each further block of it. On a row that needs input images
|
|
602
|
+
// (QUANTITY:1 or more), each input image adds `priceInput` per block, up to 4.
|
|
603
|
+
const PIXEL_BLOCK = '1024 x 1024';
|
|
604
|
+
const PIXEL_INPUT_BLOCKS_MAX = 4;
|
|
605
|
+
const PIXEL_RULES = `Pixels are counted in blocks of ${PIXEL_BLOCK} (1,048,576 pixels), and part of a block `
|
|
606
|
+
+ 'counts as a whole one: 1000 x 1050 is 2 blocks. Only the first output image is measured.';
|
|
607
|
+
function pixelInputRate(row) {
|
|
608
|
+
const needsInput = Object.values(row.inputs ?? {}).some(value => (quantityOf(value) ?? 0) >= 1);
|
|
609
|
+
return needsInput ? positiveAmount(row.priceInput) : 0;
|
|
610
|
+
}
|
|
611
|
+
/** The search line: the first block of output, then each extra that a row sets. */
|
|
612
|
+
function pixelPrice(rows) {
|
|
613
|
+
const extra = amountRange(rows.map(row => positiveAmount(row.priceExtra)).filter(amount => amount > 0));
|
|
614
|
+
const input = amountRange(rows.map(pixelInputRate).filter(amount => amount > 0));
|
|
615
|
+
let text = `${amountRange(rows.map(row => Number(row.price)))} per run for an output image up to ${PIXEL_BLOCK} pixels`;
|
|
616
|
+
if (extra)
|
|
617
|
+
text += `, plus ${extra} per further ${PIXEL_BLOCK} block`;
|
|
618
|
+
if (input)
|
|
619
|
+
text += `, plus ${input} per ${PIXEL_BLOCK} block of each input image (at most ${PIXEL_INPUT_BLOCKS_MAX} blocks per image)`;
|
|
620
|
+
return text;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* When a cp-pixel row applies: "No `inputImage`", "With `inputImage`", "1 to 2
|
|
624
|
+
* `inputImage` files". A run uses the row with the highest count at or below
|
|
625
|
+
* the number of files it sends.
|
|
626
|
+
*/
|
|
627
|
+
function pixelRowCondition(row, rows) {
|
|
628
|
+
const parts = Object.entries(row.inputs ?? {}).map(([key, value]) => {
|
|
629
|
+
const count = quantityOf(value);
|
|
630
|
+
if (count === null)
|
|
631
|
+
return `\`${key}\` ${String(value)}`;
|
|
632
|
+
if (count === 0)
|
|
633
|
+
return `no \`${key}\``;
|
|
634
|
+
const next = rows
|
|
635
|
+
.map(other => quantityOf(other.inputs?.[key]))
|
|
636
|
+
.filter((other) => other !== null && other > count)
|
|
637
|
+
.sort((a, b) => a - b)[0];
|
|
638
|
+
if (next === undefined)
|
|
639
|
+
return count === 1 ? `with \`${key}\`` : `${count} or more \`${key}\` files`;
|
|
640
|
+
if (next === count + 1)
|
|
641
|
+
return `${count} \`${key}\` file${count === 1 ? '' : 's'}`;
|
|
642
|
+
return `${count} to ${next - 1} \`${key}\` files`;
|
|
643
|
+
});
|
|
644
|
+
const text = parts.length > 0 ? parts.join(', ') : 'every run';
|
|
645
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
646
|
+
}
|
|
647
|
+
function pixelRowRates(row) {
|
|
648
|
+
const extra = positiveAmount(row.priceExtra);
|
|
649
|
+
const input = pixelInputRate(row);
|
|
650
|
+
let text = `$${formatPrice(Number(row.price))} for the first block of the output image`;
|
|
651
|
+
if (extra > 0)
|
|
652
|
+
text += `, plus $${formatPrice(extra)} for each further block`;
|
|
653
|
+
if (input > 0) {
|
|
654
|
+
text += `, plus $${formatPrice(input)} per block of each input image, at most `
|
|
655
|
+
+ `${PIXEL_INPUT_BLOCKS_MAX} blocks ($${formatPrice(input * PIXEL_INPUT_BLOCKS_MAX)}) per image`;
|
|
656
|
+
}
|
|
657
|
+
return text;
|
|
658
|
+
}
|
|
659
|
+
// get_model_schema checks the rows against the model's settings: a run uses a
|
|
660
|
+
// row only when its settings carry each of the row's values, and a run that
|
|
661
|
+
// matches no row is charged for the time it takes.
|
|
662
|
+
function settingItems(parameters) {
|
|
663
|
+
const items = new Map();
|
|
664
|
+
for (const group of parameters ?? []) {
|
|
665
|
+
for (const item of group?.items ?? [])
|
|
666
|
+
items.set(String(item.id), item);
|
|
667
|
+
}
|
|
668
|
+
return items;
|
|
669
|
+
}
|
|
670
|
+
// The values a setting accepts, or null when they are not all known: no option
|
|
671
|
+
// list, or options loaded from a list.
|
|
672
|
+
function acceptedValues(item) {
|
|
673
|
+
const loaded = item.optionsLoad;
|
|
674
|
+
if (!item.options?.length || (Array.isArray(loaded) ? loaded.length > 0 : Boolean(loaded)))
|
|
675
|
+
return null;
|
|
676
|
+
const values = item.options.map(option => toParameterOption(option).value).filter(value => value.trim() !== '');
|
|
677
|
+
return values.length > 0 ? values : null;
|
|
678
|
+
}
|
|
679
|
+
// A row value matches a setting value the way Wiro compares them: 5 matches "5".
|
|
680
|
+
function samePriceValue(priceValue, value) {
|
|
681
|
+
return typeof priceValue === 'number' ? Number(value) === priceValue : String(priceValue) === value;
|
|
682
|
+
}
|
|
683
|
+
// A count (QUANTITY:n) needs the setting unless n is 0.
|
|
684
|
+
function priceRowCanApply(row, items) {
|
|
685
|
+
return Object.entries(row.inputs ?? {}).every(([key, value]) => {
|
|
686
|
+
const quantity = quantityOf(value);
|
|
687
|
+
if (quantity !== null)
|
|
688
|
+
return quantity === 0 || items.has(key);
|
|
689
|
+
const item = items.get(key);
|
|
690
|
+
if (!item)
|
|
691
|
+
return false;
|
|
692
|
+
const accepted = acceptedValues(item);
|
|
693
|
+
return accepted === null || accepted.some(option => samePriceValue(value, option));
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
/** Whether a run can get a row Wiro bills by its method; without settings, every row counts. */
|
|
697
|
+
function fixedPriceApplies(rows, items) {
|
|
698
|
+
return rows.some(row => isBilledMethod(row) && (items.size === 0 || priceRowCanApply(row, items)));
|
|
699
|
+
}
|
|
700
|
+
// The offered values of a priced setting that no row prices.
|
|
701
|
+
function unpricedOptionLines(rows, items) {
|
|
702
|
+
const keys = [...new Set(rows.flatMap(row => Object.keys(row.inputs ?? {})))];
|
|
703
|
+
const lines = [];
|
|
704
|
+
for (const key of keys) {
|
|
705
|
+
const item = items.get(key);
|
|
706
|
+
const accepted = item ? acceptedValues(item) : null;
|
|
707
|
+
if (!accepted)
|
|
708
|
+
continue;
|
|
709
|
+
// A row that leaves the key out matches every value of it.
|
|
710
|
+
if (fixedPriceApplies(rows.filter(row => row.inputs?.[key] === undefined), items))
|
|
711
|
+
continue;
|
|
712
|
+
const priced = rows.map(row => row.inputs?.[key]).filter(value => value !== undefined);
|
|
713
|
+
if (priced.some(value => quantityOf(value) !== null))
|
|
714
|
+
continue;
|
|
715
|
+
const missing = [...new Set(accepted.filter(option => !priced.some(value => samePriceValue(value, option))))];
|
|
716
|
+
if (missing.length > 0) {
|
|
717
|
+
lines.push(`Other values of \`${key}\` (${missing.map(value => `\`${value}\``).join(', ')}): no fixed price; the charge follows the time the run takes.`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return lines;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The notes of a model's price rows, each once, in row order, as its page shows
|
|
724
|
+
* them. Rows without a fixed price leave theirs out.
|
|
725
|
+
*/
|
|
726
|
+
export function pricingNotes(model) {
|
|
727
|
+
const rows = parseDynamicPrice(model.dynamicprice);
|
|
728
|
+
if (!rows)
|
|
729
|
+
return [];
|
|
730
|
+
const live = rows.some(isLive);
|
|
731
|
+
if (!live && !fixedPriceApplies(rows, settingItems(model.parameters)))
|
|
732
|
+
return [];
|
|
733
|
+
const notes = rows.filter(row => live || isBilledMethod(row)).map(row => priceNote(row));
|
|
734
|
+
return [...new Set(notes.filter((note) => note !== undefined))];
|
|
735
|
+
}
|
|
736
|
+
// A model without a price list is billed for its run time: at its own rate,
|
|
737
|
+
// or at quegroupcps when its own rate is 0.
|
|
738
|
+
function runTimeRate(model) {
|
|
739
|
+
const own = positiveAmount(model.cps);
|
|
740
|
+
return own > 0 ? own : positiveAmount(model.quegroupcps);
|
|
741
|
+
}
|
|
742
|
+
/** One row's price in the schema; a plain price names its unit unless the table header does. */
|
|
743
|
+
function rowPriceDetailed(row, withUnit) {
|
|
744
|
+
if (!isBilledMethod(row))
|
|
745
|
+
return withUnit ? NO_FIXED_PRICE : 'No fixed price, charged for run time';
|
|
746
|
+
if (isMetered(row))
|
|
747
|
+
return meteredPrice(row);
|
|
748
|
+
if (isPixel(row))
|
|
749
|
+
return pixelRowRates(row);
|
|
750
|
+
const price = `$${formatPrice(row.price)}`;
|
|
751
|
+
return withUnit ? `${price} / ${PRICE_METHOD_LABELS[row.priceMethod]}` : price;
|
|
752
|
+
}
|
|
488
753
|
function formatPricingDetailed(model) {
|
|
489
|
-
const
|
|
754
|
+
const rows = parseDynamicPrice(model.dynamicprice);
|
|
755
|
+
const dynamicResult = rows ? formatDynamicPriceDetailed(rows, settingItems(model.parameters)) : null;
|
|
490
756
|
if (dynamicResult)
|
|
491
757
|
return dynamicResult;
|
|
492
|
-
const
|
|
493
|
-
const approx = parseFloat(model.approximatelycost ?? '0');
|
|
494
|
-
if (approx > 0 ||
|
|
758
|
+
const rate = runTimeRate(model);
|
|
759
|
+
const approx = parseFloat(String(model.approximatelycost ?? '0'));
|
|
760
|
+
if (approx > 0 || rate > 0) {
|
|
495
761
|
const lines = ['### Pricing'];
|
|
496
|
-
if (
|
|
497
|
-
lines.push(`- **Rate:** $${formatPrice(
|
|
762
|
+
if (rate > 0)
|
|
763
|
+
lines.push(`- **Rate:** $${formatPrice(rate)} per second of run time`);
|
|
498
764
|
if (approx > 0)
|
|
499
765
|
lines.push(`- **Estimated cost:** ~$${formatPrice(approx)} per run`);
|
|
500
766
|
return lines.join('\n');
|
|
501
767
|
}
|
|
502
768
|
return null;
|
|
503
769
|
}
|
|
504
|
-
function formatDynamicPriceDetailed(
|
|
505
|
-
if (!dynamicprice)
|
|
506
|
-
return null;
|
|
770
|
+
function formatDynamicPriceDetailed(rows, items) {
|
|
507
771
|
try {
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
return
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
772
|
+
const live = livePrice(rows);
|
|
773
|
+
if (live)
|
|
774
|
+
return `### Pricing\n${live}`;
|
|
775
|
+
if (!fixedPriceApplies(rows, items))
|
|
776
|
+
return `### Pricing\n${NO_FIXED_PRICE}.`;
|
|
777
|
+
if (rows.every(isMetered)) {
|
|
778
|
+
const texts = [...new Set(rows.map(meteredPrice))];
|
|
779
|
+
if (texts.length === 1)
|
|
780
|
+
return `### Pricing\n${texts[0]}`;
|
|
517
781
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
782
|
+
// A note every row shares is shown once; notes that differ go with their
|
|
783
|
+
// rows. A metered row's note is part of its price.
|
|
784
|
+
const notes = rows.map(row => (isBilledMethod(row) && !isMetered(row) ? priceNote(row) : undefined));
|
|
785
|
+
const distinctNotes = [...new Set(notes.filter((note) => note !== undefined))];
|
|
786
|
+
const sharedNote = distinctNotes.length === 1 && notes.every(note => note === distinctNotes[0])
|
|
787
|
+
? distinctNotes[0]
|
|
788
|
+
: undefined;
|
|
789
|
+
const rowNotes = distinctNotes.length > 0 && sharedNote === undefined;
|
|
790
|
+
const keys = [...new Set(rows.flatMap(row => Object.keys(row.inputs ?? {})))];
|
|
791
|
+
const pixel = rows.every(isPixel);
|
|
792
|
+
const single = !pixel && rows.length === 1 && keys.length === 0;
|
|
793
|
+
const after = [];
|
|
794
|
+
let lines;
|
|
795
|
+
if (pixel) {
|
|
796
|
+
lines = ['### Pricing (by image size)', PIXEL_RULES];
|
|
797
|
+
rows.forEach((row, i) => {
|
|
798
|
+
lines.push(`- ${pixelRowCondition(row, rows)}: ${pixelRowRates(row)}.${rowNotes && notes[i] ? ` Note: ${notes[i]}` : ''}`);
|
|
799
|
+
});
|
|
800
|
+
const plain = rows.find(row => Object.values(row.inputs ?? {}).every(value => quantityOf(value) === 0));
|
|
801
|
+
if (plain) {
|
|
802
|
+
const cost = Number(plain.price) + positiveAmount(plain.priceExtra);
|
|
803
|
+
lines.push(`- Example: a 1440 x 1440 output with no input image is 2 blocks, so $${formatPrice(cost)}.`);
|
|
523
804
|
}
|
|
524
805
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
lines
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
806
|
+
else {
|
|
807
|
+
const method = rows.find(isBilledMethod)?.priceMethod ?? '';
|
|
808
|
+
lines = [`### Pricing (${PRICE_METHOD_LABELS[method]})`];
|
|
809
|
+
if (single) {
|
|
810
|
+
lines.push(rowPriceDetailed(rows[0], true));
|
|
811
|
+
}
|
|
812
|
+
else if (keys.length > 0) {
|
|
813
|
+
// A run uses the row with the highest QUANTITY at or below the number of
|
|
814
|
+
// items it sends, so an input's highest QUANTITY row covers any more.
|
|
815
|
+
const topQuantity = new Map();
|
|
816
|
+
for (const row of rows) {
|
|
817
|
+
for (const [key, value] of Object.entries(row.inputs ?? {})) {
|
|
818
|
+
const count = quantityOf(value);
|
|
819
|
+
if (count !== null)
|
|
820
|
+
topQuantity.set(key, Math.max(topQuantity.get(key) ?? 0, count));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
lines.push(`| ${keys.join(' | ')} | Price |${rowNotes ? ' Note |' : ''}`);
|
|
824
|
+
lines.push(`| ${keys.map(() => '---').join(' | ')} | --- |${rowNotes ? ' --- |' : ''}`);
|
|
825
|
+
rows.forEach((row, i) => {
|
|
826
|
+
const values = keys.map(key => {
|
|
827
|
+
const value = String(row.inputs?.[key] ?? '-');
|
|
828
|
+
const count = quantityOf(value);
|
|
829
|
+
if (count === null)
|
|
830
|
+
return value;
|
|
831
|
+
return count > 0 && count === topQuantity.get(key) ? `qty: ${count} or more` : `qty: ${count}`;
|
|
832
|
+
});
|
|
833
|
+
const noteCell = rowNotes ? ` ${(notes[i] ?? '').replace(/\|/g, '\\|')} |` : '';
|
|
834
|
+
lines.push(`| ${values.join(' | ')} | ${rowPriceDetailed(row, false)} |${noteCell}`);
|
|
533
835
|
});
|
|
534
|
-
|
|
836
|
+
after.push(...unpricedOptionLines(rows, items));
|
|
535
837
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
838
|
+
else {
|
|
839
|
+
rows.forEach((row, i) => {
|
|
840
|
+
lines.push(`- ${rowPriceDetailed(row, true)}${rowNotes && notes[i] ? `. Note: ${notes[i]}` : ''}`);
|
|
841
|
+
});
|
|
540
842
|
}
|
|
541
843
|
}
|
|
844
|
+
// A single price is followed directly by what it counts; a table or a list
|
|
845
|
+
// first gets a blank line, so these do not read as one of its rows.
|
|
846
|
+
after.push(...new Set(rows.map(row => PRICE_METHOD_RULES[row.priceMethod]).filter(Boolean)));
|
|
847
|
+
if (sharedNote)
|
|
848
|
+
after.push(`Note: ${sharedNote}`);
|
|
849
|
+
if (after.length > 0)
|
|
850
|
+
lines.push(...(single ? [] : ['']), ...after);
|
|
542
851
|
return lines.join('\n');
|
|
543
852
|
}
|
|
544
853
|
catch {
|
|
545
854
|
return null;
|
|
546
855
|
}
|
|
547
856
|
}
|
|
548
|
-
|
|
549
|
-
|
|
857
|
+
/**
|
|
858
|
+
* The model's price on one line. `detailed`, for get_model_schema, also checks
|
|
859
|
+
* the rows against the model's settings and adds the rules a quote needs, such
|
|
860
|
+
* as how pixels are counted.
|
|
861
|
+
*/
|
|
862
|
+
export function formatPricing(model, detailed = false) {
|
|
863
|
+
const rows = parseDynamicPrice(model.dynamicprice);
|
|
864
|
+
const dynamicResult = rows ? formatDynamicPrice(rows, detailed, settingItems(detailed ? model.parameters : null)) : null;
|
|
550
865
|
if (dynamicResult)
|
|
551
866
|
return dynamicResult;
|
|
552
|
-
const
|
|
553
|
-
const approx = parseFloat(model.approximatelycost ?? '0');
|
|
867
|
+
const approx = parseFloat(String(model.approximatelycost ?? '0'));
|
|
554
868
|
if (approx > 0) {
|
|
555
869
|
return `~$${formatPrice(approx)} per run (estimated)`;
|
|
556
870
|
}
|
|
557
|
-
|
|
558
|
-
|
|
871
|
+
const rate = runTimeRate(model);
|
|
872
|
+
if (rate > 0) {
|
|
873
|
+
return `$${formatPrice(rate)} per second of run time`;
|
|
874
|
+
}
|
|
875
|
+
if (hasEmptyPriceList(model.dynamicprice)) {
|
|
876
|
+
return 'per second of run time; `get_model_schema` gives the rate';
|
|
559
877
|
}
|
|
560
878
|
return null;
|
|
561
879
|
}
|
|
562
|
-
function formatDynamicPrice(
|
|
563
|
-
if (!dynamicprice)
|
|
564
|
-
return null;
|
|
880
|
+
function formatDynamicPrice(rows, detailed, items) {
|
|
565
881
|
try {
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
568
|
-
return
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
882
|
+
const live = livePrice(rows);
|
|
883
|
+
if (live)
|
|
884
|
+
return live;
|
|
885
|
+
if (!fixedPriceApplies(rows, items))
|
|
886
|
+
return NO_FIXED_PRICE;
|
|
887
|
+
const unit = rows.filter(row => isBilledMethod(row) && !isMetered(row) && !isPixel(row));
|
|
888
|
+
const pixel = rows.filter(isPixel);
|
|
889
|
+
const parts = [];
|
|
890
|
+
if (unit.length > 0) {
|
|
891
|
+
const uniquePrices = new Map();
|
|
892
|
+
for (const p of unit) {
|
|
893
|
+
const label = PRICE_METHOD_LABELS[p.priceMethod];
|
|
894
|
+
const key = `${p.price}-${label}`;
|
|
895
|
+
if (!uniquePrices.has(key)) {
|
|
896
|
+
uniquePrices.set(key, { price: p.price, label });
|
|
897
|
+
}
|
|
575
898
|
}
|
|
899
|
+
const entries = [...uniquePrices.values()].sort((a, b) => a.price - b.price);
|
|
900
|
+
const min = entries[0];
|
|
901
|
+
const max = entries[entries.length - 1];
|
|
902
|
+
parts.push(entries.length === 1
|
|
903
|
+
? `$${formatPrice(min.price)} / ${min.label}`
|
|
904
|
+
: `$${formatPrice(min.price)} to $${formatPrice(max.price)} / ${min.label}`);
|
|
576
905
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
906
|
+
if (pixel.length > 0)
|
|
907
|
+
parts.push(pixelPrice(pixel));
|
|
908
|
+
parts.push(...new Set(rows.filter(isMetered).map(meteredPrice)));
|
|
909
|
+
const line = `${parts.join('; ')}${rows.some(row => !isBilledMethod(row)) ? ', other settings have no fixed price' : ''}`;
|
|
910
|
+
// Without settings (search rows), unpricedOptionLines finds nothing.
|
|
911
|
+
const rules = [...new Set([
|
|
912
|
+
...unpricedOptionLines(rows, items),
|
|
913
|
+
...(pixel.length > 0 ? [PIXEL_RULES] : []),
|
|
914
|
+
...unit.map(row => PRICE_METHOD_RULES[row.priceMethod]).filter(Boolean),
|
|
915
|
+
])];
|
|
916
|
+
if (!detailed || rules.length === 0)
|
|
917
|
+
return line;
|
|
918
|
+
return `${line.endsWith('.') ? line : `${line}.`} ${rules.join(' ')}`;
|
|
585
919
|
}
|
|
586
920
|
catch {
|
|
587
921
|
return null;
|
|
588
922
|
}
|
|
589
923
|
}
|
|
590
|
-
|
|
924
|
+
/**
|
|
925
|
+
* The caller's discount on a model in percent, or null when there is none. Wiro
|
|
926
|
+
* takes it off the part of a charge paid from deposits; coupon credit and store
|
|
927
|
+
* revenue pay first, at the list price.
|
|
928
|
+
*/
|
|
929
|
+
export function discountPercent(model) {
|
|
930
|
+
const { discount } = model;
|
|
931
|
+
const value = typeof discount === 'number'
|
|
932
|
+
? discount
|
|
933
|
+
: typeof discount === 'string' ? Number(discount.trim().replace(/%$/, '')) : Number.NaN;
|
|
934
|
+
return Number.isFinite(value) && value > 0 && value <= 100 ? Math.round(value * 100) / 100 : null;
|
|
935
|
+
}
|
|
936
|
+
/** A USD amount with up to 6 decimals and no trailing zeros. */
|
|
937
|
+
export function formatPrice(price) {
|
|
591
938
|
return parseFloat(price.toFixed(6)).toString();
|
|
592
939
|
}
|
|
593
940
|
export function formatSize(sizeStr) {
|