@llm4ts/core 2.42.0 → 2.44.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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.44.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 } })
|