@glubean/port 0.1.0
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 +203 -0
- package/dist/async-queue.d.ts +7 -0
- package/dist/async-queue.d.ts.map +1 -0
- package/dist/async-queue.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/json-rpc.d.ts +31 -0
- package/dist/json-rpc.d.ts.map +1 -0
- package/dist/json-rpc.js +122 -0
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +517 -0
- package/dist/options.d.ts +12 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +32 -0
- package/dist/port.d.ts +4 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +4 -0
- package/dist/provider-options.typecheck.d.ts +2 -0
- package/dist/provider-options.typecheck.d.ts.map +1 -0
- package/dist/provider-options.typecheck.js +29 -0
- package/dist/providers/adapter.d.ts +14 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +1 -0
- package/dist/providers/claude.d.ts +9 -0
- package/dist/providers/claude.d.ts.map +1 -0
- package/dist/providers/claude.js +300 -0
- package/dist/providers/codex-normalizer.d.ts +7 -0
- package/dist/providers/codex-normalizer.d.ts.map +1 -0
- package/dist/providers/codex-normalizer.js +287 -0
- package/dist/providers/codex.d.ts +19 -0
- package/dist/providers/codex.d.ts.map +1 -0
- package/dist/providers/codex.js +300 -0
- package/dist/providers/common.d.ts +6 -0
- package/dist/providers/common.d.ts.map +1 -0
- package/dist/providers/common.js +67 -0
- package/dist/providers/gemini.d.ts +9 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +378 -0
- package/dist/structured.d.ts +8 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/structured.js +127 -0
- package/dist/token-usage.d.ts +4 -0
- package/dist/token-usage.d.ts.map +1 -0
- package/dist/token-usage.js +59 -0
- package/dist/types.d.ts +407 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { AsyncQueue } from "../async-queue.js";
|
|
6
|
+
import { JsonRpcLineConnection } from "../json-rpc.js";
|
|
7
|
+
import { createPortFromAdapter } from "../port.js";
|
|
8
|
+
import { providerRuntimeOptions, resolveRuntimeOptions } from "../options.js";
|
|
9
|
+
import { normalizeTokenUsage } from "../token-usage.js";
|
|
10
|
+
import { completedTurn } from "./common.js";
|
|
11
|
+
export const geminiCapabilities = {
|
|
12
|
+
provider: "gemini",
|
|
13
|
+
nativeRpc: true,
|
|
14
|
+
transport: "stdio",
|
|
15
|
+
sessions: {
|
|
16
|
+
create: true,
|
|
17
|
+
resume: true,
|
|
18
|
+
attach: true,
|
|
19
|
+
status: true,
|
|
20
|
+
list: true,
|
|
21
|
+
close: true,
|
|
22
|
+
},
|
|
23
|
+
turns: {
|
|
24
|
+
start: true,
|
|
25
|
+
submit: true,
|
|
26
|
+
status: true,
|
|
27
|
+
wait: true,
|
|
28
|
+
cancel: true,
|
|
29
|
+
steer: false,
|
|
30
|
+
outputSchema: false,
|
|
31
|
+
},
|
|
32
|
+
events: {
|
|
33
|
+
messageDelta: true,
|
|
34
|
+
reasoningDelta: true,
|
|
35
|
+
toolLifecycle: true,
|
|
36
|
+
tokenUsage: true,
|
|
37
|
+
planUpdates: true,
|
|
38
|
+
diffUpdates: true,
|
|
39
|
+
},
|
|
40
|
+
structuredOutput: {
|
|
41
|
+
jsonMode: true,
|
|
42
|
+
nativeJsonSchema: false,
|
|
43
|
+
localValidation: true,
|
|
44
|
+
retry: true,
|
|
45
|
+
},
|
|
46
|
+
lifecycle: {
|
|
47
|
+
localState: true,
|
|
48
|
+
eventReplay: "memory",
|
|
49
|
+
status: "local",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
export async function createGeminiPort(options) {
|
|
53
|
+
return createPortFromAdapter(await createGeminiAdapter(options));
|
|
54
|
+
}
|
|
55
|
+
export async function createGeminiAdapter(options) {
|
|
56
|
+
const runtime = new GeminiAcpRuntime(options);
|
|
57
|
+
await runtime.start();
|
|
58
|
+
return runtime.adapter;
|
|
59
|
+
}
|
|
60
|
+
class GeminiAcpRuntime {
|
|
61
|
+
#options;
|
|
62
|
+
#process;
|
|
63
|
+
#rpc;
|
|
64
|
+
#events = new EventEmitter();
|
|
65
|
+
#closed = false;
|
|
66
|
+
#activeTurns = new Map();
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.#options = options;
|
|
69
|
+
}
|
|
70
|
+
get adapter() {
|
|
71
|
+
return {
|
|
72
|
+
id: "gemini",
|
|
73
|
+
capabilities: geminiCapabilities,
|
|
74
|
+
createSession: (input = {}) => this.#createSession(input),
|
|
75
|
+
resumeSession: (input) => this.#resumeSession(input),
|
|
76
|
+
startTurn: (input) => this.#startTurn(input),
|
|
77
|
+
cancelTurn: (input) => this.#cancelTurn(input),
|
|
78
|
+
events: () => this.#allEvents(),
|
|
79
|
+
close: () => this.close(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async start() {
|
|
83
|
+
if (this.#rpc)
|
|
84
|
+
return;
|
|
85
|
+
const runtimeOptions = resolveRuntimeOptions({ cwd: this.#options.cwd, options: this.#options.options });
|
|
86
|
+
const geminiOptions = providerRuntimeOptions(runtimeOptions);
|
|
87
|
+
const command = this.#options.command ?? "gemini";
|
|
88
|
+
const args = this.#options.args ?? buildGeminiArgs(runtimeOptions);
|
|
89
|
+
this.#process = spawn(command, args, {
|
|
90
|
+
cwd: runtimeOptions.cwd ?? process.cwd(),
|
|
91
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
...this.#options.env,
|
|
95
|
+
...(geminiOptions.settingsPath ? { GEMINI_CLI_SYSTEM_SETTINGS_PATH: geminiOptions.settingsPath } : {}),
|
|
96
|
+
NO_COLOR: "1",
|
|
97
|
+
},
|
|
98
|
+
shell: false,
|
|
99
|
+
});
|
|
100
|
+
this.#process.stderr.on("data", (chunk) => {
|
|
101
|
+
const message = chunk.toString("utf8").trim();
|
|
102
|
+
if (message)
|
|
103
|
+
this.#emit({ type: "error", message });
|
|
104
|
+
});
|
|
105
|
+
this.#process.on("exit", (code, signal) => {
|
|
106
|
+
if (!this.#closed) {
|
|
107
|
+
this.#emit({ type: "error", message: `Gemini ACP exited: code=${code ?? "null"} signal=${signal ?? "null"}` });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
this.#rpc = new JsonRpcLineConnection(this.#process.stdout, this.#process.stdin);
|
|
111
|
+
this.#rpc.onNotification((notification) => {
|
|
112
|
+
for (const event of this.#normalizeNotification(notification))
|
|
113
|
+
this.#emit(event);
|
|
114
|
+
});
|
|
115
|
+
this.#rpc.onRequest((request) => this.#handleClientRequest(request));
|
|
116
|
+
await this.#rpc.request("initialize", {
|
|
117
|
+
protocolVersion: 1,
|
|
118
|
+
clientInfo: { name: "glubean-port", version: "0.0.0" },
|
|
119
|
+
clientCapabilities: {
|
|
120
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
121
|
+
terminal: false,
|
|
122
|
+
auth: { terminal: false },
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async close() {
|
|
127
|
+
this.#closed = true;
|
|
128
|
+
this.#rpc?.close();
|
|
129
|
+
this.#process?.kill();
|
|
130
|
+
}
|
|
131
|
+
async #createSession(input) {
|
|
132
|
+
await this.start();
|
|
133
|
+
const runtimeOptions = resolveRuntimeOptions(input, this.#options.options);
|
|
134
|
+
const result = asRecord(await this.#requireRpc().request("session/new", {
|
|
135
|
+
cwd: runtimeOptions.cwd ?? this.#options.cwd ?? this.#options.options?.cwd ?? process.cwd(),
|
|
136
|
+
mcpServers: [],
|
|
137
|
+
}));
|
|
138
|
+
const id = asString(result.sessionId);
|
|
139
|
+
if (!id)
|
|
140
|
+
throw new Error("Gemini session/new response did not include sessionId");
|
|
141
|
+
await this.#applySessionOptions(id, runtimeOptions);
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
provider: "gemini",
|
|
145
|
+
native: { sessionId: id },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async #resumeSession(input) {
|
|
149
|
+
await this.start();
|
|
150
|
+
const runtimeOptions = resolveRuntimeOptions(input, this.#options.options);
|
|
151
|
+
await this.#requireRpc().request("session/load", {
|
|
152
|
+
sessionId: input.id,
|
|
153
|
+
cwd: runtimeOptions.cwd ?? this.#options.cwd ?? this.#options.options?.cwd ?? process.cwd(),
|
|
154
|
+
mcpServers: [],
|
|
155
|
+
});
|
|
156
|
+
await this.#applySessionOptions(input.id, runtimeOptions);
|
|
157
|
+
return {
|
|
158
|
+
id: input.id,
|
|
159
|
+
provider: "gemini",
|
|
160
|
+
native: { sessionId: input.id },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async *#startTurn(input) {
|
|
164
|
+
await this.start();
|
|
165
|
+
const runtimeOptions = resolveRuntimeOptions(input, this.#options.options);
|
|
166
|
+
const turnId = randomUUID();
|
|
167
|
+
const queue = new AsyncQueue();
|
|
168
|
+
const unsubscribe = this.#subscribe((event) => {
|
|
169
|
+
if ("sessionId" in event && event.sessionId && event.sessionId !== input.sessionId)
|
|
170
|
+
return;
|
|
171
|
+
if ("turnId" in event && event.turnId && event.turnId !== turnId)
|
|
172
|
+
return;
|
|
173
|
+
queue.push(event);
|
|
174
|
+
});
|
|
175
|
+
this.#activeTurns.set(input.sessionId, turnId);
|
|
176
|
+
const started = { type: "turn.started", sessionId: input.sessionId, turnId };
|
|
177
|
+
yield started;
|
|
178
|
+
this.#emit(started);
|
|
179
|
+
try {
|
|
180
|
+
await this.#applySessionOptions(input.sessionId, runtimeOptions);
|
|
181
|
+
const result = asRecord(await this.#requireRpc().request("session/prompt", {
|
|
182
|
+
sessionId: input.sessionId,
|
|
183
|
+
messageId: turnId,
|
|
184
|
+
prompt: toAcpPrompt(input.input),
|
|
185
|
+
}));
|
|
186
|
+
for (const event of tokenUsageEvents(result.usageMetadata ?? result.usage, input.sessionId, turnId))
|
|
187
|
+
this.#emit(event);
|
|
188
|
+
const stopReason = asString(result.stopReason);
|
|
189
|
+
const completed = completedTurn(input.sessionId, turnId, stopReason === "cancelled" ? "cancelled" : "completed");
|
|
190
|
+
this.#emit(completed);
|
|
191
|
+
queue.close();
|
|
192
|
+
for await (const event of queue)
|
|
193
|
+
yield event;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
const errorEvent = { type: "error", sessionId: input.sessionId, turnId, message };
|
|
198
|
+
const completed = completedTurn(input.sessionId, turnId, "failed", message);
|
|
199
|
+
this.#emit(errorEvent);
|
|
200
|
+
this.#emit(completed);
|
|
201
|
+
queue.close();
|
|
202
|
+
for await (const event of queue)
|
|
203
|
+
yield event;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
this.#activeTurns.delete(input.sessionId);
|
|
207
|
+
unsubscribe();
|
|
208
|
+
queue.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async #cancelTurn(input) {
|
|
212
|
+
await this.start();
|
|
213
|
+
this.#requireRpc().notify("session/cancel", { sessionId: input.sessionId });
|
|
214
|
+
}
|
|
215
|
+
async #applySessionOptions(sessionId, options) {
|
|
216
|
+
if (options.model) {
|
|
217
|
+
await this.#requireRpc().request("session/set_model", {
|
|
218
|
+
sessionId,
|
|
219
|
+
modelId: options.model,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const approvalMode = providerRuntimeOptions(options).approvalMode ?? toGeminiApprovalMode(options);
|
|
223
|
+
if (approvalMode) {
|
|
224
|
+
await this.#requireRpc().request("session/set_mode", {
|
|
225
|
+
sessionId,
|
|
226
|
+
modeId: approvalMode,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
#normalizeNotification(notification) {
|
|
231
|
+
if (notification.method !== "session/update")
|
|
232
|
+
return [];
|
|
233
|
+
const params = asRecord(notification.params);
|
|
234
|
+
const sessionId = asString(params.sessionId);
|
|
235
|
+
if (!sessionId)
|
|
236
|
+
return [];
|
|
237
|
+
const turnId = this.#activeTurns.get(sessionId);
|
|
238
|
+
const update = asRecord(params.update);
|
|
239
|
+
const kind = asString(update.sessionUpdate);
|
|
240
|
+
const usageEvents = tokenUsageEvents(update.usageMetadata ?? update.usage ?? params.usageMetadata ?? params.usage, sessionId, turnId);
|
|
241
|
+
if (kind === "agent_message_chunk") {
|
|
242
|
+
const text = textFromAcpContent(update.content);
|
|
243
|
+
return [
|
|
244
|
+
...(text ? [{ type: "message.delta", sessionId, turnId, role: "assistant", text }] : []),
|
|
245
|
+
...usageEvents,
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
if (kind === "agent_thought_chunk") {
|
|
249
|
+
const text = textFromAcpContent(update.content);
|
|
250
|
+
return [
|
|
251
|
+
...(text ? [{ type: "reasoning.delta", sessionId, turnId, text }] : []),
|
|
252
|
+
...usageEvents,
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
if (kind === "plan") {
|
|
256
|
+
const entries = Array.isArray(update.entries) ? update.entries : [];
|
|
257
|
+
return turnId
|
|
258
|
+
? [{
|
|
259
|
+
type: "plan.updated",
|
|
260
|
+
sessionId,
|
|
261
|
+
turnId,
|
|
262
|
+
steps: entries.flatMap((entry) => {
|
|
263
|
+
const record = asRecord(entry);
|
|
264
|
+
const step = asString(record.content);
|
|
265
|
+
const status = normalizePlanStatus(asString(record.status));
|
|
266
|
+
return step ? [{ step, status }] : [];
|
|
267
|
+
}),
|
|
268
|
+
}, ...usageEvents]
|
|
269
|
+
: usageEvents;
|
|
270
|
+
}
|
|
271
|
+
if (kind === "tool_call" || kind === "tool_call_update") {
|
|
272
|
+
const toolCallId = asString(update.toolCallId);
|
|
273
|
+
if (!toolCallId)
|
|
274
|
+
return usageEvents;
|
|
275
|
+
const title = asString(update.title) ?? "tool";
|
|
276
|
+
const status = asString(update.status);
|
|
277
|
+
if (status === "completed" || status === "failed") {
|
|
278
|
+
return [{ type: "tool.completed", sessionId, turnId, toolCallId, name: title, result: toJsonValue(update.rawOutput) }, ...usageEvents];
|
|
279
|
+
}
|
|
280
|
+
return [{ type: "tool.started", sessionId, turnId, toolCall: { id: toolCallId, name: title, input: toJsonValue(update.rawInput) } }, ...usageEvents];
|
|
281
|
+
}
|
|
282
|
+
return usageEvents;
|
|
283
|
+
}
|
|
284
|
+
#handleClientRequest(request) {
|
|
285
|
+
if (request.method === "session/request_permission") {
|
|
286
|
+
const params = asRecord(request.params);
|
|
287
|
+
const options = Array.isArray(params.options) ? params.options.map(asRecord) : [];
|
|
288
|
+
const rejected = options.find((option) => asString(option.kind)?.startsWith("reject"));
|
|
289
|
+
const selected = rejected ?? options[0];
|
|
290
|
+
const optionId = asString(selected?.optionId);
|
|
291
|
+
return optionId ? { outcome: "selected", optionId } : { outcome: "cancelled" };
|
|
292
|
+
}
|
|
293
|
+
throw new Error(`Unsupported Gemini client request: ${request.method}`);
|
|
294
|
+
}
|
|
295
|
+
#allEvents() {
|
|
296
|
+
const queue = new AsyncQueue();
|
|
297
|
+
const unsubscribe = this.#subscribe((event) => queue.push(event));
|
|
298
|
+
return {
|
|
299
|
+
[Symbol.asyncIterator]() {
|
|
300
|
+
const iterator = queue[Symbol.asyncIterator]();
|
|
301
|
+
return {
|
|
302
|
+
next: () => iterator.next(),
|
|
303
|
+
return: async () => {
|
|
304
|
+
unsubscribe();
|
|
305
|
+
queue.close();
|
|
306
|
+
return { value: undefined, done: true };
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
#subscribe(listener) {
|
|
313
|
+
this.#events.on("event", listener);
|
|
314
|
+
return () => this.#events.off("event", listener);
|
|
315
|
+
}
|
|
316
|
+
#emit(event) {
|
|
317
|
+
this.#events.emit("event", event);
|
|
318
|
+
}
|
|
319
|
+
#requireRpc() {
|
|
320
|
+
if (!this.#rpc)
|
|
321
|
+
throw new Error("Gemini runtime is not started");
|
|
322
|
+
return this.#rpc;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function toAcpPrompt(input) {
|
|
326
|
+
return input.map((item) => {
|
|
327
|
+
if (item.type === "text")
|
|
328
|
+
return { type: "text", text: item.text };
|
|
329
|
+
return { type: "text", text: `[local image: ${item.path}]` };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export function buildGeminiArgs(options = {}) {
|
|
333
|
+
const geminiOptions = providerRuntimeOptions(options);
|
|
334
|
+
const args = ["--acp"];
|
|
335
|
+
if (options.model)
|
|
336
|
+
args.push("--model", options.model);
|
|
337
|
+
const approvalMode = geminiOptions.approvalMode ?? toGeminiApprovalMode(options);
|
|
338
|
+
if (approvalMode)
|
|
339
|
+
args.push("--approval-mode", approvalMode);
|
|
340
|
+
if (geminiOptions.enableSandbox)
|
|
341
|
+
args.push("--sandbox");
|
|
342
|
+
return args;
|
|
343
|
+
}
|
|
344
|
+
function toGeminiApprovalMode(options) {
|
|
345
|
+
if (options.sandbox === "read-only")
|
|
346
|
+
return "plan";
|
|
347
|
+
if (options.approvalPolicy === "never")
|
|
348
|
+
return "yolo";
|
|
349
|
+
if (options.approvalPolicy === "on-failure")
|
|
350
|
+
return "auto_edit";
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
function textFromAcpContent(value) {
|
|
354
|
+
const content = asRecord(value);
|
|
355
|
+
if (asString(content.type) !== "text")
|
|
356
|
+
return undefined;
|
|
357
|
+
return asString(content.text);
|
|
358
|
+
}
|
|
359
|
+
function normalizePlanStatus(value) {
|
|
360
|
+
if (value === "in_progress" || value === "completed")
|
|
361
|
+
return value;
|
|
362
|
+
return "pending";
|
|
363
|
+
}
|
|
364
|
+
function tokenUsageEvents(value, sessionId, turnId) {
|
|
365
|
+
const usage = normalizeTokenUsage(value);
|
|
366
|
+
return usage ? [{ type: "token.usage", sessionId, ...(turnId ? { turnId } : {}), usage, native: toJsonValue(value) }] : [];
|
|
367
|
+
}
|
|
368
|
+
function asRecord(value) {
|
|
369
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
370
|
+
}
|
|
371
|
+
function asString(value) {
|
|
372
|
+
return typeof value === "string" ? value : undefined;
|
|
373
|
+
}
|
|
374
|
+
function toJsonValue(value) {
|
|
375
|
+
if (value === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
return JSON.parse(JSON.stringify(value));
|
|
378
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProviderAdapter } from "./providers/adapter.ts";
|
|
2
|
+
import type { StartStructuredTurnInput, StructuredTurnAttempt, StructuredTurnResult } from "./types.ts";
|
|
3
|
+
export declare class StructuredTurnError extends Error {
|
|
4
|
+
attempts: StructuredTurnAttempt[];
|
|
5
|
+
constructor(message: string, attempts: StructuredTurnAttempt[]);
|
|
6
|
+
}
|
|
7
|
+
export declare function startStructuredTurn<T>(adapter: ProviderAdapter, input: StartStructuredTurnInput<T>): Promise<StructuredTurnResult<T>>;
|
|
8
|
+
//# sourceMappingURL=structured.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured.d.ts","sourceRoot":"","sources":["../src/structured.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAIV,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAIpB,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,EAAE,qBAAqB,EAAE,CAAC;gBAEtB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,qBAAqB,EAAE;CAK/D;AAED,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAmDlC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
2
|
+
export class StructuredTurnError extends Error {
|
|
3
|
+
attempts;
|
|
4
|
+
constructor(message, attempts) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "StructuredTurnError";
|
|
7
|
+
this.attempts = attempts;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function startStructuredTurn(adapter, input) {
|
|
11
|
+
const outputSchema = input.outputSchema ?? (input.schema ? await zodToJsonSchema(input.schema) : undefined);
|
|
12
|
+
const maxRetries = input.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
13
|
+
const shouldForceJson = input.jsonMode ?? Boolean(input.schema || outputSchema);
|
|
14
|
+
const attempts = [];
|
|
15
|
+
let lastError = "No attempt was run.";
|
|
16
|
+
for (let index = 0; index <= maxRetries; index += 1) {
|
|
17
|
+
const events = [];
|
|
18
|
+
let text = "";
|
|
19
|
+
const turnInput = {
|
|
20
|
+
...input,
|
|
21
|
+
input: buildAttemptInput(input.input, {
|
|
22
|
+
attemptIndex: index,
|
|
23
|
+
shouldForceJson,
|
|
24
|
+
outputSchema,
|
|
25
|
+
lastError,
|
|
26
|
+
providerHasNativeSchema: adapter.capabilities.structuredOutput.nativeJsonSchema,
|
|
27
|
+
}),
|
|
28
|
+
...(outputSchema && adapter.capabilities.structuredOutput.nativeJsonSchema ? { outputSchema } : {}),
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
let failedTurnError;
|
|
32
|
+
for await (const event of adapter.startTurn(turnInput)) {
|
|
33
|
+
events.push(event);
|
|
34
|
+
if (event.type === "message.delta")
|
|
35
|
+
text += event.text;
|
|
36
|
+
if (event.type === "turn.completed" && event.status !== "completed") {
|
|
37
|
+
failedTurnError = event.error ?? `Turn ended with status ${event.status}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (failedTurnError)
|
|
41
|
+
throw new Error(failedTurnError);
|
|
42
|
+
const json = parseJsonText(text);
|
|
43
|
+
const validated = input.schema?.safeParse(json);
|
|
44
|
+
if (validated && !validated.success) {
|
|
45
|
+
lastError = formatValidationError(validated.error);
|
|
46
|
+
attempts.push({ index, events, text, error: lastError });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const data = validated?.success ? validated.data : json;
|
|
50
|
+
attempts.push({ index, events, text });
|
|
51
|
+
return { data, json, text, attempts };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
55
|
+
attempts.push({ index, events, text, error: lastError });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new StructuredTurnError(`Structured turn failed after ${attempts.length} attempt(s): ${lastError}`, attempts);
|
|
59
|
+
}
|
|
60
|
+
function buildAttemptInput(input, options) {
|
|
61
|
+
if (!options.shouldForceJson)
|
|
62
|
+
return input;
|
|
63
|
+
if (options.attemptIndex > 0) {
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: [
|
|
68
|
+
"The previous response failed JSON validation.",
|
|
69
|
+
"Return only corrected JSON. Do not include markdown fences or commentary.",
|
|
70
|
+
`Validation error: ${options.lastError}`,
|
|
71
|
+
options.outputSchema ? `Required JSON Schema: ${JSON.stringify(options.outputSchema)}` : "",
|
|
72
|
+
].filter(Boolean).join("\n"),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
const instruction = [
|
|
77
|
+
"Return only valid JSON. Do not include markdown fences or commentary.",
|
|
78
|
+
options.outputSchema && !options.providerHasNativeSchema
|
|
79
|
+
? `The JSON must match this JSON Schema: ${JSON.stringify(options.outputSchema)}`
|
|
80
|
+
: "",
|
|
81
|
+
].filter(Boolean).join("\n");
|
|
82
|
+
return [...input, { type: "text", text: instruction }];
|
|
83
|
+
}
|
|
84
|
+
function parseJsonText(text) {
|
|
85
|
+
const trimmed = text.trim();
|
|
86
|
+
if (!trimmed)
|
|
87
|
+
throw new Error("Provider returned an empty response.");
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(trimmed);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
93
|
+
if (fenced?.[1])
|
|
94
|
+
return JSON.parse(fenced[1].trim());
|
|
95
|
+
const objectStart = trimmed.indexOf("{");
|
|
96
|
+
const objectEnd = trimmed.lastIndexOf("}");
|
|
97
|
+
if (objectStart >= 0 && objectEnd > objectStart) {
|
|
98
|
+
return JSON.parse(trimmed.slice(objectStart, objectEnd + 1));
|
|
99
|
+
}
|
|
100
|
+
const arrayStart = trimmed.indexOf("[");
|
|
101
|
+
const arrayEnd = trimmed.lastIndexOf("]");
|
|
102
|
+
if (arrayStart >= 0 && arrayEnd > arrayStart) {
|
|
103
|
+
return JSON.parse(trimmed.slice(arrayStart, arrayEnd + 1));
|
|
104
|
+
}
|
|
105
|
+
throw new Error("Provider response was not parseable JSON.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function zodToJsonSchema(schema) {
|
|
109
|
+
try {
|
|
110
|
+
const zod = await import("zod");
|
|
111
|
+
const toJSONSchema = zod.toJSONSchema;
|
|
112
|
+
if (typeof toJSONSchema !== "function")
|
|
113
|
+
return undefined;
|
|
114
|
+
return JSON.parse(JSON.stringify(toJSONSchema(schema)));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function formatValidationError(error) {
|
|
121
|
+
const issues = error.issues;
|
|
122
|
+
if (Array.isArray(issues))
|
|
123
|
+
return JSON.stringify(issues);
|
|
124
|
+
if (error instanceof Error)
|
|
125
|
+
return error.message;
|
|
126
|
+
return String(error);
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-usage.d.ts","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAoC1E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAGjE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function normalizeTokenUsage(value) {
|
|
2
|
+
const record = asRecord(value);
|
|
3
|
+
const cacheCreationInputTokens = readNumber(record, ["cacheCreationInputTokens", "cache_creation_input_tokens"]);
|
|
4
|
+
const cacheReadInputTokens = readNumber(record, ["cacheReadInputTokens", "cache_read_input_tokens"]);
|
|
5
|
+
const cachedInputTokens = readNumber(record, [
|
|
6
|
+
"cachedInputTokens",
|
|
7
|
+
"cached_input_tokens",
|
|
8
|
+
"cachedContentTokenCount",
|
|
9
|
+
"cached_content_token_count",
|
|
10
|
+
"cached_tokens",
|
|
11
|
+
]) ?? readNumber(asRecord(record.input_tokens_details ?? record.inputTokensDetails), ["cached_tokens", "cachedTokens"])
|
|
12
|
+
?? addNumbers(cacheCreationInputTokens, cacheReadInputTokens);
|
|
13
|
+
const usage = {
|
|
14
|
+
inputTokens: readNumber(record, ["inputTokens", "input_tokens", "promptTokens", "prompt_tokens", "promptTokenCount"]),
|
|
15
|
+
outputTokens: readNumber(record, [
|
|
16
|
+
"outputTokens",
|
|
17
|
+
"output_tokens",
|
|
18
|
+
"completionTokens",
|
|
19
|
+
"completion_tokens",
|
|
20
|
+
"candidatesTokenCount",
|
|
21
|
+
]),
|
|
22
|
+
totalTokens: readNumber(record, ["totalTokens", "total_tokens", "totalTokenCount"]),
|
|
23
|
+
cachedInputTokens,
|
|
24
|
+
cacheCreationInputTokens,
|
|
25
|
+
cacheReadInputTokens,
|
|
26
|
+
reasoningOutputTokens: readNumber(record, [
|
|
27
|
+
"reasoningOutputTokens",
|
|
28
|
+
"reasoning_output_tokens",
|
|
29
|
+
"reasoningTokens",
|
|
30
|
+
"reasoning_tokens",
|
|
31
|
+
"thoughtsTokenCount",
|
|
32
|
+
]) ?? readNumber(asRecord(record.output_tokens_details ?? record.outputTokensDetails), ["reasoning_tokens", "reasoningTokens"]),
|
|
33
|
+
};
|
|
34
|
+
return compactUsage(usage);
|
|
35
|
+
}
|
|
36
|
+
export function toJsonValue(value) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
return JSON.parse(JSON.stringify(value));
|
|
40
|
+
}
|
|
41
|
+
function compactUsage(usage) {
|
|
42
|
+
const compact = Object.fromEntries(Object.entries(usage).filter(([, value]) => typeof value === "number"));
|
|
43
|
+
return Object.keys(compact).length > 0 ? compact : undefined;
|
|
44
|
+
}
|
|
45
|
+
function readNumber(record, keys) {
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const value = record[key];
|
|
48
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function addNumbers(...values) {
|
|
54
|
+
const present = values.filter((value) => typeof value === "number");
|
|
55
|
+
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined;
|
|
56
|
+
}
|
|
57
|
+
function asRecord(value) {
|
|
58
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
59
|
+
}
|