@unoverse-platform/studio 0.1.115 → 0.1.117
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/local/catalog.ts +3 -0
- package/local/plugin.ts +12 -9
- package/local/scaffold.d.mts +1 -0
- package/local/scaffold.mjs +12 -0
- package/package.json +1 -1
- package/screens/AgentChain.tsx +232 -0
- package/screens/AgentsView.tsx +85 -6
- package/screens/SkillsView.tsx +21 -3
- package/screens/TemplatesLibraryView.tsx +32 -3
- package/screens/TemplatesView.tsx +2 -2
- package/screens/index.ts +4 -1
- package/src/host.ts +5 -0
- package/src/index.css +11 -0
- package/vendor/base/src/agent-mcp/discovery.ts +5 -2
- package/vendor/base/src/agent-mcp/index.ts +3 -1
- package/vendor/base/src/agent-mcp/invoke.ts +199 -25
- package/vendor/base/src/agent-mcp/progressive.ts +124 -0
- package/vendor/base/src/agent-mcp/tools.ts +18 -11
- package/vendor/base/src/agent-mcp/types.ts +8 -0
- package/vendor/base/src/auth/agentSwitch.ts +52 -0
- package/vendor/base/src/auth/agentToken.ts +142 -0
- package/vendor/base/src/auth/auth.ts +7 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +1 -1
- package/vendor/base/src/definitions/load/expand/refs.ts +1 -1
- package/vendor/base/src/definitions/load/expand/walk.ts +1 -1
- package/vendor/base/src/definitions/load/load.ts +6 -1
- package/vendor/base/src/definitions/registry/agentFolders.ts +113 -0
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/objects.ts +109 -5
- package/vendor/base/src/definitions/registry/pipelines.ts +24 -5
- package/vendor/base/src/definitions/registry/taskStarts.ts +76 -0
- package/vendor/base/src/definitions/registry/taskUri.ts +67 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +39 -3
- package/vendor/base/src/items/collect.ts +10 -0
- package/vendor/base/src/items/publish.ts +10 -0
- package/vendor/base/src/lint/design/index.mjs +14 -3
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/objects/index.d.mts +1 -0
- package/vendor/base/src/lint/objects/index.mjs +121 -3
- package/vendor/base/src/lint/objects/rules.d.mts +3 -0
- package/vendor/base/src/lint/objects/rules.mjs +106 -7
- package/vendor/base/src/manifests/compose.ts +3 -0
- package/vendor/base/src/manifests/executor/callback.ts +3 -2
- package/vendor/base/src/manifests/executor/memory.ts +27 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +79 -13
- package/vendor/base/src/manifests/marketplaceMcp.ts +20 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +43 -10
- package/vendor/base/src/manifests/runtime/events.ts +1 -1
- package/vendor/base/src/manifests/runtime/http/request.ts +47 -4
- package/vendor/base/src/manifests/runtime/loops/poll.ts +27 -5
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +59 -24
- package/vendor/base/src/manifests/runtime/usage.ts +13 -5
- package/vendor/base/src/platform/agentBudget.ts +65 -0
- package/vendor/base/src/platform/agentCalendar.ts +87 -0
- package/vendor/base/src/platform/inbound.ts +67 -0
- package/vendor/base/src/platform/taskChanged.ts +25 -0
- package/vendor/base/src/platform/tokenCost.ts +414 -0
- package/vendor/base/src/security/permissions.ts +10 -2
- package/vendor/sdk/appEngine.ts +42 -1
- package/vendor/sdk/lib/core/actions.ts +10 -0
- package/vendor/sdk/lib/core/apps.ts +2 -2
- package/vendor/sdk/lib/core/client.ts +11 -0
- package/vendor/sdk/lib/core/store.ts +1 -1
- package/vendor/sdk/lib/core/types.ts +2 -0
- package/vendor/sdk/lib/dom/app.ts +3 -2
- package/vendor/sdk/lib/dom/component.ts +38 -1
- package/vendor/sdk/lib/dom/element.ts +23 -0
- package/vendor/sdk/lib/dom/markdown.ts +6 -2
- package/vendor/sdk/lib/dom/primitives.ts +97 -6
- package/vendor/sdk/lib/voiceService.ts +3 -1
- package/vendor/sdk/main.ts +1 -1
- package/vendor/sdk/pageInterfaces.ts +109 -8
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOKEN USAGE AND ITS COST: raw usage from any model, read into one shape, and priced in USD per
|
|
3
|
+
* 1M tokens (or per minute for voice). One table for the platform: Canvas and Signal show it, and
|
|
4
|
+
* an Agent's budget is counted with it (docs/architecture/signal/AGENTS.md § 9). Moved here from
|
|
5
|
+
* Canvas (`pages/dashboard/tokenConverters.js`) so the server reads the same prices.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A raw usage block as a vendor sent it. */
|
|
9
|
+
type RawUsage = any;
|
|
10
|
+
/** What a model costs, per 1M tokens (or per minute). */
|
|
11
|
+
export interface ModelPricing {
|
|
12
|
+
input?: number; cachedInput?: number; cacheWrite?: number; output?: number; perMinute?: number;
|
|
13
|
+
audioInput?: number; cachedAudioInput?: number; audioOutput?: number; imageInput?: number; cachedImageInput?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse token usage based on model type
|
|
18
|
+
* @param {string} model - The model name
|
|
19
|
+
* @param {object} usage - Raw usage object from AI model
|
|
20
|
+
* @returns {object|null} Parsed usage with standardized fields
|
|
21
|
+
*/
|
|
22
|
+
export const parseTokenUsage = (model: string, usage: RawUsage): any => {
|
|
23
|
+
if (!usage) return null;
|
|
24
|
+
|
|
25
|
+
// Handle string usage (JSON stored as string)
|
|
26
|
+
if (typeof usage === "string") {
|
|
27
|
+
try {
|
|
28
|
+
usage = JSON.parse(usage);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parsed = parseByVendor(model, usage);
|
|
35
|
+
// A VOICE CALL'S LENGTH, timed by the platform on every call. Per-minute vendors bill on it.
|
|
36
|
+
return parsed && typeof usage.seconds === "number" ? { ...parsed, seconds: usage.seconds } : parsed;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseByVendor = (model: string, usage: RawUsage): any => {
|
|
40
|
+
if (model.startsWith("gpt-")) return parseOpenAI(usage);
|
|
41
|
+
if (model.startsWith("claude-")) return parseClaude(usage);
|
|
42
|
+
if (model.startsWith("amazon.")) return parseBedrock(usage);
|
|
43
|
+
if (model.startsWith("us.anthropic.")) return parseBedrockClaude(usage);
|
|
44
|
+
if (model.startsWith("anthropic.")) return parseBedrockClaude(usage);
|
|
45
|
+
|
|
46
|
+
return parseGeneric(usage);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* OpenAI Models (GPT-4, GPT-5, etc.)
|
|
51
|
+
* Handles both Chat Completions API and Responses API
|
|
52
|
+
*/
|
|
53
|
+
const parseOpenAI = (usage: RawUsage) => {
|
|
54
|
+
// Handle string usage (JSON stored as string)
|
|
55
|
+
if (typeof usage === "string") {
|
|
56
|
+
try {
|
|
57
|
+
usage = JSON.parse(usage);
|
|
58
|
+
} catch {
|
|
59
|
+
return { total: 0, input: 0, output: 0, reasoning: 0, cached: 0, breakdown: {} };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Responses API format (GPT-5) or generic input_tokens format
|
|
64
|
+
if ("input_tokens" in usage) {
|
|
65
|
+
// TWO SPELLINGS, ONE WORD APART. Responses says `input_tokens_details`; the REALTIME API says
|
|
66
|
+
// `input_token_details`, singular. Reading only the plural made every voice call report zero
|
|
67
|
+
// reasoning and zero cached — silently, since the totals beside them were right (confirmed
|
|
68
|
+
// against a live response.done, 2026-08-12).
|
|
69
|
+
const inDetails = usage.input_tokens_details || usage.input_token_details || {};
|
|
70
|
+
const outDetails = usage.output_tokens_details || usage.output_token_details || {};
|
|
71
|
+
const cachedTokens = inDetails.cached_tokens || 0;
|
|
72
|
+
// WRITTEN TO THE CACHE, the other part of input with its own price (GPT-5.6 bills a cache write
|
|
73
|
+
// above a fresh token). Like cached, it is counted inside `input_tokens`.
|
|
74
|
+
const cacheWriteTokens = inDetails.cache_write_tokens || 0;
|
|
75
|
+
const reasoningTokens = outDetails.reasoning_tokens || 0;
|
|
76
|
+
// AUDIO IS PRICED DIFFERENTLY from text, so voice cost cannot be read off a total alone.
|
|
77
|
+
const audioIn = inDetails.audio_tokens || 0;
|
|
78
|
+
const audioOut = outDetails.audio_tokens || 0;
|
|
79
|
+
const imageIn = inDetails.image_tokens || 0;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* HOW THE CACHED TOKENS SPLIT ACROSS MODALITIES, which is what makes them priceable.
|
|
83
|
+
*
|
|
84
|
+
* A cached token costs $0.40 against $32 for fresh audio or $4 for fresh text, so getting
|
|
85
|
+
* this wrong is most of the bill. `cached_tokens` alone does not say which modality it came
|
|
86
|
+
* from, and `cached_tokens_details` — which does — is absent from every row written before
|
|
87
|
+
* the collector stopped dropping it, and from any vendor that never sends it.
|
|
88
|
+
*
|
|
89
|
+
* So: use the details when present, and otherwise APPORTION BY SHARE. Proportional is a
|
|
90
|
+
* genuine estimate rather than a fact, but it is far closer than the alternative of billing
|
|
91
|
+
* every cached token at the full fresh rate, which overcharged a real call by ~45%.
|
|
92
|
+
*/
|
|
93
|
+
const cachedTotal = inDetails.cached_tokens || 0;
|
|
94
|
+
const cachedDetails = inDetails.cached_tokens_details;
|
|
95
|
+
const freshTotal = (inDetails.text_tokens || 0) + audioIn + imageIn;
|
|
96
|
+
const share = (n: number) => (freshTotal > 0 ? Math.round((cachedTotal * n) / freshTotal) : 0);
|
|
97
|
+
const cachedTextIn = cachedDetails ? cachedDetails.text_tokens || 0 : share(inDetails.text_tokens || 0);
|
|
98
|
+
const cachedAudioIn = cachedDetails ? cachedDetails.audio_tokens || 0 : share(audioIn);
|
|
99
|
+
const cachedImageIn = cachedDetails ? cachedDetails.image_tokens || 0 : share(imageIn);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
total: usage.total_tokens || (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
103
|
+
input: usage.input_tokens || 0,
|
|
104
|
+
output: usage.output_tokens || 0,
|
|
105
|
+
reasoning: reasoningTokens,
|
|
106
|
+
cached: cachedTokens,
|
|
107
|
+
cacheWrite: cacheWriteTokens,
|
|
108
|
+
breakdown: {
|
|
109
|
+
input: usage.input_tokens || 0,
|
|
110
|
+
cached: cachedTokens,
|
|
111
|
+
...(cacheWriteTokens ? { cacheWrite: cacheWriteTokens } : {}),
|
|
112
|
+
reasoning: reasoningTokens,
|
|
113
|
+
completion: (usage.output_tokens || 0) - reasoningTokens,
|
|
114
|
+
// Present only on a voice call; omitted rather than shown as 0 for text models, so a
|
|
115
|
+
// zero in the panel always means "no audio" rather than "not measured".
|
|
116
|
+
...(audioIn || audioOut ? { audioIn, audioOut } : {}),
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* THE PER-MODALITY SPLIT, for pricing. Only present when the vendor sent one.
|
|
120
|
+
*
|
|
121
|
+
* CACHED IS A SUBSET, not a fourth category: `cached_tokens_details` breaks the cached
|
|
122
|
+
* count down by modality, so a cached audio token is already counted in `audio_tokens`.
|
|
123
|
+
* Subtracting it is what stops it being billed twice, once at $32 and again at $0.40.
|
|
124
|
+
*/
|
|
125
|
+
...(audioIn || audioOut || inDetails.text_tokens !== undefined
|
|
126
|
+
? {
|
|
127
|
+
modality: {
|
|
128
|
+
textIn: Math.max(0, (inDetails.text_tokens || 0) - cachedTextIn),
|
|
129
|
+
cachedTextIn,
|
|
130
|
+
audioIn: Math.max(0, audioIn - cachedAudioIn),
|
|
131
|
+
cachedAudioIn,
|
|
132
|
+
imageIn: Math.max(0, imageIn - cachedImageIn),
|
|
133
|
+
cachedImageIn,
|
|
134
|
+
textOut: outDetails.text_tokens || 0,
|
|
135
|
+
audioOut,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Chat Completions API format (GPT-4, GPT-3.5)
|
|
143
|
+
const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
|
|
144
|
+
const input = usage.prompt_tokens || usage.promptTokens || 0;
|
|
145
|
+
const output = usage.completion_tokens || usage.completionTokens || 0;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
total: usage.total_tokens || usage.totalTokens || input + output,
|
|
149
|
+
input,
|
|
150
|
+
output,
|
|
151
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
152
|
+
cached: cachedTokens,
|
|
153
|
+
breakdown: {
|
|
154
|
+
prompt: input,
|
|
155
|
+
cached: cachedTokens,
|
|
156
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
157
|
+
completion: output,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Claude Models (Anthropic)
|
|
164
|
+
*/
|
|
165
|
+
const parseClaude = (usage: RawUsage) => {
|
|
166
|
+
return {
|
|
167
|
+
total: (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
168
|
+
input: usage.input_tokens || 0,
|
|
169
|
+
output: usage.output_tokens || 0,
|
|
170
|
+
reasoning: 0,
|
|
171
|
+
breakdown: {
|
|
172
|
+
input: usage.input_tokens || 0,
|
|
173
|
+
output: usage.output_tokens || 0,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bedrock Models (AWS)
|
|
180
|
+
*/
|
|
181
|
+
const parseBedrock = (usage: RawUsage) => {
|
|
182
|
+
return {
|
|
183
|
+
total: (usage.inputTokens || 0) + (usage.outputTokens || 0),
|
|
184
|
+
input: usage.inputTokens || 0,
|
|
185
|
+
output: usage.outputTokens || 0,
|
|
186
|
+
reasoning: 0,
|
|
187
|
+
breakdown: {
|
|
188
|
+
input: usage.inputTokens || 0,
|
|
189
|
+
output: usage.outputTokens || 0,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Bedrock Claude Models
|
|
196
|
+
* Handles both native Bedrock format (inputTokens) and OpenAI-style format (prompt_tokens)
|
|
197
|
+
*/
|
|
198
|
+
const parseBedrockClaude = (usage: RawUsage) => {
|
|
199
|
+
const input = usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0;
|
|
200
|
+
const output = usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0;
|
|
201
|
+
return {
|
|
202
|
+
total: usage.total_tokens || usage.totalTokens || input + output,
|
|
203
|
+
input,
|
|
204
|
+
output,
|
|
205
|
+
reasoning: 0,
|
|
206
|
+
breakdown: { input, output },
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Generic fallback for unknown models
|
|
212
|
+
*/
|
|
213
|
+
const parseGeneric = (usage: RawUsage) => {
|
|
214
|
+
const input = usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0;
|
|
215
|
+
const output = usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0;
|
|
216
|
+
// CACHED IS A SUBSET OF INPUT, as OpenAI reports it, and every OpenAI-compatible vendor (Z.AI
|
|
217
|
+
// GLM) copies that shape. Dropping it priced every cached token at the fresh rate: a GLM Agent's
|
|
218
|
+
// turn cost about three times what Z.AI bills.
|
|
219
|
+
const cached = usage.prompt_tokens_details?.cached_tokens || usage.input_tokens_details?.cached_tokens || 0;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
total: usage.total_tokens || input + output,
|
|
223
|
+
input,
|
|
224
|
+
output,
|
|
225
|
+
reasoning: 0,
|
|
226
|
+
cached,
|
|
227
|
+
breakdown: { input, cached, output },
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Calculate cost based on model and parsed usage
|
|
233
|
+
* @param {string} model - The model name
|
|
234
|
+
* @param {object} parsedUsage - Parsed usage object
|
|
235
|
+
* @returns {string} Cost in USD (formatted to 4 decimal places)
|
|
236
|
+
*/
|
|
237
|
+
export const calculateTokenCost = (model: string, parsedUsage: any): string => {
|
|
238
|
+
if (!parsedUsage) return "0.0000";
|
|
239
|
+
|
|
240
|
+
const pricing = getModelPricing(model);
|
|
241
|
+
if (!pricing) return "0.0000";
|
|
242
|
+
|
|
243
|
+
// PER MINUTE, for vendors that bill a voice call on its length rather than its tokens.
|
|
244
|
+
if (pricing.perMinute && parsedUsage.seconds) {
|
|
245
|
+
return ((parsedUsage.seconds / 60) * pricing.perMinute).toFixed(4);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const per = (tokens: number | undefined, rate: number | undefined) => ((tokens || 0) / 1000000) * (rate || 0);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* PER-MODALITY, when the model prices that way AND the usage block carried the split.
|
|
252
|
+
*
|
|
253
|
+
* Realtime bills audio at up to 8x text ($32 against $4 per 1M in), so a voice call priced on
|
|
254
|
+
* flat input/output is not approximately right, it is wrong by most of the bill. Both
|
|
255
|
+
* conditions are required: a model with no audio rates, or a usage block with no split, falls
|
|
256
|
+
* through to the flat path below and behaves exactly as it always did.
|
|
257
|
+
*/
|
|
258
|
+
if (pricing.audioInput && parsedUsage.modality) {
|
|
259
|
+
const m = parsedUsage.modality;
|
|
260
|
+
return (
|
|
261
|
+
per(m.textIn, pricing.input) +
|
|
262
|
+
per(m.cachedTextIn, pricing.cachedInput ?? pricing.input) +
|
|
263
|
+
per(m.audioIn, pricing.audioInput) +
|
|
264
|
+
per(m.cachedAudioIn, pricing.cachedAudioInput ?? pricing.audioInput) +
|
|
265
|
+
per(m.imageIn, pricing.imageInput ?? pricing.input) +
|
|
266
|
+
per(m.cachedImageIn, pricing.cachedImageInput ?? pricing.imageInput ?? pricing.input) +
|
|
267
|
+
per(m.textOut, pricing.output) +
|
|
268
|
+
per(m.audioOut, pricing.audioOutput ?? pricing.output)
|
|
269
|
+
).toFixed(4);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Cached input and cache writes are parts of input, each billed at its own rate where the vendor
|
|
273
|
+
// has one; the rest is fresh.
|
|
274
|
+
const cached = Math.min(parsedUsage.cached || 0, parsedUsage.input || 0);
|
|
275
|
+
const written = Math.min(parsedUsage.cacheWrite || 0, (parsedUsage.input || 0) - cached);
|
|
276
|
+
const inputCost =
|
|
277
|
+
per((parsedUsage.input || 0) - cached - written, pricing.input) +
|
|
278
|
+
per(cached, pricing.cachedInput ?? pricing.input) +
|
|
279
|
+
per(written, pricing.cacheWrite ?? pricing.input);
|
|
280
|
+
const outputCost = per(parsedUsage.output, pricing.output);
|
|
281
|
+
|
|
282
|
+
return (inputCost + outputCost).toFixed(4);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get pricing for a model (per million tokens)
|
|
287
|
+
*/
|
|
288
|
+
export const getModelPricing = (model: string): ModelPricing => {
|
|
289
|
+
const pricing: Record<string, ModelPricing> = {
|
|
290
|
+
// GPT-5.6 family (Sol/Terra/Luna): standard, short context (up to 272K input), verified against
|
|
291
|
+
// developers.openai.com/api/docs/pricing 2026-10-11. Cached input and cache writes have their own
|
|
292
|
+
// rates; without them every cached token was billed fresh.
|
|
293
|
+
"gpt-5.6": { input: 4.0, cachedInput: 0.4, cacheWrite: 5.0, output: 20.0 }, // alias of gpt-5.6-sol
|
|
294
|
+
"gpt-5.6-sol": { input: 4.0, cachedInput: 0.4, cacheWrite: 5.0, output: 20.0 },
|
|
295
|
+
"gpt-5.6-terra": { input: 2.0, cachedInput: 0.2, cacheWrite: 2.5, output: 12.0 },
|
|
296
|
+
"gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, cacheWrite: 0.25, output: 1.2 },
|
|
297
|
+
|
|
298
|
+
// Z.AI GLM — verified against docs.z.ai/guides/overview/pricing 2026-10-09
|
|
299
|
+
"glm-5.3": { input: 1.4, cachedInput: 0.26, output: 4.4 },
|
|
300
|
+
"glm-5.3-flash": { input: 0.15, cachedInput: 0.03, output: 0.5 },
|
|
301
|
+
"glm-5.2": { input: 1.4, cachedInput: 0.26, output: 4.4 },
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* GPT-Realtime family — PRICED PER MODALITY, verified against the OpenAI model pages
|
|
305
|
+
* 2026-08-12 (developers.openai.com/api/docs/models/gpt-realtime-2.1{,-mini}).
|
|
306
|
+
*
|
|
307
|
+
* `input`/`output` here are the TEXT rates, and pricing a voice call at them — which is
|
|
308
|
+
* what this table did — understates it by up to 8x, because a call is almost entirely
|
|
309
|
+
* AUDIO tokens: $32/$64 against $4/$24 on the pro model. The label said "(text tokens)",
|
|
310
|
+
* so the limitation was known; nothing downstream acted on it.
|
|
311
|
+
*
|
|
312
|
+
* `calculateTokenCost` uses these per-modality fields when the usage block carries the
|
|
313
|
+
* split, and falls back to flat input/output when it does not. Every other model in this
|
|
314
|
+
* table is unaffected.
|
|
315
|
+
*/
|
|
316
|
+
"gpt-realtime-2.1": {
|
|
317
|
+
input: 4.0, cachedInput: 0.4, output: 24.0,
|
|
318
|
+
audioInput: 32.0, cachedAudioInput: 0.4, audioOutput: 64.0,
|
|
319
|
+
imageInput: 5.0, cachedImageInput: 0.5,
|
|
320
|
+
},
|
|
321
|
+
"gpt-realtime-2.1-mini": {
|
|
322
|
+
input: 0.6, cachedInput: 0.06, output: 2.4,
|
|
323
|
+
audioInput: 10.0, cachedAudioInput: 0.3, audioOutput: 20.0,
|
|
324
|
+
imageInput: 0.8, cachedImageInput: 0.08,
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* xAI GROK VOICE — the vendor bills per MINUTE, not per token: $0.08/min audio plus
|
|
329
|
+
* $0.004 per text input (docs.x.ai/developers/pricing, 2026-08-15). The realtime
|
|
330
|
+
* `response.done.usage` carries only flat input/output/total token counts — no
|
|
331
|
+
* audio/text split, no duration — so a per-minute price cannot be computed from what
|
|
332
|
+
* this table is given.
|
|
333
|
+
*
|
|
334
|
+
* The platform now times every call and records `seconds`, so a call is priced at the
|
|
335
|
+
* vendor's $0.08/min. The per-token rates below are an ESTIMATE kept only for rows written
|
|
336
|
+
* before calls were timed. The $0.004 text-input charge is not included; it is small beside
|
|
337
|
+
* the minutes.
|
|
338
|
+
*/
|
|
339
|
+
"grok-voice-think-fast-2.0": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
340
|
+
"grok-voice-latest": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
341
|
+
"grok-voice-think-fast-1.0": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* OpenAI GPT-LIVE — billed per second at $0.05/min (developers.openai.com/api/docs/models/
|
|
345
|
+
* gpt-live-1). Priced on the `seconds` the platform times; it reports no tokens of its own.
|
|
346
|
+
*/
|
|
347
|
+
"gpt-live-1": { perMinute: 0.05 },
|
|
348
|
+
|
|
349
|
+
// GPT-5.x family (older, kept for historical executions)
|
|
350
|
+
"gpt-5.2": { input: 1.75, output: 14.0 },
|
|
351
|
+
"gpt-5": { input: 1.25, output: 10.0 },
|
|
352
|
+
"gpt-5-mini": { input: 0.25, output: 2.0 },
|
|
353
|
+
"gpt-5-nano": { input: 0.05, output: 0.4 },
|
|
354
|
+
"gpt-5-pro": { input: 15.0, output: 120.0 },
|
|
355
|
+
"gpt-5-chat-latest": { input: 1.25, output: 10.0 },
|
|
356
|
+
"gpt-5-codex": { input: 1.25, output: 10.0 },
|
|
357
|
+
|
|
358
|
+
// GPT-4.1 family
|
|
359
|
+
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
360
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
|
|
361
|
+
|
|
362
|
+
// GPT-4o family
|
|
363
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
364
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
365
|
+
|
|
366
|
+
// GPT-3.5 family
|
|
367
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
368
|
+
"gpt-3.5-turbo-16k": { input: 1.0, output: 2.0 },
|
|
369
|
+
|
|
370
|
+
// Claude models
|
|
371
|
+
"claude-3-opus": { input: 15.0, output: 75.0 },
|
|
372
|
+
"claude-3-sonnet": { input: 3.0, output: 15.0 },
|
|
373
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
374
|
+
|
|
375
|
+
// Bedrock Claude 4.5 (latest)
|
|
376
|
+
"us.anthropic.claude-opus-4-5-20250514-v1:0": { input: 5.0, output: 25.0 },
|
|
377
|
+
"us.anthropic.claude-sonnet-4-5-20250514-v1:0": { input: 3.0, output: 15.0 },
|
|
378
|
+
"us.anthropic.claude-haiku-4-5-20251001-v1:0": { input: 1.0, output: 5.0 },
|
|
379
|
+
|
|
380
|
+
// Bedrock Claude 4
|
|
381
|
+
"us.anthropic.claude-sonnet-4-20250514-v1:0": { input: 3.0, output: 15.0 },
|
|
382
|
+
|
|
383
|
+
// Bedrock Claude 3.5
|
|
384
|
+
"us.anthropic.claude-3-5-sonnet-20241022-v2:0": { input: 3.0, output: 15.0 },
|
|
385
|
+
"us.anthropic.claude-3-5-haiku-20241022-v1:0": { input: 0.25, output: 1.25 },
|
|
386
|
+
|
|
387
|
+
// Bedrock Claude 3
|
|
388
|
+
"anthropic.claude-3-sonnet-20240229-v1:0": { input: 3.0, output: 15.0 },
|
|
389
|
+
"anthropic.claude-3-haiku-20240307-v1:0": { input: 0.25, output: 1.25 },
|
|
390
|
+
|
|
391
|
+
// AWS Nova
|
|
392
|
+
"amazon.nova-sonic-v1:0": { input: 3.4, output: 13.6 },
|
|
393
|
+
"amazon.nova-2-sonic-v1:0": { input: 3.4, output: 13.6 },
|
|
394
|
+
|
|
395
|
+
// Embedding models
|
|
396
|
+
"text-embedding-ada-002": { input: 0.1, output: 0 },
|
|
397
|
+
"text-embedding-3-small": { input: 0.02, output: 0 },
|
|
398
|
+
"text-embedding-3-large": { input: 0.13, output: 0 },
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Try exact match first
|
|
402
|
+
if (pricing[model]) return pricing[model];
|
|
403
|
+
|
|
404
|
+
// Try partial match for Bedrock models
|
|
405
|
+
for (const [key, value] of Object.entries(pricing)) {
|
|
406
|
+
if (model.includes(key) || key.includes(model)) {
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Default fallback - use Claude Sonnet pricing as reasonable default
|
|
412
|
+
console.warn(`Unknown model pricing: ${model}, using default $3/$15 per 1M tokens`);
|
|
413
|
+
return { input: 3.0, output: 15.0 };
|
|
414
|
+
};
|
|
@@ -53,7 +53,15 @@ export const PERMISSIONS = [
|
|
|
53
53
|
* what widens it. A route table would be the wrong shape for that, which is why memory
|
|
54
54
|
* shares this vocabulary and not the policy table.
|
|
55
55
|
*/
|
|
56
|
-
"
|
|
56
|
+
"signal:read-all",
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add a person to Signal, or add to one already held, WITHOUT reading anyone
|
|
60
|
+
* (docs/architecture/signal/AGENTS.md § 7a). Apart from `signal:read-all` so a caller that finds
|
|
61
|
+
* leads, an Agent above all, never needs to read every contact and user to save one. The Signal
|
|
62
|
+
* Save node answers only whether it was saved, never the record.
|
|
63
|
+
*/
|
|
64
|
+
"signal:write",
|
|
57
65
|
] as const;
|
|
58
66
|
|
|
59
67
|
export type Permission = (typeof PERMISSIONS)[number];
|
|
@@ -68,7 +76,7 @@ const AUTHOR_GRANTS: Permission[] = ["workflow:author", "marketplace:publish"];
|
|
|
68
76
|
|
|
69
77
|
export const ROLE_GRANTS: Record<string, readonly Permission[]> = {
|
|
70
78
|
author: AUTHOR_GRANTS,
|
|
71
|
-
admin: [...AUTHOR_GRANTS, "credentials:manage", "
|
|
79
|
+
admin: [...AUTHOR_GRANTS, "credentials:manage", "signal:read-all", "signal:write"],
|
|
72
80
|
};
|
|
73
81
|
|
|
74
82
|
/**
|
package/vendor/sdk/appEngine.ts
CHANGED
|
@@ -65,6 +65,7 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
65
65
|
let voiceKey = "";
|
|
66
66
|
let app: AppMount | null = null;
|
|
67
67
|
let page: { destroy(): void } | null = null;
|
|
68
|
+
let pageFor: unknown[] = [];
|
|
68
69
|
let lastWidth: string | undefined;
|
|
69
70
|
let stopStore = () => {};
|
|
70
71
|
let stopVoice = () => {};
|
|
@@ -126,9 +127,36 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
126
127
|
: inertVoice();
|
|
127
128
|
stopVoice = voice.subscribe(syncConnection);
|
|
128
129
|
app?.update({ voice });
|
|
130
|
+
voiceListeners.forEach((l) => l());
|
|
129
131
|
mountPage();
|
|
130
132
|
};
|
|
131
133
|
|
|
134
|
+
// THE PAGE'S VOICE STAYS ONE OBJECT. The real service is swapped underneath when the app's
|
|
135
|
+
// settings arrive, so the page's templates (and the task each already asked for) are kept.
|
|
136
|
+
const voiceListeners = new Set<() => void>();
|
|
137
|
+
const pageVoice: VoiceService = {
|
|
138
|
+
get state() { return voice.state; },
|
|
139
|
+
subscribe(listener) {
|
|
140
|
+
let stop = voice.subscribe(listener);
|
|
141
|
+
let bound = voice;
|
|
142
|
+
const rebind = () => {
|
|
143
|
+
if (bound === voice) return;
|
|
144
|
+
stop();
|
|
145
|
+
bound = voice;
|
|
146
|
+
stop = voice.subscribe(listener);
|
|
147
|
+
listener();
|
|
148
|
+
};
|
|
149
|
+
voiceListeners.add(rebind);
|
|
150
|
+
return () => { voiceListeners.delete(rebind); stop(); };
|
|
151
|
+
},
|
|
152
|
+
startCall: (start) => voice.startCall(start),
|
|
153
|
+
joinCall: (workflowId) => voice.joinCall(workflowId),
|
|
154
|
+
unlock: () => voice.unlock(),
|
|
155
|
+
endCall: () => voice.endCall(),
|
|
156
|
+
toggleMute: () => voice.toggleMute(),
|
|
157
|
+
destroy: () => {},
|
|
158
|
+
};
|
|
159
|
+
|
|
132
160
|
/**
|
|
133
161
|
* THE CONNECTION, open once the user is known AND there is something to hear from: the app's
|
|
134
162
|
* own workflow, a call, or a task the page is in. Nothing is held open while a visitor reads.
|
|
@@ -172,6 +200,7 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
172
200
|
lastWidth = undefined;
|
|
173
201
|
syncConnection();
|
|
174
202
|
syncVoice();
|
|
203
|
+
mountPage();
|
|
175
204
|
const orgFromId = appId.includes("/") ? appId.split("/")[0] : null;
|
|
176
205
|
if (orgFromId) loadTheme(`${orgFromId}/light`, token);
|
|
177
206
|
client
|
|
@@ -224,6 +253,14 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
224
253
|
});
|
|
225
254
|
};
|
|
226
255
|
|
|
256
|
+
// THE TASK A PAGE TEMPLATE CARRIES (docs/design/lifecycle-hooks.md): the server runs the YAML in
|
|
257
|
+
// the template's folder as this viewer and answers with the template's fields.
|
|
258
|
+
const runTask = async (uri: string, phase: "onStart", input?: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
259
|
+
const res = (await client.callTool("template_task", { uri, phase, ...(input ? { input } : {}) })) as { content?: Array<{ text?: string }> };
|
|
260
|
+
const text = res?.content?.[0]?.text;
|
|
261
|
+
return text ? (JSON.parse(text) as Record<string, unknown>) : {};
|
|
262
|
+
};
|
|
263
|
+
|
|
227
264
|
// A PAGE TEMPLATE'S TASK, called at its own workflow's trigger with the user's token.
|
|
228
265
|
// `metadata.task` names the task its Task Runner loads; the submit carries the answers.
|
|
229
266
|
const startTask = async (b: Binding, task: string, submit?: Record<string, unknown>) => {
|
|
@@ -322,9 +359,13 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
322
359
|
el.append(...app.nodes);
|
|
323
360
|
};
|
|
324
361
|
// The page's own templates, drawn into its elements by this one runtime.
|
|
362
|
+
/** Drawn again only when what the page reads from changes: the org, the client, the store. */
|
|
325
363
|
const mountPage = () => {
|
|
364
|
+
const wanted = [appId.split("/")[0], client, store];
|
|
365
|
+
if (page && wanted.every((v, i) => v === pageFor[i])) return;
|
|
366
|
+
pageFor = wanted;
|
|
326
367
|
page?.destroy();
|
|
327
|
-
page = mountPageInterfaces({ org: appId.split("/")[0], client, voice, appStore: store, toServer, startTask });
|
|
368
|
+
page = mountPageInterfaces({ org: appId.split("/")[0], client, voice: pageVoice, appStore: store, toServer, startTask, runTask });
|
|
328
369
|
};
|
|
329
370
|
|
|
330
371
|
const watchStore = () => {
|
|
@@ -22,6 +22,8 @@ export interface ActionContext {
|
|
|
22
22
|
/** Where APP state lives, when it is not `store`: a template a website page draws keeps its
|
|
23
23
|
* own slice in its own store and shares the app's state with everything on the page. */
|
|
24
24
|
appStore?: ComponentStore;
|
|
25
|
+
/** Print what is on screen (host-provided; the `print` action). Resolves when it is done. */
|
|
26
|
+
print?: () => void | Promise<void>;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** Resolve a value: a `"{{path}}"` template reads from `scope` (dot-path); anything
|
|
@@ -103,6 +105,14 @@ export function dispatchAction(
|
|
|
103
105
|
if (ctx.nodeId) ctx.store.apply({ type: "COMPONENT_DATA", chatId: ctx.chatId, nodeId: ctx.nodeId, data: { [field]: scope.text } });
|
|
104
106
|
else (ctx.appStore ?? ctx.store).mergeAppState({ [field]: scope.text });
|
|
105
107
|
}
|
|
108
|
+
} else if (spec.type === "print") {
|
|
109
|
+
// PRINT asks the host to print what is on screen (`ctx.print`; the web host waits for a still
|
|
110
|
+
// screen first, dom/element.ts printWhenStill). `then` runs once the print is done. A host
|
|
111
|
+
// that cannot print does nothing.
|
|
112
|
+
if (!ctx.print) return;
|
|
113
|
+
const then = spec.then;
|
|
114
|
+
void Promise.resolve(ctx.print()).then(() => { if (then) dispatchAction(then, scope, ctx); });
|
|
115
|
+
return;
|
|
106
116
|
} else {
|
|
107
117
|
// SERVER — narrow the payload by `send` if given, else forward the whole scope.
|
|
108
118
|
const payload = spec.send ? Object.fromEntries(spec.send.map((k) => [k, scope[k]])) : scope;
|
|
@@ -57,7 +57,7 @@ export const cssWidth = (w: unknown): string | undefined => (typeof w === "strin
|
|
|
57
57
|
*
|
|
58
58
|
* ONE function for every caller, because the slot frame once had its own copy without the
|
|
59
59
|
* `flex` word: beside a full-width app it rendered the task surface in a zero-width frame
|
|
60
|
-
* the moment the org's `flex` token went (
|
|
60
|
+
* the moment the org's `flex` token went (a bank's comparison, blank, 2026-09-07).
|
|
61
61
|
*/
|
|
62
62
|
export function placeWidthStyle(declared: unknown, appSize?: Record<string, string>): Record<string, unknown> | null {
|
|
63
63
|
const width = cssWidth(typeof declared === "string" ? (appSize?.[declared] ?? declared) : undefined);
|
|
@@ -377,7 +377,7 @@ export function selectPointers(store: ComponentStore, node: UnoverseNode, scope?
|
|
|
377
377
|
// THE MOST RECENT WINS (the `limit: 1` law) for EVERY capped select, not only a state
|
|
378
378
|
// select. A slot pinned to a component by `type` with `limit: 1` kept the OLDEST card:
|
|
379
379
|
// the sort lived inside the `where` branch, so a name-pinned cap sliced arrival order
|
|
380
|
-
// (
|
|
380
|
+
// (a bank's voice app, two Account Agent cards, 2026-09-10). A cap means "the newest N"; an
|
|
381
381
|
// uncapped flow keeps arrival order.
|
|
382
382
|
if (sel.where || sel.limit != null) pointers = [...pointers].sort((a, b) => store.touchOf(b) - store.touchOf(a));
|
|
383
383
|
if (sel.limit != null) pointers = pointers.slice(0, sel.limit);
|
|
@@ -129,6 +129,17 @@ export class UnoverseClient {
|
|
|
129
129
|
return this.withSession(() => this.client.callTool({ name, arguments: args }));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* THE TASK A COMPONENT CARRIES (docs/design/lifecycle-hooks.md), run as this viewer: the server
|
|
134
|
+
* runs the hook in the component's own folder with the instance's fields as `props` and answers
|
|
135
|
+
* with the fields it filled. A component with no hook for the phase answers `{}`.
|
|
136
|
+
*/
|
|
137
|
+
async runTask(uri: string, phase: "onStart" | "onEnterView", props?: Record<string, unknown>, state?: string): Promise<Record<string, unknown>> {
|
|
138
|
+
const res = (await this.callTool("template_task", { uri, phase, ...(props ? { props } : {}), ...(state ? { state } : {}) })) as { content?: Array<{ text?: string }> };
|
|
139
|
+
const text = res?.content?.[0]?.text;
|
|
140
|
+
return text ? (JSON.parse(text) as Record<string, unknown>) : {};
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
|
|
133
144
|
/** resources/read a definition by URI (e.g. unoverse://components/card). Cached. A hash on
|
|
134
145
|
* the address names the state it opens in (`stateOfUri`) and is never sent to the server. */
|
|
@@ -493,7 +493,7 @@ export class ComponentStore {
|
|
|
493
493
|
// recomputed order on EVERY update, which is stable only while one thing is updating.
|
|
494
494
|
// A held Agent task has two: the task's face ticking through its steps, and the
|
|
495
495
|
// reply resuming once the result lands. They alternate, so the bottom slot alternated
|
|
496
|
-
// with them and the pair visibly swapped places on screen (
|
|
496
|
+
// with them and the pair visibly swapped places on screen (a bank's account agent,
|
|
497
497
|
// 2026-09-08). The case it was written for is served by arrival order too: text
|
|
498
498
|
// placed before a form stays above it.
|
|
499
499
|
this.merge(key, this.shieldGuestFace(key, event.data));
|
|
@@ -42,6 +42,8 @@ export interface UnoverseNode {
|
|
|
42
42
|
* content grows (new turn or streaming text), unless the user has scrolled up. Generic
|
|
43
43
|
* scroll-container behavior, opt-in via data (e.g. the conversation state's scroll box). */
|
|
44
44
|
autoScroll?: boolean;
|
|
45
|
+
/** Drag to move around this box's overflow, keeping the centre when its content changes size. */
|
|
46
|
+
pan?: boolean;
|
|
45
47
|
/** Box: a sticky bar (its own `position: sticky` + `top` in the design definition) that stays HIDDEN while
|
|
46
48
|
* the element before it (its previous sibling — e.g. a hero image) is on screen, then
|
|
47
49
|
* fades in once that sibling scrolls away. Overlays the content (no flow gap). Generic
|
|
@@ -29,7 +29,7 @@ import { guestHeldPointers } from "../transition/guestHold";
|
|
|
29
29
|
import { keyframesCss, styleToCss, type CssStyle } from "../style";
|
|
30
30
|
import type { ActionHandler } from "../actions";
|
|
31
31
|
import { mountComponent, type ComponentMount, type DefinitionSource } from "./component";
|
|
32
|
-
import { setChildren, setStyle } from "./element";
|
|
32
|
+
import { printWhenStill, setChildren, setStyle } from "./element";
|
|
33
33
|
import { isolatedRoot } from "./isolate";
|
|
34
34
|
import { mountQueue } from "./mounted";
|
|
35
35
|
import { regionView, type Leaf, type Region, type SlotCompute } from "./region";
|
|
@@ -273,6 +273,7 @@ export function mountApp(options: AppOptions): AppMount {
|
|
|
273
273
|
chatId: "",
|
|
274
274
|
nodeId: "",
|
|
275
275
|
sendToServer: o.onAction ? (t, payload) => o.onAction!(t, payload, meta) : undefined,
|
|
276
|
+
print: printWhenStill,
|
|
276
277
|
});
|
|
277
278
|
};
|
|
278
279
|
|
|
@@ -298,7 +299,7 @@ export function mountApp(options: AppOptions): AppMount {
|
|
|
298
299
|
|
|
299
300
|
const leaf = (pointer: string, extraData?: Data, fill?: boolean) => {
|
|
300
301
|
const { chatId, nodeId } = store.split(pointer);
|
|
301
|
-
const opts = { client: o.client, store, chatId, nodeId, onAction: dispatch, theme, extraData, fill, doc };
|
|
302
|
+
const opts = { client: o.client, store, chatId, nodeId, onAction: dispatch, theme, extraData, fill, doc, appStore: store };
|
|
302
303
|
// Keyed by the INSTANCE: one re-delivered later keeps its elements (no flicker).
|
|
303
304
|
return { key: nodeId, make: (): Leaf => mountComponent(opts), refresh: (l: Leaf) => (l as ComponentMount).update(opts) };
|
|
304
305
|
};
|