@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/src/codec.ts ADDED
@@ -0,0 +1,658 @@
1
+ import * as pi from "@earendil-works/pi-ai";
2
+ import type {
3
+ AssistantMessage,
4
+ Context,
5
+ Message,
6
+ Model,
7
+ SimpleStreamOptions,
8
+ Tool,
9
+ TranscriptContext,
10
+ Usage,
11
+ ThinkingLevelMap,
12
+ } from "@earendil-works/pi-ai";
13
+ import { XgateError, type Route } from "@lucid-agents/xgate-client/types";
14
+ import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
15
+ import { createHash } from "node:crypto";
16
+ export type Entry = {
17
+ model: Model<any>;
18
+ catalog: any;
19
+ route: Route;
20
+ efforts: string[];
21
+ };
22
+ const need = (x: unknown, code: string): void => {
23
+ if (!x) throw new XgateError(code);
24
+ };
25
+ const object = (x: any): boolean =>
26
+ x !== null && typeof x === "object" && !Array.isArray(x);
27
+ const rate = (x: any): number => {
28
+ need(
29
+ x &&
30
+ typeof x.numerator === "string" &&
31
+ /^[0-9]{1,13}$/.test(x.numerator) &&
32
+ Number(x.numerator) <= 1e12 &&
33
+ Number.isSafeInteger(x.denominator) &&
34
+ x.denominator > 0 &&
35
+ x.denominator <= 1e6,
36
+ "invalid_catalog_rate",
37
+ );
38
+ return Number(x.numerator) / x.denominator;
39
+ };
40
+ export function catalogEntries(rows: any[]): Entry[] {
41
+ need(Array.isArray(rows) && rows.length <= 128, "invalid_catalog");
42
+ const entries: Entry[] = [];
43
+ for (const row of rows) {
44
+ const p = row?.execution_profile;
45
+ if (
46
+ !object(p) ||
47
+ p.model !== row.id ||
48
+ !Array.isArray(row.qualified_profiles)
49
+ )
50
+ continue;
51
+ const anthropic = row.owned_by === "anthropic",
52
+ cap = anthropic ? p.capabilities : (capabilities as any)[row.id];
53
+ if (!cap || (!anthropic && !cap.tools)) continue;
54
+ const op = anthropic ? "messages" : "responses";
55
+ const q = row.qualified_profiles.find(
56
+ (q: any) =>
57
+ q.model === row.id &&
58
+ q.operation === op &&
59
+ q.provider === (anthropic ? "anthropic" : "open_ai") &&
60
+ q.normalizer_revision ===
61
+ (anthropic
62
+ ? "anthropic-anonymous-usage-v3"
63
+ : "openai-anonymous-usage-v1") &&
64
+ q.transformation_revision ===
65
+ (anthropic
66
+ ? "anthropic-anonymous-messages-v1"
67
+ : "openai-anonymous-tools-v2"),
68
+ );
69
+ if (
70
+ !q ||
71
+ row.privacy?.revision !== "anonymous-volatile-v1" ||
72
+ row.privacy.result_replay_available !== false ||
73
+ (anthropic && q.cache_mode !== "anthropic_no_cache")
74
+ )
75
+ continue;
76
+ if (
77
+ q.valid_until !== undefined &&
78
+ Number(q.valid_until) * 1000 <= Date.now()
79
+ )
80
+ continue;
81
+ if (
82
+ row.lifecycle?.stop_before != null &&
83
+ Number(row.lifecycle.stop_before) * 1000 <= Date.now()
84
+ )
85
+ continue;
86
+ const contextWindow = Math.min(
87
+ p.max_input_tokens,
88
+ p.provider_limits?.context_window ??
89
+ cap.context_window ??
90
+ p.max_input_tokens,
91
+ ),
92
+ maxTokens = Math.min(8192, p.max_output_tokens);
93
+ need(
94
+ Number.isSafeInteger(contextWindow) &&
95
+ contextWindow > 0 &&
96
+ Number.isSafeInteger(maxTokens) &&
97
+ maxTokens > 0,
98
+ "invalid_catalog_limits",
99
+ );
100
+ const efforts: string[] = anthropic ? cap.effort : cap.reasoning_efforts;
101
+ const levels = [
102
+ "minimal",
103
+ "low",
104
+ "medium",
105
+ "high",
106
+ "xhigh",
107
+ "max",
108
+ ] as const;
109
+ const thinkingLevelMap: ThinkingLevelMap = {
110
+ off: anthropic
111
+ ? cap.thinking.disabled
112
+ ? "disabled"
113
+ : null
114
+ : efforts.length === 0 || efforts.includes("none")
115
+ ? "none"
116
+ : null,
117
+ };
118
+ for (const level of levels) {
119
+ const allowed =
120
+ efforts.includes(level) ||
121
+ (anthropic &&
122
+ cap.thinking.enabled &&
123
+ ["minimal", "low", "medium", "high"].includes(level));
124
+ thinkingLevelMap[level] = allowed ? level : null;
125
+ }
126
+ entries.push({
127
+ catalog: row,
128
+ route: anthropic
129
+ ? "/providers/anthropic/v1/messages"
130
+ : "/providers/openai/v1/responses",
131
+ efforts,
132
+ model: {
133
+ id: row.id,
134
+ name: `${row.id} (xgate)`,
135
+ api: anthropic ? "xgate-messages" : "xgate-responses",
136
+ provider: "xgate",
137
+ baseUrl: row.xgate_origin ?? "https://api.xgate.run",
138
+ input: ["text"],
139
+ contextWindow,
140
+ maxTokens,
141
+ reasoning: anthropic
142
+ ? Boolean(cap.thinking?.adaptive || cap.thinking?.enabled)
143
+ : efforts.some((effort) => effort !== "none"),
144
+ thinkingLevelMap,
145
+ cost: {
146
+ input: rate(p.rates.input_uncached),
147
+ output: rate(p.rates.output),
148
+ cacheRead: rate(p.rates.input_cache_read),
149
+ cacheWrite: p.rates.input_cache_write
150
+ ? rate(p.rates.input_cache_write)
151
+ : 0,
152
+ },
153
+ inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
154
+ },
155
+ });
156
+ }
157
+ return entries;
158
+ }
159
+ /**
160
+ * Pi 1.1 passes a normalized transcript; Pi 0.85 passes `systemPrompt` and `tools` beside the
161
+ * messages. Structural, so published types do not name a type only one Pi version has.
162
+ */
163
+ export type PiContext = {
164
+ messages: Message[];
165
+ systemPrompt?: string;
166
+ tools?: Tool[];
167
+ };
168
+ type Turn = Exclude<Message, { role: "system" }>;
169
+ export function transcript(context: PiContext): {
170
+ system: string;
171
+ tools: Tool[];
172
+ messages: Turn[];
173
+ } {
174
+ const legacy = "systemPrompt" in context || "tools" in context;
175
+ if (typeof pi.collapseSystemMessages !== "function") {
176
+ need(
177
+ !context.messages.some((m) => m.role === "system"),
178
+ "unsupported_context",
179
+ );
180
+ const raw = context as Context;
181
+ return {
182
+ system: raw.systemPrompt ?? "",
183
+ tools: raw.tools ?? [],
184
+ messages: raw.messages as Turn[],
185
+ };
186
+ }
187
+ const collapsed = pi.collapseSystemMessages(
188
+ legacy
189
+ ? pi.normalizeContext(context as Context)
190
+ : (context as TranscriptContext),
191
+ );
192
+ return {
193
+ system: pi.getCurrentSystemPrompt(collapsed.messages),
194
+ tools: pi.getCurrentTools(collapsed.messages),
195
+ messages: collapsed.messages.filter((m): m is Turn => m.role !== "system"),
196
+ };
197
+ }
198
+ function text(content: any): string {
199
+ if (typeof content === "string") return content;
200
+ need(
201
+ Array.isArray(content) &&
202
+ content.every((b) => b.type === "text" && typeof b.text === "string"),
203
+ "text_only_request",
204
+ );
205
+ return content.map((b: any) => b.text).join("\n");
206
+ }
207
+ function callId(id: string): string {
208
+ return /^[A-Za-z0-9_:.\-]{1,128}$/.test(id)
209
+ ? id
210
+ : "call_" + createHash("sha256").update(id).digest("hex").slice(0, 40);
211
+ }
212
+ function validateTools(tools: any[], anthropic: boolean): void {
213
+ need(tools.length <= (anthropic ? 32 : 8), "too_many_tools");
214
+ need(
215
+ tools.every(
216
+ (t) =>
217
+ /^[A-Za-z0-9_-]{1,64}$/.test(t.name) &&
218
+ object(t.parameters) &&
219
+ t.parameters.type === "object",
220
+ ),
221
+ "unsupported_tool_schema",
222
+ );
223
+ need(
224
+ new Set(tools.map((t) => t.name)).size === tools.length,
225
+ "duplicate_tools",
226
+ );
227
+ }
228
+ export function encode(
229
+ entry: Entry,
230
+ context: PiContext,
231
+ options: SimpleStreamOptions = {},
232
+ ): any {
233
+ need(
234
+ options.toolChoice === undefined || options.toolChoice === "auto",
235
+ "unsupported_tool_choice",
236
+ );
237
+ need(
238
+ options.reasoning === undefined || typeof options.reasoning === "string",
239
+ "unsupported_reasoning",
240
+ );
241
+ need(
242
+ options.temperature === undefined &&
243
+ (!options.samplingParams ||
244
+ Object.keys(options.samplingParams).length === 0),
245
+ "unsupported_sampling",
246
+ );
247
+ const anthropic = entry.route.endsWith("/messages"),
248
+ { system, tools, messages } = transcript(context);
249
+ validateTools(tools, anthropic);
250
+ const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
251
+ need(
252
+ Number.isSafeInteger(max) && max > 0 && max <= entry.model.maxTokens,
253
+ "output_limit",
254
+ );
255
+ const requested = options.reasoning,
256
+ effort =
257
+ requested && entry.efforts.includes(requested)
258
+ ? requested
259
+ : entry.efforts[0];
260
+ if (requested)
261
+ need(
262
+ entry.model.thinkingLevelMap?.[requested] !== null &&
263
+ (entry.efforts.includes(requested) ||
264
+ (anthropic &&
265
+ entry.catalog.execution_profile.capabilities.thinking.enabled)),
266
+ "unsupported_reasoning",
267
+ );
268
+ const history: any[] = [];
269
+ const append = (role: string, content: any[]) => {
270
+ const previous = history.at(-1);
271
+ if (previous?.role === role) previous.content.push(...content);
272
+ else history.push({ role, content });
273
+ };
274
+ if (!anthropic && system) history.push({ role: "system", content: system });
275
+ for (const message of messages) {
276
+ if (message.role === "user") {
277
+ const value = text(message.content);
278
+ if (anthropic) append("user", [{ type: "text", text: value }]);
279
+ else history.push({ role: "user", content: value });
280
+ continue;
281
+ }
282
+ if (message.role === "toolResult") {
283
+ const value = text(message.content),
284
+ id = callId(message.toolCallId);
285
+ if (anthropic)
286
+ append("user", [
287
+ {
288
+ type: "tool_result",
289
+ tool_use_id: id,
290
+ content: value,
291
+ ...(message.isError ? { is_error: true } : {}),
292
+ },
293
+ ]);
294
+ else
295
+ history.push({
296
+ type: "function_call_output",
297
+ call_id: id,
298
+ output: message.isError ? "Tool error: " + value : value,
299
+ });
300
+ continue;
301
+ }
302
+ need(message.role === "assistant", "unsupported_message");
303
+ if (message.stopReason === "error" || message.stopReason === "aborted")
304
+ continue;
305
+ const same =
306
+ message.provider === "xgate" &&
307
+ message.model === entry.model.id &&
308
+ message.api === entry.model.api;
309
+ const blocks: any[] = [];
310
+ for (const block of message.content) {
311
+ if (block.type === "text") {
312
+ if (anthropic) {
313
+ if (block.text) blocks.push({ type: "text", text: block.text });
314
+ } else if (block.text) {
315
+ let signature: any;
316
+ try {
317
+ signature = block.textSignature
318
+ ? JSON.parse(block.textSignature)
319
+ : undefined;
320
+ } catch {}
321
+ const previous = history.at(-1);
322
+ if (
323
+ same &&
324
+ signature?.id &&
325
+ previous?.type === "message" &&
326
+ previous.id === signature.id &&
327
+ previous.phase === signature.phase
328
+ ) {
329
+ previous.content.push({ type: "output_text", text: block.text });
330
+ continue;
331
+ }
332
+ history.push({
333
+ type: "message",
334
+ role: "assistant",
335
+ content: [{ type: "output_text", text: block.text }],
336
+ ...(same && signature?.id
337
+ ? {
338
+ id: signature.id,
339
+ ...(signature.phase ? { phase: signature.phase } : {}),
340
+ }
341
+ : {}),
342
+ });
343
+ }
344
+ } else if (block.type === "toolCall") {
345
+ need(
346
+ !block.namespace && object(block.arguments),
347
+ "unsupported_tool_call",
348
+ );
349
+ const id = callId(block.id);
350
+ if (anthropic) {
351
+ need(
352
+ tools.some((t) => t.name === block.name),
353
+ "history_tool_not_declared",
354
+ );
355
+ blocks.push({
356
+ type: "tool_use",
357
+ id,
358
+ name: block.name,
359
+ input: block.arguments,
360
+ });
361
+ } else
362
+ history.push({
363
+ type: "function_call",
364
+ call_id: id,
365
+ name: block.name,
366
+ arguments: JSON.stringify(block.arguments),
367
+ });
368
+ } else if (block.type === "thinking" && same) {
369
+ if (anthropic) {
370
+ need(
371
+ typeof block.thinkingSignature === "string" &&
372
+ block.thinkingSignature.length > 0,
373
+ "missing_thinking_signature",
374
+ );
375
+ blocks.push(
376
+ block.redacted
377
+ ? { type: "redacted_thinking", data: block.thinkingSignature }
378
+ : {
379
+ type: "thinking",
380
+ thinking: block.thinking,
381
+ signature: block.thinkingSignature,
382
+ },
383
+ );
384
+ } else if (block.thinkingSignature) {
385
+ const reasoning = JSON.parse(block.thinkingSignature);
386
+ need(
387
+ reasoning.type === "reasoning" &&
388
+ typeof reasoning.encrypted_content === "string",
389
+ "invalid_reasoning_history",
390
+ );
391
+ history.push(reasoning);
392
+ }
393
+ }
394
+ }
395
+ if (anthropic && blocks.length) append("assistant", blocks);
396
+ }
397
+ need(history.length > 0 && history.length <= 256, "history_limit");
398
+ let request: any;
399
+ if (anthropic) {
400
+ const cap = entry.catalog.execution_profile.capabilities;
401
+ request = {
402
+ model: entry.model.id,
403
+ messages: history,
404
+ max_tokens: max,
405
+ stream: false,
406
+ ...(system ? { system } : {}),
407
+ ...(tools.length
408
+ ? {
409
+ tools: tools.map((t) => ({
410
+ name: t.name,
411
+ description: t.description,
412
+ input_schema: t.parameters,
413
+ })),
414
+ }
415
+ : {}),
416
+ };
417
+ if (effort) request.output_config = { effort };
418
+ if (cap.thinking.adaptive) request.thinking = { type: "adaptive" };
419
+ else if (requested && cap.thinking.enabled) {
420
+ need(max > 1024, "thinking_output_limit");
421
+ request.thinking = {
422
+ type: "enabled",
423
+ budget_tokens: Math.min(2048, max - 1),
424
+ };
425
+ }
426
+ } else
427
+ request = {
428
+ model: entry.model.id,
429
+ input: history,
430
+ max_output_tokens: max,
431
+ stream: false,
432
+ store: false,
433
+ include: ["reasoning.encrypted_content"],
434
+ ...(effort ? { reasoning: { effort } } : {}),
435
+ ...(tools.length
436
+ ? {
437
+ tools: tools.map((t) => ({
438
+ type: "function",
439
+ name: t.name,
440
+ description: t.description,
441
+ parameters: t.parameters,
442
+ })),
443
+ parallel_tool_calls: false,
444
+ }
445
+ : {}),
446
+ };
447
+ if (request.tools)
448
+ need(
449
+ Buffer.byteLength(JSON.stringify(request.tools)) <=
450
+ (anthropic ? 65536 : 8192),
451
+ "tool_schema_bound",
452
+ );
453
+ need(
454
+ Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024,
455
+ "request_bound",
456
+ );
457
+ return request;
458
+ }
459
+ function tokens(value: any): number {
460
+ need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
461
+ return value;
462
+ }
463
+ export function decode(
464
+ entry: Entry,
465
+ body: any,
466
+ ): {
467
+ content: AssistantMessage["content"];
468
+ usage: Usage;
469
+ stopReason: "stop" | "toolUse";
470
+ responseId: string;
471
+ } {
472
+ need(
473
+ object(body) && typeof body.id === "string" && object(body.usage),
474
+ "invalid_result",
475
+ );
476
+ const anthropic = entry.route.endsWith("/messages"),
477
+ content: AssistantMessage["content"] = [];
478
+ let stopReason: "stop" | "toolUse";
479
+ if (anthropic) {
480
+ need(
481
+ body.type === "message" &&
482
+ body.role === "assistant" &&
483
+ body.model === entry.model.id &&
484
+ ["end_turn", "tool_use", "stop_sequence"].includes(body.stop_reason) &&
485
+ Array.isArray(body.content),
486
+ "invalid_result",
487
+ );
488
+ stopReason = body.stop_reason === "tool_use" ? "toolUse" : "stop";
489
+ for (const b of body.content) {
490
+ if (b.type === "text") {
491
+ need(typeof b.text === "string", "invalid_text");
492
+ content.push({ type: "text", text: b.text });
493
+ } else if (b.type === "tool_use") {
494
+ need(
495
+ typeof b.id === "string" &&
496
+ typeof b.name === "string" &&
497
+ object(b.input),
498
+ "invalid_tool_result",
499
+ );
500
+ content.push({
501
+ type: "toolCall",
502
+ id: b.id,
503
+ name: b.name,
504
+ arguments: b.input,
505
+ });
506
+ } else if (b.type === "thinking") {
507
+ need(
508
+ typeof b.thinking === "string" && typeof b.signature === "string",
509
+ "invalid_thinking",
510
+ );
511
+ content.push({
512
+ type: "thinking",
513
+ thinking: b.thinking,
514
+ thinkingSignature: b.signature,
515
+ });
516
+ } else if (b.type === "redacted_thinking") {
517
+ need(typeof b.data === "string", "invalid_thinking");
518
+ content.push({
519
+ type: "thinking",
520
+ thinking: "",
521
+ thinkingSignature: b.data,
522
+ redacted: true,
523
+ });
524
+ } else throw new XgateError("unsupported_result_block");
525
+ }
526
+ } else {
527
+ const cap = (capabilities as any)[entry.model.id];
528
+ need(
529
+ cap.response_models.includes(body.model) &&
530
+ body.status === "completed" &&
531
+ !body.error &&
532
+ !body.incomplete_details &&
533
+ Array.isArray(body.output),
534
+ "invalid_result",
535
+ );
536
+ stopReason = "stop";
537
+ for (const b of body.output) {
538
+ if (b.type === "message") {
539
+ need(
540
+ b.status === "completed" && Array.isArray(b.content),
541
+ "invalid_result",
542
+ );
543
+ for (const t of b.content) {
544
+ need(
545
+ t.type === "output_text" && typeof t.text === "string",
546
+ "invalid_text",
547
+ );
548
+ content.push({
549
+ type: "text",
550
+ text: t.text,
551
+ ...(b.id
552
+ ? {
553
+ textSignature: JSON.stringify({
554
+ v: 1,
555
+ id: b.id,
556
+ ...(b.phase ? { phase: b.phase } : {}),
557
+ }),
558
+ }
559
+ : {}),
560
+ });
561
+ }
562
+ } else if (b.type === "function_call") {
563
+ need(
564
+ typeof b.call_id === "string" &&
565
+ typeof b.name === "string" &&
566
+ typeof b.arguments === "string",
567
+ "invalid_tool_result",
568
+ );
569
+ const args = JSON.parse(b.arguments);
570
+ need(object(args), "invalid_tool_result");
571
+ content.push({
572
+ type: "toolCall",
573
+ id: b.call_id,
574
+ name: b.name,
575
+ arguments: args,
576
+ });
577
+ stopReason = "toolUse";
578
+ } else if (b.type === "reasoning") {
579
+ need(Array.isArray(b.summary), "invalid_thinking");
580
+ const summary = b.summary
581
+ .map((s: any) => {
582
+ need(
583
+ s.type === "summary_text" && typeof s.text === "string",
584
+ "invalid_thinking",
585
+ );
586
+ return s.text;
587
+ })
588
+ .join("\n");
589
+ need(
590
+ typeof b.encrypted_content === "string" &&
591
+ b.encrypted_content.length > 0,
592
+ "missing_encrypted_reasoning",
593
+ );
594
+ content.push({
595
+ type: "thinking",
596
+ thinking: summary,
597
+ thinkingSignature: JSON.stringify({
598
+ type: "reasoning",
599
+ id: b.id,
600
+ summary: b.summary,
601
+ encrypted_content: b.encrypted_content,
602
+ ...(b.status === "completed" ? { status: b.status } : {}),
603
+ }),
604
+ });
605
+ } else throw new XgateError("unsupported_result_block");
606
+ }
607
+ }
608
+ need(
609
+ content.some(
610
+ (c) => (c.type === "text" && c.text.length > 0) || c.type === "toolCall",
611
+ ),
612
+ "empty_result",
613
+ );
614
+ need(
615
+ (stopReason === "toolUse") === content.some((c) => c.type === "toolCall"),
616
+ "tool_stop_mismatch",
617
+ );
618
+ const u = body.usage,
619
+ read = tokens(
620
+ anthropic
621
+ ? (u.cache_read_input_tokens ?? 0)
622
+ : u.input_tokens_details?.cached_tokens,
623
+ ),
624
+ write = tokens(
625
+ anthropic
626
+ ? (u.cache_creation_input_tokens ?? 0)
627
+ : (u.input_tokens_details?.cache_write_tokens ?? 0),
628
+ ),
629
+ totalInput = tokens(u.input_tokens),
630
+ output = tokens(u.output_tokens),
631
+ input = anthropic ? totalInput : totalInput - read - write;
632
+ need(
633
+ input >= 0 && (!anthropic || (read === 0 && write === 0)),
634
+ "cache_usage_mismatch",
635
+ );
636
+ const rates = entry.model.cost;
637
+ const cost = {
638
+ input: (input * rates.input) / 1e6,
639
+ output: (output * rates.output) / 1e6,
640
+ cacheRead: (read * rates.cacheRead) / 1e6,
641
+ cacheWrite: (write * rates.cacheWrite) / 1e6,
642
+ total: 0,
643
+ };
644
+ cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
645
+ return {
646
+ content,
647
+ usage: {
648
+ input,
649
+ output,
650
+ cacheRead: read,
651
+ cacheWrite: write,
652
+ totalTokens: input + read + write + output,
653
+ cost,
654
+ },
655
+ stopReason,
656
+ responseId: body.id,
657
+ };
658
+ }