@wiro-ai/wiro-mcp 1.3.4 → 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.
Files changed (283) hide show
  1. package/README.md +183 -16
  2. package/dist/client.d.ts +16 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +126 -24
  5. package/dist/client.js.map +1 -1
  6. package/dist/index.js +4 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/instructions.d.ts +2 -0
  9. package/dist/instructions.d.ts.map +1 -0
  10. package/dist/instructions.js +29 -0
  11. package/dist/instructions.js.map +1 -0
  12. package/dist/playbooks.d.ts +8 -0
  13. package/dist/playbooks.d.ts.map +1 -0
  14. package/dist/playbooks.js +667 -0
  15. package/dist/playbooks.js.map +1 -0
  16. package/dist/prompts.d.ts +4 -0
  17. package/dist/prompts.d.ts.map +1 -0
  18. package/dist/prompts.js +133 -0
  19. package/dist/prompts.js.map +1 -0
  20. package/dist/server.d.ts +15 -2
  21. package/dist/server.d.ts.map +1 -1
  22. package/dist/server.js +12 -7
  23. package/dist/server.js.map +1 -1
  24. package/dist/tools/apply-agent-skills.d.ts +4 -0
  25. package/dist/tools/apply-agent-skills.d.ts.map +1 -0
  26. package/dist/tools/apply-agent-skills.js +601 -0
  27. package/dist/tools/apply-agent-skills.js.map +1 -0
  28. package/dist/tools/cancel-agent-message.d.ts +4 -0
  29. package/dist/tools/cancel-agent-message.d.ts.map +1 -0
  30. package/dist/tools/cancel-agent-message.js +126 -0
  31. package/dist/tools/cancel-agent-message.js.map +1 -0
  32. package/dist/tools/cancel-task.js +3 -3
  33. package/dist/tools/cancel-task.js.map +1 -1
  34. package/dist/tools/clone-workflow.d.ts +4 -0
  35. package/dist/tools/clone-workflow.d.ts.map +1 -0
  36. package/dist/tools/clone-workflow.js +83 -0
  37. package/dist/tools/clone-workflow.js.map +1 -0
  38. package/dist/tools/connect-agent-oauth.d.ts +4 -0
  39. package/dist/tools/connect-agent-oauth.d.ts.map +1 -0
  40. package/dist/tools/connect-agent-oauth.js +508 -0
  41. package/dist/tools/connect-agent-oauth.js.map +1 -0
  42. package/dist/tools/control-agent.d.ts +4 -0
  43. package/dist/tools/control-agent.d.ts.map +1 -0
  44. package/dist/tools/control-agent.js +289 -0
  45. package/dist/tools/control-agent.js.map +1 -0
  46. package/dist/tools/create-workflow.d.ts +100 -0
  47. package/dist/tools/create-workflow.d.ts.map +1 -0
  48. package/dist/tools/create-workflow.js +102 -0
  49. package/dist/tools/create-workflow.js.map +1 -0
  50. package/dist/tools/delete-agent-session.d.ts +6 -0
  51. package/dist/tools/delete-agent-session.d.ts.map +1 -0
  52. package/dist/tools/delete-agent-session.js +134 -0
  53. package/dist/tools/delete-agent-session.js.map +1 -0
  54. package/dist/tools/delete-agent.d.ts +4 -0
  55. package/dist/tools/delete-agent.d.ts.map +1 -0
  56. package/dist/tools/delete-agent.js +145 -0
  57. package/dist/tools/delete-agent.js.map +1 -0
  58. package/dist/tools/delete-workflow.d.ts +4 -0
  59. package/dist/tools/delete-workflow.d.ts.map +1 -0
  60. package/dist/tools/delete-workflow.js +77 -0
  61. package/dist/tools/delete-workflow.js.map +1 -0
  62. package/dist/tools/deploy-agent.d.ts +4 -0
  63. package/dist/tools/deploy-agent.d.ts.map +1 -0
  64. package/dist/tools/deploy-agent.js +463 -0
  65. package/dist/tools/deploy-agent.js.map +1 -0
  66. package/dist/tools/disconnect-agent-oauth.d.ts +4 -0
  67. package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -0
  68. package/dist/tools/disconnect-agent-oauth.js +118 -0
  69. package/dist/tools/disconnect-agent-oauth.js.map +1 -0
  70. package/dist/tools/get-account.d.ts +4 -0
  71. package/dist/tools/get-account.d.ts.map +1 -0
  72. package/dist/tools/get-account.js +112 -0
  73. package/dist/tools/get-account.js.map +1 -0
  74. package/dist/tools/get-agent-activity.d.ts +4 -0
  75. package/dist/tools/get-agent-activity.d.ts.map +1 -0
  76. package/dist/tools/get-agent-activity.js +383 -0
  77. package/dist/tools/get-agent-activity.js.map +1 -0
  78. package/dist/tools/get-agent-conversation.d.ts +4 -0
  79. package/dist/tools/get-agent-conversation.d.ts.map +1 -0
  80. package/dist/tools/get-agent-conversation.js +228 -0
  81. package/dist/tools/get-agent-conversation.js.map +1 -0
  82. package/dist/tools/get-agent-message.d.ts +4 -0
  83. package/dist/tools/get-agent-message.d.ts.map +1 -0
  84. package/dist/tools/get-agent-message.js +68 -0
  85. package/dist/tools/get-agent-message.js.map +1 -0
  86. package/dist/tools/get-agent-template.d.ts +4 -0
  87. package/dist/tools/get-agent-template.d.ts.map +1 -0
  88. package/dist/tools/get-agent-template.js +128 -0
  89. package/dist/tools/get-agent-template.js.map +1 -0
  90. package/dist/tools/get-model-schema.d.ts +5 -1
  91. package/dist/tools/get-model-schema.d.ts.map +1 -1
  92. package/dist/tools/get-model-schema.js +138 -8
  93. package/dist/tools/get-model-schema.js.map +1 -1
  94. package/dist/tools/get-my-agent.d.ts +4 -0
  95. package/dist/tools/get-my-agent.d.ts.map +1 -0
  96. package/dist/tools/get-my-agent.js +117 -0
  97. package/dist/tools/get-my-agent.js.map +1 -0
  98. package/dist/tools/get-playbook.d.ts +3 -0
  99. package/dist/tools/get-playbook.d.ts.map +1 -0
  100. package/dist/tools/get-playbook.js +50 -0
  101. package/dist/tools/get-playbook.js.map +1 -0
  102. package/dist/tools/get-task-price.d.ts +1 -1
  103. package/dist/tools/get-task-price.d.ts.map +1 -1
  104. package/dist/tools/get-task-price.js +64 -16
  105. package/dist/tools/get-task-price.js.map +1 -1
  106. package/dist/tools/get-task.d.ts.map +1 -1
  107. package/dist/tools/get-task.js +26 -9
  108. package/dist/tools/get-task.js.map +1 -1
  109. package/dist/tools/get-workflow.d.ts +9 -0
  110. package/dist/tools/get-workflow.d.ts.map +1 -0
  111. package/dist/tools/get-workflow.js +105 -0
  112. package/dist/tools/get-workflow.js.map +1 -0
  113. package/dist/tools/index.d.ts +63 -2
  114. package/dist/tools/index.d.ts.map +1 -1
  115. package/dist/tools/index.js +131 -14
  116. package/dist/tools/index.js.map +1 -1
  117. package/dist/tools/kill-task.d.ts.map +1 -1
  118. package/dist/tools/kill-task.js +10 -6
  119. package/dist/tools/kill-task.js.map +1 -1
  120. package/dist/tools/list-agent-skills.d.ts +4 -0
  121. package/dist/tools/list-agent-skills.d.ts.map +1 -0
  122. package/dist/tools/list-agent-skills.js +242 -0
  123. package/dist/tools/list-agent-skills.js.map +1 -0
  124. package/dist/tools/list-agent-templates.d.ts +4 -0
  125. package/dist/tools/list-agent-templates.d.ts.map +1 -0
  126. package/dist/tools/list-agent-templates.js +110 -0
  127. package/dist/tools/list-agent-templates.js.map +1 -0
  128. package/dist/tools/list-my-agents.d.ts +4 -0
  129. package/dist/tools/list-my-agents.d.ts.map +1 -0
  130. package/dist/tools/list-my-agents.js +93 -0
  131. package/dist/tools/list-my-agents.js.map +1 -0
  132. package/dist/tools/list-studio-results.d.ts +4 -0
  133. package/dist/tools/list-studio-results.d.ts.map +1 -0
  134. package/dist/tools/list-studio-results.js +95 -0
  135. package/dist/tools/list-studio-results.js.map +1 -0
  136. package/dist/tools/list-tasks.d.ts.map +1 -1
  137. package/dist/tools/list-tasks.js +7 -5
  138. package/dist/tools/list-tasks.js.map +1 -1
  139. package/dist/tools/list-workflow-runs.d.ts +4 -0
  140. package/dist/tools/list-workflow-runs.d.ts.map +1 -0
  141. package/dist/tools/list-workflow-runs.js +110 -0
  142. package/dist/tools/list-workflow-runs.js.map +1 -0
  143. package/dist/tools/list-workflows.d.ts +4 -0
  144. package/dist/tools/list-workflows.d.ts.map +1 -0
  145. package/dist/tools/list-workflows.js +143 -0
  146. package/dist/tools/list-workflows.js.map +1 -0
  147. package/dist/tools/manage-agent-billing.d.ts +4 -0
  148. package/dist/tools/manage-agent-billing.d.ts.map +1 -0
  149. package/dist/tools/manage-agent-billing.js +788 -0
  150. package/dist/tools/manage-agent-billing.js.map +1 -0
  151. package/dist/tools/manage-agent-custom-skill.d.ts +4 -0
  152. package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -0
  153. package/dist/tools/manage-agent-custom-skill.js +531 -0
  154. package/dist/tools/manage-agent-custom-skill.js.map +1 -0
  155. package/dist/tools/recommend-model.d.ts.map +1 -1
  156. package/dist/tools/recommend-model.js +6 -5
  157. package/dist/tools/recommend-model.js.map +1 -1
  158. package/dist/tools/resume-workflow-run.d.ts +4 -0
  159. package/dist/tools/resume-workflow-run.d.ts.map +1 -0
  160. package/dist/tools/resume-workflow-run.js +69 -0
  161. package/dist/tools/resume-workflow-run.js.map +1 -0
  162. package/dist/tools/run-model.d.ts.map +1 -1
  163. package/dist/tools/run-model.js +40 -4
  164. package/dist/tools/run-model.js.map +1 -1
  165. package/dist/tools/run-workflow.d.ts +6 -0
  166. package/dist/tools/run-workflow.d.ts.map +1 -0
  167. package/dist/tools/run-workflow.js +132 -0
  168. package/dist/tools/run-workflow.js.map +1 -0
  169. package/dist/tools/save-workflow-from-agent.d.ts +4 -0
  170. package/dist/tools/save-workflow-from-agent.d.ts.map +1 -0
  171. package/dist/tools/save-workflow-from-agent.js +280 -0
  172. package/dist/tools/save-workflow-from-agent.js.map +1 -0
  173. package/dist/tools/schemas.d.ts +182 -115
  174. package/dist/tools/schemas.d.ts.map +1 -1
  175. package/dist/tools/schemas.js +26 -6
  176. package/dist/tools/schemas.js.map +1 -1
  177. package/dist/tools/search-models.d.ts.map +1 -1
  178. package/dist/tools/search-models.js +9 -6
  179. package/dist/tools/search-models.js.map +1 -1
  180. package/dist/tools/send-agent-message.d.ts +4 -0
  181. package/dist/tools/send-agent-message.d.ts.map +1 -0
  182. package/dist/tools/send-agent-message.js +156 -0
  183. package/dist/tools/send-agent-message.js.map +1 -0
  184. package/dist/tools/set-agent-credentials.d.ts +4 -0
  185. package/dist/tools/set-agent-credentials.d.ts.map +1 -0
  186. package/dist/tools/set-agent-credentials.js +540 -0
  187. package/dist/tools/set-agent-credentials.js.map +1 -0
  188. package/dist/tools/update-agent.d.ts +4 -0
  189. package/dist/tools/update-agent.d.ts.map +1 -0
  190. package/dist/tools/update-agent.js +179 -0
  191. package/dist/tools/update-agent.js.map +1 -0
  192. package/dist/tools/update-workflow.d.ts +201 -0
  193. package/dist/tools/update-workflow.d.ts.map +1 -0
  194. package/dist/tools/update-workflow.js +168 -0
  195. package/dist/tools/update-workflow.js.map +1 -0
  196. package/dist/tools/upload-file.d.ts.map +1 -1
  197. package/dist/tools/upload-file.js +12 -7
  198. package/dist/tools/upload-file.js.map +1 -1
  199. package/dist/tools/wait-for-task.d.ts.map +1 -1
  200. package/dist/tools/wait-for-task.js +20 -2
  201. package/dist/tools/wait-for-task.js.map +1 -1
  202. package/dist/tools/wait-for-workflow-run.d.ts +259 -0
  203. package/dist/tools/wait-for-workflow-run.d.ts.map +1 -0
  204. package/dist/tools/wait-for-workflow-run.js +190 -0
  205. package/dist/tools/wait-for-workflow-run.js.map +1 -0
  206. package/dist/types.d.ts +18 -0
  207. package/dist/types.d.ts.map +1 -1
  208. package/dist/types.js.map +1 -1
  209. package/dist/utils/agent-catalog.d.ts +1256 -0
  210. package/dist/utils/agent-catalog.d.ts.map +1 -0
  211. package/dist/utils/agent-catalog.js +387 -0
  212. package/dist/utils/agent-catalog.js.map +1 -0
  213. package/dist/utils/agent-config.d.ts +443 -0
  214. package/dist/utils/agent-config.d.ts.map +1 -0
  215. package/dist/utils/agent-config.js +1676 -0
  216. package/dist/utils/agent-config.js.map +1 -0
  217. package/dist/utils/agent-message.d.ts +112 -0
  218. package/dist/utils/agent-message.d.ts.map +1 -0
  219. package/dist/utils/agent-message.js +243 -0
  220. package/dist/utils/agent-message.js.map +1 -0
  221. package/dist/utils/agent-targets.d.ts +76 -0
  222. package/dist/utils/agent-targets.d.ts.map +1 -0
  223. package/dist/utils/agent-targets.js +412 -0
  224. package/dist/utils/agent-targets.js.map +1 -0
  225. package/dist/utils/agent-turns.d.ts +354 -0
  226. package/dist/utils/agent-turns.d.ts.map +1 -0
  227. package/dist/utils/agent-turns.js +660 -0
  228. package/dist/utils/agent-turns.js.map +1 -0
  229. package/dist/utils/api.d.ts +40 -0
  230. package/dist/utils/api.d.ts.map +1 -0
  231. package/dist/utils/api.js +111 -0
  232. package/dist/utils/api.js.map +1 -0
  233. package/dist/utils/format.d.ts +65 -2
  234. package/dist/utils/format.d.ts.map +1 -1
  235. package/dist/utils/format.js +583 -81
  236. package/dist/utils/format.js.map +1 -1
  237. package/dist/utils/json-schema-dialect.d.ts +8 -1
  238. package/dist/utils/json-schema-dialect.d.ts.map +1 -1
  239. package/dist/utils/json-schema-dialect.js +23 -1
  240. package/dist/utils/json-schema-dialect.js.map +1 -1
  241. package/dist/utils/media.d.ts +28 -0
  242. package/dist/utils/media.d.ts.map +1 -0
  243. package/dist/utils/media.js +65 -0
  244. package/dist/utils/media.js.map +1 -0
  245. package/dist/utils/poll.d.ts +39 -0
  246. package/dist/utils/poll.d.ts.map +1 -0
  247. package/dist/utils/poll.js +111 -0
  248. package/dist/utils/poll.js.map +1 -0
  249. package/dist/utils/remote-file.js +2 -2
  250. package/dist/utils/remote-file.js.map +1 -1
  251. package/dist/utils/structured.d.ts +4 -0
  252. package/dist/utils/structured.d.ts.map +1 -1
  253. package/dist/utils/structured.js +23 -7
  254. package/dist/utils/structured.js.map +1 -1
  255. package/dist/utils/studio-runs.d.ts +87 -0
  256. package/dist/utils/studio-runs.d.ts.map +1 -0
  257. package/dist/utils/studio-runs.js +167 -0
  258. package/dist/utils/studio-runs.js.map +1 -0
  259. package/dist/utils/user-agent.d.ts +1199 -0
  260. package/dist/utils/user-agent.d.ts.map +1 -0
  261. package/dist/utils/user-agent.js +897 -0
  262. package/dist/utils/user-agent.js.map +1 -0
  263. package/dist/utils/workflow-agent.d.ts +151 -0
  264. package/dist/utils/workflow-agent.d.ts.map +1 -0
  265. package/dist/utils/workflow-agent.js +292 -0
  266. package/dist/utils/workflow-agent.js.map +1 -0
  267. package/dist/utils/workflow-artifacts.d.ts +160 -0
  268. package/dist/utils/workflow-artifacts.d.ts.map +1 -0
  269. package/dist/utils/workflow-artifacts.js +801 -0
  270. package/dist/utils/workflow-artifacts.js.map +1 -0
  271. package/dist/utils/workflow-save.d.ts +120 -0
  272. package/dist/utils/workflow-save.d.ts.map +1 -0
  273. package/dist/utils/workflow-save.js +728 -0
  274. package/dist/utils/workflow-save.js.map +1 -0
  275. package/dist/utils/workflow.d.ts +564 -0
  276. package/dist/utils/workflow.d.ts.map +1 -0
  277. package/dist/utils/workflow.js +712 -0
  278. package/dist/utils/workflow.js.map +1 -0
  279. package/dist/utils/workspace.d.ts +38 -0
  280. package/dist/utils/workspace.d.ts.map +1 -0
  281. package/dist/utils/workspace.js +90 -0
  282. package/dist/utils/workspace.js.map +1 -0
  283. package/package.json +6 -4
