@lucid-agents/pi-xgate 0.1.5 → 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/README.md +87 -4
- 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 +20 -8
- package/src/codec.ts +53 -16
- package/src/extension.ts +60 -12
- package/src/index.ts +45 -0
- package/src/provider.ts +68 -26
- package/src/taskmarket.ts +484 -0
- package/src/transport.ts +94 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# xgate provider for Pi
|
|
2
2
|
|
|
3
|
-
A Pi
|
|
3
|
+
A Pi provider extension for wallet-paid inference, supported on Pi **1.1.0** and
|
|
4
|
+
**0.85.1**. It uses native
|
|
4
5
|
Anthropic Messages and structured OpenAI Responses through xgate. Pi executes its
|
|
5
6
|
own tools; xgate never executes local tools. Text, tool calls/results and supported
|
|
6
7
|
reasoning signatures survive the full agent loop.
|
|
@@ -12,7 +13,7 @@ and the reviewed model-capability snapshot; the internal router contracts packag
|
|
|
12
13
|
is not required. With Node 24.19+ (below 25) and Pi 1.1.0:
|
|
13
14
|
|
|
14
15
|
```sh
|
|
15
|
-
pi install npm:@lucid-agents/pi-xgate@0.1.
|
|
16
|
+
pi install npm:@lucid-agents/pi-xgate@0.1.6
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
For repository development before a release is available:
|
|
@@ -23,7 +24,8 @@ pi install ./packages/pi-xgate
|
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Pi supplies `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent` as peers.
|
|
26
|
-
This release
|
|
27
|
+
This release supports exactly 1.1.0 and 0.85.1 (the version TaskMarket's agent
|
|
28
|
+
runtime pins); other versions need their own check.
|
|
27
29
|
|
|
28
30
|
## Connect a wallet broker
|
|
29
31
|
|
|
@@ -73,6 +75,83 @@ Pi cannot raise the service budget. Authorized ceilings remain consumed after
|
|
|
73
75
|
successful settlement even when most of a deposit is returned. A new budget is an
|
|
74
76
|
explicit wallet-operator action, never automatic rollover or a fresh journal.
|
|
75
77
|
|
|
78
|
+
## Host-injected transport
|
|
79
|
+
|
|
80
|
+
A host that owns payment, such as TaskMarket, builds the provider around its own
|
|
81
|
+
transport instead of the wallet broker. The supported entry point ships built
|
|
82
|
+
JavaScript and type declarations:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
createXgateExtension,
|
|
87
|
+
createXgateProvider,
|
|
88
|
+
type XgateTransport,
|
|
89
|
+
} from "@lucid-agents/pi-xgate";
|
|
90
|
+
|
|
91
|
+
const transport: XgateTransport = {
|
|
92
|
+
models: (signal) => hostCatalog(signal), // router catalog rows
|
|
93
|
+
purchase: (invocation, signal) => hostPurchase(invocation, signal),
|
|
94
|
+
};
|
|
95
|
+
// As a Pi extension: the broker configuration and `/xgate setup` are not used.
|
|
96
|
+
export default createXgateExtension({ transport });
|
|
97
|
+
// Or directly, e.g. for an agent's stream function.
|
|
98
|
+
const provider = await createXgateProvider(transport, {
|
|
99
|
+
sessionId: () => sessionId,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`createXgateExtension({ transport })` also accepts a factory, called on load and on
|
|
104
|
+
`/xgate refresh`. `/xgate status`, `budget`, `receipts` and `reconcile` use the
|
|
105
|
+
transport's optional methods of the same names. The default extension
|
|
106
|
+
(`@lucid-agents/pi-xgate/extension`, and Pi's `pi.extensions` entry) keeps the
|
|
107
|
+
standalone `BrokerClient`.
|
|
108
|
+
|
|
109
|
+
The provider keeps catalog qualification, native codecs and invocation identity;
|
|
110
|
+
the transport only moves the frozen request and payment. A transport must send at
|
|
111
|
+
most one paid submission per `Invocation.id`, never retry a paid request, reject a
|
|
112
|
+
repeated ID rather than pay again, and resolve only with a verified result.
|
|
113
|
+
`BrokerClient` satisfies this contract.
|
|
114
|
+
|
|
115
|
+
### TaskMarket payer
|
|
116
|
+
|
|
117
|
+
`createTaskMarketTransport` adapts TaskMarket's x402 payer (`@taskmarket/x402-payer`
|
|
118
|
+
API v1). That package is private, so this one declares a structural
|
|
119
|
+
`TaskMarketPayer` (`request`, `status`, `reconcile`) and TaskMarket injects its
|
|
120
|
+
instance. The payer remains the only wallet, signer and payment journal; the
|
|
121
|
+
adapter keeps no payment state.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const transport = createTaskMarketTransport({
|
|
125
|
+
payer, // TaskMarket's X402Payer
|
|
126
|
+
origin: "https://api.xgate.run",
|
|
127
|
+
allocationId, // the agent session's inference allocation
|
|
128
|
+
nonInteractive: true,
|
|
129
|
+
balances: () => allocations.balances(), // optional, for display
|
|
130
|
+
});
|
|
131
|
+
const provider = await createXgateProvider(transport, {
|
|
132
|
+
sessionId: () => sessionId,
|
|
133
|
+
onPaid: (r) => show(r.payment), // network, scheme, asset, atomic amounts, channel
|
|
134
|
+
onPurchaseFailed: (f) => record(f), // original invocation ID, code, pending
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- The Pi invocation ID is the payer `invocationId`. A retried turn reuses it, so
|
|
139
|
+
the payer refuses to send it again (`invocation_duplicate`).
|
|
140
|
+
- The provider refreshes the router catalog (an unpaid `GET /v1/models`) before
|
|
141
|
+
every paid request. The payer's rail echoes the router quote binding and verifies
|
|
142
|
+
settlement.
|
|
143
|
+
- A result is accepted only with a `settled` charge, the Anonymous privacy
|
|
144
|
+
headers and a native JSON body. Every other outcome rejects with
|
|
145
|
+
`XgatePurchaseError`. It is `pending` unless the payer proved no unresolved
|
|
146
|
+
payment; unknown future charge states count as unverified.
|
|
147
|
+
- Pi gets a terminal `error`/`aborted` message naming the original invocation ID,
|
|
148
|
+
worded so Pi's transient-error retry does not fire. Only payer error codes reach
|
|
149
|
+
Pi, never payer messages. Hooks see only `content-type` and `cache-control`.
|
|
150
|
+
- Payment details are rail-neutral (`XgatePayment`): CAIP-2 network, scheme, asset,
|
|
151
|
+
payee, rail kind, authorized and charged atomic amounts, status, reference and,
|
|
152
|
+
for a single matching session deposit, its channel balance. Pi displays them
|
|
153
|
+
and never selects or configures a rail.
|
|
154
|
+
|
|
76
155
|
## Behavior and limits
|
|
77
156
|
|
|
78
157
|
Responses are **buffered**, not token-streamed over the network. After a complete
|
|
@@ -118,7 +197,11 @@ from the current tool set is rejected rather than silently losing that history.
|
|
|
118
197
|
|
|
119
198
|
Run `pnpm pi:check` at the repository root. Tests instantiate Pi's real agent core,
|
|
120
199
|
execute a fixture tool, and verify the next request contains its exact result and
|
|
121
|
-
reasoning history.
|
|
200
|
+
reasoning history. `pnpm packages:check` installs the packed tarballs with Pi 1.1.0
|
|
201
|
+
and 0.85.1 and runs each real Pi CLI through the mock broker and an injected
|
|
202
|
+
transport, type-checks a consumer of the supported exports, and runs the TaskMarket
|
|
203
|
+
payer loop suite (`test/installed-loop.test.ts`: continuation, qualification,
|
|
204
|
+
cancellation, duplicate and lost response) against each installed Pi. Separate wallet tests cover durable financial boundaries.
|
|
122
205
|
These are fixture tests. The [9 October staging campaign](../../docs/validation/pi-client-staging-20261009.md)
|
|
123
206
|
separately exercised the installed Pi CLI with live providers and test tokens.
|
|
124
207
|
That evidence is staging-only; it does not qualify production payment execution.
|
package/dist/codec.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AssistantMessage, Message, Model, SimpleStreamOptions, Tool, Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Route } from "@lucid-agents/xgate-client/types";
|
|
3
|
+
export type Entry = {
|
|
4
|
+
model: Model<any>;
|
|
5
|
+
catalog: any;
|
|
6
|
+
route: Route;
|
|
7
|
+
efforts: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function catalogEntries(rows: any[]): Entry[];
|
|
10
|
+
/**
|
|
11
|
+
* Pi 1.1 passes a normalized transcript; Pi 0.85 passes `systemPrompt` and `tools` beside the
|
|
12
|
+
* messages. Structural, so published types do not name a type only one Pi version has.
|
|
13
|
+
*/
|
|
14
|
+
export type PiContext = {
|
|
15
|
+
messages: Message[];
|
|
16
|
+
systemPrompt?: string;
|
|
17
|
+
tools?: Tool[];
|
|
18
|
+
};
|
|
19
|
+
type Turn = Exclude<Message, {
|
|
20
|
+
role: "system";
|
|
21
|
+
}>;
|
|
22
|
+
export declare function transcript(context: PiContext): {
|
|
23
|
+
system: string;
|
|
24
|
+
tools: Tool[];
|
|
25
|
+
messages: Turn[];
|
|
26
|
+
};
|
|
27
|
+
export declare function encode(entry: Entry, context: PiContext, options?: SimpleStreamOptions): any;
|
|
28
|
+
export declare function decode(entry: Entry, body: any): {
|
|
29
|
+
content: AssistantMessage["content"];
|
|
30
|
+
usage: Usage;
|
|
31
|
+
stopReason: "stop" | "toolUse";
|
|
32
|
+
responseId: string;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
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;
|