@lucid-agents/pi-xgate 0.0.0-stage → 0.1.5

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