@llm4ts/core 2.42.0 → 2.43.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.
@@ -0,0 +1,108 @@
1
+ import { type JsonRecord, type JsonValue } from "./CliSupport.ts";
2
+ /**
3
+ * Gemini CLI as a plain model for pi (ADR 0037): the pure half of the
4
+ * bridge's `/v1/chat/completions` route. pi's OpenAI-shaped request becomes
5
+ * a system file (pi's own system prompt, a text tool-call protocol and pi's
6
+ * tool schemas) plus a transcript for one `gemini` call; Gemini's reply
7
+ * becomes an OpenAI completion carrying either text or one tool call. The
8
+ * runner owns the process and the HTTP; nothing here does I/O.
9
+ */
10
+ export interface BridgeTool {
11
+ readonly name: string;
12
+ readonly description: string;
13
+ readonly parameters: JsonValue;
14
+ }
15
+ /** One message of pi's request, as the transcript needs it. */
16
+ export type BridgeMessage = {
17
+ readonly role: "user";
18
+ readonly text: string;
19
+ } | {
20
+ readonly role: "assistant";
21
+ readonly text: string;
22
+ readonly toolCalls: ReadonlyArray<{
23
+ readonly id: string;
24
+ readonly name: string;
25
+ readonly arguments: string;
26
+ }>;
27
+ } | {
28
+ readonly role: "tool";
29
+ readonly callId: string;
30
+ readonly text: string;
31
+ };
32
+ export type ChatRequest = {
33
+ readonly _tag: "Request";
34
+ readonly model: string;
35
+ readonly stream: boolean;
36
+ readonly system: string;
37
+ readonly tools: ReadonlyArray<BridgeTool>;
38
+ readonly messages: ReadonlyArray<BridgeMessage>;
39
+ } | {
40
+ readonly _tag: "Invalid";
41
+ readonly reason: string;
42
+ };
43
+ /** pi's OpenAI chat-completions body, read once at the boundary. */
44
+ export declare const chatRequestFrom: (body: JsonValue) => ChatRequest;
45
+ /** The protocol the model answers in: one `<tool_call>` block, or plain text. */
46
+ export declare const toolCallProtocol: string;
47
+ /**
48
+ * The file `GEMINI_SYSTEM_MD` points at: pi's system prompt, the protocol,
49
+ * every tool with its schema, and how the conversation is laid out. It
50
+ * replaces Gemini CLI's own system prompt entirely.
51
+ */
52
+ export declare const systemFileFor: (request: Extract<ChatRequest, {
53
+ _tag: "Request";
54
+ }>) => string;
55
+ /** The conversation as the prompt of one `gemini` call. */
56
+ export declare const transcriptFor: (messages: ReadonlyArray<BridgeMessage>) => string;
57
+ export type ModelReply = {
58
+ readonly _tag: "Text";
59
+ readonly text: string;
60
+ } | {
61
+ readonly _tag: "ToolCall";
62
+ /** Whatever the model wrote before the block. */
63
+ readonly text: string;
64
+ readonly name: string;
65
+ readonly arguments: JsonRecord;
66
+ } | {
67
+ readonly _tag: "Malformed";
68
+ readonly text: string;
69
+ readonly error: string;
70
+ };
71
+ /** Gemini's reply as text, one tool call, or a block that is not a valid call. */
72
+ export declare const parseModelReply: (reply: string, toolNames: ReadonlyArray<string>) => ModelReply;
73
+ /** The one retry after a malformed call: the model sees its reply and why it failed. */
74
+ export declare const correctionTranscript: (transcript: string, reply: string, error: string) => string;
75
+ /** Gemini CLI's stats as llm4ts reads them: input is the uncached prompt. */
76
+ export interface GeminiTokens {
77
+ readonly inputTokens?: number;
78
+ readonly outputTokens?: number;
79
+ readonly cached?: number;
80
+ }
81
+ export interface OpenAiUsage {
82
+ readonly prompt_tokens: number;
83
+ readonly completion_tokens: number;
84
+ readonly total_tokens: number;
85
+ readonly prompt_tokens_details: {
86
+ readonly cached_tokens: number;
87
+ };
88
+ }
89
+ /** OpenAI's usage: the prompt includes cached tokens, which are also reported apart. */
90
+ export declare const openAiUsage: (tokens: GeminiTokens | undefined) => OpenAiUsage;
91
+ /** A non-streaming completion. A malformed reply that survived the retry goes back as text. */
92
+ export declare const completionJson: (id: string, model: string, reply: ModelReply, usage: OpenAiUsage, created: number, callId: string) => JsonRecord;
93
+ /**
94
+ * The same completion as stream chunks: the role, the text, the tool call,
95
+ * the finish reason, then the usage (`stream_options.include_usage`'s
96
+ * shape). The reply is whole before the first chunk: Gemini CLI runs to
97
+ * its end before the bridge parses it.
98
+ */
99
+ export declare const completionChunks: (id: string, model: string, reply: ModelReply, usage: OpenAiUsage, created: number, callId: string) => ReadonlyArray<JsonRecord>;
100
+ /**
101
+ * The admin policy that takes every tool out of the model's view: a deny
102
+ * rule without an argument pattern excludes the tool entirely, and `*`
103
+ * matches every tool, sub-agents included (probe runs 2–4, ADR 0037).
104
+ */
105
+ export declare const denyAllToolsPolicyToml: string;
106
+ /** Gemini CLI's system settings for a model-mode call: no sub-agents. */
107
+ export declare const modelModeSettingsJson: string;
108
+ //# sourceMappingURL=GeminiModelBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiModelBridge.d.ts","sourceRoot":"","sources":["../../src/providers/GeminiModelBridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,256 @@
1
+ import { isJsonRecord, jsonArray, jsonField, jsonStringField, jsonText, parseJsonLine } from "./CliSupport.js";
2
+ /** A message's text: a string, or the text parts of a content list (images are named, not sent). */
3
+ const contentText = (content) => {
4
+ if (typeof content === "string") {
5
+ return content;
6
+ }
7
+ return jsonArray(content)
8
+ .map((part) => jsonStringField(part, "type") === "text"
9
+ ? (jsonStringField(part, "text") ?? "")
10
+ : `[${jsonStringField(part, "type") ?? "part"} omitted]`)
11
+ .join("\n");
12
+ };
13
+ /** pi's OpenAI chat-completions body, read once at the boundary. */
14
+ export const chatRequestFrom = (body) => {
15
+ const model = jsonStringField(body, "model");
16
+ const raw = jsonField(body, "messages");
17
+ if (model === undefined || !Array.isArray(raw)) {
18
+ return { _tag: "Invalid", reason: "a chat request needs a model and a messages array" };
19
+ }
20
+ const system = [];
21
+ const messages = [];
22
+ for (const message of jsonArray(raw)) {
23
+ const role = jsonStringField(message, "role");
24
+ const text = contentText(jsonField(message, "content"));
25
+ if (role === "system" || role === "developer") {
26
+ system.push(text);
27
+ }
28
+ else if (role === "user") {
29
+ messages.push({ role: "user", text });
30
+ }
31
+ else if (role === "assistant") {
32
+ messages.push({
33
+ role: "assistant",
34
+ text,
35
+ toolCalls: jsonArray(jsonField(message, "tool_calls")).map((call) => ({
36
+ id: jsonStringField(call, "id") ?? "",
37
+ name: jsonStringField(jsonField(call, "function"), "name") ?? "",
38
+ arguments: jsonStringField(jsonField(call, "function"), "arguments") ?? "{}"
39
+ }))
40
+ });
41
+ }
42
+ else if (role === "tool") {
43
+ messages.push({ role: "tool", callId: jsonStringField(message, "tool_call_id") ?? "", text });
44
+ }
45
+ }
46
+ const tools = jsonArray(jsonField(body, "tools")).flatMap((tool) => {
47
+ const fn = jsonField(tool, "function");
48
+ const name = jsonStringField(fn, "name");
49
+ return name === undefined
50
+ ? []
51
+ : [
52
+ {
53
+ name,
54
+ description: jsonStringField(fn, "description") ?? "",
55
+ parameters: jsonField(fn, "parameters") ?? {}
56
+ }
57
+ ];
58
+ });
59
+ return {
60
+ _tag: "Request",
61
+ model,
62
+ stream: jsonField(body, "stream") === true,
63
+ system: system.filter((part) => part.trim().length > 0).join("\n\n"),
64
+ tools,
65
+ messages
66
+ };
67
+ };
68
+ /** The protocol the model answers in: one `<tool_call>` block, or plain text. */
69
+ export const toolCallProtocol = [
70
+ "## Tools",
71
+ "",
72
+ "You cannot act yourself: you ask the caller to run a tool, and its result comes back as",
73
+ "the next message. To call a tool, reply with exactly one block and nothing after it:",
74
+ "",
75
+ '<tool_call>{"name": "<tool>", "arguments": {...}}</tool_call>',
76
+ "",
77
+ "The arguments are a JSON object matching the tool's parameters. Call one tool at a time.",
78
+ "When no tool is needed, reply in plain text without any <tool_call> block."
79
+ ].join("\n");
80
+ /**
81
+ * The file `GEMINI_SYSTEM_MD` points at: pi's system prompt, the protocol,
82
+ * every tool with its schema, and how the conversation is laid out. It
83
+ * replaces Gemini CLI's own system prompt entirely.
84
+ */
85
+ export const systemFileFor = (request) => [
86
+ request.system,
87
+ toolCallProtocol,
88
+ "",
89
+ "Available tools:",
90
+ ...request.tools.map((tool) => `- ${tool.name}: ${tool.description.trim()}\n parameters (JSON Schema): ${jsonText(tool.parameters)}`),
91
+ "",
92
+ "## The conversation",
93
+ "",
94
+ "The conversation so far is given as a transcript of [user], [assistant] and [tool result]",
95
+ "sections, oldest first. Reply with your next assistant message only, without a section label."
96
+ ]
97
+ .filter((part) => part !== undefined)
98
+ .join("\n")
99
+ .trim();
100
+ /** Normalised JSON of a call's arguments, so past calls read like the protocol asks. */
101
+ const argumentsJson = (raw) => {
102
+ try {
103
+ const parsed = JSON.parse(raw);
104
+ return JSON.stringify(parsed);
105
+ }
106
+ catch {
107
+ return raw;
108
+ }
109
+ };
110
+ /** The conversation as the prompt of one `gemini` call. */
111
+ export const transcriptFor = (messages) => {
112
+ const names = new Map();
113
+ const sections = messages.map((message) => {
114
+ switch (message.role) {
115
+ case "user":
116
+ return `[user]\n${message.text}`;
117
+ case "assistant": {
118
+ const calls = message.toolCalls.map((call) => {
119
+ names.set(call.id, call.name);
120
+ return `<tool_call>{"name":${JSON.stringify(call.name)},"arguments":${argumentsJson(call.arguments)}}</tool_call>`;
121
+ });
122
+ return `[assistant]\n${[message.text, ...calls].filter((part) => part.trim().length > 0).join("\n")}`;
123
+ }
124
+ case "tool":
125
+ return `[tool result for ${names.get(message.callId) ?? "a tool"} (${message.callId})]\n${message.text}`;
126
+ }
127
+ });
128
+ return `${sections.join("\n\n")}\n`;
129
+ };
130
+ /** Gemini's reply as text, one tool call, or a block that is not a valid call. */
131
+ export const parseModelReply = (reply, toolNames) => {
132
+ const opened = reply.indexOf("<tool_call>");
133
+ if (opened < 0) {
134
+ return { _tag: "Text", text: reply.trim() };
135
+ }
136
+ const before = reply.slice(0, opened).trim();
137
+ const closed = reply.indexOf("</tool_call>", opened);
138
+ if (closed < 0) {
139
+ return { _tag: "Malformed", text: reply, error: "the <tool_call> block is not closed" };
140
+ }
141
+ const json = parseJsonLine(reply.slice(opened + "<tool_call>".length, closed).trim());
142
+ if (json === undefined || !isJsonRecord(json)) {
143
+ return { _tag: "Malformed", text: reply, error: "the block is not a valid JSON object" };
144
+ }
145
+ const name = jsonStringField(json, "name");
146
+ if (name === undefined || !toolNames.includes(name)) {
147
+ return {
148
+ _tag: "Malformed",
149
+ text: reply,
150
+ error: `"${name ?? "(no name)"}" is not one of the available tools (${toolNames.join(", ")})`
151
+ };
152
+ }
153
+ const args = jsonField(json, "arguments") ?? {};
154
+ return isJsonRecord(args)
155
+ ? { _tag: "ToolCall", text: before, name, arguments: args }
156
+ : { _tag: "Malformed", text: reply, error: "arguments is not a JSON object" };
157
+ };
158
+ /** The one retry after a malformed call: the model sees its reply and why it failed. */
159
+ export const correctionTranscript = (transcript, reply, error) => [
160
+ transcript.trimEnd(),
161
+ "",
162
+ `[assistant]\n${reply.trim()}`,
163
+ "",
164
+ `[user]\nYour last reply could not be used: ${error}. Reply again with exactly one valid <tool_call> block, or with plain text if no tool is needed.`,
165
+ ""
166
+ ].join("\n");
167
+ /** OpenAI's usage: the prompt includes cached tokens, which are also reported apart. */
168
+ export const openAiUsage = (tokens) => {
169
+ const cached = tokens?.cached ?? 0;
170
+ const prompt = (tokens?.inputTokens ?? 0) + cached;
171
+ const completion = tokens?.outputTokens ?? 0;
172
+ return {
173
+ prompt_tokens: prompt,
174
+ completion_tokens: completion,
175
+ total_tokens: prompt + completion,
176
+ prompt_tokens_details: { cached_tokens: cached }
177
+ };
178
+ };
179
+ const usageJson = (usage) => ({
180
+ prompt_tokens: usage.prompt_tokens,
181
+ completion_tokens: usage.completion_tokens,
182
+ total_tokens: usage.total_tokens,
183
+ prompt_tokens_details: { cached_tokens: usage.prompt_tokens_details.cached_tokens }
184
+ });
185
+ const replyText = (reply) => reply.text;
186
+ const toolCallJson = (reply, callId) => reply._tag === "ToolCall"
187
+ ? [
188
+ {
189
+ id: callId,
190
+ type: "function",
191
+ function: { name: reply.name, arguments: JSON.stringify(reply.arguments) }
192
+ }
193
+ ]
194
+ : [];
195
+ /** A non-streaming completion. A malformed reply that survived the retry goes back as text. */
196
+ export const completionJson = (id, model, reply, usage, created, callId) => {
197
+ const calls = toolCallJson(reply, callId);
198
+ const text = replyText(reply);
199
+ return {
200
+ id,
201
+ object: "chat.completion",
202
+ created,
203
+ model,
204
+ choices: [
205
+ {
206
+ index: 0,
207
+ message: {
208
+ role: "assistant",
209
+ content: text.length === 0 ? null : text,
210
+ ...(calls.length === 0 ? {} : { tool_calls: calls })
211
+ },
212
+ finish_reason: calls.length === 0 ? "stop" : "tool_calls"
213
+ }
214
+ ],
215
+ usage: usageJson(usage)
216
+ };
217
+ };
218
+ /**
219
+ * The same completion as stream chunks: the role, the text, the tool call,
220
+ * the finish reason, then the usage (`stream_options.include_usage`'s
221
+ * shape). The reply is whole before the first chunk: Gemini CLI runs to
222
+ * its end before the bridge parses it.
223
+ */
224
+ export const completionChunks = (id, model, reply, usage, created, callId) => {
225
+ const chunk = (delta, finish) => ({
226
+ id,
227
+ object: "chat.completion.chunk",
228
+ created,
229
+ model,
230
+ choices: [{ index: 0, delta, finish_reason: finish }]
231
+ });
232
+ const text = replyText(reply);
233
+ const calls = toolCallJson(reply, callId);
234
+ return [
235
+ chunk({ role: "assistant", content: "" }, null),
236
+ ...(text.length === 0 ? [] : [chunk({ content: text }, null)]),
237
+ ...calls.map((call, index) => chunk({ tool_calls: [{ index, ...call }] }, null)),
238
+ chunk({}, calls.length === 0 ? "stop" : "tool_calls"),
239
+ { id, object: "chat.completion.chunk", created, model, choices: [], usage: usageJson(usage) }
240
+ ];
241
+ };
242
+ /**
243
+ * The admin policy that takes every tool out of the model's view: a deny
244
+ * rule without an argument pattern excludes the tool entirely, and `*`
245
+ * matches every tool, sub-agents included (probe runs 2–4, ADR 0037).
246
+ */
247
+ export const denyAllToolsPolicyToml = [
248
+ "[[rule]]",
249
+ 'toolName = "*"',
250
+ 'decision = "deny"',
251
+ "priority = 999",
252
+ ""
253
+ ].join("\n");
254
+ /** Gemini CLI's system settings for a model-mode call: no sub-agents. */
255
+ export const modelModeSettingsJson = JSON.stringify({ experimental: { enableAgents: false } });
256
+ //# sourceMappingURL=GeminiModelBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiModelBridge.js","sourceRoot":"","sources":["../../src/providers/GeminiModelBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,eAAe,EACf,QAAQ,EACR,aAAa,EAGd,MAAM,iBAAiB,CAAA;AA0CxB,oGAAoG;AACpG,MAAM,WAAW,GAAG,CAAC,OAA8B,EAAU,EAAE;IAC7D,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,OAAO,SAAS,CAAC,OAAO,CAAC;SACtB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACZ,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,MAAM;QACtC,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QACvC,CAAC,CAAC,IAAI,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,WAAW,CAC3D;SACA,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC,CAAA;AAED,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAe,EAAe,EAAE;IAC9D,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACvC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,mDAAmD,EAAE,CAAA;IACzF,CAAC;IACD,MAAM,MAAM,GAAkB,EAAE,CAAA;IAChC,MAAM,QAAQ,GAAyB,EAAE,CAAA;IACzC,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC7C,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;QACvD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YAC9C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;aAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACvC,CAAC;aAAM,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,WAAW;gBACjB,IAAI;gBACJ,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;oBACpE,EAAE,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;oBACrC,IAAI,EAAE,eAAe,CAAC,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE;oBAChE,SAAS,EAAE,eAAe,CAAC,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,IAAI,IAAI;iBAC7E,CAAC,CAAC;aACJ,CAAC,CAAA;QACJ,CAAC;aAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/F,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAA6B,EAAE;QAC5F,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;QACtC,MAAM,IAAI,GAAG,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QACxC,OAAO,IAAI,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE;oBACE,IAAI;oBACJ,WAAW,EAAE,eAAe,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE;oBACrD,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE;iBAC9C;aACF,CAAA;IACP,CAAC,CAAC,CAAA;IACF,OAAO;QACL,IAAI,EAAE,SAAS;QACf,KAAK;QACL,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,IAAI;QAC1C,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QACpE,KAAK;QACL,QAAQ;KACT,CAAA;AACH,CAAC,CAAA;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,UAAU;IACV,EAAE;IACF,yFAAyF;IACzF,sFAAsF;IACtF,EAAE;IACF,+DAA+D;IAC/D,EAAE;IACF,0FAA0F;IAC1F,4EAA4E;CAC7E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAkD,EAAU,EAAE,CAC1F;IACE,OAAO,CAAC,MAAM;IACd,gBAAgB;IAChB,EAAE;IACF,kBAAkB;IAClB,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAClB,CAAC,IAAI,EAAE,EAAE,CACP,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,iCAAiC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CACzG;IACD,EAAE;IACF,qBAAqB;IACrB,EAAE;IACF,2FAA2F;IAC3F,+FAA+F;CAChG;KACE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC;KACpC,IAAI,CAAC,IAAI,CAAC;KACV,IAAI,EAAE,CAAA;AAEX,wFAAwF;AACxF,MAAM,aAAa,GAAG,CAAC,GAAW,EAAU,EAAE;IAC5C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACvC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;AACH,CAAC,CAAA;AAED,2DAA2D;AAC3D,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAsC,EAAU,EAAE;IAC9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACxC,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,KAAK,MAAM;gBACT,OAAO,WAAW,OAAO,CAAC,IAAI,EAAE,CAAA;YAClC,KAAK,WAAW,CAAC,CAAC,CAAC;gBACjB,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;oBAC3C,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;oBAC7B,OAAO,sBAAsB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAA;gBACpH,CAAC,CAAC,CAAA;gBACF,OAAO,gBAAgB,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;YACvG,CAAC;YACD,KAAK,MAAM;gBACT,OAAO,oBAAoB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,QAAQ,KAAK,OAAO,CAAC,MAAM,OAAO,OAAO,CAAC,IAAI,EAAE,CAAA;QAC5G,CAAC;IACH,CAAC,CAAC,CAAA;IACF,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;AACrC,CAAC,CAAA;AAaD,kFAAkF;AAClF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAa,EAAE,SAAgC,EAAc,EAAE;IAC7F,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IAC3C,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,CAAA;IAC7C,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;IACpD,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAA;IACzF,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACrF,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAA;IAC1F,CAAC;IACD,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAC1C,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,IAAI,IAAI,IAAI,WAAW,wCAAwC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SAC9F,CAAA;IACH,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,CAAA;IAC/C,OAAO,YAAY,CAAC,IAAI,CAAC;QACvB,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;QAC3D,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAA;AACjF,CAAC,CAAA;AAED,wFAAwF;AACxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAkB,EAAE,KAAa,EAAE,KAAa,EAAU,EAAE,CAC/F;IACE,UAAU,CAAC,OAAO,EAAE;IACpB,EAAE;IACF,gBAAgB,KAAK,CAAC,IAAI,EAAE,EAAE;IAC9B,EAAE;IACF,8CAA8C,KAAK,kGAAkG;IACrJ,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAgBd,wFAAwF;AACxF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAgC,EAAe,EAAE;IAC3E,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,CAAC,CAAA;IAClC,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,IAAI,CAAC,CAAC,GAAG,MAAM,CAAA;IAClD,MAAM,UAAU,GAAG,MAAM,EAAE,YAAY,IAAI,CAAC,CAAA;IAC5C,OAAO;QACL,aAAa,EAAE,MAAM;QACrB,iBAAiB,EAAE,UAAU;QAC7B,YAAY,EAAE,MAAM,GAAG,UAAU;QACjC,qBAAqB,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE;KACjD,CAAA;AACH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,KAAkB,EAAc,EAAE,CAAC,CAAC;IACrD,aAAa,EAAE,KAAK,CAAC,aAAa;IAClC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;IAC1C,YAAY,EAAE,KAAK,CAAC,YAAY;IAChC,qBAAqB,EAAE,EAAE,aAAa,EAAE,KAAK,CAAC,qBAAqB,CAAC,aAAa,EAAE;CACpF,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,CAAC,KAAiB,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAA;AAE3D,MAAM,YAAY,GAAG,CAAC,KAAiB,EAAE,MAAc,EAA6B,EAAE,CACpF,KAAK,CAAC,IAAI,KAAK,UAAU;IACvB,CAAC,CAAC;QACE;YACE,EAAE,EAAE,MAAM;YACV,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;SAC3E;KACF;IACH,CAAC,CAAC,EAAE,CAAA;AAER,+FAA+F;AAC/F,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,EAAU,EACV,KAAa,EACb,KAAiB,EACjB,KAAkB,EAClB,OAAe,EACf,MAAc,EACF,EAAE;IACd,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACzC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC7B,OAAO;QACL,EAAE;QACF,MAAM,EAAE,iBAAiB;QACzB,OAAO;QACP,KAAK;QACL,OAAO,EAAE;YACP;gBACE,KAAK,EAAE,CAAC;gBACR,OAAO,EAAE;oBACP,IAAI,EAAE,WAAW;oBACjB,OAAO,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;oBACxC,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;iBACrD;gBACD,aAAa,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY;aAC1D;SACF;QACD,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC;KACxB,CAAA;AACH,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,EAAU,EACV,KAAa,EACb,KAAiB,EACjB,KAAkB,EAClB,OAAe,EACf,MAAc,EACa,EAAE;IAC7B,MAAM,KAAK,GAAG,CAAC,KAAiB,EAAE,MAAqB,EAAc,EAAE,CAAC,CAAC;QACvE,EAAE;QACF,MAAM,EAAE,uBAAuB;QAC/B,OAAO;QACP,KAAK;QACL,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;KACtD,CAAC,CAAA;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACzC,OAAO;QACL,KAAK,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC;QAC/C,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9D,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAChF,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC;QACrD,EAAE,EAAE,EAAE,MAAM,EAAE,uBAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE;KAC9F,CAAA;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,UAAU;IACV,gBAAgB;IAChB,mBAAmB;IACnB,gBAAgB;IAChB,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,YAAY,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/core",
3
- "version": "2.42.0",
3
+ "version": "2.43.0",
4
4
  "description": "Effect-native LLM contracts, providers, tools, evaluation, and observability",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -59,6 +59,7 @@
