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