@elvatis_com/openclaw-cli-bridge-elvatis 2.2.2 → 2.4.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/.ai/handoff/DASHBOARD.md +7 -5
- package/.ai/handoff/LOG.md +19 -0
- package/.ai/handoff/NEXT_ACTIONS.md +2 -1
- package/.ai/handoff/STATUS.md +12 -11
- package/README.md +9 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +206 -25
- package/src/metrics.ts +85 -0
- package/src/proxy-server.ts +135 -50
- package/src/status-template.ts +122 -0
- package/src/tool-protocol.ts +269 -0
- package/test/cli-runner-extended.test.ts +4 -4
- package/test/cli-runner.test.ts +3 -2
- package/test/proxy-e2e.test.ts +31 -28
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-protocol.ts
|
|
3
|
+
*
|
|
4
|
+
* Translates between the OpenAI tool-calling protocol and CLI text I/O.
|
|
5
|
+
*
|
|
6
|
+
* - buildToolPromptBlock(): injects tool definitions + instructions into the prompt
|
|
7
|
+
* - buildToolCallJsonSchema(): returns JSON schema for Claude's --json-schema flag
|
|
8
|
+
* - parseToolCallResponse(): extracts tool_calls from CLI output text/JSON
|
|
9
|
+
* - generateCallId(): unique call IDs for tool_calls
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Types
|
|
16
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface ToolDefinition {
|
|
19
|
+
type: "function";
|
|
20
|
+
function: {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
parameters: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToolCall {
|
|
28
|
+
id: string;
|
|
29
|
+
type: "function";
|
|
30
|
+
function: {
|
|
31
|
+
name: string;
|
|
32
|
+
arguments: string; // JSON-encoded arguments
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CliToolResult {
|
|
37
|
+
content: string | null;
|
|
38
|
+
tool_calls?: ToolCall[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Prompt building
|
|
43
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a text block describing available tools and response format instructions.
|
|
47
|
+
* This block is prepended to the system message (or added as a new system message).
|
|
48
|
+
*/
|
|
49
|
+
export function buildToolPromptBlock(tools: ToolDefinition[]): string {
|
|
50
|
+
const toolDescriptions = tools
|
|
51
|
+
.map((t) => {
|
|
52
|
+
const fn = t.function;
|
|
53
|
+
const params = JSON.stringify(fn.parameters);
|
|
54
|
+
return `- name: ${fn.name}\n description: ${fn.description}\n parameters: ${params}`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
"You have access to the following tools.",
|
|
60
|
+
"",
|
|
61
|
+
"IMPORTANT: You must respond with ONLY valid JSON in one of these two formats:",
|
|
62
|
+
"",
|
|
63
|
+
'To call one or more tools, respond with ONLY:',
|
|
64
|
+
'{"tool_calls":[{"name":"<tool_name>","arguments":{<parameters as JSON object>}}]}',
|
|
65
|
+
"",
|
|
66
|
+
'To respond with text (no tool call needed), respond with ONLY:',
|
|
67
|
+
'{"content":"<your text response>"}',
|
|
68
|
+
"",
|
|
69
|
+
"Do NOT include any text outside the JSON. Do NOT wrap in markdown code blocks.",
|
|
70
|
+
"",
|
|
71
|
+
"Available tools:",
|
|
72
|
+
toolDescriptions,
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// JSON Schema for Claude's --json-schema flag
|
|
78
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns a JSON schema that constrains Claude's output to either:
|
|
82
|
+
* - { "content": "text response" }
|
|
83
|
+
* - { "tool_calls": [{ "name": "...", "arguments": { ... } }] }
|
|
84
|
+
*/
|
|
85
|
+
export function buildToolCallJsonSchema(): object {
|
|
86
|
+
return {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
content: { type: "string" },
|
|
90
|
+
tool_calls: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
name: { type: "string" },
|
|
96
|
+
arguments: { type: "object" },
|
|
97
|
+
},
|
|
98
|
+
required: ["name", "arguments"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Response parsing
|
|
108
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse CLI output text into a CliToolResult.
|
|
112
|
+
*
|
|
113
|
+
* Tries to extract JSON from the text. If valid JSON with tool_calls is found,
|
|
114
|
+
* returns structured tool calls. Otherwise returns the text as content.
|
|
115
|
+
*
|
|
116
|
+
* Never throws — always returns a valid result.
|
|
117
|
+
*/
|
|
118
|
+
export function parseToolCallResponse(text: string): CliToolResult {
|
|
119
|
+
const trimmed = text.trim();
|
|
120
|
+
|
|
121
|
+
// Check for Claude's --output-format json wrapper FIRST.
|
|
122
|
+
// Claude returns: { "type": "result", "result": "..." }
|
|
123
|
+
// The inner `result` field contains the actual model output (with tool_calls or content).
|
|
124
|
+
const claudeResult = tryExtractClaudeJsonResult(trimmed);
|
|
125
|
+
if (claudeResult) {
|
|
126
|
+
const inner = tryParseJson(claudeResult);
|
|
127
|
+
if (inner) return normalizeResult(inner);
|
|
128
|
+
// Claude result is plain text
|
|
129
|
+
return { content: claudeResult };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try direct JSON parse (for non-Claude outputs)
|
|
133
|
+
const parsed = tryParseJson(trimmed);
|
|
134
|
+
if (parsed) return normalizeResult(parsed);
|
|
135
|
+
|
|
136
|
+
// Try extracting JSON from markdown code blocks: ```json ... ```
|
|
137
|
+
const codeBlock = tryExtractCodeBlock(trimmed);
|
|
138
|
+
if (codeBlock) {
|
|
139
|
+
const inner = tryParseJson(codeBlock);
|
|
140
|
+
if (inner) return normalizeResult(inner);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Try finding a JSON object anywhere in the text
|
|
144
|
+
const embedded = tryExtractEmbeddedJson(trimmed);
|
|
145
|
+
if (embedded) {
|
|
146
|
+
const inner = tryParseJson(embedded);
|
|
147
|
+
if (inner) return normalizeResult(inner);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fallback: treat entire text as content
|
|
151
|
+
return { content: trimmed || null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Normalize a parsed JSON object into a CliToolResult.
|
|
156
|
+
*/
|
|
157
|
+
function normalizeResult(obj: Record<string, unknown>): CliToolResult {
|
|
158
|
+
// Check for tool_calls array
|
|
159
|
+
if (Array.isArray(obj.tool_calls) && obj.tool_calls.length > 0) {
|
|
160
|
+
const toolCalls: ToolCall[] = obj.tool_calls.map((tc: Record<string, unknown>) => ({
|
|
161
|
+
id: generateCallId(),
|
|
162
|
+
type: "function" as const,
|
|
163
|
+
function: {
|
|
164
|
+
name: String(tc.name ?? ""),
|
|
165
|
+
arguments: typeof tc.arguments === "string"
|
|
166
|
+
? tc.arguments
|
|
167
|
+
: JSON.stringify(tc.arguments ?? {}),
|
|
168
|
+
},
|
|
169
|
+
}));
|
|
170
|
+
return { content: null, tool_calls: toolCalls };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for content field
|
|
174
|
+
if (typeof obj.content === "string") {
|
|
175
|
+
return { content: obj.content };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Unknown structure — serialize as content
|
|
179
|
+
return { content: JSON.stringify(obj) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function tryParseJson(text: string): Record<string, unknown> | null {
|
|
183
|
+
try {
|
|
184
|
+
const obj = JSON.parse(text);
|
|
185
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
|
186
|
+
return obj as Record<string, unknown>;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract the model output from Claude's JSON output wrapper.
|
|
196
|
+
* Claude CLI with --output-format json returns:
|
|
197
|
+
* { "type": "result", "result": "the model output",
|
|
198
|
+
* "structured_output": { "content": "..." }, ... }
|
|
199
|
+
*
|
|
200
|
+
* When --json-schema is used, the `result` field is the JSON-schema-constrained output.
|
|
201
|
+
* The `structured_output.content` field may also contain the raw output.
|
|
202
|
+
*/
|
|
203
|
+
function tryExtractClaudeJsonResult(text: string): string | null {
|
|
204
|
+
try {
|
|
205
|
+
const obj = JSON.parse(text);
|
|
206
|
+
if (obj?.type === "result") {
|
|
207
|
+
// Prefer structured_output.content if available
|
|
208
|
+
if (typeof obj.structured_output?.content === "string") {
|
|
209
|
+
return obj.structured_output.content;
|
|
210
|
+
}
|
|
211
|
+
if (typeof obj.result === "string") {
|
|
212
|
+
return obj.result;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Extract JSON from ```json ... ``` or ``` ... ``` code blocks. */
|
|
222
|
+
function tryExtractCodeBlock(text: string): string | null {
|
|
223
|
+
const match = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
224
|
+
return match?.[1]?.trim() ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Find the first { ... } JSON object in text (greedy, balanced braces). */
|
|
228
|
+
function tryExtractEmbeddedJson(text: string): string | null {
|
|
229
|
+
const start = text.indexOf("{");
|
|
230
|
+
if (start === -1) return null;
|
|
231
|
+
|
|
232
|
+
let depth = 0;
|
|
233
|
+
let inString = false;
|
|
234
|
+
let escaped = false;
|
|
235
|
+
|
|
236
|
+
for (let i = start; i < text.length; i++) {
|
|
237
|
+
const ch = text[i];
|
|
238
|
+
if (escaped) {
|
|
239
|
+
escaped = false;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ch === "\\") {
|
|
243
|
+
escaped = true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === '"') {
|
|
247
|
+
inString = !inString;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (inString) continue;
|
|
251
|
+
if (ch === "{") depth++;
|
|
252
|
+
if (ch === "}") {
|
|
253
|
+
depth--;
|
|
254
|
+
if (depth === 0) {
|
|
255
|
+
return text.slice(start, i + 1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
// Utilities
|
|
264
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/** Generate a unique tool call ID: "call_" + 12 random hex characters. */
|
|
267
|
+
export function generateCallId(): string {
|
|
268
|
+
return "call_" + randomBytes(6).toString("hex");
|
|
269
|
+
}
|
|
@@ -189,7 +189,7 @@ describe("routeToCliRunner — new model prefixes", () => {
|
|
|
189
189
|
[{ role: "user", content: "hi" }],
|
|
190
190
|
5000
|
|
191
191
|
);
|
|
192
|
-
expect(result).
|
|
192
|
+
expect(result).toEqual({ content: "routed output" });
|
|
193
193
|
expect(mockSpawn).toHaveBeenCalledWith("codex", expect.any(Array), expect.any(Object));
|
|
194
194
|
});
|
|
195
195
|
|
|
@@ -200,7 +200,7 @@ describe("routeToCliRunner — new model prefixes", () => {
|
|
|
200
200
|
5000,
|
|
201
201
|
{ allowedModels: null }
|
|
202
202
|
);
|
|
203
|
-
expect(result).
|
|
203
|
+
expect(result).toEqual({ content: "routed output" });
|
|
204
204
|
expect(mockSpawn).toHaveBeenCalledWith("codex", expect.any(Array), expect.any(Object));
|
|
205
205
|
});
|
|
206
206
|
|
|
@@ -210,7 +210,7 @@ describe("routeToCliRunner — new model prefixes", () => {
|
|
|
210
210
|
[{ role: "user", content: "hi" }],
|
|
211
211
|
5000
|
|
212
212
|
);
|
|
213
|
-
expect(result).
|
|
213
|
+
expect(result).toEqual({ content: "routed output" });
|
|
214
214
|
expect(mockSpawn).toHaveBeenCalledWith("opencode", expect.any(Array), expect.any(Object));
|
|
215
215
|
});
|
|
216
216
|
|
|
@@ -220,7 +220,7 @@ describe("routeToCliRunner — new model prefixes", () => {
|
|
|
220
220
|
[{ role: "user", content: "hi" }],
|
|
221
221
|
5000
|
|
222
222
|
);
|
|
223
|
-
expect(result).
|
|
223
|
+
expect(result).toEqual({ content: "routed output" });
|
|
224
224
|
expect(mockSpawn).toHaveBeenCalledWith("pi", expect.any(Array), expect.any(Object));
|
|
225
225
|
});
|
|
226
226
|
|
package/test/cli-runner.test.ts
CHANGED
|
@@ -123,7 +123,7 @@ describe("formatPrompt", () => {
|
|
|
123
123
|
expect(result).toContain("Part two");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
it("
|
|
126
|
+
it("includes placeholder for non-text ContentParts (e.g. image)", () => {
|
|
127
127
|
const result = formatPrompt([
|
|
128
128
|
{
|
|
129
129
|
role: "user",
|
|
@@ -133,7 +133,8 @@ describe("formatPrompt", () => {
|
|
|
133
133
|
],
|
|
134
134
|
},
|
|
135
135
|
]);
|
|
136
|
-
expect(result).
|
|
136
|
+
expect(result).toContain("describe this");
|
|
137
|
+
expect(result).toContain("[Attached image");
|
|
137
138
|
});
|
|
138
139
|
|
|
139
140
|
it("coerces plain object content to JSON string (not [object Object])", () => {
|
package/test/proxy-e2e.test.ts
CHANGED
|
@@ -71,8 +71,11 @@ vi.mock("../src/cli-runner.js", async (importOriginal) => {
|
|
|
71
71
|
if (!normalized.startsWith("cli-gemini/") && !normalized.startsWith("cli-claude/") && !normalized.startsWith("openai-codex/") && !normalized.startsWith("opencode/") && !normalized.startsWith("pi/")) {
|
|
72
72
|
throw new Error(`Unknown CLI bridge model: "${model}"`);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// Returns CliToolResult (content + optional tool_calls)
|
|
75
|
+
return { content: `Mock response from ${normalized}` };
|
|
75
76
|
}),
|
|
77
|
+
extractMultimodalParts: vi.fn((messages: unknown[]) => ({ cleanMessages: messages, mediaFiles: [] })),
|
|
78
|
+
cleanupMediaFiles: vi.fn(),
|
|
76
79
|
};
|
|
77
80
|
});
|
|
78
81
|
|
|
@@ -444,29 +447,29 @@ describe("Error handling", () => {
|
|
|
444
447
|
// Tool/function call rejection for CLI-proxy models
|
|
445
448
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
446
449
|
|
|
447
|
-
describe("Tool call
|
|
448
|
-
it("
|
|
450
|
+
describe("Tool call support", () => {
|
|
451
|
+
it("accepts tools for cli-gemini models (200)", async () => {
|
|
449
452
|
const res = await json("/v1/chat/completions", {
|
|
450
453
|
model: "cli-gemini/gemini-2.5-pro",
|
|
451
454
|
messages: [{ role: "user", content: "hi" }],
|
|
452
455
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
453
456
|
});
|
|
454
457
|
|
|
455
|
-
expect(res.status).toBe(
|
|
458
|
+
expect(res.status).toBe(200);
|
|
456
459
|
const body = JSON.parse(res.body);
|
|
457
|
-
expect(body.
|
|
460
|
+
expect(body.choices[0].message.content).toBeDefined();
|
|
458
461
|
});
|
|
459
462
|
|
|
460
|
-
it("
|
|
463
|
+
it("accepts tools for cli-claude models (200)", async () => {
|
|
461
464
|
const res = await json("/v1/chat/completions", {
|
|
462
465
|
model: "cli-claude/claude-sonnet-4-6",
|
|
463
466
|
messages: [{ role: "user", content: "hi" }],
|
|
464
467
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
465
468
|
});
|
|
466
469
|
|
|
467
|
-
expect(res.status).toBe(
|
|
470
|
+
expect(res.status).toBe(200);
|
|
468
471
|
const body = JSON.parse(res.body);
|
|
469
|
-
expect(body.
|
|
472
|
+
expect(body.choices[0].message.content).toBeDefined();
|
|
470
473
|
});
|
|
471
474
|
|
|
472
475
|
it("does NOT reject tools for web-grok models (returns 503 no session)", async () => {
|
|
@@ -476,7 +479,7 @@ describe("Tool call rejection", () => {
|
|
|
476
479
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
477
480
|
});
|
|
478
481
|
|
|
479
|
-
//
|
|
482
|
+
// Reaches provider logic, gets 503 (no session)
|
|
480
483
|
expect(res.status).not.toBe(400);
|
|
481
484
|
expect(res.status).toBe(503);
|
|
482
485
|
const body = JSON.parse(res.body);
|
|
@@ -489,23 +492,23 @@ describe("Tool call rejection", () => {
|
|
|
489
492
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
490
493
|
|
|
491
494
|
describe("Model capabilities", () => {
|
|
492
|
-
it("cli-gemini models have capabilities.tools===
|
|
495
|
+
it("cli-gemini models have capabilities.tools===true", async () => {
|
|
493
496
|
const res = await fetch("/v1/models");
|
|
494
497
|
const body = JSON.parse(res.body);
|
|
495
498
|
const cliGeminiModels = body.data.filter((m: { id: string }) => m.id.startsWith("cli-gemini/"));
|
|
496
499
|
expect(cliGeminiModels.length).toBeGreaterThan(0);
|
|
497
500
|
for (const m of cliGeminiModels) {
|
|
498
|
-
expect(m.capabilities.tools).toBe(
|
|
501
|
+
expect(m.capabilities.tools).toBe(true);
|
|
499
502
|
}
|
|
500
503
|
});
|
|
501
504
|
|
|
502
|
-
it("cli-claude models have capabilities.tools===
|
|
505
|
+
it("cli-claude models have capabilities.tools===true", async () => {
|
|
503
506
|
const res = await fetch("/v1/models");
|
|
504
507
|
const body = JSON.parse(res.body);
|
|
505
508
|
const cliClaudeModels = body.data.filter((m: { id: string }) => m.id.startsWith("cli-claude/"));
|
|
506
509
|
expect(cliClaudeModels.length).toBeGreaterThan(0);
|
|
507
510
|
for (const m of cliClaudeModels) {
|
|
508
|
-
expect(m.capabilities.tools).toBe(
|
|
511
|
+
expect(m.capabilities.tools).toBe(true);
|
|
509
512
|
}
|
|
510
513
|
});
|
|
511
514
|
|
|
@@ -519,33 +522,33 @@ describe("Model capabilities", () => {
|
|
|
519
522
|
}
|
|
520
523
|
});
|
|
521
524
|
|
|
522
|
-
it("openai-codex models have capabilities.tools===
|
|
525
|
+
it("openai-codex models have capabilities.tools===true", async () => {
|
|
523
526
|
const res = await fetch("/v1/models");
|
|
524
527
|
const body = JSON.parse(res.body);
|
|
525
528
|
const codexModels = body.data.filter((m: { id: string }) => m.id.startsWith("openai-codex/"));
|
|
526
529
|
expect(codexModels.length).toBeGreaterThan(0);
|
|
527
530
|
for (const m of codexModels) {
|
|
528
|
-
expect(m.capabilities.tools).toBe(
|
|
531
|
+
expect(m.capabilities.tools).toBe(true);
|
|
529
532
|
}
|
|
530
533
|
});
|
|
531
534
|
|
|
532
|
-
it("opencode models have capabilities.tools===
|
|
535
|
+
it("opencode models have capabilities.tools===true", async () => {
|
|
533
536
|
const res = await fetch("/v1/models");
|
|
534
537
|
const body = JSON.parse(res.body);
|
|
535
538
|
const ocModels = body.data.filter((m: { id: string }) => m.id.startsWith("opencode/"));
|
|
536
539
|
expect(ocModels.length).toBeGreaterThan(0);
|
|
537
540
|
for (const m of ocModels) {
|
|
538
|
-
expect(m.capabilities.tools).toBe(
|
|
541
|
+
expect(m.capabilities.tools).toBe(true);
|
|
539
542
|
}
|
|
540
543
|
});
|
|
541
544
|
|
|
542
|
-
it("pi models have capabilities.tools===
|
|
545
|
+
it("pi models have capabilities.tools===true", async () => {
|
|
543
546
|
const res = await fetch("/v1/models");
|
|
544
547
|
const body = JSON.parse(res.body);
|
|
545
548
|
const piModels = body.data.filter((m: { id: string }) => m.id.startsWith("pi/"));
|
|
546
549
|
expect(piModels.length).toBeGreaterThan(0);
|
|
547
550
|
for (const m of piModels) {
|
|
548
|
-
expect(m.capabilities.tools).toBe(
|
|
551
|
+
expect(m.capabilities.tools).toBe(true);
|
|
549
552
|
}
|
|
550
553
|
});
|
|
551
554
|
});
|
|
@@ -585,34 +588,34 @@ describe("POST /v1/chat/completions — new model prefixes", () => {
|
|
|
585
588
|
expect(body.choices[0].message.content).toBe("Mock response from pi/default");
|
|
586
589
|
});
|
|
587
590
|
|
|
588
|
-
it("
|
|
591
|
+
it("accepts tools for openai-codex models", async () => {
|
|
589
592
|
const res = await json("/v1/chat/completions", {
|
|
590
593
|
model: "openai-codex/gpt-5.3-codex",
|
|
591
594
|
messages: [{ role: "user", content: "hi" }],
|
|
592
595
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
593
596
|
});
|
|
594
|
-
expect(res.status).toBe(
|
|
595
|
-
expect(JSON.parse(res.body).
|
|
597
|
+
expect(res.status).toBe(200);
|
|
598
|
+
expect(JSON.parse(res.body).choices[0].message.content).toBeDefined();
|
|
596
599
|
});
|
|
597
600
|
|
|
598
|
-
it("
|
|
601
|
+
it("accepts tools for opencode models", async () => {
|
|
599
602
|
const res = await json("/v1/chat/completions", {
|
|
600
603
|
model: "opencode/default",
|
|
601
604
|
messages: [{ role: "user", content: "hi" }],
|
|
602
605
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
603
606
|
});
|
|
604
|
-
expect(res.status).toBe(
|
|
605
|
-
expect(JSON.parse(res.body).
|
|
607
|
+
expect(res.status).toBe(200);
|
|
608
|
+
expect(JSON.parse(res.body).choices[0].message.content).toBeDefined();
|
|
606
609
|
});
|
|
607
610
|
|
|
608
|
-
it("
|
|
611
|
+
it("accepts tools for pi models", async () => {
|
|
609
612
|
const res = await json("/v1/chat/completions", {
|
|
610
613
|
model: "pi/default",
|
|
611
614
|
messages: [{ role: "user", content: "hi" }],
|
|
612
615
|
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
613
616
|
});
|
|
614
|
-
expect(res.status).toBe(
|
|
615
|
-
expect(JSON.parse(res.body).
|
|
617
|
+
expect(res.status).toBe(200);
|
|
618
|
+
expect(JSON.parse(res.body).choices[0].message.content).toBeDefined();
|
|
616
619
|
});
|
|
617
620
|
});
|
|
618
621
|
|