59
59
  "./providers/GeminiAcpSession": "./dist/providers/GeminiAcpSession.js",
60
60
  "./providers/GeminiApiProvider": "./dist/providers/GeminiApiProvider.js",
61
61
  "./providers/GeminiCliProvider": "./dist/providers/GeminiCliProvider.js",
62
+ "./providers/GeminiModelBridge": "./dist/providers/GeminiModelBridge.js",
62
63
  "./providers/GeminiModels": "./dist/providers/GeminiModels.js",
63
64
  "./providers/LmStudioModels": "./dist/providers/LmStudioModels.js",
64
65
  "./providers/LmStudioProvider": "./dist/providers/LmStudioProvider.js",
@@ -0,0 +1,363 @@
1
+ import {
2
+ isJsonRecord,
3
+ jsonArray,
4
+ jsonField,
5
+ jsonStringField,
6
+ jsonText,
7
+ parseJsonLine,
8
+ type JsonRecord,
9
+ type JsonValue
10
+ } from "./CliSupport.ts"
11
+
12
+ /**
13
+ * Gemini CLI as a plain model for pi (ADR 0037): the pure half of the
14
+ * bridge's `/v1/chat/completions` route. pi's OpenAI-shaped request becomes
15
+ * a system file (pi's own system prompt, a text tool-call protocol and pi's
16
+ * tool schemas) plus a transcript for one `gemini` call; Gemini's reply
17
+ * becomes an OpenAI completion carrying either text or one tool call. The
18
+ * runner owns the process and the HTTP; nothing here does I/O.
19
+ */
20
+
21
+ export interface BridgeTool {
22
+ readonly name: string
23
+ readonly description: string
24
+ readonly parameters: JsonValue
25
+ }
26
+
27
+ /** One message of pi's request, as the transcript needs it. */
28
+ export type BridgeMessage =
29
+ | { readonly role: "user"; readonly text: string }
30
+ | {
31
+ readonly role: "assistant"
32
+ readonly text: string
33
+ readonly toolCalls: ReadonlyArray<{
34
+ readonly id: string
35
+ readonly name: string
36
+ readonly arguments: string
37
+ }>
38
+ }
39
+ | { readonly role: "tool"; readonly callId: string; readonly text: string }
40
+
41
+ export type ChatRequest =
42
+ | {
43
+ readonly _tag: "Request"
44
+ readonly model: string
45
+ readonly stream: boolean
46
+ readonly system: string
47
+ readonly tools: ReadonlyArray<BridgeTool>
48
+ readonly messages: ReadonlyArray<BridgeMessage>
49
+ }
50
+ | { readonly _tag: "Invalid"; readonly reason: string }
51
+
52
+ /** A message's text: a string, or the text parts of a content list (images are named, not sent). */
53
+ const contentText = (content: JsonValue | undefined): string => {
54
+ if (typeof content === "string") {
55
+ return content
56
+ }
57
+ return jsonArray(content)
58
+ .map((part) =>
59
+ jsonStringField(part, "type") === "text"
60
+ ? (jsonStringField(part, "text") ?? "")
61
+ : `[${jsonStringField(part, "type") ?? "part"} omitted]`
62
+ )
63
+ .join("\n")
64
+ }
65
+
66
+ /** pi's OpenAI chat-completions body, read once at the boundary. */
67
+ export const chatRequestFrom = (body: JsonValue): ChatRequest => {
68
+ const model = jsonStringField(body, "model")
69
+ const raw = jsonField(body, "messages")
70
+ if (model === undefined || !Array.isArray(raw)) {
71
+ return { _tag: "Invalid", reason: "a chat request needs a model and a messages array" }
72
+ }
73
+ const system: Array<string> = []
74
+ const messages: Array<BridgeMessage> = []
75
+ for (const message of jsonArray(raw)) {
76
+ const role = jsonStringField(message, "role")
77
+ const text = contentText(jsonField(message, "content"))
78
+ if (role === "system" || role === "developer") {
79
+ system.push(text)
80
+ } else if (role === "user") {
81
+ messages.push({ role: "user", text })
82
+ } else if (role === "assistant") {
83
+ messages.push({
84
+ role: "assistant",
85
+ text,
86
+ toolCalls: jsonArray(jsonField(message, "tool_calls")).map((call) => ({
87
+ id: jsonStringField(call, "id") ?? "",
88
+ name: jsonStringField(jsonField(call, "function"), "name") ?? "",
89
+ arguments: jsonStringField(jsonField(call, "function"), "arguments") ?? "{}"
90
+ }))
91
+ })
92
+ } else if (role === "tool") {
93
+ messages.push({ role: "tool", callId: jsonStringField(message, "tool_call_id") ?? "", text })
94
+ }
95
+ }
96
+ const tools = jsonArray(jsonField(body, "tools")).flatMap((tool): ReadonlyArray<BridgeTool> => {
97
+ const fn = jsonField(tool, "function")
98
+ const name = jsonStringField(fn, "name")
99
+ return name === undefined
100
+ ? []
101
+ : [
102
+ {
103
+ name,
104
+ description: jsonStringField(fn, "description") ?? "",
105
+ parameters: jsonField(fn, "parameters") ?? {}
106
+ }
107
+ ]
108
+ })
109
+ return {
110
+ _tag: "Request",
111
+ model,
112
+ stream: jsonField(body, "stream") === true,
113
+ system: system.filter((part) => part.trim().length > 0).join("\n\n"),
114
+ tools,
115
+ messages
116
+ }
117
+ }
118
+
119
+ /** The protocol the model answers in: one `<tool_call>` block, or plain text. */
120
+ export const toolCallProtocol = [
121
+ "## Tools",
122
+ "",
123
+ "You cannot act yourself: you ask the caller to run a tool, and its result comes back as",
124
+ "the next message. To call a tool, reply with exactly one block and nothing after it:",
125
+ "",
126
+ '<tool_call>{"name": "<tool>", "arguments": {...}}</tool_call>',
127
+ "",
128
+ "The arguments are a JSON object matching the tool's parameters. Call one tool at a time.",
129
+ "When no tool is needed, reply in plain text without any <tool_call> block."
130
+ ].join("\n")
131
+
132
+ /**
133
+ * The file `GEMINI_SYSTEM_MD` points at: pi's system prompt, the protocol,
134
+ * every tool with its schema, and how the conversation is laid out. It
135
+ * replaces Gemini CLI's own system prompt entirely.
136
+ */
137
+ export const systemFileFor = (request: Extract<ChatRequest, { _tag: "Request" }>): string =>
138
+ [
139
+ request.system,
140
+ toolCallProtocol,
141
+ "",
142
+ "Available tools:",
143
+ ...request.tools.map(
144
+ (tool) =>
145
+ `- ${tool.name}: ${tool.description.trim()}\n parameters (JSON Schema): ${jsonText(tool.parameters)}`
146
+ ),
147
+ "",
148
+ "## The conversation",
149
+ "",
150
+ "The conversation so far is given as a transcript of [user], [assistant] and [tool result]",
151
+ "sections, oldest first. Reply with your next assistant message only, without a section label."
152
+ ]
153
+ .filter((part) => part !== undefined)
154
+ .join("\n")
155
+ .trim()
156
+
157
+ /** Normalised JSON of a call's arguments, so past calls read like the protocol asks. */
158
+ const argumentsJson = (raw: string): string => {
159
+ try {
160
+ const parsed: unknown = JSON.parse(raw)
161
+ return JSON.stringify(parsed)
162
+ } catch {
163
+ return raw
164
+ }
165
+ }
166
+
167
+ /** The conversation as the prompt of one `gemini` call. */
168
+ export const transcriptFor = (messages: ReadonlyArray<BridgeMessage>): string => {
169
+ const names = new Map<string, string>()
170
+ const sections = messages.map((message) => {
171
+ switch (message.role) {
172
+ case "user":
173
+ return `[user]\n${message.text}`
174
+ case "assistant": {
175
+ const calls = message.toolCalls.map((call) => {
176
+ names.set(call.id, call.name)
177
+ return `<tool_call>{"name":${JSON.stringify(call.name)},"arguments":${argumentsJson(call.arguments)}}</tool_call>`
178
+ })
179
+ return `[assistant]\n${[message.text, ...calls].filter((part) => part.trim().length > 0).join("\n")}`
180
+ }
181
+ case "tool":
182
+ return `[tool result for ${names.get(message.callId) ?? "a tool"} (${message.callId})]\n${message.text}`
183
+ }
184
+ })
185
+ return `${sections.join("\n\n")}\n`
186
+ }
187
+
188
+ export type ModelReply =
189
+ | { readonly _tag: "Text"; readonly text: string }
190
+ | {
191
+ readonly _tag: "ToolCall"
192
+ /** Whatever the model wrote before the block. */
193
+ readonly text: string
194
+ readonly name: string
195
+ readonly arguments: JsonRecord
196
+ }
197
+ | { readonly _tag: "Malformed"; readonly text: string; readonly error: string }
198
+
199
+ /** Gemini's reply as text, one tool call, or a block that is not a valid call. */
200
+ export const parseModelReply = (reply: string, toolNames: ReadonlyArray<string>): ModelReply => {
201
+ const opened = reply.indexOf("<tool_call>")
202
+ if (opened < 0) {
203
+ return { _tag: "Text", text: reply.trim() }
204
+ }
205
+ const before = reply.slice(0, opened).trim()
206
+ const closed = reply.indexOf("</tool_call>", opened)
207
+ if (closed < 0) {
208
+ return { _tag: "Malformed", text: reply, error: "the <tool_call> block is not closed" }
209
+ }
210
+ const json = parseJsonLine(reply.slice(opened + "<tool_call>".length, closed).trim())
211
+ if (json === undefined || !isJsonRecord(json)) {
212
+ return { _tag: "Malformed", text: reply, error: "the block is not a valid JSON object" }
213
+ }
214
+ const name = jsonStringField(json, "name")
215
+ if (name === undefined || !toolNames.includes(name)) {
216
+ return {
217
+ _tag: "Malformed",
218
+ text: reply,
219
+ error: `"${name ?? "(no name)"}" is not one of the available tools (${toolNames.join(", ")})`
220
+ }
221
+ }
222
+ const args = jsonField(json, "arguments") ?? {}
223
+ return isJsonRecord(args)
224
+ ? { _tag: "ToolCall", text: before, name, arguments: args }
225
+ : { _tag: "Malformed", text: reply, error: "arguments is not a JSON object" }
226
+ }
227
+
228
+ /** The one retry after a malformed call: the model sees its reply and why it failed. */
229
+ export const correctionTranscript = (transcript: string, reply: string, error: string): string =>
230
+ [
231
+ transcript.trimEnd(),
232
+ "",
233
+ `[assistant]\n${reply.trim()}`,
234
+ "",
235
+ `[user]\nYour last reply could not be used: ${error}. Reply again with exactly one valid <tool_call> block, or with plain text if no tool is needed.`,
236
+ ""
237
+ ].join("\n")
238
+
239
+ /** Gemini CLI's stats as llm4ts reads them: input is the uncached prompt. */
240
+ export interface GeminiTokens {
241
+ readonly inputTokens?: number
242
+ readonly outputTokens?: number
243
+ readonly cached?: number
244
+ }
245
+
246
+ export interface OpenAiUsage {
247
+ readonly prompt_tokens: number
248
+ readonly completion_tokens: number
249
+ readonly total_tokens: number
250
+ readonly prompt_tokens_details: { readonly cached_tokens: number }
251
+ }
252
+
253
+ /** OpenAI's usage: the prompt includes cached tokens, which are also reported apart. */
254
+ export const openAiUsage = (tokens: GeminiTokens | undefined): OpenAiUsage => {
255
+ const cached = tokens?.cached ?? 0
256
+ const prompt = (tokens?.inputTokens ?? 0) + cached
257
+ const completion = tokens?.outputTokens ?? 0
258
+ return {
259
+ prompt_tokens: prompt,
260
+ completion_tokens: completion,
261
+ total_tokens: prompt + completion,
262
+ prompt_tokens_details: { cached_tokens: cached }
263
+ }
264
+ }
265
+
266
+ const usageJson = (usage: OpenAiUsage): JsonRecord => ({
267
+ prompt_tokens: usage.prompt_tokens,
268
+ completion_tokens: usage.completion_tokens,
269
+ total_tokens: usage.total_tokens,
270
+ prompt_tokens_details: { cached_tokens: usage.prompt_tokens_details.cached_tokens }
271
+ })
272
+
273
+ const replyText = (reply: ModelReply): string => reply.text
274
+
275
+ const toolCallJson = (reply: ModelReply, callId: string): ReadonlyArray<JsonRecord> =>
276
+ reply._tag === "ToolCall"
277
+ ? [
278
+ {
279
+ id: callId,
280
+ type: "function",
281
+ function: { name: reply.name, arguments: JSON.stringify(reply.arguments) }
282
+ }
283
+ ]
284
+ : []
285
+
286
+ /** A non-streaming completion. A malformed reply that survived the retry goes back as text. */
287
+ export const completionJson = (
288
+ id: string,
289
+ model: string,
290
+ reply: ModelReply,
291
+ usage: OpenAiUsage,
292
+ created: number,
293
+ callId: string
294
+ ): JsonRecord => {
295
+ const calls = toolCallJson(reply, callId)
296
+ const text = replyText(reply)
297
+ return {
298
+ id,
299
+ object: "chat.completion",
300
+ created,
301
+ model,
302
+ choices: [
303
+ {
304
+ index: 0,
305
+ message: {
306
+ role: "assistant",
307
+ content: text.length === 0 ? null : text,
308
+ ...(calls.length === 0 ? {} : { tool_calls: calls })
309
+ },
310
+ finish_reason: calls.length === 0 ? "stop" : "tool_calls"
311
+ }
312
+ ],
313
+ usage: usageJson(usage)
314
+ }
315
+ }
316
+
317
+ /**
318
+ * The same completion as stream chunks: the role, the text, the tool call,
319
+ * the finish reason, then the usage (`stream_options.include_usage`'s
320
+ * shape). The reply is whole before the first chunk: Gemini CLI runs to
321
+ * its end before the bridge parses it.
322
+ */
323
+ export const completionChunks = (
324
+ id: string,
325
+ model: string,
326
+ reply: ModelReply,
327
+ usage: OpenAiUsage,
328
+ created: number,
329
+ callId: string
330
+ ): ReadonlyArray<JsonRecord> => {
331
+ const chunk = (delta: JsonRecord, finish: string | null): JsonRecord => ({
332
+ id,
333
+ object: "chat.completion.chunk",
334
+ created,
335
+ model,
336
+ choices: [{ index: 0, delta, finish_reason: finish }]
337
+ })
338
+ const text = replyText(reply)
339
+ const calls = toolCallJson(reply, callId)
340
+ return [
341
+ chunk({ role: "assistant", content: "" }, null),
342
+ ...(text.length === 0 ? [] : [chunk({ content: text }, null)]),
343
+ ...calls.map((call, index) => chunk({ tool_calls: [{ index, ...call }] }, null)),
344
+ chunk({}, calls.length === 0 ? "stop" : "tool_calls"),
345
+ { id, object: "chat.completion.chunk", created, model, choices: [], usage: usageJson(usage) }
346
+ ]
347
+ }
348
+
349
+ /**
350
+ * The admin policy that takes every tool out of the model's view: a deny
351
+ * rule without an argument pattern excludes the tool entirely, and `*`
352
+ * matches every tool, sub-agents included (probe runs 2–4, ADR 0037).
353
+ */
354
+ export const denyAllToolsPolicyToml = [
355
+ "[[rule]]",
356
+ 'toolName = "*"',
357
+ 'decision = "deny"',
358
+ "priority = 999",
359
+ ""
360
+ ].join("\n")
361
+
362
+ /** Gemini CLI's system settings for a model-mode call: no sub-agents. */
363
+ export const modelModeSettingsJson = JSON.stringify({ experimental: { enableAgents: false } })