@@ -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.
@@ -6,6 +7,56 @@ export function toParameterOption(option) {
6
7
  const modelSlug = option.slugowner && option.slugproject ? `${option.slugowner}/${option.slugproject}` : undefined;
7
8
  return { label: String(option.label ?? option.text ?? option.name ?? modelSlug ?? value), value };
8
9
  }
10
+ function usdAmount(value) {
11
+ if (value === undefined || value === null || value === '')
12
+ return null;
13
+ const parsed = Number(value);
14
+ return Number.isFinite(parsed) ? parsed : null;
15
+ }
16
+ /**
17
+ * A successful task that Wiro has not billed yet. It reads as finished a moment
18
+ * before its charge is recorded, so only the list price is known.
19
+ */
20
+ export function awaitingCharge(task) {
21
+ const list = usdAmount(task.totalcost);
22
+ return task.status === 'task_postprocess_end' && task.pexit === '0'
23
+ && usdAmount(task.chargedcost) === null && list !== null && list > 0;
24
+ }
25
+ /**
26
+ * A cancelled task that no worker ever took, so it never ran and is never
27
+ * charged. A row without accepttime is not counted as one.
28
+ */
29
+ export function cancelledWhileQueued(task) {
30
+ const accepted = task.accepttime;
31
+ return task.status === 'task_cancel'
32
+ && accepted !== undefined && accepted !== null && accepted !== ''
33
+ && Number(accepted) === 0;
34
+ }
35
+ /**
36
+ * A task's cost for the user: what Wiro charged once it billed the task, with
37
+ * the list price beside it when a discount made them differ. Before Wiro bills
38
+ * a successful task, the list price, marked as such. A cancelled task can still
39
+ * be charged once work it had already started ends, so $0 there is only the
40
+ * amount so far, unless it was cancelled while queued.
41
+ */
42
+ export function formatTaskCost(task) {
43
+ const list = usdAmount(task.totalcost);
44
+ const charged = usdAmount(task.chargedcost);
45
+ if (cancelledWhileQueued(task) && !(charged !== null && charged > 0)) {
46
+ return '$0 (cancelled while queued, not charged)';
47
+ }
48
+ if (awaitingCharge(task))
49
+ return `$${list} list price; the charged amount is not recorded yet`;
50
+ if (charged !== null) {
51
+ if (charged === 0)
52
+ return task.status === 'task_cancel' ? 'no charge recorded yet' : '$0 (not charged)';
53
+ if (list !== null && list > 0 && Math.abs(list - charged) > 1e-9) {
54
+ return `$${charged} charged (list price $${list})`;
55
+ }
56
+ return `$${charged}`;
57
+ }
58
+ return list !== null && list > 0 ? `$${list}` : null;
59
+ }
9
60
  export function formatTaskResult(task) {
10
61
  const lines = [];
11
62
  const requireCompleteTurn = task.status === 'task_postprocess_end'
@@ -30,6 +81,8 @@ export function formatTaskResult(task) {
30
81
  lines.push('');
31
82
  lines.push(`**Status:** ${task.status}`);
32
83
  lines.push(`**Task ID:** ${task.id}`);
84
+ if (task.socketaccesstoken)
85
+ lines.push(`**Task Token:** ${task.socketaccesstoken}`);
33
86
  if (task.modelslugowner && task.modelslugproject) {
34
87
  lines.push(`**Model:** \`${task.modelslugowner}/${task.modelslugproject}\``);
35
88
  }
@@ -45,9 +98,9 @@ export function formatTaskResult(task) {
45
98
  if (task.elapsedseconds) {
46
99
  lines.push(`**Duration:** ${task.elapsedseconds}s`);
47
100
  }
48
- if (task.totalcost && task.totalcost !== '0') {
49
- lines.push(`**Cost:** $${task.totalcost}`);
50
- }
101
+ const cost = formatTaskCost(task);
102
+ if (cost)
103
+ lines.push(`**Cost:** ${cost}`);
51
104
  if (hasInvalidProjection) {
52
105
  lines.push('');
53
106
  lines.push('### Response');
@@ -181,6 +234,14 @@ export function formatTaskSubmitted(taskid, tasktoken) {
181
234
  + 'Use `wait_for_task` with this token to wait for the result. '
182
235
  + 'Do not submit the model again for the same request.';
183
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.';
184
245
  export function formatTaskPending(params) {
185
246
  const lines = ['## Task Still Running', ''];
186
247
  if (params.taskid)
@@ -222,9 +283,9 @@ export function formatTaskList(tasks, total, start = 0) {
222
283
  lines.push(`- **Started:** ${task.starttime}`);
223
284
  if (task.elapsedseconds)
224
285
  lines.push(`- **Duration:** ${task.elapsedseconds}s`);
225
- if (task.totalcost && task.totalcost !== '0') {
226
- lines.push(`- **Cost:** $${task.totalcost}`);
227
- }
286
+ const cost = formatTaskCost(task);
287
+ if (cost)
288
+ lines.push(`- **Cost:** ${cost}`);
228
289
  const outputLinks = (task.outputs ?? [])
229
290
  .filter(output => output.url)
230
291
  .slice(0, 3);
@@ -236,6 +297,102 @@ export function formatTaskList(tasks, total, start = 0) {
236
297
  }
237
298
  return lines.join('\n');
238
299
  }
300
+ /** A number from Tool/Detail's text fields, where "" means none. */
301
+ function rangeNumber(value) {
302
+ if (value === undefined || value === null || value === '')
303
+ return undefined;
304
+ const parsed = Number(value);
305
+ return Number.isFinite(parsed) ? parsed : undefined;
306
+ }
307
+ // A checkbox's catalog value means on or off. true, 1, yes, on and any other
308
+ // non-empty text are on (some models keep a command-line flag such as
309
+ // "--do_sample" there); "", false, 0, no, off, null and undefined are off.
310
+ const CHECKBOX_OFF_VALUES = new Set(['', 'false', '0', 'no', 'off', 'null', 'undefined']);
311
+ function checkboxIsOn(value) {
312
+ if (typeof value === 'boolean')
313
+ return value;
314
+ if (typeof value === 'number')
315
+ return value !== 0;
316
+ if (typeof value !== 'string')
317
+ return false;
318
+ return !CHECKBOX_OFF_VALUES.has(value.trim().toLowerCase());
319
+ }
320
+ /**
321
+ * The catalog value of a parameter: a checkbox's state as "true" or "false"
322
+ * (from Tool/Detail's `value`, else `defaultvalue`), otherwise `defaultvalue`,
323
+ * then `value`. Arrays are JSON text; empty means none.
324
+ */
325
+ export function parameterDefault(item) {
326
+ if (item.type === 'checkbox') {
327
+ const initial = Object.prototype.hasOwnProperty.call(item, 'value') ? item.value : item.defaultvalue;
328
+ return checkboxIsOn(initial) ? 'true' : 'false';
329
+ }
330
+ const raw = item.defaultvalue ?? item.value ?? item.default;
331
+ if (raw === undefined || raw === null || raw === '')
332
+ return undefined;
333
+ if (Array.isArray(raw))
334
+ return raw.length > 0 ? JSON.stringify(raw) : undefined;
335
+ return typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
336
+ }
337
+ // A run fills an optional parameter left out of its request with the catalog
338
+ // value, except these, whose catalog value is a sample shown on the model's page:
339
+ // file inputs, ids containing "input", the prompt and the structured LLM fields.
340
+ // A run refuses a required parameter that is left out before it fills anything.
341
+ // A workflow step that leaves out any of them gets the catalog value. Two ids
342
+ // follow their own rule: a run picks a random seed when `seed` is left out or 0,
343
+ // and `parallel_tool_calls` is true whenever `tools` are sent.
344
+ const SAMPLE_TYPES = new Set(['fileinput', 'multifileinput', 'multifileinputdrop', 'combinefileinput', 'fileexplorer']);
345
+ const SAMPLE_IDS = new Set(['prompt', 'messages', 'tools', 'tool_choice', 'previousTaskToken', 'toolOutputs', 'response_format']);
346
+ export function isSampleParameter(item) {
347
+ const id = String(item.id);
348
+ return SAMPLE_TYPES.has(String(item.type)) || id.includes('input') || SAMPLE_IDS.has(id);
349
+ }
350
+ /** What a left-out parameter gets: `default` in a run, `sample` only in a workflow step. */
351
+ export function parameterFallback(item) {
352
+ const id = String(item.id);
353
+ if (id === 'parallel_tool_calls')
354
+ return { default: 'true' };
355
+ const value = parameterDefault(item);
356
+ if (value === undefined)
357
+ return {};
358
+ if (id === 'seed')
359
+ return Number(value) === 0 ? {} : { sample: value };
360
+ const required = item.required === true || String(item.required) === 'true';
361
+ return !required && !isSampleParameter(item) ? { default: value } : { sample: value };
362
+ }
363
+ /** How the parameter list shows what a left-out parameter gets. */
364
+ export function parameterFallbackText(item) {
365
+ const id = String(item.id);
366
+ const { default: value, sample } = parameterFallback(item);
367
+ const shown = (text) => (text.length > 120 ? `${text.slice(0, 120)}...` : text);
368
+ if (id === 'parallel_tool_calls')
369
+ return ' (default: `true` when `tools` are sent)';
370
+ if (id === 'seed') {
371
+ return ` (a run picks a random seed when it is left out or 0${sample !== undefined
372
+ ? `; a workflow step that leaves it out gets \`${shown(sample)}\``
373
+ : ''})`;
374
+ }
375
+ if (value !== undefined)
376
+ return ` (default: \`${shown(value)}\`)`;
377
+ if (sample === undefined)
378
+ return '';
379
+ return /\/sampleinputs\//.test(sample)
380
+ ? ' (sample files on the model\'s page, used only by a workflow step that leaves it out)'
381
+ : ` (sample: \`${shown(sample)}\`, used only by a workflow step that leaves it out)`;
382
+ }
383
+ // Only number and float parameters get a range, as in the run form, which
384
+ // draws a slider only for those. Bounds on other types, such as 0 to 0 on a
385
+ // text or select field, are not limits, so they get none.
386
+ const RANGE_TYPES = new Set(['number', 'float', 'range']);
387
+ export function parameterRange(item) {
388
+ if (!RANGE_TYPES.has(String(item.type)))
389
+ return {};
390
+ return {
391
+ min: rangeNumber(item.minvalue) ?? rangeNumber(item.min),
392
+ max: rangeNumber(item.maxvalue) ?? rangeNumber(item.max),
393
+ step: rangeNumber(item.incrementby) ?? rangeNumber(item.step),
394
+ };
395
+ }
239
396
  export function formatModelList(models, header) {
240
397
  if (models.length === 0)
241
398
  return 'No models found.';
@@ -245,7 +402,7 @@ export function formatModelList(models, header) {
245
402
  for (const model of models) {
246
403
  const slug = `${model.cleanslugowner}/${model.cleanslugproject}`;
247
404
  const cats = model.categories?.filter(c => c !== 'tool').join(', ') || '';
248
- lines.push(`### ${model.title}`);
405
+ lines.push(`### ${model.title || slug}`);
249
406
  lines.push(`- **Model:** \`${slug}\``);
250
407
  if (cats)
251
408
  lines.push(`- **Categories:** ${cats}`);
@@ -254,6 +411,9 @@ export function formatModelList(models, header) {
254
411
  const pricing = formatPricing(model);
255
412
  if (pricing)
256
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`);
257
417
  lines.push('');
258
418
  }
259
419
  return lines.join('\n');
@@ -261,7 +421,7 @@ export function formatModelList(models, header) {
261
421
  export function formatModelSchema(model) {
262
422
  const lines = [];
263
423
  const slug = `${model.cleanslugowner}/${model.cleanslugproject}`;
264
- lines.push(`## ${model.title}`);
424
+ lines.push(`## ${model.title || slug}`);
265
425
  lines.push(`**Model:** \`${slug}\``);
266
426
  if (model.seodescription)
267
427
  lines.push(`**Description:** ${model.seodescription}`);
@@ -274,6 +434,11 @@ export function formatModelSchema(model) {
274
434
  lines.push(pricingSection);
275
435
  lines.push('');
276
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
+ }
277
442
  if (model.parameters && model.parameters.length > 0) {
278
443
  lines.push('### Parameters');
279
444
  lines.push('');
@@ -284,7 +449,7 @@ export function formatModelSchema(model) {
284
449
  }
285
450
  for (const param of group.items) {
286
451
  const required = param.required ? ' **(required)**' : '';
287
- const defaultVal = param.default ? ` (default: \`${param.default}\`)` : '';
452
+ const defaultVal = parameterFallbackText(param);
288
453
  lines.push(`- **\`${param.id}\`** (${param.type})${required}${defaultVal}`);
289
454
  if (param.label)
290
455
  lines.push(` ${param.label}`);
@@ -297,7 +462,7 @@ export function formatModelSchema(model) {
297
462
  if (param.type === 'fileinput')
298
463
  lines.push(` Pass a URL directly (e.g. \`${param.id}: "https://..."\`) or use \`${param.id}Url\` for the URL field.`);
299
464
  if (param.type === 'multifileinput')
300
- lines.push(` Pass URLs via \`${param.id}Url\` (comma-separated) or upload files in \`${param.id}\`.`);
465
+ lines.push(` Pass the URLs comma-separated in \`${param.id.replace('Multiple', '')}UrlMultiple\`.`);
301
466
  if (param.type === 'combinefileinput')
302
467
  lines.push(` Pass an array of URLs directly: \`${param.id}: ["https://...", "https://..."]\`. No upload needed.`);
303
468
  if (param.type === 'json' || param.type === 'json-array')
@@ -309,8 +474,9 @@ export function formatModelSchema(model) {
309
474
  .join(', ');
310
475
  lines.push(` Options: ${optionValues}`);
311
476
  }
312
- if (param.type === 'range' && param.min != null && param.max != null) {
313
- lines.push(` Range: ${param.min} - ${param.max}${param.step ? `, step: ${param.step}` : ''}`);
477
+ const range = parameterRange(param);
478
+ if (range.min !== undefined || range.max !== undefined) {
479
+ lines.push(` Range: ${range.min ?? 'no minimum'} to ${range.max ?? 'no maximum'}${range.step ? `, step ${range.step}` : ''}`);
314
480
  }
315
481
  }
316
482
  lines.push('');
@@ -324,121 +490,457 @@ export function formatModelSchema(model) {
324
490
  lines.push('```');
325
491
  return lines.join('\n');
326
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.
327
496
  const PRICE_METHOD_LABELS = {
328
497
  cpr: 'per request',
329
- cps: 'per second',
498
+ cps: 'per second of run time',
330
499
  cpo: 'per output',
331
- cpt: 'per token',
332
- 'cp-pixel': 'per pixel',
333
- 'cp-audiosecondslength': 'per audio second',
334
- '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',
335
504
  'cp-outputVideoLength': 'per video second',
336
505
  'cp-realtimeturn': 'per turn',
337
- 'cp-readoutput': 'model-reported',
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.',
338
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
+ }
339
759
  function formatPricingDetailed(model) {
340
- const dynamicResult = formatDynamicPriceDetailed(model.dynamicprice);
760
+ const rows = parseDynamicPrice(model.dynamicprice);
761
+ const dynamicResult = rows ? formatDynamicPriceDetailed(rows, settingItems(model.parameters)) : null;
341
762
  if (dynamicResult)
342
763
  return dynamicResult;
343
- const cps = parseFloat(model.cps ?? '0');
344
- const approx = parseFloat(model.approximatelycost ?? '0');
345
- if (approx > 0 || cps > 0) {
764
+ const rate = runTimeRate(model);
765
+ const approx = parseFloat(String(model.approximatelycost ?? '0'));
766
+ if (approx > 0 || rate > 0) {
346
767
  const lines = ['### Pricing'];
347
- if (cps > 0)
348
- lines.push(`- **Rate:** $${formatPrice(cps)} per second`);
768
+ if (rate > 0)
769
+ lines.push(`- **Rate:** $${formatPrice(rate)} per second of run time`);
349
770
  if (approx > 0)
350
771
  lines.push(`- **Estimated cost:** ~$${formatPrice(approx)} per run`);
351
772
  return lines.join('\n');
352
773
  }
353
774
  return null;
354
775
  }
355
- function formatDynamicPriceDetailed(dynamicprice) {
356
- if (!dynamicprice)
357
- return null;
776
+ function formatDynamicPriceDetailed(rows, items) {
358
777
  try {
359
- const parsed = typeof dynamicprice === 'string' ? JSON.parse(dynamicprice) : dynamicprice;
360
- if (!Array.isArray(parsed) || parsed.length === 0)
361
- return null;
362
- const method = parsed[0].priceMethod;
363
- const methodLabel = PRICE_METHOD_LABELS[method] ?? method;
364
- const lines = [`### Pricing (${methodLabel})`];
365
- if (parsed.length === 1 && (!parsed[0].inputs || Object.keys(parsed[0].inputs).length === 0)) {
366
- lines.push(`$${formatPrice(parsed[0].price)} / ${methodLabel}`);
367
- return lines.join('\n');
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]}`;
368
787
  }
369
- const inputKeys = new Set();
370
- for (const p of parsed) {
371
- if (p.inputs) {
372
- for (const k of Object.keys(p.inputs))
373
- inputKeys.add(k);
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)}.`);
374
810
  }
375
811
  }
376
- const keys = [...inputKeys];
377
- if (keys.length > 0) {
378
- lines.push(`| ${keys.join(' | ')} | Price |`);
379
- lines.push(`| ${keys.map(() => '---').join(' | ')} | --- |`);
380
- for (const p of parsed) {
381
- const values = keys.map(k => {
382
- const v = p.inputs?.[k] ?? '-';
383
- return v.startsWith('QUANTITY:') ? `qty: ${v.split(':')[1]}` : v;
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}`);
384
841
  });
385
- lines.push(`| ${values.join(' | ')} | $${formatPrice(p.price)} |`);
842
+ after.push(...unpricedOptionLines(rows, items));
386
843
  }
387
- }
388
- else {
389
- for (const p of parsed) {
390
- lines.push(`- $${formatPrice(p.price)} / ${PRICE_METHOD_LABELS[p.priceMethod] ?? p.priceMethod}`);
844
+ else {
845
+ rows.forEach((row, i) => {
846
+ lines.push(`- ${rowPriceDetailed(row, true)}${rowNotes && notes[i] ? `. Note: ${notes[i]}` : ''}`);
847
+ });
391
848
  }
392
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);
393
857
  return lines.join('\n');
394
858
  }
395
859
  catch {
396
860
  return null;
397
861
  }
398
862
  }
399
- export function formatPricing(model) {
400
- const dynamicResult = formatDynamicPrice(model.dynamicprice);
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;
401
871
  if (dynamicResult)
402
872
  return dynamicResult;
403
- const cps = parseFloat(model.cps ?? '0');
404
- const approx = parseFloat(model.approximatelycost ?? '0');
873
+ const approx = parseFloat(String(model.approximatelycost ?? '0'));
405
874
  if (approx > 0) {
406
875
  return `~$${formatPrice(approx)} per run (estimated)`;
407
876
  }
408
- if (cps > 0) {
409
- return `$${formatPrice(cps)} per second`;
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';
410
883
  }
411
884
  return null;
412
885
  }
413
- function formatDynamicPrice(dynamicprice) {
414
- if (!dynamicprice)
415
- return null;
886
+ function formatDynamicPrice(rows, detailed, items) {
416
887
  try {
417
- const parsed = typeof dynamicprice === 'string' ? JSON.parse(dynamicprice) : dynamicprice;
418
- if (!Array.isArray(parsed) || parsed.length === 0)
419
- return null;
420
- const uniquePrices = new Map();
421
- for (const p of parsed) {
422
- const label = PRICE_METHOD_LABELS[p.priceMethod] ?? p.priceMethod;
423
- const key = `${p.price}-${label}`;
424
- if (!uniquePrices.has(key)) {
425
- uniquePrices.set(key, { price: p.price, label });
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
+ }
426
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}`);
427
911
  }
428
- const entries = [...uniquePrices.values()].sort((a, b) => a.price - b.price);
429
- if (entries.length === 1) {
430
- const e = entries[0];
431
- return `$${formatPrice(e.price)} / ${e.label}`;
432
- }
433
- const min = entries[0];
434
- const max = entries[entries.length - 1];
435
- return `$${formatPrice(min.price)} – $${formatPrice(max.price)} / ${min.label}`;
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(' ')}`;
436
925
  }
437
926
  catch {
438
927
  return null;
439
928
  }
440
929
  }
441
- function formatPrice(price) {
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) {
442
944
  return parseFloat(price.toFixed(6)).toString();
443
945
  }
444
946
  export function formatSize(sizeStr) {