@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.
Files changed (248) hide show
  1. package/README.md +50 -17
  2. package/dist/client.d.ts +25 -6
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +129 -29
  5. package/dist/client.js.map +1 -1
  6. package/dist/instructions.d.ts.map +1 -1
  7. package/dist/instructions.js +24 -17
  8. package/dist/instructions.js.map +1 -1
  9. package/dist/playbooks.d.ts.map +1 -1
  10. package/dist/playbooks.js +61 -48
  11. package/dist/playbooks.js.map +1 -1
  12. package/dist/prompts.d.ts.map +1 -1
  13. package/dist/prompts.js +4 -3
  14. package/dist/prompts.js.map +1 -1
  15. package/dist/server.d.ts +6 -0
  16. package/dist/server.d.ts.map +1 -1
  17. package/dist/server.js +3 -4
  18. package/dist/server.js.map +1 -1
  19. package/dist/tools/apply-agent-skills.d.ts.map +1 -1
  20. package/dist/tools/apply-agent-skills.js +400 -88
  21. package/dist/tools/apply-agent-skills.js.map +1 -1
  22. package/dist/tools/bind-agent-project.d.ts +4 -0
  23. package/dist/tools/bind-agent-project.d.ts.map +1 -0
  24. package/dist/tools/bind-agent-project.js +403 -0
  25. package/dist/tools/bind-agent-project.js.map +1 -0
  26. package/dist/tools/cancel-agent-message.d.ts.map +1 -1
  27. package/dist/tools/cancel-agent-message.js +6 -3
  28. package/dist/tools/cancel-agent-message.js.map +1 -1
  29. package/dist/tools/cancel-task.js +3 -3
  30. package/dist/tools/cancel-task.js.map +1 -1
  31. package/dist/tools/clone-workflow.d.ts.map +1 -1
  32. package/dist/tools/clone-workflow.js +5 -4
  33. package/dist/tools/clone-workflow.js.map +1 -1
  34. package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
  35. package/dist/tools/connect-agent-oauth.js +144 -44
  36. package/dist/tools/connect-agent-oauth.js.map +1 -1
  37. package/dist/tools/control-agent.d.ts.map +1 -1
  38. package/dist/tools/control-agent.js +133 -45
  39. package/dist/tools/control-agent.js.map +1 -1
  40. package/dist/tools/create-workflow.d.ts +9 -6
  41. package/dist/tools/create-workflow.d.ts.map +1 -1
  42. package/dist/tools/create-workflow.js +13 -13
  43. package/dist/tools/create-workflow.js.map +1 -1
  44. package/dist/tools/delete-agent-session.d.ts.map +1 -1
  45. package/dist/tools/delete-agent-session.js +20 -14
  46. package/dist/tools/delete-agent-session.js.map +1 -1
  47. package/dist/tools/delete-agent.d.ts.map +1 -1
  48. package/dist/tools/delete-agent.js +14 -10
  49. package/dist/tools/delete-agent.js.map +1 -1
  50. package/dist/tools/delete-workflow.d.ts.map +1 -1
  51. package/dist/tools/delete-workflow.js +6 -5
  52. package/dist/tools/delete-workflow.js.map +1 -1
  53. package/dist/tools/deploy-agent.d.ts.map +1 -1
  54. package/dist/tools/deploy-agent.js +215 -47
  55. package/dist/tools/deploy-agent.js.map +1 -1
  56. package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
  57. package/dist/tools/disconnect-agent-oauth.js +32 -12
  58. package/dist/tools/disconnect-agent-oauth.js.map +1 -1
  59. package/dist/tools/get-account.d.ts.map +1 -1
  60. package/dist/tools/get-account.js +157 -49
  61. package/dist/tools/get-account.js.map +1 -1
  62. package/dist/tools/get-agent-activity.d.ts.map +1 -1
  63. package/dist/tools/get-agent-activity.js +186 -36
  64. package/dist/tools/get-agent-activity.js.map +1 -1
  65. package/dist/tools/get-agent-conversation.d.ts.map +1 -1
  66. package/dist/tools/get-agent-conversation.js +65 -39
  67. package/dist/tools/get-agent-conversation.js.map +1 -1
  68. package/dist/tools/get-agent-message.d.ts.map +1 -1
  69. package/dist/tools/get-agent-message.js +6 -4
  70. package/dist/tools/get-agent-message.js.map +1 -1
  71. package/dist/tools/get-agent-template.d.ts.map +1 -1
  72. package/dist/tools/get-agent-template.js +33 -17
  73. package/dist/tools/get-agent-template.js.map +1 -1
  74. package/dist/tools/get-model-schema.d.ts.map +1 -1
  75. package/dist/tools/get-model-schema.js +7 -5
  76. package/dist/tools/get-model-schema.js.map +1 -1
  77. package/dist/tools/get-my-agent.d.ts.map +1 -1
  78. package/dist/tools/get-my-agent.js +54 -11
  79. package/dist/tools/get-my-agent.js.map +1 -1
  80. package/dist/tools/get-playbook.d.ts.map +1 -1
  81. package/dist/tools/get-playbook.js +3 -2
  82. package/dist/tools/get-playbook.js.map +1 -1
  83. package/dist/tools/get-task-price.d.ts +1 -1
  84. package/dist/tools/get-task-price.d.ts.map +1 -1
  85. package/dist/tools/get-task-price.js +29 -8
  86. package/dist/tools/get-task-price.js.map +1 -1
  87. package/dist/tools/get-task.d.ts.map +1 -1
  88. package/dist/tools/get-task.js +25 -9
  89. package/dist/tools/get-task.js.map +1 -1
  90. package/dist/tools/get-workflow.d.ts.map +1 -1
  91. package/dist/tools/get-workflow.js +46 -21
  92. package/dist/tools/get-workflow.js.map +1 -1
  93. package/dist/tools/index.d.ts +4 -1
  94. package/dist/tools/index.d.ts.map +1 -1
  95. package/dist/tools/index.js +10 -1
  96. package/dist/tools/index.js.map +1 -1
  97. package/dist/tools/kill-task.js +3 -3
  98. package/dist/tools/kill-task.js.map +1 -1
  99. package/dist/tools/list-agent-skills.d.ts.map +1 -1
  100. package/dist/tools/list-agent-skills.js +5 -4
  101. package/dist/tools/list-agent-skills.js.map +1 -1
  102. package/dist/tools/list-agent-templates.js +4 -4
  103. package/dist/tools/list-agent-templates.js.map +1 -1
  104. package/dist/tools/list-my-agents.d.ts.map +1 -1
  105. package/dist/tools/list-my-agents.js +14 -8
  106. package/dist/tools/list-my-agents.js.map +1 -1
  107. package/dist/tools/list-projects.d.ts +4 -0
  108. package/dist/tools/list-projects.d.ts.map +1 -0
  109. package/dist/tools/list-projects.js +141 -0
  110. package/dist/tools/list-projects.js.map +1 -0
  111. package/dist/tools/list-studio-results.js +3 -3
  112. package/dist/tools/list-studio-results.js.map +1 -1
  113. package/dist/tools/list-tasks.js +3 -3
  114. package/dist/tools/list-tasks.js.map +1 -1
  115. package/dist/tools/list-workflow-runs.d.ts.map +1 -1
  116. package/dist/tools/list-workflow-runs.js +6 -4
  117. package/dist/tools/list-workflow-runs.js.map +1 -1
  118. package/dist/tools/list-workflows.d.ts.map +1 -1
  119. package/dist/tools/list-workflows.js +7 -6
  120. package/dist/tools/list-workflows.js.map +1 -1
  121. package/dist/tools/manage-agent-billing.d.ts.map +1 -1
  122. package/dist/tools/manage-agent-billing.js +259 -87
  123. package/dist/tools/manage-agent-billing.js.map +1 -1
  124. package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
  125. package/dist/tools/manage-agent-custom-skill.js +91 -21
  126. package/dist/tools/manage-agent-custom-skill.js.map +1 -1
  127. package/dist/tools/recommend-model.js +3 -3
  128. package/dist/tools/recommend-model.js.map +1 -1
  129. package/dist/tools/resume-workflow-run.d.ts.map +1 -1
  130. package/dist/tools/resume-workflow-run.js +3 -2
  131. package/dist/tools/resume-workflow-run.js.map +1 -1
  132. package/dist/tools/run-model.js +5 -5
  133. package/dist/tools/run-model.js.map +1 -1
  134. package/dist/tools/run-workflow.d.ts.map +1 -1
  135. package/dist/tools/run-workflow.js +3 -2
  136. package/dist/tools/run-workflow.js.map +1 -1
  137. package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
  138. package/dist/tools/save-workflow-from-agent.js +22 -20
  139. package/dist/tools/save-workflow-from-agent.js.map +1 -1
  140. package/dist/tools/schemas.d.ts +150 -127
  141. package/dist/tools/schemas.d.ts.map +1 -1
  142. package/dist/tools/schemas.js +76 -3
  143. package/dist/tools/schemas.js.map +1 -1
  144. package/dist/tools/search-docs.js +3 -3
  145. package/dist/tools/search-docs.js.map +1 -1
  146. package/dist/tools/search-models.d.ts.map +1 -1
  147. package/dist/tools/search-models.js +7 -4
  148. package/dist/tools/search-models.js.map +1 -1
  149. package/dist/tools/send-agent-message.d.ts.map +1 -1
  150. package/dist/tools/send-agent-message.js +32 -11
  151. package/dist/tools/send-agent-message.js.map +1 -1
  152. package/dist/tools/set-agent-credentials.d.ts.map +1 -1
  153. package/dist/tools/set-agent-credentials.js +60 -59
  154. package/dist/tools/set-agent-credentials.js.map +1 -1
  155. package/dist/tools/update-agent.d.ts.map +1 -1
  156. package/dist/tools/update-agent.js +59 -21
  157. package/dist/tools/update-agent.js.map +1 -1
  158. package/dist/tools/update-workflow.d.ts +205 -6
  159. package/dist/tools/update-workflow.d.ts.map +1 -1
  160. package/dist/tools/update-workflow.js +20 -7
  161. package/dist/tools/update-workflow.js.map +1 -1
  162. package/dist/tools/upload-file.d.ts.map +1 -1
  163. package/dist/tools/upload-file.js +13 -9
  164. package/dist/tools/upload-file.js.map +1 -1
  165. package/dist/tools/wait-for-task.d.ts.map +1 -1
  166. package/dist/tools/wait-for-task.js +23 -5
  167. package/dist/tools/wait-for-task.js.map +1 -1
  168. package/dist/tools/wait-for-workflow-run.d.ts +18 -18
  169. package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
  170. package/dist/tools/wait-for-workflow-run.js +7 -6
  171. package/dist/tools/wait-for-workflow-run.js.map +1 -1
  172. package/dist/types.d.ts +6 -0
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js.map +1 -1
  175. package/dist/utils/agent-catalog.d.ts +168 -139
  176. package/dist/utils/agent-catalog.d.ts.map +1 -1
  177. package/dist/utils/agent-catalog.js +34 -14
  178. package/dist/utils/agent-catalog.js.map +1 -1
  179. package/dist/utils/agent-config.d.ts +111 -8
  180. package/dist/utils/agent-config.d.ts.map +1 -1
  181. package/dist/utils/agent-config.js +290 -40
  182. package/dist/utils/agent-config.js.map +1 -1
  183. package/dist/utils/agent-message.d.ts +41 -22
  184. package/dist/utils/agent-message.d.ts.map +1 -1
  185. package/dist/utils/agent-message.js +94 -31
  186. package/dist/utils/agent-message.js.map +1 -1
  187. package/dist/utils/agent-targets.d.ts +8 -4
  188. package/dist/utils/agent-targets.d.ts.map +1 -1
  189. package/dist/utils/agent-targets.js +51 -35
  190. package/dist/utils/agent-targets.js.map +1 -1
  191. package/dist/utils/agent-turns.d.ts +31 -24
  192. package/dist/utils/agent-turns.d.ts.map +1 -1
  193. package/dist/utils/agent-turns.js +154 -35
  194. package/dist/utils/agent-turns.js.map +1 -1
  195. package/dist/utils/api.d.ts +6 -0
  196. package/dist/utils/api.d.ts.map +1 -1
  197. package/dist/utils/api.js +22 -1
  198. package/dist/utils/api.js.map +1 -1
  199. package/dist/utils/format.d.ts +27 -1
  200. package/dist/utils/format.d.ts.map +1 -1
  201. package/dist/utils/format.js +429 -82
  202. package/dist/utils/format.js.map +1 -1
  203. package/dist/utils/json-schema-dialect.d.ts +8 -1
  204. package/dist/utils/json-schema-dialect.d.ts.map +1 -1
  205. package/dist/utils/json-schema-dialect.js +27 -1
  206. package/dist/utils/json-schema-dialect.js.map +1 -1
  207. package/dist/utils/media.d.ts +8 -0
  208. package/dist/utils/media.d.ts.map +1 -1
  209. package/dist/utils/media.js +29 -0
  210. package/dist/utils/media.js.map +1 -1
  211. package/dist/utils/poll.d.ts +5 -2
  212. package/dist/utils/poll.d.ts.map +1 -1
  213. package/dist/utils/poll.js +10 -9
  214. package/dist/utils/poll.js.map +1 -1
  215. package/dist/utils/quoted-text.d.ts +28 -0
  216. package/dist/utils/quoted-text.d.ts.map +1 -0
  217. package/dist/utils/quoted-text.js +64 -0
  218. package/dist/utils/quoted-text.js.map +1 -0
  219. package/dist/utils/structured.d.ts +2 -0
  220. package/dist/utils/structured.d.ts.map +1 -1
  221. package/dist/utils/structured.js +15 -2
  222. package/dist/utils/structured.js.map +1 -1
  223. package/dist/utils/studio-runs.d.ts +4 -4
  224. package/dist/utils/studio-runs.d.ts.map +1 -1
  225. package/dist/utils/studio-runs.js +11 -8
  226. package/dist/utils/studio-runs.js.map +1 -1
  227. package/dist/utils/user-agent.d.ts +376 -241
  228. package/dist/utils/user-agent.d.ts.map +1 -1
  229. package/dist/utils/user-agent.js +302 -89
  230. package/dist/utils/user-agent.js.map +1 -1
  231. package/dist/utils/wiro-project.d.ts +45 -0
  232. package/dist/utils/wiro-project.d.ts.map +1 -0
  233. package/dist/utils/wiro-project.js +66 -0
  234. package/dist/utils/wiro-project.js.map +1 -0
  235. package/dist/utils/workflow-agent.d.ts +4 -4
  236. package/dist/utils/workflow-save.d.ts +23 -5
  237. package/dist/utils/workflow-save.d.ts.map +1 -1
  238. package/dist/utils/workflow-save.js +130 -38
  239. package/dist/utils/workflow-save.js.map +1 -1
  240. package/dist/utils/workflow.d.ts +79 -61
  241. package/dist/utils/workflow.d.ts.map +1 -1
  242. package/dist/utils/workflow.js +149 -19
  243. package/dist/utils/workflow.js.map +1 -1
  244. package/dist/utils/workspace.d.ts +64 -0
  245. package/dist/utils/workspace.d.ts.map +1 -0
  246. package/dist/utils/workspace.js +173 -0
  247. package/dist/utils/workspace.js.map +1 -0
  248. package/package.json +6 -4
