@lucid-agents/pi-xgate 0.1.6 → 0.1.8
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/README.md +17 -4
- package/dist/codec.d.ts +6 -0
- package/dist/codec.js +53 -10
- package/package.json +7 -7
- package/src/codec.ts +64 -10
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ reasoning signatures survive the full agent loop.
|
|
|
10
10
|
|
|
11
11
|
The release package is `@lucid-agents/pi-xgate`. It includes the client dependency
|
|
12
12
|
and the reviewed model-capability snapshot; the internal router contracts package
|
|
13
|
-
is not required. With Node 24.19
|
|
13
|
+
is not required. With Node 24.19 or later and Pi 1.1.0:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
pi install npm:@lucid-agents/pi-xgate@0.1.
|
|
16
|
+
pi install npm:@lucid-agents/pi-xgate@0.1.8
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
For repository development before a release is available:
|
|
@@ -56,8 +56,21 @@ Both files must be regular, nonsymlink files with no group/other permissions.
|
|
|
56
56
|
Select a model through `/model`. Only the broker policy's models with compatible
|
|
57
57
|
live qualifications are offered. OpenAI requires the new
|
|
58
58
|
`openai-anonymous-tools-v2` router profile; an older server will expose no OpenAI
|
|
59
|
-
coding models. Anthropic requires `anthropic-anonymous-usage-v3`
|
|
60
|
-
`anthropic_no_cache`
|
|
59
|
+
coding models. Anthropic requires `anthropic-anonymous-usage-v3` native Messages
|
|
60
|
+
scopes qualified as `anthropic_no_cache`, `anthropic_ephemeral_5m` or
|
|
61
|
+
`anthropic_ephemeral_1h`.
|
|
62
|
+
|
|
63
|
+
Anthropic caches a prompt only at explicit `cache_control` breakpoints. On a
|
|
64
|
+
cache-qualified scope the provider marks, like Pi's direct Anthropic provider, the
|
|
65
|
+
last tool, the system prompt and the newest user turn (three of Anthropic's four
|
|
66
|
+
breakpoints), so each call in a conversation can read the prefix the previous call
|
|
67
|
+
wrote. Breakpoints are five-minute `ephemeral` writes; Pi's `cacheRetention: "long"`
|
|
68
|
+
selects one-hour writes only on an `anthropic_ephemeral_1h` scope, and
|
|
69
|
+
`cacheRetention: "none"` sends none. An `anthropic_no_cache` scope gets no
|
|
70
|
+
breakpoints, and cache usage it reports is rejected as an invalid result. The
|
|
71
|
+
breakpoints carry no session ID or other key: the provider cache is keyed by the
|
|
72
|
+
content prefix alone. Cache warming stays off, so no unpaid or extra paid call
|
|
73
|
+
is made to keep a cache alive.
|
|
61
74
|
An expired/revoked catalog scope cannot authorize payment just because Pi remembers
|
|
62
75
|
an earlier model list: the provider refreshes before each invocation and the wallet
|
|
63
76
|
client validates the actual quote again.
|
package/dist/codec.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { AssistantMessage, Message, Model, SimpleStreamOptions, Tool, Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Route } from "@lucid-agents/xgate-client/types";
|
|
3
|
+
/** The widest Anthropic prompt-cache write lifetime a scope's qualification permits. */
|
|
4
|
+
export type CacheTtl = "none" | "5m" | "1h";
|
|
3
5
|
export type Entry = {
|
|
4
6
|
model: Model<any>;
|
|
5
7
|
catalog: any;
|
|
6
8
|
route: Route;
|
|
7
9
|
efforts: string[];
|
|
10
|
+
/** Anthropic only: requests may carry `cache_control` breakpoints up to this lifetime. */
|
|
11
|
+
cache: CacheTtl;
|
|
12
|
+
/** Anthropic only: the per-million-token price of a one-hour cache write. */
|
|
13
|
+
cacheWrite1h: number;
|
|
8
14
|
};
|
|
9
15
|
export declare function catalogEntries(rows: any[]): Entry[];
|
|
10
16
|
/**
|
package/dist/codec.js
CHANGED
|
@@ -2,6 +2,16 @@ import * as pi from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { XgateError } from "@lucid-agents/xgate-client/types";
|
|
3
3
|
import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
|
|
4
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
|
+
};
|
|
5
15
|
const need = (x, code) => {
|
|
6
16
|
if (!x)
|
|
7
17
|
throw new XgateError(code);
|
|
@@ -44,7 +54,7 @@ export function catalogEntries(rows) {
|
|
|
44
54
|
if (!q ||
|
|
45
55
|
row.privacy?.revision !== "anonymous-volatile-v1" ||
|
|
46
56
|
row.privacy.result_replay_available !== false ||
|
|
47
|
-
(anthropic && q.cache_mode
|
|
57
|
+
(anthropic && !Object.hasOwn(ANTHROPIC_CACHE, q.cache_mode)))
|
|
48
58
|
continue;
|
|
49
59
|
if (q.valid_until !== undefined &&
|
|
50
60
|
Number(q.valid_until) * 1000 <= Date.now())
|
|
@@ -84,8 +94,13 @@ export function catalogEntries(rows) {
|
|
|
84
94
|
["minimal", "low", "medium", "high"].includes(level));
|
|
85
95
|
thinkingLevelMap[level] = allowed ? level : null;
|
|
86
96
|
}
|
|
97
|
+
const write5m = p.rates.input_cache_write_5m ?? p.rates.input_cache_write;
|
|
87
98
|
entries.push({
|
|
88
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,
|
|
89
104
|
route: anthropic
|
|
90
105
|
? "/providers/anthropic/v1/messages"
|
|
91
106
|
: "/providers/openai/v1/responses",
|
|
@@ -107,9 +122,7 @@ export function catalogEntries(rows) {
|
|
|
107
122
|
input: rate(p.rates.input_uncached),
|
|
108
123
|
output: rate(p.rates.output),
|
|
109
124
|
cacheRead: rate(p.rates.input_cache_read),
|
|
110
|
-
cacheWrite:
|
|
111
|
-
? rate(p.rates.input_cache_write)
|
|
112
|
-
: 0,
|
|
125
|
+
cacheWrite: write5m ? rate(write5m) : 0,
|
|
113
126
|
},
|
|
114
127
|
inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
|
|
115
128
|
},
|
|
@@ -301,23 +314,38 @@ export function encode(entry, context, options = {}) {
|
|
|
301
314
|
need(history.length > 0 && history.length <= 256, "history_limit");
|
|
302
315
|
let request;
|
|
303
316
|
if (anthropic) {
|
|
304
|
-
const cap = entry.catalog.execution_profile.capabilities;
|
|
317
|
+
const cap = entry.catalog.execution_profile.capabilities, cache = cacheControl(entry.cache, options.cacheRetention);
|
|
305
318
|
request = {
|
|
306
319
|
model: entry.model.id,
|
|
307
320
|
messages: history,
|
|
308
321
|
max_tokens: max,
|
|
309
322
|
stream: false,
|
|
310
|
-
...(system
|
|
323
|
+
...(system
|
|
324
|
+
? {
|
|
325
|
+
system: cache
|
|
326
|
+
? [{ type: "text", text: system, cache_control: cache }]
|
|
327
|
+
: system,
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
311
330
|
...(tools.length
|
|
312
331
|
? {
|
|
313
|
-
tools: tools.map((t) => ({
|
|
332
|
+
tools: tools.map((t, i) => ({
|
|
314
333
|
name: t.name,
|
|
315
334
|
description: t.description,
|
|
316
335
|
input_schema: t.parameters,
|
|
336
|
+
...(cache && i === tools.length - 1
|
|
337
|
+
? { cache_control: cache }
|
|
338
|
+
: {}),
|
|
317
339
|
})),
|
|
318
340
|
}
|
|
319
341
|
: {}),
|
|
320
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;
|
|
321
349
|
if (effort)
|
|
322
350
|
request.output_config = { effort };
|
|
323
351
|
if (cap.thinking.adaptive)
|
|
@@ -357,6 +385,13 @@ export function encode(entry, context, options = {}) {
|
|
|
357
385
|
need(Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024, "request_bound");
|
|
358
386
|
return request;
|
|
359
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
|
+
}
|
|
360
395
|
function tokens(value) {
|
|
361
396
|
need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
|
|
362
397
|
return value;
|
|
@@ -483,14 +518,22 @@ export function decode(entry, body) {
|
|
|
483
518
|
? (u.cache_read_input_tokens ?? 0)
|
|
484
519
|
: u.input_tokens_details?.cached_tokens), write = tokens(anthropic
|
|
485
520
|
? (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
|
-
|
|
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");
|
|
488
530
|
const rates = entry.model.cost;
|
|
489
531
|
const cost = {
|
|
490
532
|
input: (input * rates.input) / 1e6,
|
|
491
533
|
output: (output * rates.output) / 1e6,
|
|
492
534
|
cacheRead: (read * rates.cacheRead) / 1e6,
|
|
493
|
-
cacheWrite: (write * rates.cacheWrite) /
|
|
535
|
+
cacheWrite: ((write - write1h) * rates.cacheWrite + write1h * entry.cacheWrite1h) /
|
|
536
|
+
1e6,
|
|
494
537
|
total: 0,
|
|
495
538
|
};
|
|
496
539
|
cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucid-agents/pi-xgate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"LICENSE"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lucid-agents/xgate-client": "0.1.
|
|
35
|
+
"@lucid-agents/xgate-client": "0.1.8"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-ai": "0.85.1 || 1.1.0",
|
|
39
39
|
"@earendil-works/pi-coding-agent": "0.85.1 || 1.1.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=24.19
|
|
42
|
+
"node": ">=24.19"
|
|
43
43
|
},
|
|
44
44
|
"description": "Wallet-paid OpenAI and Anthropic inference through xgate for the Pi coding agent",
|
|
45
45
|
"repository": {
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"registry": "https://registry.npmjs.org"
|
|
57
57
|
},
|
|
58
58
|
"license": "MIT",
|
|
59
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "d410140f8e57fd3fc4309a1c54772b8a9f99ccd9",
|
|
60
60
|
"xgateRelease": {
|
|
61
|
-
"version": "v0.1.
|
|
62
|
-
"sourceSha": "
|
|
63
|
-
"candidateId": "rc-
|
|
61
|
+
"version": "v0.1.8",
|
|
62
|
+
"sourceSha": "d410140f8e57fd3fc4309a1c54772b8a9f99ccd9",
|
|
63
|
+
"candidateId": "rc-d410140f8e57-42908627975-1"
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/codec.ts
CHANGED
|
@@ -13,11 +13,27 @@ import type {
|
|
|
13
13
|
import { XgateError, type Route } from "@lucid-agents/xgate-client/types";
|
|
14
14
|
import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
+
/** The widest Anthropic prompt-cache write lifetime a scope's qualification permits. */
|
|
17
|
+
export type CacheTtl = "none" | "5m" | "1h";
|
|
16
18
|
export type Entry = {
|
|
17
19
|
model: Model<any>;
|
|
18
20
|
catalog: any;
|
|
19
21
|
route: Route;
|
|
20
22
|
efforts: string[];
|
|
23
|
+
/** Anthropic only: requests may carry `cache_control` breakpoints up to this lifetime. */
|
|
24
|
+
cache: CacheTtl;
|
|
25
|
+
/** Anthropic only: the per-million-token price of a one-hour cache write. */
|
|
26
|
+
cacheWrite1h: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The router quotes the class a request's `cache_control` markers select, and serves it only
|
|
30
|
+
* when the scope's qualification covers that class. A wider qualification covers narrower
|
|
31
|
+
* requests, so a cache-qualified scope still serves a request without markers.
|
|
32
|
+
*/
|
|
33
|
+
const ANTHROPIC_CACHE: Record<string, CacheTtl> = {
|
|
34
|
+
anthropic_no_cache: "none",
|
|
35
|
+
anthropic_ephemeral_5m: "5m",
|
|
36
|
+
anthropic_ephemeral_1h: "1h",
|
|
21
37
|
};
|
|
22
38
|
const need = (x: unknown, code: string): void => {
|
|
23
39
|
if (!x) throw new XgateError(code);
|
|
@@ -70,7 +86,7 @@ export function catalogEntries(rows: any[]): Entry[] {
|
|
|
70
86
|
!q ||
|
|
71
87
|
row.privacy?.revision !== "anonymous-volatile-v1" ||
|
|
72
88
|
row.privacy.result_replay_available !== false ||
|
|
73
|
-
(anthropic && q.cache_mode
|
|
89
|
+
(anthropic && !Object.hasOwn(ANTHROPIC_CACHE, q.cache_mode))
|
|
74
90
|
)
|
|
75
91
|
continue;
|
|
76
92
|
if (
|
|
@@ -123,8 +139,13 @@ export function catalogEntries(rows: any[]): Entry[] {
|
|
|
123
139
|
["minimal", "low", "medium", "high"].includes(level));
|
|
124
140
|
thinkingLevelMap[level] = allowed ? level : null;
|
|
125
141
|
}
|
|
142
|
+
const write5m = p.rates.input_cache_write_5m ?? p.rates.input_cache_write;
|
|
126
143
|
entries.push({
|
|
127
144
|
catalog: row,
|
|
145
|
+
cache: anthropic ? ANTHROPIC_CACHE[q.cache_mode]! : "none",
|
|
146
|
+
cacheWrite1h: p.rates.input_cache_write_1h
|
|
147
|
+
? rate(p.rates.input_cache_write_1h)
|
|
148
|
+
: 0,
|
|
128
149
|
route: anthropic
|
|
129
150
|
? "/providers/anthropic/v1/messages"
|
|
130
151
|
: "/providers/openai/v1/responses",
|
|
@@ -146,9 +167,7 @@ export function catalogEntries(rows: any[]): Entry[] {
|
|
|
146
167
|
input: rate(p.rates.input_uncached),
|
|
147
168
|
output: rate(p.rates.output),
|
|
148
169
|
cacheRead: rate(p.rates.input_cache_read),
|
|
149
|
-
cacheWrite:
|
|
150
|
-
? rate(p.rates.input_cache_write)
|
|
151
|
-
: 0,
|
|
170
|
+
cacheWrite: write5m ? rate(write5m) : 0,
|
|
152
171
|
},
|
|
153
172
|
inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
|
|
154
173
|
},
|
|
@@ -397,23 +416,39 @@ export function encode(
|
|
|
397
416
|
need(history.length > 0 && history.length <= 256, "history_limit");
|
|
398
417
|
let request: any;
|
|
399
418
|
if (anthropic) {
|
|
400
|
-
const cap = entry.catalog.execution_profile.capabilities
|
|
419
|
+
const cap = entry.catalog.execution_profile.capabilities,
|
|
420
|
+
cache = cacheControl(entry.cache, options.cacheRetention);
|
|
401
421
|
request = {
|
|
402
422
|
model: entry.model.id,
|
|
403
423
|
messages: history,
|
|
404
424
|
max_tokens: max,
|
|
405
425
|
stream: false,
|
|
406
|
-
...(system
|
|
426
|
+
...(system
|
|
427
|
+
? {
|
|
428
|
+
system: cache
|
|
429
|
+
? [{ type: "text", text: system, cache_control: cache }]
|
|
430
|
+
: system,
|
|
431
|
+
}
|
|
432
|
+
: {}),
|
|
407
433
|
...(tools.length
|
|
408
434
|
? {
|
|
409
|
-
tools: tools.map((t) => ({
|
|
435
|
+
tools: tools.map((t, i) => ({
|
|
410
436
|
name: t.name,
|
|
411
437
|
description: t.description,
|
|
412
438
|
input_schema: t.parameters,
|
|
439
|
+
...(cache && i === tools.length - 1
|
|
440
|
+
? { cache_control: cache }
|
|
441
|
+
: {}),
|
|
413
442
|
})),
|
|
414
443
|
}
|
|
415
444
|
: {}),
|
|
416
445
|
};
|
|
446
|
+
// Anthropic caches nothing without a breakpoint. Like Pi's direct Anthropic provider, mark
|
|
447
|
+
// the tools, the system prompt and the newest turn: the next request in the conversation
|
|
448
|
+
// reads the whole previous prefix. A final user turn ends in text or a tool result.
|
|
449
|
+
const last = history.at(-1);
|
|
450
|
+
if (cache && last.role === "user")
|
|
451
|
+
last.content.at(-1).cache_control = cache;
|
|
417
452
|
if (effort) request.output_config = { effort };
|
|
418
453
|
if (cap.thinking.adaptive) request.thinking = { type: "adaptive" };
|
|
419
454
|
else if (requested && cap.thinking.enabled) {
|
|
@@ -456,6 +491,15 @@ export function encode(
|
|
|
456
491
|
);
|
|
457
492
|
return request;
|
|
458
493
|
}
|
|
494
|
+
function cacheControl(
|
|
495
|
+
qualified: CacheTtl,
|
|
496
|
+
retention: SimpleStreamOptions["cacheRetention"],
|
|
497
|
+
): { type: "ephemeral"; ttl?: "1h" } | undefined {
|
|
498
|
+
if (qualified === "none" || retention === "none") return undefined;
|
|
499
|
+
return retention === "long" && qualified === "1h"
|
|
500
|
+
? { type: "ephemeral", ttl: "1h" }
|
|
501
|
+
: { type: "ephemeral" };
|
|
502
|
+
}
|
|
459
503
|
function tokens(value: any): number {
|
|
460
504
|
need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
|
|
461
505
|
return value;
|
|
@@ -628,9 +672,17 @@ export function decode(
|
|
|
628
672
|
),
|
|
629
673
|
totalInput = tokens(u.input_tokens),
|
|
630
674
|
output = tokens(u.output_tokens),
|
|
631
|
-
input = anthropic ? totalInput : totalInput - read - write
|
|
675
|
+
input = anthropic ? totalInput : totalInput - read - write,
|
|
676
|
+
// Anthropic splits writes by lifetime; an unsplit write is the default five minutes.
|
|
677
|
+
write1h =
|
|
678
|
+
anthropic && object(u.cache_creation)
|
|
679
|
+
? tokens(u.cache_creation.ephemeral_1h_input_tokens ?? 0)
|
|
680
|
+
: 0;
|
|
632
681
|
need(
|
|
633
|
-
input >= 0 &&
|
|
682
|
+
input >= 0 &&
|
|
683
|
+
write1h <= write &&
|
|
684
|
+
(!anthropic || entry.cache !== "none" || (read === 0 && write === 0)) &&
|
|
685
|
+
(!anthropic || entry.cache === "1h" || write1h === 0),
|
|
634
686
|
"cache_usage_mismatch",
|
|
635
687
|
);
|
|
636
688
|
const rates = entry.model.cost;
|
|
@@ -638,7 +690,9 @@ export function decode(
|
|
|
638
690
|
input: (input * rates.input) / 1e6,
|
|
639
691
|
output: (output * rates.output) / 1e6,
|
|
640
692
|
cacheRead: (read * rates.cacheRead) / 1e6,
|
|
641
|
-
cacheWrite:
|
|
693
|
+
cacheWrite:
|
|
694
|
+
((write - write1h) * rates.cacheWrite + write1h * entry.cacheWrite1h) /
|
|
695
|
+
1e6,
|
|
642
696
|
total: 0,
|
|
643
697
|
};
|
|
644
698
|
cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
|