@lucid-agents/pi-xgate 0.1.5 → 0.1.7

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/dist/codec.js ADDED
@@ -0,0 +1,553 @@
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
+ /**
6
+ * The router quotes the class a request's `cache_control` markers select, and serves it only
7
+ * when the scope's qualification covers that class. A wider qualification covers narrower
8
+ * requests, so a cache-qualified scope still serves a request without markers.
9
+ */
10
+ const ANTHROPIC_CACHE = {
11
+ anthropic_no_cache: "none",
12
+ anthropic_ephemeral_5m: "5m",
13
+ anthropic_ephemeral_1h: "1h",
14
+ };
15
+ const need = (x, code) => {
16
+ if (!x)
17
+ throw new XgateError(code);
18
+ };
19
+ const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
20
+ const rate = (x) => {
21
+ need(x &&
22
+ typeof x.numerator === "string" &&
23
+ /^[0-9]{1,13}$/.test(x.numerator) &&
24
+ Number(x.numerator) <= 1e12 &&
25
+ Number.isSafeInteger(x.denominator) &&
26
+ x.denominator > 0 &&
27
+ x.denominator <= 1e6, "invalid_catalog_rate");
28
+ return Number(x.numerator) / x.denominator;
29
+ };
30
+ export function catalogEntries(rows) {
31
+ need(Array.isArray(rows) && rows.length <= 128, "invalid_catalog");
32
+ const entries = [];
33
+ for (const row of rows) {
34
+ const p = row?.execution_profile;
35
+ if (!object(p) ||
36
+ p.model !== row.id ||
37
+ !Array.isArray(row.qualified_profiles))
38
+ continue;
39
+ const anthropic = row.owned_by === "anthropic", cap = anthropic ? p.capabilities : capabilities[row.id];
40
+ if (!cap || (!anthropic && !cap.tools))
41
+ continue;
42
+ const op = anthropic ? "messages" : "responses";
43
+ const q = row.qualified_profiles.find((q) => q.model === row.id &&
44
+ q.operation === op &&
45
+ q.provider === (anthropic ? "anthropic" : "open_ai") &&
46
+ q.normalizer_revision ===
47
+ (anthropic
48
+ ? "anthropic-anonymous-usage-v3"
49
+ : "openai-anonymous-usage-v1") &&
50
+ q.transformation_revision ===
51
+ (anthropic
52
+ ? "anthropic-anonymous-messages-v1"
53
+ : "openai-anonymous-tools-v2"));
54
+ if (!q ||
55
+ row.privacy?.revision !== "anonymous-volatile-v1" ||
56
+ row.privacy.result_replay_available !== false ||
57
+ (anthropic && !Object.hasOwn(ANTHROPIC_CACHE, q.cache_mode)))
58
+ continue;
59
+ if (q.valid_until !== undefined &&
60
+ Number(q.valid_until) * 1000 <= Date.now())
61
+ continue;
62
+ if (row.lifecycle?.stop_before != null &&
63
+ Number(row.lifecycle.stop_before) * 1000 <= Date.now())
64
+ continue;
65
+ const contextWindow = Math.min(p.max_input_tokens, p.provider_limits?.context_window ??
66
+ cap.context_window ??
67
+ p.max_input_tokens), maxTokens = Math.min(8192, p.max_output_tokens);
68
+ need(Number.isSafeInteger(contextWindow) &&
69
+ contextWindow > 0 &&
70
+ Number.isSafeInteger(maxTokens) &&
71
+ maxTokens > 0, "invalid_catalog_limits");
72
+ const efforts = anthropic ? cap.effort : cap.reasoning_efforts;
73
+ const levels = [
74
+ "minimal",
75
+ "low",
76
+ "medium",
77
+ "high",
78
+ "xhigh",
79
+ "max",
80
+ ];
81
+ const thinkingLevelMap = {
82
+ off: anthropic
83
+ ? cap.thinking.disabled
84
+ ? "disabled"
85
+ : null
86
+ : efforts.length === 0 || efforts.includes("none")
87
+ ? "none"
88
+ : null,
89
+ };
90
+ for (const level of levels) {
91
+ const allowed = efforts.includes(level) ||
92
+ (anthropic &&
93
+ cap.thinking.enabled &&
94
+ ["minimal", "low", "medium", "high"].includes(level));
95
+ thinkingLevelMap[level] = allowed ? level : null;
96
+ }
97
+ const write5m = p.rates.input_cache_write_5m ?? p.rates.input_cache_write;
98
+ entries.push({
99
+ catalog: row,
100
+ cache: anthropic ? ANTHROPIC_CACHE[q.cache_mode] : "none",
101
+ cacheWrite1h: p.rates.input_cache_write_1h
102
+ ? rate(p.rates.input_cache_write_1h)
103
+ : 0,
104
+ route: anthropic
105
+ ? "/providers/anthropic/v1/messages"
106
+ : "/providers/openai/v1/responses",
107
+ efforts,
108
+ model: {
109
+ id: row.id,
110
+ name: `${row.id} (xgate)`,
111
+ api: anthropic ? "xgate-messages" : "xgate-responses",
112
+ provider: "xgate",
113
+ baseUrl: row.xgate_origin ?? "https://api.xgate.run",
114
+ input: ["text"],
115
+ contextWindow,
116
+ maxTokens,
117
+ reasoning: anthropic
118
+ ? Boolean(cap.thinking?.adaptive || cap.thinking?.enabled)
119
+ : efforts.some((effort) => effort !== "none"),
120
+ thinkingLevelMap,
121
+ cost: {
122
+ input: rate(p.rates.input_uncached),
123
+ output: rate(p.rates.output),
124
+ cacheRead: rate(p.rates.input_cache_read),
125
+ cacheWrite: write5m ? rate(write5m) : 0,
126
+ },
127
+ inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
128
+ },
129
+ });
130
+ }
131
+ return entries;
132
+ }
133
+ export function transcript(context) {
134
+ const legacy = "systemPrompt" in context || "tools" in context;
135
+ if (typeof pi.collapseSystemMessages !== "function") {
136
+ need(!context.messages.some((m) => m.role === "system"), "unsupported_context");
137
+ const raw = context;
138
+ return {
139
+ system: raw.systemPrompt ?? "",
140
+ tools: raw.tools ?? [],
141
+ messages: raw.messages,
142
+ };
143
+ }
144
+ const collapsed = pi.collapseSystemMessages(legacy
145
+ ? pi.normalizeContext(context)
146
+ : context);
147
+ return {
148
+ system: pi.getCurrentSystemPrompt(collapsed.messages),
149
+ tools: pi.getCurrentTools(collapsed.messages),
150
+ messages: collapsed.messages.filter((m) => m.role !== "system"),
151
+ };
152
+ }
153
+ function text(content) {
154
+ if (typeof content === "string")
155
+ return content;
156
+ need(Array.isArray(content) &&
157
+ content.every((b) => b.type === "text" && typeof b.text === "string"), "text_only_request");
158
+ return content.map((b) => b.text).join("\n");
159
+ }
160
+ function callId(id) {
161
+ return /^[A-Za-z0-9_:.\-]{1,128}$/.test(id)
162
+ ? id
163
+ : "call_" + createHash("sha256").update(id).digest("hex").slice(0, 40);
164
+ }
165
+ function validateTools(tools, anthropic) {
166
+ need(tools.length <= (anthropic ? 32 : 8), "too_many_tools");
167
+ need(tools.every((t) => /^[A-Za-z0-9_-]{1,64}$/.test(t.name) &&
168
+ object(t.parameters) &&
169
+ t.parameters.type === "object"), "unsupported_tool_schema");
170
+ need(new Set(tools.map((t) => t.name)).size === tools.length, "duplicate_tools");
171
+ }
172
+ export function encode(entry, context, options = {}) {
173
+ need(options.toolChoice === undefined || options.toolChoice === "auto", "unsupported_tool_choice");
174
+ need(options.reasoning === undefined || typeof options.reasoning === "string", "unsupported_reasoning");
175
+ need(options.temperature === undefined &&
176
+ (!options.samplingParams ||
177
+ Object.keys(options.samplingParams).length === 0), "unsupported_sampling");
178
+ const anthropic = entry.route.endsWith("/messages"), { system, tools, messages } = transcript(context);
179
+ validateTools(tools, anthropic);
180
+ const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
181
+ need(Number.isSafeInteger(max) && max > 0 && max <= entry.model.maxTokens, "output_limit");
182
+ const requested = options.reasoning, effort = requested && entry.efforts.includes(requested)
183
+ ? requested
184
+ : entry.efforts[0];
185
+ if (requested)
186
+ need(entry.model.thinkingLevelMap?.[requested] !== null &&
187
+ (entry.efforts.includes(requested) ||
188
+ (anthropic &&
189
+ entry.catalog.execution_profile.capabilities.thinking.enabled)), "unsupported_reasoning");
190
+ const history = [];
191
+ const append = (role, content) => {
192
+ const previous = history.at(-1);
193
+ if (previous?.role === role)
194
+ previous.content.push(...content);
195
+ else
196
+ history.push({ role, content });
197
+ };
198
+ if (!anthropic && system)
199
+ history.push({ role: "system", content: system });
200
+ for (const message of messages) {
201
+ if (message.role === "user") {
202
+ const value = text(message.content);
203
+ if (anthropic)
204
+ append("user", [{ type: "text", text: value }]);
205
+ else
206
+ history.push({ role: "user", content: value });
207
+ continue;
208
+ }
209
+ if (message.role === "toolResult") {
210
+ const value = text(message.content), id = callId(message.toolCallId);
211
+ if (anthropic)
212
+ append("user", [
213
+ {
214
+ type: "tool_result",
215
+ tool_use_id: id,
216
+ content: value,
217
+ ...(message.isError ? { is_error: true } : {}),
218
+ },
219
+ ]);
220
+ else
221
+ history.push({
222
+ type: "function_call_output",
223
+ call_id: id,
224
+ output: message.isError ? "Tool error: " + value : value,
225
+ });
226
+ continue;
227
+ }
228
+ need(message.role === "assistant", "unsupported_message");
229
+ if (message.stopReason === "error" || message.stopReason === "aborted")
230
+ continue;
231
+ const same = message.provider === "xgate" &&
232
+ message.model === entry.model.id &&
233
+ message.api === entry.model.api;
234
+ const blocks = [];
235
+ for (const block of message.content) {
236
+ if (block.type === "text") {
237
+ if (anthropic) {
238
+ if (block.text)
239
+ blocks.push({ type: "text", text: block.text });
240
+ }
241
+ else if (block.text) {
242
+ let signature;
243
+ try {
244
+ signature = block.textSignature
245
+ ? JSON.parse(block.textSignature)
246
+ : undefined;
247
+ }
248
+ catch { }
249
+ const previous = history.at(-1);
250
+ if (same &&
251
+ signature?.id &&
252
+ previous?.type === "message" &&
253
+ previous.id === signature.id &&
254
+ previous.phase === signature.phase) {
255
+ previous.content.push({ type: "output_text", text: block.text });
256
+ continue;
257
+ }
258
+ history.push({
259
+ type: "message",
260
+ role: "assistant",
261
+ content: [{ type: "output_text", text: block.text }],
262
+ ...(same && signature?.id
263
+ ? {
264
+ id: signature.id,
265
+ ...(signature.phase ? { phase: signature.phase } : {}),
266
+ }
267
+ : {}),
268
+ });
269
+ }
270
+ }
271
+ else if (block.type === "toolCall") {
272
+ need(!block.namespace && object(block.arguments), "unsupported_tool_call");
273
+ const id = callId(block.id);
274
+ if (anthropic) {
275
+ need(tools.some((t) => t.name === block.name), "history_tool_not_declared");
276
+ blocks.push({
277
+ type: "tool_use",
278
+ id,
279
+ name: block.name,
280
+ input: block.arguments,
281
+ });
282
+ }
283
+ else
284
+ history.push({
285
+ type: "function_call",
286
+ call_id: id,
287
+ name: block.name,
288
+ arguments: JSON.stringify(block.arguments),
289
+ });
290
+ }
291
+ else if (block.type === "thinking" && same) {
292
+ if (anthropic) {
293
+ need(typeof block.thinkingSignature === "string" &&
294
+ block.thinkingSignature.length > 0, "missing_thinking_signature");
295
+ blocks.push(block.redacted
296
+ ? { type: "redacted_thinking", data: block.thinkingSignature }
297
+ : {
298
+ type: "thinking",
299
+ thinking: block.thinking,
300
+ signature: block.thinkingSignature,
301
+ });
302
+ }
303
+ else if (block.thinkingSignature) {
304
+ const reasoning = JSON.parse(block.thinkingSignature);
305
+ need(reasoning.type === "reasoning" &&
306
+ typeof reasoning.encrypted_content === "string", "invalid_reasoning_history");
307
+ history.push(reasoning);
308
+ }
309
+ }
310
+ }
311
+ if (anthropic && blocks.length)
312
+ append("assistant", blocks);
313
+ }
314
+ need(history.length > 0 && history.length <= 256, "history_limit");
315
+ let request;
316
+ if (anthropic) {
317
+ const cap = entry.catalog.execution_profile.capabilities, cache = cacheControl(entry.cache, options.cacheRetention);
318
+ request = {
319
+ model: entry.model.id,
320
+ messages: history,
321
+ max_tokens: max,
322
+ stream: false,
323
+ ...(system
324
+ ? {
325
+ system: cache
326
+ ? [{ type: "text", text: system, cache_control: cache }]
327
+ : system,
328
+ }
329
+ : {}),
330
+ ...(tools.length
331
+ ? {
332
+ tools: tools.map((t, i) => ({
333
+ name: t.name,
334
+ description: t.description,
335
+ input_schema: t.parameters,
336
+ ...(cache && i === tools.length - 1
337
+ ? { cache_control: cache }
338
+ : {}),
339
+ })),
340
+ }
341
+ : {}),
342
+ };
343
+ // Anthropic caches nothing without a breakpoint. Like Pi's direct Anthropic provider, mark
344
+ // the tools, the system prompt and the newest turn: the next request in the conversation
345
+ // reads the whole previous prefix. A final user turn ends in text or a tool result.
346
+ const last = history.at(-1);
347
+ if (cache && last.role === "user")
348
+ last.content.at(-1).cache_control = cache;
349
+ if (effort)
350
+ request.output_config = { effort };
351
+ if (cap.thinking.adaptive)
352
+ request.thinking = { type: "adaptive" };
353
+ else if (requested && cap.thinking.enabled) {
354
+ need(max > 1024, "thinking_output_limit");
355
+ request.thinking = {
356
+ type: "enabled",
357
+ budget_tokens: Math.min(2048, max - 1),
358
+ };
359
+ }
360
+ }
361
+ else
362
+ request = {
363
+ model: entry.model.id,
364
+ input: history,
365
+ max_output_tokens: max,
366
+ stream: false,
367
+ store: false,
368
+ include: ["reasoning.encrypted_content"],
369
+ ...(effort ? { reasoning: { effort } } : {}),
370
+ ...(tools.length
371
+ ? {
372
+ tools: tools.map((t) => ({
373
+ type: "function",
374
+ name: t.name,
375
+ description: t.description,
376
+ parameters: t.parameters,
377
+ })),
378
+ parallel_tool_calls: false,
379
+ }
380
+ : {}),
381
+ };
382
+ if (request.tools)
383
+ need(Buffer.byteLength(JSON.stringify(request.tools)) <=
384
+ (anthropic ? 65536 : 8192), "tool_schema_bound");
385
+ need(Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024, "request_bound");
386
+ return request;
387
+ }
388
+ function cacheControl(qualified, retention) {
389
+ if (qualified === "none" || retention === "none")
390
+ return undefined;
391
+ return retention === "long" && qualified === "1h"
392
+ ? { type: "ephemeral", ttl: "1h" }
393
+ : { type: "ephemeral" };
394
+ }
395
+ function tokens(value) {
396
+ need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
397
+ return value;
398
+ }
399
+ export function decode(entry, body) {
400
+ need(object(body) && typeof body.id === "string" && object(body.usage), "invalid_result");
401
+ const anthropic = entry.route.endsWith("/messages"), content = [];
402
+ let stopReason;
403
+ if (anthropic) {
404
+ need(body.type === "message" &&
405
+ body.role === "assistant" &&
406
+ body.model === entry.model.id &&
407
+ ["end_turn", "tool_use", "stop_sequence"].includes(body.stop_reason) &&
408
+ Array.isArray(body.content), "invalid_result");
409
+ stopReason = body.stop_reason === "tool_use" ? "toolUse" : "stop";
410
+ for (const b of body.content) {
411
+ if (b.type === "text") {
412
+ need(typeof b.text === "string", "invalid_text");
413
+ content.push({ type: "text", text: b.text });
414
+ }
415
+ else if (b.type === "tool_use") {
416
+ need(typeof b.id === "string" &&
417
+ typeof b.name === "string" &&
418
+ object(b.input), "invalid_tool_result");
419
+ content.push({
420
+ type: "toolCall",
421
+ id: b.id,
422
+ name: b.name,
423
+ arguments: b.input,
424
+ });
425
+ }
426
+ else if (b.type === "thinking") {
427
+ need(typeof b.thinking === "string" && typeof b.signature === "string", "invalid_thinking");
428
+ content.push({
429
+ type: "thinking",
430
+ thinking: b.thinking,
431
+ thinkingSignature: b.signature,
432
+ });
433
+ }
434
+ else if (b.type === "redacted_thinking") {
435
+ need(typeof b.data === "string", "invalid_thinking");
436
+ content.push({
437
+ type: "thinking",
438
+ thinking: "",
439
+ thinkingSignature: b.data,
440
+ redacted: true,
441
+ });
442
+ }
443
+ else
444
+ throw new XgateError("unsupported_result_block");
445
+ }
446
+ }
447
+ else {
448
+ const cap = capabilities[entry.model.id];
449
+ need(cap.response_models.includes(body.model) &&
450
+ body.status === "completed" &&
451
+ !body.error &&
452
+ !body.incomplete_details &&
453
+ Array.isArray(body.output), "invalid_result");
454
+ stopReason = "stop";
455
+ for (const b of body.output) {
456
+ if (b.type === "message") {
457
+ need(b.status === "completed" && Array.isArray(b.content), "invalid_result");
458
+ for (const t of b.content) {
459
+ need(t.type === "output_text" && typeof t.text === "string", "invalid_text");
460
+ content.push({
461
+ type: "text",
462
+ text: t.text,
463
+ ...(b.id
464
+ ? {
465
+ textSignature: JSON.stringify({
466
+ v: 1,
467
+ id: b.id,
468
+ ...(b.phase ? { phase: b.phase } : {}),
469
+ }),
470
+ }
471
+ : {}),
472
+ });
473
+ }
474
+ }
475
+ else if (b.type === "function_call") {
476
+ need(typeof b.call_id === "string" &&
477
+ typeof b.name === "string" &&
478
+ typeof b.arguments === "string", "invalid_tool_result");
479
+ const args = JSON.parse(b.arguments);
480
+ need(object(args), "invalid_tool_result");
481
+ content.push({
482
+ type: "toolCall",
483
+ id: b.call_id,
484
+ name: b.name,
485
+ arguments: args,
486
+ });
487
+ stopReason = "toolUse";
488
+ }
489
+ else if (b.type === "reasoning") {
490
+ need(Array.isArray(b.summary), "invalid_thinking");
491
+ const summary = b.summary
492
+ .map((s) => {
493
+ need(s.type === "summary_text" && typeof s.text === "string", "invalid_thinking");
494
+ return s.text;
495
+ })
496
+ .join("\n");
497
+ need(typeof b.encrypted_content === "string" &&
498
+ b.encrypted_content.length > 0, "missing_encrypted_reasoning");
499
+ content.push({
500
+ type: "thinking",
501
+ thinking: summary,
502
+ thinkingSignature: JSON.stringify({
503
+ type: "reasoning",
504
+ id: b.id,
505
+ summary: b.summary,
506
+ encrypted_content: b.encrypted_content,
507
+ ...(b.status === "completed" ? { status: b.status } : {}),
508
+ }),
509
+ });
510
+ }
511
+ else
512
+ throw new XgateError("unsupported_result_block");
513
+ }
514
+ }
515
+ need(content.some((c) => (c.type === "text" && c.text.length > 0) || c.type === "toolCall"), "empty_result");
516
+ need((stopReason === "toolUse") === content.some((c) => c.type === "toolCall"), "tool_stop_mismatch");
517
+ const u = body.usage, read = tokens(anthropic
518
+ ? (u.cache_read_input_tokens ?? 0)
519
+ : u.input_tokens_details?.cached_tokens), write = tokens(anthropic
520
+ ? (u.cache_creation_input_tokens ?? 0)
521
+ : (u.input_tokens_details?.cache_write_tokens ?? 0)), totalInput = tokens(u.input_tokens), output = tokens(u.output_tokens), input = anthropic ? totalInput : totalInput - read - write,
522
+ // Anthropic splits writes by lifetime; an unsplit write is the default five minutes.
523
+ write1h = anthropic && object(u.cache_creation)
524
+ ? tokens(u.cache_creation.ephemeral_1h_input_tokens ?? 0)
525
+ : 0;
526
+ need(input >= 0 &&
527
+ write1h <= write &&
528
+ (!anthropic || entry.cache !== "none" || (read === 0 && write === 0)) &&
529
+ (!anthropic || entry.cache === "1h" || write1h === 0), "cache_usage_mismatch");
530
+ const rates = entry.model.cost;
531
+ const cost = {
532
+ input: (input * rates.input) / 1e6,
533
+ output: (output * rates.output) / 1e6,
534
+ cacheRead: (read * rates.cacheRead) / 1e6,
535
+ cacheWrite: ((write - write1h) * rates.cacheWrite + write1h * entry.cacheWrite1h) /
536
+ 1e6,
537
+ total: 0,
538
+ };
539
+ cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
540
+ return {
541
+ content,
542
+ usage: {
543
+ input,
544
+ output,
545
+ cacheRead: read,
546
+ cacheWrite: write,
547
+ totalTokens: input + read + write + output,
548
+ cost,
549
+ },
550
+ stopReason,
551
+ responseId: body.id,
552
+ };
553
+ }
@@ -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;