@unoverse-platform/studio 0.1.114 → 0.1.116
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/package.json +1 -1
- package/screens/AgentChain.tsx +216 -0
- package/screens/AgentsView.tsx +45 -6
- package/screens/SkillsView.tsx +21 -3
- package/screens/index.ts +3 -0
- package/vendor/base/src/agent-mcp/discovery.ts +4 -1
- package/vendor/base/src/agent-mcp/invoke.ts +155 -25
- package/vendor/base/src/agent-mcp/tools.ts +17 -10
- 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/load.ts +6 -1
- package/vendor/base/src/definitions/registry/agentFolders.ts +113 -0
- package/vendor/base/src/definitions/registry/objects.ts +16 -0
- package/vendor/base/src/definitions/registry/pipelines.ts +13 -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/lint/design/index.mjs +10 -2
- package/vendor/base/src/lint/objects/index.d.mts +1 -0
- package/vendor/base/src/lint/objects/index.mjs +120 -2
- package/vendor/base/src/lint/objects/rules.mjs +16 -5
- package/vendor/base/src/manifests/executor/toolBridge.ts +10 -3
- package/vendor/base/src/manifests/marketplaceMcp.ts +20 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +4 -1
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +58 -23
- 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/audioManager.ts +15 -2
- package/vendor/base/src/platform/taskChanged.ts +25 -0
- package/vendor/base/src/platform/tokenCost.ts +395 -0
- package/vendor/base/src/security/permissions.ts +10 -2
- package/vendor/sdk/appEngine.ts +42 -1
- package/vendor/sdk/lib/dom/markdown.ts +6 -2
- package/vendor/sdk/pageInterfaces.ts +109 -8
|
@@ -0,0 +1,395 @@
|
|
|
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; 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
|
+
const reasoningTokens = outDetails.reasoning_tokens || 0;
|
|
73
|
+
// AUDIO IS PRICED DIFFERENTLY from text, so voice cost cannot be read off a total alone.
|
|
74
|
+
const audioIn = inDetails.audio_tokens || 0;
|
|
75
|
+
const audioOut = outDetails.audio_tokens || 0;
|
|
76
|
+
const imageIn = inDetails.image_tokens || 0;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* HOW THE CACHED TOKENS SPLIT ACROSS MODALITIES, which is what makes them priceable.
|
|
80
|
+
*
|
|
81
|
+
* A cached token costs $0.40 against $32 for fresh audio or $4 for fresh text, so getting
|
|
82
|
+
* this wrong is most of the bill. `cached_tokens` alone does not say which modality it came
|
|
83
|
+
* from, and `cached_tokens_details` — which does — is absent from every row written before
|
|
84
|
+
* the collector stopped dropping it, and from any vendor that never sends it.
|
|
85
|
+
*
|
|
86
|
+
* So: use the details when present, and otherwise APPORTION BY SHARE. Proportional is a
|
|
87
|
+
* genuine estimate rather than a fact, but it is far closer than the alternative of billing
|
|
88
|
+
* every cached token at the full fresh rate, which overcharged a real call by ~45%.
|
|
89
|
+
*/
|
|
90
|
+
const cachedTotal = inDetails.cached_tokens || 0;
|
|
91
|
+
const cachedDetails = inDetails.cached_tokens_details;
|
|
92
|
+
const freshTotal = (inDetails.text_tokens || 0) + audioIn + imageIn;
|
|
93
|
+
const share = (n: number) => (freshTotal > 0 ? Math.round((cachedTotal * n) / freshTotal) : 0);
|
|
94
|
+
const cachedTextIn = cachedDetails ? cachedDetails.text_tokens || 0 : share(inDetails.text_tokens || 0);
|
|
95
|
+
const cachedAudioIn = cachedDetails ? cachedDetails.audio_tokens || 0 : share(audioIn);
|
|
96
|
+
const cachedImageIn = cachedDetails ? cachedDetails.image_tokens || 0 : share(imageIn);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
total: usage.total_tokens || (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
100
|
+
input: usage.input_tokens || 0,
|
|
101
|
+
output: usage.output_tokens || 0,
|
|
102
|
+
reasoning: reasoningTokens,
|
|
103
|
+
cached: cachedTokens,
|
|
104
|
+
breakdown: {
|
|
105
|
+
input: usage.input_tokens || 0,
|
|
106
|
+
cached: cachedTokens,
|
|
107
|
+
reasoning: reasoningTokens,
|
|
108
|
+
completion: (usage.output_tokens || 0) - reasoningTokens,
|
|
109
|
+
// Present only on a voice call; omitted rather than shown as 0 for text models, so a
|
|
110
|
+
// zero in the panel always means "no audio" rather than "not measured".
|
|
111
|
+
...(audioIn || audioOut ? { audioIn, audioOut } : {}),
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* THE PER-MODALITY SPLIT, for pricing. Only present when the vendor sent one.
|
|
115
|
+
*
|
|
116
|
+
* CACHED IS A SUBSET, not a fourth category: `cached_tokens_details` breaks the cached
|
|
117
|
+
* count down by modality, so a cached audio token is already counted in `audio_tokens`.
|
|
118
|
+
* Subtracting it is what stops it being billed twice, once at $32 and again at $0.40.
|
|
119
|
+
*/
|
|
120
|
+
...(audioIn || audioOut || inDetails.text_tokens !== undefined
|
|
121
|
+
? {
|
|
122
|
+
modality: {
|
|
123
|
+
textIn: Math.max(0, (inDetails.text_tokens || 0) - cachedTextIn),
|
|
124
|
+
cachedTextIn,
|
|
125
|
+
audioIn: Math.max(0, audioIn - cachedAudioIn),
|
|
126
|
+
cachedAudioIn,
|
|
127
|
+
imageIn: Math.max(0, imageIn - cachedImageIn),
|
|
128
|
+
cachedImageIn,
|
|
129
|
+
textOut: outDetails.text_tokens || 0,
|
|
130
|
+
audioOut,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Chat Completions API format (GPT-4, GPT-3.5)
|
|
138
|
+
const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
|
|
139
|
+
const input = usage.prompt_tokens || usage.promptTokens || 0;
|
|
140
|
+
const output = usage.completion_tokens || usage.completionTokens || 0;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
total: usage.total_tokens || usage.totalTokens || input + output,
|
|
144
|
+
input,
|
|
145
|
+
output,
|
|
146
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
147
|
+
cached: cachedTokens,
|
|
148
|
+
breakdown: {
|
|
149
|
+
prompt: input,
|
|
150
|
+
cached: cachedTokens,
|
|
151
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
152
|
+
completion: output,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Claude Models (Anthropic)
|
|
159
|
+
*/
|
|
160
|
+
const parseClaude = (usage: RawUsage) => {
|
|
161
|
+
return {
|
|
162
|
+
total: (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
163
|
+
input: usage.input_tokens || 0,
|
|
164
|
+
output: usage.output_tokens || 0,
|
|
165
|
+
reasoning: 0,
|
|
166
|
+
breakdown: {
|
|
167
|
+
input: usage.input_tokens || 0,
|
|
168
|
+
output: usage.output_tokens || 0,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Bedrock Models (AWS)
|
|
175
|
+
*/
|
|
176
|
+
const parseBedrock = (usage: RawUsage) => {
|
|
177
|
+
return {
|
|
178
|
+
total: (usage.inputTokens || 0) + (usage.outputTokens || 0),
|
|
179
|
+
input: usage.inputTokens || 0,
|
|
180
|
+
output: usage.outputTokens || 0,
|
|
181
|
+
reasoning: 0,
|
|
182
|
+
breakdown: {
|
|
183
|
+
input: usage.inputTokens || 0,
|
|
184
|
+
output: usage.outputTokens || 0,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Bedrock Claude Models
|
|
191
|
+
* Handles both native Bedrock format (inputTokens) and OpenAI-style format (prompt_tokens)
|
|
192
|
+
*/
|
|
193
|
+
const parseBedrockClaude = (usage: RawUsage) => {
|
|
194
|
+
const input = usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0;
|
|
195
|
+
const output = usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0;
|
|
196
|
+
return {
|
|
197
|
+
total: usage.total_tokens || usage.totalTokens || input + output,
|
|
198
|
+
input,
|
|
199
|
+
output,
|
|
200
|
+
reasoning: 0,
|
|
201
|
+
breakdown: { input, output },
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generic fallback for unknown models
|
|
207
|
+
*/
|
|
208
|
+
const parseGeneric = (usage: RawUsage) => {
|
|
209
|
+
const input = usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0;
|
|
210
|
+
const output = usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0;
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
total: usage.total_tokens || input + output,
|
|
214
|
+
input,
|
|
215
|
+
output,
|
|
216
|
+
reasoning: 0,
|
|
217
|
+
breakdown: { input, output },
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Calculate cost based on model and parsed usage
|
|
223
|
+
* @param {string} model - The model name
|
|
224
|
+
* @param {object} parsedUsage - Parsed usage object
|
|
225
|
+
* @returns {string} Cost in USD (formatted to 4 decimal places)
|
|
226
|
+
*/
|
|
227
|
+
export const calculateTokenCost = (model: string, parsedUsage: any): string => {
|
|
228
|
+
if (!parsedUsage) return "0.0000";
|
|
229
|
+
|
|
230
|
+
const pricing = getModelPricing(model);
|
|
231
|
+
if (!pricing) return "0.0000";
|
|
232
|
+
|
|
233
|
+
// PER MINUTE, for vendors that bill a voice call on its length rather than its tokens.
|
|
234
|
+
if (pricing.perMinute && parsedUsage.seconds) {
|
|
235
|
+
return ((parsedUsage.seconds / 60) * pricing.perMinute).toFixed(4);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const per = (tokens: number | undefined, rate: number | undefined) => ((tokens || 0) / 1000000) * (rate || 0);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* PER-MODALITY, when the model prices that way AND the usage block carried the split.
|
|
242
|
+
*
|
|
243
|
+
* Realtime bills audio at up to 8x text ($32 against $4 per 1M in), so a voice call priced on
|
|
244
|
+
* flat input/output is not approximately right, it is wrong by most of the bill. Both
|
|
245
|
+
* conditions are required: a model with no audio rates, or a usage block with no split, falls
|
|
246
|
+
* through to the flat path below and behaves exactly as it always did.
|
|
247
|
+
*/
|
|
248
|
+
if (pricing.audioInput && parsedUsage.modality) {
|
|
249
|
+
const m = parsedUsage.modality;
|
|
250
|
+
return (
|
|
251
|
+
per(m.textIn, pricing.input) +
|
|
252
|
+
per(m.cachedTextIn, pricing.cachedInput ?? pricing.input) +
|
|
253
|
+
per(m.audioIn, pricing.audioInput) +
|
|
254
|
+
per(m.cachedAudioIn, pricing.cachedAudioInput ?? pricing.audioInput) +
|
|
255
|
+
per(m.imageIn, pricing.imageInput ?? pricing.input) +
|
|
256
|
+
per(m.cachedImageIn, pricing.cachedImageInput ?? pricing.imageInput ?? pricing.input) +
|
|
257
|
+
per(m.textOut, pricing.output) +
|
|
258
|
+
per(m.audioOut, pricing.audioOutput ?? pricing.output)
|
|
259
|
+
).toFixed(4);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const inputCost = per(parsedUsage.input, pricing.input);
|
|
263
|
+
const outputCost = per(parsedUsage.output, pricing.output);
|
|
264
|
+
|
|
265
|
+
return (inputCost + outputCost).toFixed(4);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get pricing for a model (per million tokens)
|
|
270
|
+
*/
|
|
271
|
+
export const getModelPricing = (model: string): ModelPricing => {
|
|
272
|
+
const pricing: Record<string, ModelPricing> = {
|
|
273
|
+
// GPT-5.6 family (Sol/Terra/Luna) — verified against the OpenAI pricing page 2026-08-07
|
|
274
|
+
"gpt-5.6": { input: 5.0, output: 30.0 }, // alias of gpt-5.6-sol
|
|
275
|
+
"gpt-5.6-sol": { input: 5.0, output: 30.0 },
|
|
276
|
+
"gpt-5.6-terra": { input: 2.0, output: 12.0 },
|
|
277
|
+
"gpt-5.6-luna": { input: 0.2, output: 1.2 },
|
|
278
|
+
|
|
279
|
+
// Z.AI GLM — verified against docs.z.ai/guides/overview/pricing 2026-10-09
|
|
280
|
+
"glm-5.3": { input: 1.4, cachedInput: 0.26, output: 4.4 },
|
|
281
|
+
"glm-5.3-flash": { input: 0.15, cachedInput: 0.03, output: 0.5 },
|
|
282
|
+
"glm-5.2": { input: 1.4, cachedInput: 0.26, output: 4.4 },
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* GPT-Realtime family — PRICED PER MODALITY, verified against the OpenAI model pages
|
|
286
|
+
* 2026-08-12 (developers.openai.com/api/docs/models/gpt-realtime-2.1{,-mini}).
|
|
287
|
+
*
|
|
288
|
+
* `input`/`output` here are the TEXT rates, and pricing a voice call at them — which is
|
|
289
|
+
* what this table did — understates it by up to 8x, because a call is almost entirely
|
|
290
|
+
* AUDIO tokens: $32/$64 against $4/$24 on the pro model. The label said "(text tokens)",
|
|
291
|
+
* so the limitation was known; nothing downstream acted on it.
|
|
292
|
+
*
|
|
293
|
+
* `calculateTokenCost` uses these per-modality fields when the usage block carries the
|
|
294
|
+
* split, and falls back to flat input/output when it does not. Every other model in this
|
|
295
|
+
* table is unaffected.
|
|
296
|
+
*/
|
|
297
|
+
"gpt-realtime-2.1": {
|
|
298
|
+
input: 4.0, cachedInput: 0.4, output: 24.0,
|
|
299
|
+
audioInput: 32.0, cachedAudioInput: 0.4, audioOutput: 64.0,
|
|
300
|
+
imageInput: 5.0, cachedImageInput: 0.5,
|
|
301
|
+
},
|
|
302
|
+
"gpt-realtime-2.1-mini": {
|
|
303
|
+
input: 0.6, cachedInput: 0.06, output: 2.4,
|
|
304
|
+
audioInput: 10.0, cachedAudioInput: 0.3, audioOutput: 20.0,
|
|
305
|
+
imageInput: 0.8, cachedImageInput: 0.08,
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* xAI GROK VOICE — the vendor bills per MINUTE, not per token: $0.08/min audio plus
|
|
310
|
+
* $0.004 per text input (docs.x.ai/developers/pricing, 2026-08-15). The realtime
|
|
311
|
+
* `response.done.usage` carries only flat input/output/total token counts — no
|
|
312
|
+
* audio/text split, no duration — so a per-minute price cannot be computed from what
|
|
313
|
+
* this table is given.
|
|
314
|
+
*
|
|
315
|
+
* The platform now times every call and records `seconds`, so a call is priced at the
|
|
316
|
+
* vendor's $0.08/min. The per-token rates below are an ESTIMATE kept only for rows written
|
|
317
|
+
* before calls were timed. The $0.004 text-input charge is not included; it is small beside
|
|
318
|
+
* the minutes.
|
|
319
|
+
*/
|
|
320
|
+
"grok-voice-think-fast-2.0": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
321
|
+
"grok-voice-latest": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
322
|
+
"grok-voice-think-fast-1.0": { perMinute: 0.08, input: 4.0, output: 8.0 },
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* OpenAI GPT-LIVE — billed per second at $0.05/min (developers.openai.com/api/docs/models/
|
|
326
|
+
* gpt-live-1). Priced on the `seconds` the platform times; it reports no tokens of its own.
|
|
327
|
+
*/
|
|
328
|
+
"gpt-live-1": { perMinute: 0.05 },
|
|
329
|
+
|
|
330
|
+
// GPT-5.x family (older, kept for historical executions)
|
|
331
|
+
"gpt-5.2": { input: 1.75, output: 14.0 },
|
|
332
|
+
"gpt-5": { input: 1.25, output: 10.0 },
|
|
333
|
+
"gpt-5-mini": { input: 0.25, output: 2.0 },
|
|
334
|
+
"gpt-5-nano": { input: 0.05, output: 0.4 },
|
|
335
|
+
"gpt-5-pro": { input: 15.0, output: 120.0 },
|
|
336
|
+
"gpt-5-chat-latest": { input: 1.25, output: 10.0 },
|
|
337
|
+
"gpt-5-codex": { input: 1.25, output: 10.0 },
|
|
338
|
+
|
|
339
|
+
// GPT-4.1 family
|
|
340
|
+
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
341
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
|
|
342
|
+
|
|
343
|
+
// GPT-4o family
|
|
344
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
345
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
346
|
+
|
|
347
|
+
// GPT-3.5 family
|
|
348
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
349
|
+
"gpt-3.5-turbo-16k": { input: 1.0, output: 2.0 },
|
|
350
|
+
|
|
351
|
+
// Claude models
|
|
352
|
+
"claude-3-opus": { input: 15.0, output: 75.0 },
|
|
353
|
+
"claude-3-sonnet": { input: 3.0, output: 15.0 },
|
|
354
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
355
|
+
|
|
356
|
+
// Bedrock Claude 4.5 (latest)
|
|
357
|
+
"us.anthropic.claude-opus-4-5-20250514-v1:0": { input: 5.0, output: 25.0 },
|
|
358
|
+
"us.anthropic.claude-sonnet-4-5-20250514-v1:0": { input: 3.0, output: 15.0 },
|
|
359
|
+
"us.anthropic.claude-haiku-4-5-20251001-v1:0": { input: 1.0, output: 5.0 },
|
|
360
|
+
|
|
361
|
+
// Bedrock Claude 4
|
|
362
|
+
"us.anthropic.claude-sonnet-4-20250514-v1:0": { input: 3.0, output: 15.0 },
|
|
363
|
+
|
|
364
|
+
// Bedrock Claude 3.5
|
|
365
|
+
"us.anthropic.claude-3-5-sonnet-20241022-v2:0": { input: 3.0, output: 15.0 },
|
|
366
|
+
"us.anthropic.claude-3-5-haiku-20241022-v1:0": { input: 0.25, output: 1.25 },
|
|
367
|
+
|
|
368
|
+
// Bedrock Claude 3
|
|
369
|
+
"anthropic.claude-3-sonnet-20240229-v1:0": { input: 3.0, output: 15.0 },
|
|
370
|
+
"anthropic.claude-3-haiku-20240307-v1:0": { input: 0.25, output: 1.25 },
|
|
371
|
+
|
|
372
|
+
// AWS Nova
|
|
373
|
+
"amazon.nova-sonic-v1:0": { input: 3.4, output: 13.6 },
|
|
374
|
+
"amazon.nova-2-sonic-v1:0": { input: 3.4, output: 13.6 },
|
|
375
|
+
|
|
376
|
+
// Embedding models
|
|
377
|
+
"text-embedding-ada-002": { input: 0.1, output: 0 },
|
|
378
|
+
"text-embedding-3-small": { input: 0.02, output: 0 },
|
|
379
|
+
"text-embedding-3-large": { input: 0.13, output: 0 },
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Try exact match first
|
|
383
|
+
if (pricing[model]) return pricing[model];
|
|
384
|
+
|
|
385
|
+
// Try partial match for Bedrock models
|
|
386
|
+
for (const [key, value] of Object.entries(pricing)) {
|
|
387
|
+
if (model.includes(key) || key.includes(model)) {
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Default fallback - use Claude Sonnet pricing as reasonable default
|
|
393
|
+
console.warn(`Unknown model pricing: ${model}, using default $3/$15 per 1M tokens`);
|
|
394
|
+
return { input: 3.0, output: 15.0 };
|
|
395
|
+
};
|
|
@@ -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 = () => {
|
|
@@ -143,10 +143,14 @@ function externalLink(env: Env): Rec {
|
|
|
143
143
|
const { props, children: kids } = t as El;
|
|
144
144
|
const theme = env.theme();
|
|
145
145
|
const { s } = prose(theme);
|
|
146
|
-
|
|
146
|
+
// A LINK TO THE SAME SITE (a path, `/signal/person/?id=…`) is the page's own next view: it
|
|
147
|
+
// opens in place, without the external glyph. Any other address opens in a new tab.
|
|
148
|
+
const href = typeof (props as { href?: unknown }).href === "string" ? String((props as { href?: unknown }).href) : "";
|
|
149
|
+
const ownSite = href.startsWith("/") && !href.startsWith("//");
|
|
150
|
+
attrs = setProps(el, ownSite ? { ...props } : { ...props, target: "_blank", rel: "noopener noreferrer" }, attrs);
|
|
147
151
|
setStyle(el, env.inheritLink() ? { ...s("link"), color: "inherit" } : s("link"));
|
|
148
152
|
const li = (theme.icons?.externalLink ?? {}) as Parameters<typeof svgOf>[1];
|
|
149
|
-
if (!li.children?.length) arrow = null;
|
|
153
|
+
if (ownSite || !li.children?.length) arrow = null;
|
|
150
154
|
else if (arrow?.glyph !== li) arrow = { glyph: li, svg: svgOf(env.doc, li, s("linkIcon")) };
|
|
151
155
|
children(env, el, recs, kids);
|
|
152
156
|
if (arrow) el.appendChild(arrow.svg);
|