@@ -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('**Thinking:**');
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('**Answer:**');
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' ? '**Thinking:**' : '**Answer:**');
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': 'per pixel',
482
- 'cp-audiosecondslength': 'per audio second',
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': 'model-reported',
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 dynamicResult = formatDynamicPriceDetailed(model.dynamicprice);
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 cps = parseFloat(model.cps ?? '0');
493
- const approx = parseFloat(model.approximatelycost ?? '0');
494
- if (approx > 0 || cps > 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 (cps > 0)
497
- lines.push(`- **Rate:** $${formatPrice(cps)} per second`);
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(dynamicprice) {
505
- if (!dynamicprice)
506
- return null;
770
+ function formatDynamicPriceDetailed(rows, items) {
507
771
  try {
508
- const parsed = typeof dynamicprice === 'string' ? JSON.parse(dynamicprice) : dynamicprice;
509
- if (!Array.isArray(parsed) || parsed.length === 0)
510
- return null;
511
- const method = parsed[0].priceMethod;
512
- const methodLabel = PRICE_METHOD_LABELS[method] ?? method;
513
- const lines = [`### Pricing (${methodLabel})`];
514
- if (parsed.length === 1 && (!parsed[0].inputs || Object.keys(parsed[0].inputs).length === 0)) {
515
- lines.push(`$${formatPrice(parsed[0].price)} / ${methodLabel}`);
516
- return lines.join('\n');
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
- const inputKeys = new Set();
519
- for (const p of parsed) {
520
- if (p.inputs) {
521
- for (const k of Object.keys(p.inputs))
522
- inputKeys.add(k);
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
- const keys = [...inputKeys];
526
- if (keys.length > 0) {
527
- lines.push(`| ${keys.join(' | ')} | Price |`);
528
- lines.push(`| ${keys.map(() => '---').join(' | ')} | --- |`);
529
- for (const p of parsed) {
530
- const values = keys.map(k => {
531
- const v = p.inputs?.[k] ?? '-';
532
- return v.startsWith('QUANTITY:') ? `qty: ${v.split(':')[1]}` : v;
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
- lines.push(`| ${values.join(' | ')} | $${formatPrice(p.price)} |`);
836
+ after.push(...unpricedOptionLines(rows, items));
535
837
  }
536
- }
537
- else {
538
- for (const p of parsed) {
539
- lines.push(`- $${formatPrice(p.price)} / ${PRICE_METHOD_LABELS[p.priceMethod] ?? p.priceMethod}`);
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
- export function formatPricing(model) {
549
- const dynamicResult = formatDynamicPrice(model.dynamicprice);
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 cps = parseFloat(model.cps ?? '0');
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
- if (cps > 0) {
558
- return `$${formatPrice(cps)} per second`;
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(dynamicprice) {
563
- if (!dynamicprice)
564
- return null;
880
+ function formatDynamicPrice(rows, detailed, items) {
565
881
  try {
566
- const parsed = typeof dynamicprice === 'string' ? JSON.parse(dynamicprice) : dynamicprice;
567
- if (!Array.isArray(parsed) || parsed.length === 0)
568
- return null;
569
- const uniquePrices = new Map();
570
- for (const p of parsed) {
571
- const label = PRICE_METHOD_LABELS[p.priceMethod] ?? p.priceMethod;
572
- const key = `${p.price}-${label}`;
573
- if (!uniquePrices.has(key)) {
574
- uniquePrices.set(key, { price: p.price, label });
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
- const entries = [...uniquePrices.values()].sort((a, b) => a.price - b.price);
578
- if (entries.length === 1) {
579
- const e = entries[0];
580
- return `$${formatPrice(e.price)} / ${e.label}`;
581
- }
582
- const min = entries[0];
583
- const max = entries[entries.length - 1];
584
- return `$${formatPrice(min.price)} to $${formatPrice(max.price)} / ${min.label}`;
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
- function formatPrice(price) {
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) {