@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.
- package/README.md +183 -16
- package/dist/client.d.ts +16 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +126 -24
- package/dist/client.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +29 -0
- package/dist/instructions.js.map +1 -0
- package/dist/playbooks.d.ts +8 -0
- package/dist/playbooks.d.ts.map +1 -0
- package/dist/playbooks.js +667 -0
- package/dist/playbooks.js.map +1 -0
- package/dist/prompts.d.ts +4 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +133 -0
- package/dist/prompts.js.map +1 -0
- package/dist/server.d.ts +15 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +12 -7
- package/dist/server.js.map +1 -1
- package/dist/tools/apply-agent-skills.d.ts +4 -0
- package/dist/tools/apply-agent-skills.d.ts.map +1 -0
- package/dist/tools/apply-agent-skills.js +601 -0
- package/dist/tools/apply-agent-skills.js.map +1 -0
- package/dist/tools/cancel-agent-message.d.ts +4 -0
- package/dist/tools/cancel-agent-message.d.ts.map +1 -0
- package/dist/tools/cancel-agent-message.js +126 -0
- package/dist/tools/cancel-agent-message.js.map +1 -0
- package/dist/tools/cancel-task.js +3 -3
- package/dist/tools/cancel-task.js.map +1 -1
- package/dist/tools/clone-workflow.d.ts +4 -0
- package/dist/tools/clone-workflow.d.ts.map +1 -0
- package/dist/tools/clone-workflow.js +83 -0
- package/dist/tools/clone-workflow.js.map +1 -0
- package/dist/tools/connect-agent-oauth.d.ts +4 -0
- package/dist/tools/connect-agent-oauth.d.ts.map +1 -0
- package/dist/tools/connect-agent-oauth.js +508 -0
- package/dist/tools/connect-agent-oauth.js.map +1 -0
- package/dist/tools/control-agent.d.ts +4 -0
- package/dist/tools/control-agent.d.ts.map +1 -0
- package/dist/tools/control-agent.js +289 -0
- package/dist/tools/control-agent.js.map +1 -0
- package/dist/tools/create-workflow.d.ts +100 -0
- package/dist/tools/create-workflow.d.ts.map +1 -0
- package/dist/tools/create-workflow.js +102 -0
- package/dist/tools/create-workflow.js.map +1 -0
- package/dist/tools/delete-agent-session.d.ts +6 -0
- package/dist/tools/delete-agent-session.d.ts.map +1 -0
- package/dist/tools/delete-agent-session.js +134 -0
- package/dist/tools/delete-agent-session.js.map +1 -0
- package/dist/tools/delete-agent.d.ts +4 -0
- package/dist/tools/delete-agent.d.ts.map +1 -0
- package/dist/tools/delete-agent.js +145 -0
- package/dist/tools/delete-agent.js.map +1 -0
- package/dist/tools/delete-workflow.d.ts +4 -0
- package/dist/tools/delete-workflow.d.ts.map +1 -0
- package/dist/tools/delete-workflow.js +77 -0
- package/dist/tools/delete-workflow.js.map +1 -0
- package/dist/tools/deploy-agent.d.ts +4 -0
- package/dist/tools/deploy-agent.d.ts.map +1 -0
- package/dist/tools/deploy-agent.js +463 -0
- package/dist/tools/deploy-agent.js.map +1 -0
- package/dist/tools/disconnect-agent-oauth.d.ts +4 -0
- package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -0
- package/dist/tools/disconnect-agent-oauth.js +118 -0
- package/dist/tools/disconnect-agent-oauth.js.map +1 -0
- package/dist/tools/get-account.d.ts +4 -0
- package/dist/tools/get-account.d.ts.map +1 -0
- package/dist/tools/get-account.js +112 -0
- package/dist/tools/get-account.js.map +1 -0
- package/dist/tools/get-agent-activity.d.ts +4 -0
- package/dist/tools/get-agent-activity.d.ts.map +1 -0
- package/dist/tools/get-agent-activity.js +383 -0
- package/dist/tools/get-agent-activity.js.map +1 -0
- package/dist/tools/get-agent-conversation.d.ts +4 -0
- package/dist/tools/get-agent-conversation.d.ts.map +1 -0
- package/dist/tools/get-agent-conversation.js +228 -0
- package/dist/tools/get-agent-conversation.js.map +1 -0
- package/dist/tools/get-agent-message.d.ts +4 -0
- package/dist/tools/get-agent-message.d.ts.map +1 -0
- package/dist/tools/get-agent-message.js +68 -0
- package/dist/tools/get-agent-message.js.map +1 -0
- package/dist/tools/get-agent-template.d.ts +4 -0
- package/dist/tools/get-agent-template.d.ts.map +1 -0
- package/dist/tools/get-agent-template.js +128 -0
- package/dist/tools/get-agent-template.js.map +1 -0
- package/dist/tools/get-model-schema.d.ts +5 -1
- package/dist/tools/get-model-schema.d.ts.map +1 -1
- package/dist/tools/get-model-schema.js +138 -8
- package/dist/tools/get-model-schema.js.map +1 -1
- package/dist/tools/get-my-agent.d.ts +4 -0
- package/dist/tools/get-my-agent.d.ts.map +1 -0
- package/dist/tools/get-my-agent.js +117 -0
- package/dist/tools/get-my-agent.js.map +1 -0
- package/dist/tools/get-playbook.d.ts +3 -0
- package/dist/tools/get-playbook.d.ts.map +1 -0
- package/dist/tools/get-playbook.js +50 -0
- package/dist/tools/get-playbook.js.map +1 -0
- package/dist/tools/get-task-price.d.ts +1 -1
- package/dist/tools/get-task-price.d.ts.map +1 -1
- package/dist/tools/get-task-price.js +64 -16
- package/dist/tools/get-task-price.js.map +1 -1
- package/dist/tools/get-task.d.ts.map +1 -1
- package/dist/tools/get-task.js +26 -9
- package/dist/tools/get-task.js.map +1 -1
- package/dist/tools/get-workflow.d.ts +9 -0
- package/dist/tools/get-workflow.d.ts.map +1 -0
- package/dist/tools/get-workflow.js +105 -0
- package/dist/tools/get-workflow.js.map +1 -0
- package/dist/tools/index.d.ts +63 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +131 -14
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/kill-task.d.ts.map +1 -1
- package/dist/tools/kill-task.js +10 -6
- package/dist/tools/kill-task.js.map +1 -1
- package/dist/tools/list-agent-skills.d.ts +4 -0
- package/dist/tools/list-agent-skills.d.ts.map +1 -0
- package/dist/tools/list-agent-skills.js +242 -0
- package/dist/tools/list-agent-skills.js.map +1 -0
- package/dist/tools/list-agent-templates.d.ts +4 -0
- package/dist/tools/list-agent-templates.d.ts.map +1 -0
- package/dist/tools/list-agent-templates.js +110 -0
- package/dist/tools/list-agent-templates.js.map +1 -0
- package/dist/tools/list-my-agents.d.ts +4 -0
- package/dist/tools/list-my-agents.d.ts.map +1 -0
- package/dist/tools/list-my-agents.js +93 -0
- package/dist/tools/list-my-agents.js.map +1 -0
- package/dist/tools/list-studio-results.d.ts +4 -0
- package/dist/tools/list-studio-results.d.ts.map +1 -0
- package/dist/tools/list-studio-results.js +95 -0
- package/dist/tools/list-studio-results.js.map +1 -0
- package/dist/tools/list-tasks.d.ts.map +1 -1
- package/dist/tools/list-tasks.js +7 -5
- package/dist/tools/list-tasks.js.map +1 -1
- package/dist/tools/list-workflow-runs.d.ts +4 -0
- package/dist/tools/list-workflow-runs.d.ts.map +1 -0
- package/dist/tools/list-workflow-runs.js +110 -0
- package/dist/tools/list-workflow-runs.js.map +1 -0
- package/dist/tools/list-workflows.d.ts +4 -0
- package/dist/tools/list-workflows.d.ts.map +1 -0
- package/dist/tools/list-workflows.js +143 -0
- package/dist/tools/list-workflows.js.map +1 -0
- package/dist/tools/manage-agent-billing.d.ts +4 -0
- package/dist/tools/manage-agent-billing.d.ts.map +1 -0
- package/dist/tools/manage-agent-billing.js +788 -0
- package/dist/tools/manage-agent-billing.js.map +1 -0
- package/dist/tools/manage-agent-custom-skill.d.ts +4 -0
- package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -0
- package/dist/tools/manage-agent-custom-skill.js +531 -0
- package/dist/tools/manage-agent-custom-skill.js.map +1 -0
- package/dist/tools/recommend-model.d.ts.map +1 -1
- package/dist/tools/recommend-model.js +6 -5
- package/dist/tools/recommend-model.js.map +1 -1
- package/dist/tools/resume-workflow-run.d.ts +4 -0
- package/dist/tools/resume-workflow-run.d.ts.map +1 -0
- package/dist/tools/resume-workflow-run.js +69 -0
- package/dist/tools/resume-workflow-run.js.map +1 -0
- package/dist/tools/run-model.d.ts.map +1 -1
- package/dist/tools/run-model.js +40 -4
- package/dist/tools/run-model.js.map +1 -1
- package/dist/tools/run-workflow.d.ts +6 -0
- package/dist/tools/run-workflow.d.ts.map +1 -0
- package/dist/tools/run-workflow.js +132 -0
- package/dist/tools/run-workflow.js.map +1 -0
- package/dist/tools/save-workflow-from-agent.d.ts +4 -0
- package/dist/tools/save-workflow-from-agent.d.ts.map +1 -0
- package/dist/tools/save-workflow-from-agent.js +280 -0
- package/dist/tools/save-workflow-from-agent.js.map +1 -0
- package/dist/tools/schemas.d.ts +182 -115
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +26 -6
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/search-models.d.ts.map +1 -1
- package/dist/tools/search-models.js +9 -6
- package/dist/tools/search-models.js.map +1 -1
- package/dist/tools/send-agent-message.d.ts +4 -0
- package/dist/tools/send-agent-message.d.ts.map +1 -0
- package/dist/tools/send-agent-message.js +156 -0
- package/dist/tools/send-agent-message.js.map +1 -0
- package/dist/tools/set-agent-credentials.d.ts +4 -0
- package/dist/tools/set-agent-credentials.d.ts.map +1 -0
- package/dist/tools/set-agent-credentials.js +540 -0
- package/dist/tools/set-agent-credentials.js.map +1 -0
- package/dist/tools/update-agent.d.ts +4 -0
- package/dist/tools/update-agent.d.ts.map +1 -0
- package/dist/tools/update-agent.js +179 -0
- package/dist/tools/update-agent.js.map +1 -0
- package/dist/tools/update-workflow.d.ts +201 -0
- package/dist/tools/update-workflow.d.ts.map +1 -0
- package/dist/tools/update-workflow.js +168 -0
- package/dist/tools/update-workflow.js.map +1 -0
- package/dist/tools/upload-file.d.ts.map +1 -1
- package/dist/tools/upload-file.js +12 -7
- package/dist/tools/upload-file.js.map +1 -1
- package/dist/tools/wait-for-task.d.ts.map +1 -1
- package/dist/tools/wait-for-task.js +20 -2
- package/dist/tools/wait-for-task.js.map +1 -1
- package/dist/tools/wait-for-workflow-run.d.ts +259 -0
- package/dist/tools/wait-for-workflow-run.d.ts.map +1 -0
- package/dist/tools/wait-for-workflow-run.js +190 -0
- package/dist/tools/wait-for-workflow-run.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/agent-catalog.d.ts +1256 -0
- package/dist/utils/agent-catalog.d.ts.map +1 -0
- package/dist/utils/agent-catalog.js +387 -0
- package/dist/utils/agent-catalog.js.map +1 -0
- package/dist/utils/agent-config.d.ts +443 -0
- package/dist/utils/agent-config.d.ts.map +1 -0
- package/dist/utils/agent-config.js +1676 -0
- package/dist/utils/agent-config.js.map +1 -0
- package/dist/utils/agent-message.d.ts +112 -0
- package/dist/utils/agent-message.d.ts.map +1 -0
- package/dist/utils/agent-message.js +243 -0
- package/dist/utils/agent-message.js.map +1 -0
- package/dist/utils/agent-targets.d.ts +76 -0
- package/dist/utils/agent-targets.d.ts.map +1 -0
- package/dist/utils/agent-targets.js +412 -0
- package/dist/utils/agent-targets.js.map +1 -0
- package/dist/utils/agent-turns.d.ts +354 -0
- package/dist/utils/agent-turns.d.ts.map +1 -0
- package/dist/utils/agent-turns.js +660 -0
- package/dist/utils/agent-turns.js.map +1 -0
- package/dist/utils/api.d.ts +40 -0
- package/dist/utils/api.d.ts.map +1 -0
- package/dist/utils/api.js +111 -0
- package/dist/utils/api.js.map +1 -0
- package/dist/utils/format.d.ts +65 -2
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +583 -81
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/json-schema-dialect.d.ts +8 -1
- package/dist/utils/json-schema-dialect.d.ts.map +1 -1
- package/dist/utils/json-schema-dialect.js +23 -1
- package/dist/utils/json-schema-dialect.js.map +1 -1
- package/dist/utils/media.d.ts +28 -0
- package/dist/utils/media.d.ts.map +1 -0
- package/dist/utils/media.js +65 -0
- package/dist/utils/media.js.map +1 -0
- package/dist/utils/poll.d.ts +39 -0
- package/dist/utils/poll.d.ts.map +1 -0
- package/dist/utils/poll.js +111 -0
- package/dist/utils/poll.js.map +1 -0
- package/dist/utils/remote-file.js +2 -2
- package/dist/utils/remote-file.js.map +1 -1
- package/dist/utils/structured.d.ts +4 -0
- package/dist/utils/structured.d.ts.map +1 -1
- package/dist/utils/structured.js +23 -7
- package/dist/utils/structured.js.map +1 -1
- package/dist/utils/studio-runs.d.ts +87 -0
- package/dist/utils/studio-runs.d.ts.map +1 -0
- package/dist/utils/studio-runs.js +167 -0
- package/dist/utils/studio-runs.js.map +1 -0
- package/dist/utils/user-agent.d.ts +1199 -0
- package/dist/utils/user-agent.d.ts.map +1 -0
- package/dist/utils/user-agent.js +897 -0
- package/dist/utils/user-agent.js.map +1 -0
- package/dist/utils/workflow-agent.d.ts +151 -0
- package/dist/utils/workflow-agent.d.ts.map +1 -0
- package/dist/utils/workflow-agent.js +292 -0
- package/dist/utils/workflow-agent.js.map +1 -0
- package/dist/utils/workflow-artifacts.d.ts +160 -0
- package/dist/utils/workflow-artifacts.d.ts.map +1 -0
- package/dist/utils/workflow-artifacts.js +801 -0
- package/dist/utils/workflow-artifacts.js.map +1 -0
- package/dist/utils/workflow-save.d.ts +120 -0
- package/dist/utils/workflow-save.d.ts.map +1 -0
- package/dist/utils/workflow-save.js +728 -0
- package/dist/utils/workflow-save.js.map +1 -0
- package/dist/utils/workflow.d.ts +564 -0
- package/dist/utils/workflow.d.ts.map +1 -0
- package/dist/utils/workflow.js +712 -0
- package/dist/utils/workflow.js.map +1 -0
- package/dist/utils/workspace.d.ts +38 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +90 -0
- package/dist/utils/workspace.js.map +1 -0
- package/package.json +6 -4
package/dist/utils/format.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from './api.js';
|
|
1
2
|
import { isToolCallSegment, normalizeTaskRawContent, } from './task-segments.js';
|
|
2
3
|
// The value Wiro accepts for an option, and a label to show next to it. Wiro
|
|
3
4
|
// checks an option's value, or its id when it has no value.
|
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
313
|
-
|
|
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': '
|
|
333
|
-
'cp-audiosecondslength': 'per audio
|
|
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': '
|
|
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
|
|
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
|
|
344
|
-
const approx = parseFloat(model.approximatelycost ?? '0');
|
|
345
|
-
if (approx > 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 (
|
|
348
|
-
lines.push(`- **Rate:** $${formatPrice(
|
|
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(
|
|
356
|
-
if (!dynamicprice)
|
|
357
|
-
return null;
|
|
776
|
+
function formatDynamicPriceDetailed(rows, items) {
|
|
358
777
|
try {
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
lines
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
842
|
+
after.push(...unpricedOptionLines(rows, items));
|
|
386
843
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
|
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
|
-
|
|
409
|
-
|
|
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(
|
|
414
|
-
if (!dynamicprice)
|
|
415
|
-
return null;
|
|
886
|
+
function formatDynamicPrice(rows, detailed, items) {
|
|
416
887
|
try {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
return
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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) {
|