@lucid-agents/pi-xgate 0.0.0-stage → 0.1.6
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/LICENSE +21 -0
- package/README.md +211 -2
- package/dist/codec.d.ts +34 -0
- package/dist/codec.js +510 -0
- package/dist/extension.d.ts +16 -0
- package/dist/extension.js +132 -0
- package/dist/generated/openai-model-capabilities.json +847 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/provider.d.ts +24 -0
- package/dist/provider.js +243 -0
- package/dist/taskmarket.d.ts +131 -0
- package/dist/taskmarket.js +275 -0
- package/dist/transport.d.ts +72 -0
- package/dist/transport.js +21 -0
- package/package.json +63 -4
- package/src/codec.ts +658 -0
- package/src/extension.ts +178 -0
- package/src/generated/openai-model-capabilities.json +847 -0
- package/src/index.ts +45 -0
- package/src/provider.ts +313 -0
- package/src/taskmarket.ts +484 -0
- package/src/transport.ts +94 -0
package/dist/codec.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import * as pi from "@earendil-works/pi-ai";
|
|
2
|
+
import { XgateError } from "@lucid-agents/xgate-client/types";
|
|
3
|
+
import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
const need = (x, code) => {
|
|
6
|
+
if (!x)
|
|
7
|
+
throw new XgateError(code);
|
|
8
|
+
};
|
|
9
|
+
const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
10
|
+
const rate = (x) => {
|
|
11
|
+
need(x &&
|
|
12
|
+
typeof x.numerator === "string" &&
|
|
13
|
+
/^[0-9]{1,13}$/.test(x.numerator) &&
|
|
14
|
+
Number(x.numerator) <= 1e12 &&
|
|
15
|
+
Number.isSafeInteger(x.denominator) &&
|
|
16
|
+
x.denominator > 0 &&
|
|
17
|
+
x.denominator <= 1e6, "invalid_catalog_rate");
|
|
18
|
+
return Number(x.numerator) / x.denominator;
|
|
19
|
+
};
|
|
20
|
+
export function catalogEntries(rows) {
|
|
21
|
+
need(Array.isArray(rows) && rows.length <= 128, "invalid_catalog");
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
const p = row?.execution_profile;
|
|
25
|
+
if (!object(p) ||
|
|
26
|
+
p.model !== row.id ||
|
|
27
|
+
!Array.isArray(row.qualified_profiles))
|
|
28
|
+
continue;
|
|
29
|
+
const anthropic = row.owned_by === "anthropic", cap = anthropic ? p.capabilities : capabilities[row.id];
|
|
30
|
+
if (!cap || (!anthropic && !cap.tools))
|
|
31
|
+
continue;
|
|
32
|
+
const op = anthropic ? "messages" : "responses";
|
|
33
|
+
const q = row.qualified_profiles.find((q) => q.model === row.id &&
|
|
34
|
+
q.operation === op &&
|
|
35
|
+
q.provider === (anthropic ? "anthropic" : "open_ai") &&
|
|
36
|
+
q.normalizer_revision ===
|
|
37
|
+
(anthropic
|
|
38
|
+
? "anthropic-anonymous-usage-v3"
|
|
39
|
+
: "openai-anonymous-usage-v1") &&
|
|
40
|
+
q.transformation_revision ===
|
|
41
|
+
(anthropic
|
|
42
|
+
? "anthropic-anonymous-messages-v1"
|
|
43
|
+
: "openai-anonymous-tools-v2"));
|
|
44
|
+
if (!q ||
|
|
45
|
+
row.privacy?.revision !== "anonymous-volatile-v1" ||
|
|
46
|
+
row.privacy.result_replay_available !== false ||
|
|
47
|
+
(anthropic && q.cache_mode !== "anthropic_no_cache"))
|
|
48
|
+
continue;
|
|
49
|
+
if (q.valid_until !== undefined &&
|
|
50
|
+
Number(q.valid_until) * 1000 <= Date.now())
|
|
51
|
+
continue;
|
|
52
|
+
if (row.lifecycle?.stop_before != null &&
|
|
53
|
+
Number(row.lifecycle.stop_before) * 1000 <= Date.now())
|
|
54
|
+
continue;
|
|
55
|
+
const contextWindow = Math.min(p.max_input_tokens, p.provider_limits?.context_window ??
|
|
56
|
+
cap.context_window ??
|
|
57
|
+
p.max_input_tokens), maxTokens = Math.min(8192, p.max_output_tokens);
|
|
58
|
+
need(Number.isSafeInteger(contextWindow) &&
|
|
59
|
+
contextWindow > 0 &&
|
|
60
|
+
Number.isSafeInteger(maxTokens) &&
|
|
61
|
+
maxTokens > 0, "invalid_catalog_limits");
|
|
62
|
+
const efforts = anthropic ? cap.effort : cap.reasoning_efforts;
|
|
63
|
+
const levels = [
|
|
64
|
+
"minimal",
|
|
65
|
+
"low",
|
|
66
|
+
"medium",
|
|
67
|
+
"high",
|
|
68
|
+
"xhigh",
|
|
69
|
+
"max",
|
|
70
|
+
];
|
|
71
|
+
const thinkingLevelMap = {
|
|
72
|
+
off: anthropic
|
|
73
|
+
? cap.thinking.disabled
|
|
74
|
+
? "disabled"
|
|
75
|
+
: null
|
|
76
|
+
: efforts.length === 0 || efforts.includes("none")
|
|
77
|
+
? "none"
|
|
78
|
+
: null,
|
|
79
|
+
};
|
|
80
|
+
for (const level of levels) {
|
|
81
|
+
const allowed = efforts.includes(level) ||
|
|
82
|
+
(anthropic &&
|
|
83
|
+
cap.thinking.enabled &&
|
|
84
|
+
["minimal", "low", "medium", "high"].includes(level));
|
|
85
|
+
thinkingLevelMap[level] = allowed ? level : null;
|
|
86
|
+
}
|
|
87
|
+
entries.push({
|
|
88
|
+
catalog: row,
|
|
89
|
+
route: anthropic
|
|
90
|
+
? "/providers/anthropic/v1/messages"
|
|
91
|
+
: "/providers/openai/v1/responses",
|
|
92
|
+
efforts,
|
|
93
|
+
model: {
|
|
94
|
+
id: row.id,
|
|
95
|
+
name: `${row.id} (xgate)`,
|
|
96
|
+
api: anthropic ? "xgate-messages" : "xgate-responses",
|
|
97
|
+
provider: "xgate",
|
|
98
|
+
baseUrl: row.xgate_origin ?? "https://api.xgate.run",
|
|
99
|
+
input: ["text"],
|
|
100
|
+
contextWindow,
|
|
101
|
+
maxTokens,
|
|
102
|
+
reasoning: anthropic
|
|
103
|
+
? Boolean(cap.thinking?.adaptive || cap.thinking?.enabled)
|
|
104
|
+
: efforts.some((effort) => effort !== "none"),
|
|
105
|
+
thinkingLevelMap,
|
|
106
|
+
cost: {
|
|
107
|
+
input: rate(p.rates.input_uncached),
|
|
108
|
+
output: rate(p.rates.output),
|
|
109
|
+
cacheRead: rate(p.rates.input_cache_read),
|
|
110
|
+
cacheWrite: p.rates.input_cache_write
|
|
111
|
+
? rate(p.rates.input_cache_write)
|
|
112
|
+
: 0,
|
|
113
|
+
},
|
|
114
|
+
inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
export function transcript(context) {
|
|
121
|
+
const legacy = "systemPrompt" in context || "tools" in context;
|
|
122
|
+
if (typeof pi.collapseSystemMessages !== "function") {
|
|
123
|
+
need(!context.messages.some((m) => m.role === "system"), "unsupported_context");
|
|
124
|
+
const raw = context;
|
|
125
|
+
return {
|
|
126
|
+
system: raw.systemPrompt ?? "",
|
|
127
|
+
tools: raw.tools ?? [],
|
|
128
|
+
messages: raw.messages,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const collapsed = pi.collapseSystemMessages(legacy
|
|
132
|
+
? pi.normalizeContext(context)
|
|
133
|
+
: context);
|
|
134
|
+
return {
|
|
135
|
+
system: pi.getCurrentSystemPrompt(collapsed.messages),
|
|
136
|
+
tools: pi.getCurrentTools(collapsed.messages),
|
|
137
|
+
messages: collapsed.messages.filter((m) => m.role !== "system"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function text(content) {
|
|
141
|
+
if (typeof content === "string")
|
|
142
|
+
return content;
|
|
143
|
+
need(Array.isArray(content) &&
|
|
144
|
+
content.every((b) => b.type === "text" && typeof b.text === "string"), "text_only_request");
|
|
145
|
+
return content.map((b) => b.text).join("\n");
|
|
146
|
+
}
|
|
147
|
+
function callId(id) {
|
|
148
|
+
return /^[A-Za-z0-9_:.\-]{1,128}$/.test(id)
|
|
149
|
+
? id
|
|
150
|
+
: "call_" + createHash("sha256").update(id).digest("hex").slice(0, 40);
|
|
151
|
+
}
|
|
152
|
+
function validateTools(tools, anthropic) {
|
|
153
|
+
need(tools.length <= (anthropic ? 32 : 8), "too_many_tools");
|
|
154
|
+
need(tools.every((t) => /^[A-Za-z0-9_-]{1,64}$/.test(t.name) &&
|
|
155
|
+
object(t.parameters) &&
|
|
156
|
+
t.parameters.type === "object"), "unsupported_tool_schema");
|
|
157
|
+
need(new Set(tools.map((t) => t.name)).size === tools.length, "duplicate_tools");
|
|
158
|
+
}
|
|
159
|
+
export function encode(entry, context, options = {}) {
|
|
160
|
+
need(options.toolChoice === undefined || options.toolChoice === "auto", "unsupported_tool_choice");
|
|
161
|
+
need(options.reasoning === undefined || typeof options.reasoning === "string", "unsupported_reasoning");
|
|
162
|
+
need(options.temperature === undefined &&
|
|
163
|
+
(!options.samplingParams ||
|
|
164
|
+
Object.keys(options.samplingParams).length === 0), "unsupported_sampling");
|
|
165
|
+
const anthropic = entry.route.endsWith("/messages"), { system, tools, messages } = transcript(context);
|
|
166
|
+
validateTools(tools, anthropic);
|
|
167
|
+
const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
|
|
168
|
+
need(Number.isSafeInteger(max) && max > 0 && max <= entry.model.maxTokens, "output_limit");
|
|
169
|
+
const requested = options.reasoning, effort = requested && entry.efforts.includes(requested)
|
|
170
|
+
? requested
|
|
171
|
+
: entry.efforts[0];
|
|
172
|
+
if (requested)
|
|
173
|
+
need(entry.model.thinkingLevelMap?.[requested] !== null &&
|
|
174
|
+
(entry.efforts.includes(requested) ||
|
|
175
|
+
(anthropic &&
|
|
176
|
+
entry.catalog.execution_profile.capabilities.thinking.enabled)), "unsupported_reasoning");
|
|
177
|
+
const history = [];
|
|
178
|
+
const append = (role, content) => {
|
|
179
|
+
const previous = history.at(-1);
|
|
180
|
+
if (previous?.role === role)
|
|
181
|
+
previous.content.push(...content);
|
|
182
|
+
else
|
|
183
|
+
history.push({ role, content });
|
|
184
|
+
};
|
|
185
|
+
if (!anthropic && system)
|
|
186
|
+
history.push({ role: "system", content: system });
|
|
187
|
+
for (const message of messages) {
|
|
188
|
+
if (message.role === "user") {
|
|
189
|
+
const value = text(message.content);
|
|
190
|
+
if (anthropic)
|
|
191
|
+
append("user", [{ type: "text", text: value }]);
|
|
192
|
+
else
|
|
193
|
+
history.push({ role: "user", content: value });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (message.role === "toolResult") {
|
|
197
|
+
const value = text(message.content), id = callId(message.toolCallId);
|
|
198
|
+
if (anthropic)
|
|
199
|
+
append("user", [
|
|
200
|
+
{
|
|
201
|
+
type: "tool_result",
|
|
202
|
+
tool_use_id: id,
|
|
203
|
+
content: value,
|
|
204
|
+
...(message.isError ? { is_error: true } : {}),
|
|
205
|
+
},
|
|
206
|
+
]);
|
|
207
|
+
else
|
|
208
|
+
history.push({
|
|
209
|
+
type: "function_call_output",
|
|
210
|
+
call_id: id,
|
|
211
|
+
output: message.isError ? "Tool error: " + value : value,
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
need(message.role === "assistant", "unsupported_message");
|
|
216
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
217
|
+
continue;
|
|
218
|
+
const same = message.provider === "xgate" &&
|
|
219
|
+
message.model === entry.model.id &&
|
|
220
|
+
message.api === entry.model.api;
|
|
221
|
+
const blocks = [];
|
|
222
|
+
for (const block of message.content) {
|
|
223
|
+
if (block.type === "text") {
|
|
224
|
+
if (anthropic) {
|
|
225
|
+
if (block.text)
|
|
226
|
+
blocks.push({ type: "text", text: block.text });
|
|
227
|
+
}
|
|
228
|
+
else if (block.text) {
|
|
229
|
+
let signature;
|
|
230
|
+
try {
|
|
231
|
+
signature = block.textSignature
|
|
232
|
+
? JSON.parse(block.textSignature)
|
|
233
|
+
: undefined;
|
|
234
|
+
}
|
|
235
|
+
catch { }
|
|
236
|
+
const previous = history.at(-1);
|
|
237
|
+
if (same &&
|
|
238
|
+
signature?.id &&
|
|
239
|
+
previous?.type === "message" &&
|
|
240
|
+
previous.id === signature.id &&
|
|
241
|
+
previous.phase === signature.phase) {
|
|
242
|
+
previous.content.push({ type: "output_text", text: block.text });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
history.push({
|
|
246
|
+
type: "message",
|
|
247
|
+
role: "assistant",
|
|
248
|
+
content: [{ type: "output_text", text: block.text }],
|
|
249
|
+
...(same && signature?.id
|
|
250
|
+
? {
|
|
251
|
+
id: signature.id,
|
|
252
|
+
...(signature.phase ? { phase: signature.phase } : {}),
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (block.type === "toolCall") {
|
|
259
|
+
need(!block.namespace && object(block.arguments), "unsupported_tool_call");
|
|
260
|
+
const id = callId(block.id);
|
|
261
|
+
if (anthropic) {
|
|
262
|
+
need(tools.some((t) => t.name === block.name), "history_tool_not_declared");
|
|
263
|
+
blocks.push({
|
|
264
|
+
type: "tool_use",
|
|
265
|
+
id,
|
|
266
|
+
name: block.name,
|
|
267
|
+
input: block.arguments,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
history.push({
|
|
272
|
+
type: "function_call",
|
|
273
|
+
call_id: id,
|
|
274
|
+
name: block.name,
|
|
275
|
+
arguments: JSON.stringify(block.arguments),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else if (block.type === "thinking" && same) {
|
|
279
|
+
if (anthropic) {
|
|
280
|
+
need(typeof block.thinkingSignature === "string" &&
|
|
281
|
+
block.thinkingSignature.length > 0, "missing_thinking_signature");
|
|
282
|
+
blocks.push(block.redacted
|
|
283
|
+
? { type: "redacted_thinking", data: block.thinkingSignature }
|
|
284
|
+
: {
|
|
285
|
+
type: "thinking",
|
|
286
|
+
thinking: block.thinking,
|
|
287
|
+
signature: block.thinkingSignature,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (block.thinkingSignature) {
|
|
291
|
+
const reasoning = JSON.parse(block.thinkingSignature);
|
|
292
|
+
need(reasoning.type === "reasoning" &&
|
|
293
|
+
typeof reasoning.encrypted_content === "string", "invalid_reasoning_history");
|
|
294
|
+
history.push(reasoning);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (anthropic && blocks.length)
|
|
299
|
+
append("assistant", blocks);
|
|
300
|
+
}
|
|
301
|
+
need(history.length > 0 && history.length <= 256, "history_limit");
|
|
302
|
+
let request;
|
|
303
|
+
if (anthropic) {
|
|
304
|
+
const cap = entry.catalog.execution_profile.capabilities;
|
|
305
|
+
request = {
|
|
306
|
+
model: entry.model.id,
|
|
307
|
+
messages: history,
|
|
308
|
+
max_tokens: max,
|
|
309
|
+
stream: false,
|
|
310
|
+
...(system ? { system } : {}),
|
|
311
|
+
...(tools.length
|
|
312
|
+
? {
|
|
313
|
+
tools: tools.map((t) => ({
|
|
314
|
+
name: t.name,
|
|
315
|
+
description: t.description,
|
|
316
|
+
input_schema: t.parameters,
|
|
317
|
+
})),
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
321
|
+
if (effort)
|
|
322
|
+
request.output_config = { effort };
|
|
323
|
+
if (cap.thinking.adaptive)
|
|
324
|
+
request.thinking = { type: "adaptive" };
|
|
325
|
+
else if (requested && cap.thinking.enabled) {
|
|
326
|
+
need(max > 1024, "thinking_output_limit");
|
|
327
|
+
request.thinking = {
|
|
328
|
+
type: "enabled",
|
|
329
|
+
budget_tokens: Math.min(2048, max - 1),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else
|
|
334
|
+
request = {
|
|
335
|
+
model: entry.model.id,
|
|
336
|
+
input: history,
|
|
337
|
+
max_output_tokens: max,
|
|
338
|
+
stream: false,
|
|
339
|
+
store: false,
|
|
340
|
+
include: ["reasoning.encrypted_content"],
|
|
341
|
+
...(effort ? { reasoning: { effort } } : {}),
|
|
342
|
+
...(tools.length
|
|
343
|
+
? {
|
|
344
|
+
tools: tools.map((t) => ({
|
|
345
|
+
type: "function",
|
|
346
|
+
name: t.name,
|
|
347
|
+
description: t.description,
|
|
348
|
+
parameters: t.parameters,
|
|
349
|
+
})),
|
|
350
|
+
parallel_tool_calls: false,
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
353
|
+
};
|
|
354
|
+
if (request.tools)
|
|
355
|
+
need(Buffer.byteLength(JSON.stringify(request.tools)) <=
|
|
356
|
+
(anthropic ? 65536 : 8192), "tool_schema_bound");
|
|
357
|
+
need(Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024, "request_bound");
|
|
358
|
+
return request;
|
|
359
|
+
}
|
|
360
|
+
function tokens(value) {
|
|
361
|
+
need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
export function decode(entry, body) {
|
|
365
|
+
need(object(body) && typeof body.id === "string" && object(body.usage), "invalid_result");
|
|
366
|
+
const anthropic = entry.route.endsWith("/messages"), content = [];
|
|
367
|
+
let stopReason;
|
|
368
|
+
if (anthropic) {
|
|
369
|
+
need(body.type === "message" &&
|
|
370
|
+
body.role === "assistant" &&
|
|
371
|
+
body.model === entry.model.id &&
|
|
372
|
+
["end_turn", "tool_use", "stop_sequence"].includes(body.stop_reason) &&
|
|
373
|
+
Array.isArray(body.content), "invalid_result");
|
|
374
|
+
stopReason = body.stop_reason === "tool_use" ? "toolUse" : "stop";
|
|
375
|
+
for (const b of body.content) {
|
|
376
|
+
if (b.type === "text") {
|
|
377
|
+
need(typeof b.text === "string", "invalid_text");
|
|
378
|
+
content.push({ type: "text", text: b.text });
|
|
379
|
+
}
|
|
380
|
+
else if (b.type === "tool_use") {
|
|
381
|
+
need(typeof b.id === "string" &&
|
|
382
|
+
typeof b.name === "string" &&
|
|
383
|
+
object(b.input), "invalid_tool_result");
|
|
384
|
+
content.push({
|
|
385
|
+
type: "toolCall",
|
|
386
|
+
id: b.id,
|
|
387
|
+
name: b.name,
|
|
388
|
+
arguments: b.input,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else if (b.type === "thinking") {
|
|
392
|
+
need(typeof b.thinking === "string" && typeof b.signature === "string", "invalid_thinking");
|
|
393
|
+
content.push({
|
|
394
|
+
type: "thinking",
|
|
395
|
+
thinking: b.thinking,
|
|
396
|
+
thinkingSignature: b.signature,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else if (b.type === "redacted_thinking") {
|
|
400
|
+
need(typeof b.data === "string", "invalid_thinking");
|
|
401
|
+
content.push({
|
|
402
|
+
type: "thinking",
|
|
403
|
+
thinking: "",
|
|
404
|
+
thinkingSignature: b.data,
|
|
405
|
+
redacted: true,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
else
|
|
409
|
+
throw new XgateError("unsupported_result_block");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const cap = capabilities[entry.model.id];
|
|
414
|
+
need(cap.response_models.includes(body.model) &&
|
|
415
|
+
body.status === "completed" &&
|
|
416
|
+
!body.error &&
|
|
417
|
+
!body.incomplete_details &&
|
|
418
|
+
Array.isArray(body.output), "invalid_result");
|
|
419
|
+
stopReason = "stop";
|
|
420
|
+
for (const b of body.output) {
|
|
421
|
+
if (b.type === "message") {
|
|
422
|
+
need(b.status === "completed" && Array.isArray(b.content), "invalid_result");
|
|
423
|
+
for (const t of b.content) {
|
|
424
|
+
need(t.type === "output_text" && typeof t.text === "string", "invalid_text");
|
|
425
|
+
content.push({
|
|
426
|
+
type: "text",
|
|
427
|
+
text: t.text,
|
|
428
|
+
...(b.id
|
|
429
|
+
? {
|
|
430
|
+
textSignature: JSON.stringify({
|
|
431
|
+
v: 1,
|
|
432
|
+
id: b.id,
|
|
433
|
+
...(b.phase ? { phase: b.phase } : {}),
|
|
434
|
+
}),
|
|
435
|
+
}
|
|
436
|
+
: {}),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (b.type === "function_call") {
|
|
441
|
+
need(typeof b.call_id === "string" &&
|
|
442
|
+
typeof b.name === "string" &&
|
|
443
|
+
typeof b.arguments === "string", "invalid_tool_result");
|
|
444
|
+
const args = JSON.parse(b.arguments);
|
|
445
|
+
need(object(args), "invalid_tool_result");
|
|
446
|
+
content.push({
|
|
447
|
+
type: "toolCall",
|
|
448
|
+
id: b.call_id,
|
|
449
|
+
name: b.name,
|
|
450
|
+
arguments: args,
|
|
451
|
+
});
|
|
452
|
+
stopReason = "toolUse";
|
|
453
|
+
}
|
|
454
|
+
else if (b.type === "reasoning") {
|
|
455
|
+
need(Array.isArray(b.summary), "invalid_thinking");
|
|
456
|
+
const summary = b.summary
|
|
457
|
+
.map((s) => {
|
|
458
|
+
need(s.type === "summary_text" && typeof s.text === "string", "invalid_thinking");
|
|
459
|
+
return s.text;
|
|
460
|
+
})
|
|
461
|
+
.join("\n");
|
|
462
|
+
need(typeof b.encrypted_content === "string" &&
|
|
463
|
+
b.encrypted_content.length > 0, "missing_encrypted_reasoning");
|
|
464
|
+
content.push({
|
|
465
|
+
type: "thinking",
|
|
466
|
+
thinking: summary,
|
|
467
|
+
thinkingSignature: JSON.stringify({
|
|
468
|
+
type: "reasoning",
|
|
469
|
+
id: b.id,
|
|
470
|
+
summary: b.summary,
|
|
471
|
+
encrypted_content: b.encrypted_content,
|
|
472
|
+
...(b.status === "completed" ? { status: b.status } : {}),
|
|
473
|
+
}),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else
|
|
477
|
+
throw new XgateError("unsupported_result_block");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
need(content.some((c) => (c.type === "text" && c.text.length > 0) || c.type === "toolCall"), "empty_result");
|
|
481
|
+
need((stopReason === "toolUse") === content.some((c) => c.type === "toolCall"), "tool_stop_mismatch");
|
|
482
|
+
const u = body.usage, read = tokens(anthropic
|
|
483
|
+
? (u.cache_read_input_tokens ?? 0)
|
|
484
|
+
: u.input_tokens_details?.cached_tokens), write = tokens(anthropic
|
|
485
|
+
? (u.cache_creation_input_tokens ?? 0)
|
|
486
|
+
: (u.input_tokens_details?.cache_write_tokens ?? 0)), totalInput = tokens(u.input_tokens), output = tokens(u.output_tokens), input = anthropic ? totalInput : totalInput - read - write;
|
|
487
|
+
need(input >= 0 && (!anthropic || (read === 0 && write === 0)), "cache_usage_mismatch");
|
|
488
|
+
const rates = entry.model.cost;
|
|
489
|
+
const cost = {
|
|
490
|
+
input: (input * rates.input) / 1e6,
|
|
491
|
+
output: (output * rates.output) / 1e6,
|
|
492
|
+
cacheRead: (read * rates.cacheRead) / 1e6,
|
|
493
|
+
cacheWrite: (write * rates.cacheWrite) / 1e6,
|
|
494
|
+
total: 0,
|
|
495
|
+
};
|
|
496
|
+
cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
|
|
497
|
+
return {
|
|
498
|
+
content,
|
|
499
|
+
usage: {
|
|
500
|
+
input,
|
|
501
|
+
output,
|
|
502
|
+
cacheRead: read,
|
|
503
|
+
cacheWrite: write,
|
|
504
|
+
totalTokens: input + read + write + output,
|
|
505
|
+
cost,
|
|
506
|
+
},
|
|
507
|
+
stopReason,
|
|
508
|
+
responseId: body.id,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { XgatePayment, XgateTransport } from "./transport.ts";
|
|
3
|
+
export interface XgateExtensionOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Host-managed transport, or a factory called on load and `/xgate refresh`. When set, the
|
|
6
|
+
* extension reads no broker configuration and `/xgate setup` is unavailable. Without it, the
|
|
7
|
+
* standalone `BrokerClient` from `XGATE_CONFIG` or `~/.pi/agent/xgate.json` is used.
|
|
8
|
+
*/
|
|
9
|
+
transport?: XgateTransport | (() => XgateTransport | Promise<XgateTransport>);
|
|
10
|
+
}
|
|
11
|
+
/** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
|
|
12
|
+
export declare function paymentStatus(p: XgatePayment): string;
|
|
13
|
+
/** Pi extension factory. The default export is the standalone wallet-broker extension. */
|
|
14
|
+
export declare function createXgateExtension(options?: XgateExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
|
|
15
|
+
declare const _default: (pi: ExtensionAPI) => Promise<void>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { BrokerClient } from "@lucid-agents/xgate-client/broker";
|
|
2
|
+
import { XgateError } from "@lucid-agents/xgate-client/types";
|
|
3
|
+
import { readFileSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { createXgateProvider } from "./provider.js";
|
|
7
|
+
function privateText(path, max = 8192) {
|
|
8
|
+
if (!isAbsolute(path))
|
|
9
|
+
throw new XgateError("absolute_config_path_required");
|
|
10
|
+
const s = lstatSync(path);
|
|
11
|
+
if (!s.isFile() ||
|
|
12
|
+
s.isSymbolicLink() ||
|
|
13
|
+
(s.mode & 0o077) !== 0 ||
|
|
14
|
+
s.size > max)
|
|
15
|
+
throw new XgateError("private_config_required");
|
|
16
|
+
return readFileSync(path, "utf8").trim();
|
|
17
|
+
}
|
|
18
|
+
function standalone(configPath) {
|
|
19
|
+
const cfg = JSON.parse(privateText(configPath));
|
|
20
|
+
if (typeof cfg.url !== "string" || typeof cfg.tokenFile !== "string")
|
|
21
|
+
throw new XgateError("invalid_config");
|
|
22
|
+
return new BrokerClient(cfg.url, privateText(cfg.tokenFile, 128));
|
|
23
|
+
}
|
|
24
|
+
/** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
|
|
25
|
+
export function paymentStatus(p) {
|
|
26
|
+
const left = p.channel
|
|
27
|
+
? `; channel ${p.channel.refundableAtomic} refundable`
|
|
28
|
+
: "";
|
|
29
|
+
return `xgate paid ${p.chargedAtomic ?? "?"}/${p.authorizedAtomic} ${p.asset} on ${p.network} (${p.scheme})${left}`;
|
|
30
|
+
}
|
|
31
|
+
/** Pi extension factory. The default export is the standalone wallet-broker extension. */
|
|
32
|
+
export function createXgateExtension(options = {}) {
|
|
33
|
+
return (pi) => register(pi, options);
|
|
34
|
+
}
|
|
35
|
+
export default createXgateExtension();
|
|
36
|
+
async function register(pi, options) {
|
|
37
|
+
const injected = options.transport;
|
|
38
|
+
const configPath = process.env.XGATE_CONFIG ?? join(homedir(), ".pi", "agent", "xgate.json");
|
|
39
|
+
let broker;
|
|
40
|
+
let context;
|
|
41
|
+
const load = async () => {
|
|
42
|
+
const next = injected === undefined
|
|
43
|
+
? standalone(configPath)
|
|
44
|
+
: typeof injected === "function"
|
|
45
|
+
? await injected()
|
|
46
|
+
: injected;
|
|
47
|
+
const provider = await createXgateProvider(next, {
|
|
48
|
+
sessionId: () => context?.sessionManager.getSessionId() ?? "standalone",
|
|
49
|
+
onPaid: (r) => context?.ui.setStatus("xgate", r.payment
|
|
50
|
+
? paymentStatus(r.payment)
|
|
51
|
+
: `xgate paid ${(Number(r.settlement.chargedAtomic) / 1e6).toFixed(6)}`),
|
|
52
|
+
onPurchaseFailed: (f) => context?.ui.setStatus("xgate", `xgate ${f.pending ? "unresolved" : "failed"}: ${f.invocationId}`),
|
|
53
|
+
});
|
|
54
|
+
pi.registerProvider(provider);
|
|
55
|
+
broker = next;
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
await load();
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
62
|
+
context = ctx;
|
|
63
|
+
ctx.ui.setStatus("xgate", broker ? "xgate wallet ready" : "xgate: /xgate refresh or /xgate setup");
|
|
64
|
+
});
|
|
65
|
+
pi.on("cache_warming_decision", async (event, ctx) => ctx.model?.provider === "xgate" ? { action: "stop" } : undefined);
|
|
66
|
+
pi.registerCommand("xgate", {
|
|
67
|
+
description: "Set up the wallet broker; show budget or reconcile an existing purchase.",
|
|
68
|
+
handler: async (args, ctx) => {
|
|
69
|
+
context = ctx;
|
|
70
|
+
const [command, id] = args.trim().split(/\s+/);
|
|
71
|
+
try {
|
|
72
|
+
if (command === "setup") {
|
|
73
|
+
if (injected !== undefined)
|
|
74
|
+
throw new XgateError("transport_managed_by_host");
|
|
75
|
+
if (!ctx.hasUI)
|
|
76
|
+
throw new XgateError("configure_XGATE_CONFIG_for_headless_mode");
|
|
77
|
+
const url = await ctx.ui.input("xgate wallet service", "http://127.0.0.1:4021");
|
|
78
|
+
if (!url)
|
|
79
|
+
return;
|
|
80
|
+
const tokenFile = await ctx.ui.input("Absolute path to the broker token file", "/private/path/broker-token");
|
|
81
|
+
if (!tokenFile)
|
|
82
|
+
return;
|
|
83
|
+
const next = new BrokerClient(url, privateText(tokenFile, 128));
|
|
84
|
+
await next.status();
|
|
85
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
86
|
+
writeFileSync(configPath, JSON.stringify({ url, tokenFile }) + "\n", {
|
|
87
|
+
mode: 0o600,
|
|
88
|
+
flag: "wx",
|
|
89
|
+
});
|
|
90
|
+
await load();
|
|
91
|
+
ctx.ui.notify("xgate configured. Select a qualified model with /model.", "info");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (command === "refresh") {
|
|
95
|
+
await load();
|
|
96
|
+
ctx.ui.notify("xgate model list refreshed.", "info");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!broker)
|
|
100
|
+
throw new XgateError("wallet_not_configured");
|
|
101
|
+
if (command === "reconcile") {
|
|
102
|
+
if (!id)
|
|
103
|
+
throw new XgateError("purchase_id_required");
|
|
104
|
+
if (!broker.reconcile)
|
|
105
|
+
throw new XgateError("unsupported_by_transport");
|
|
106
|
+
const result = await broker.reconcile(id);
|
|
107
|
+
ctx.ui.notify(JSON.stringify(result), "info");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (command === "receipts") {
|
|
111
|
+
if (!broker.receipts)
|
|
112
|
+
throw new XgateError("unsupported_by_transport");
|
|
113
|
+
ctx.ui.notify(JSON.stringify(await broker.receipts(), null, 2), "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (command === "status" || command === "budget" || !command) {
|
|
117
|
+
if (!broker.status)
|
|
118
|
+
throw new XgateError("unsupported_by_transport");
|
|
119
|
+
const status = await broker.status();
|
|
120
|
+
ctx.ui.notify(JSON.stringify(status, null, 2), "info");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ctx.ui.notify("Use /xgate setup, refresh, status, budget, receipts or reconcile <invocation ID>. Budgets are configured at the wallet service.", "info");
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
ctx.ui.notify(error instanceof XgateError
|
|
127
|
+
? error.message
|
|
128
|
+
: "xgate setup failed; check the private configuration and wallet service.", "error");
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|