@clinebot/llms 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -188
- package/dist/index.browser.js +1 -1
- package/dist/index.js +13 -6
- package/dist/providers/handlers/openai-responses.d.ts +16 -4
- package/dist/providers/types/config.d.ts +1 -1
- package/dist/providers/types/messages.d.ts +2 -0
- package/dist/providers/types/settings.d.ts +2 -0
- package/package.json +3 -4
- package/src/live-providers.test.ts +4 -3
- package/src/models/generated.ts +79 -9
- package/src/models/providers/gemini.ts +1 -1
- package/src/providers/handlers/anthropic-base.ts +1 -1
- package/src/providers/handlers/bedrock-base.ts +1 -1
- package/src/providers/handlers/gemini-base.test.ts +221 -0
- package/src/providers/handlers/gemini-base.ts +10 -7
- package/src/providers/handlers/openai-base.ts +3 -2
- package/src/providers/handlers/openai-responses.test.ts +213 -0
- package/src/providers/handlers/openai-responses.ts +142 -110
- package/src/providers/handlers/r1-base.ts +3 -2
- package/src/providers/handlers/vertex.ts +1 -1
- package/src/providers/transform/format-conversion.test.ts +54 -0
- package/src/providers/transform/gemini-format.ts +28 -9
- package/src/providers/types/config.ts +1 -1
- package/src/providers/types/messages.ts +2 -0
- package/src/providers/types/settings.ts +1 -1
- package/src/providers/utils/tool-processor.test.ts +141 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Converts our unified Message format to Google Gemini's Content format.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { formatFileContentBlock } from "@clinebot/shared";
|
|
7
|
+
import { formatFileContentBlock, parseJsonStream } from "@clinebot/shared";
|
|
8
8
|
import type { Content, FunctionDeclaration, Part } from "@google/genai";
|
|
9
9
|
import type {
|
|
10
10
|
ContentBlock,
|
|
@@ -25,10 +25,16 @@ import {
|
|
|
25
25
|
* Convert messages to Gemini format
|
|
26
26
|
*/
|
|
27
27
|
export function convertToGeminiMessages(messages: Message[]): Content[] {
|
|
28
|
-
|
|
28
|
+
const toolNameByCallId = new Map<string, string>();
|
|
29
|
+
return messages
|
|
30
|
+
.map((message) => convertMessage(message, toolNameByCallId))
|
|
31
|
+
.filter((m): m is Content => m !== null);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
function convertMessage(
|
|
34
|
+
function convertMessage(
|
|
35
|
+
message: Message,
|
|
36
|
+
toolNameByCallId: Map<string, string>,
|
|
37
|
+
): Content | null {
|
|
32
38
|
const { role, content } = message;
|
|
33
39
|
|
|
34
40
|
// Map roles: Gemini uses "user" and "model"
|
|
@@ -43,7 +49,7 @@ function convertMessage(message: Message): Content | null {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
// Array content
|
|
46
|
-
const parts = convertContentBlocks(content);
|
|
52
|
+
const parts = convertContentBlocks(content, toolNameByCallId);
|
|
47
53
|
if (parts.length === 0) {
|
|
48
54
|
return null;
|
|
49
55
|
}
|
|
@@ -54,11 +60,17 @@ function convertMessage(message: Message): Content | null {
|
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function convertContentBlocks(
|
|
63
|
+
function convertContentBlocks(
|
|
64
|
+
content: ContentBlock[],
|
|
65
|
+
toolNameByCallId: Map<string, string>,
|
|
66
|
+
): Part[] {
|
|
58
67
|
const parts: Part[] = [];
|
|
59
68
|
|
|
60
69
|
for (const block of content) {
|
|
61
|
-
|
|
70
|
+
if (block.type === "tool_use") {
|
|
71
|
+
toolNameByCallId.set(block.id, block.name);
|
|
72
|
+
}
|
|
73
|
+
const converted = convertContentBlock(block, toolNameByCallId);
|
|
62
74
|
if (converted) {
|
|
63
75
|
parts.push(converted);
|
|
64
76
|
}
|
|
@@ -67,7 +79,10 @@ function convertContentBlocks(content: ContentBlock[]): Part[] {
|
|
|
67
79
|
return parts;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
function convertContentBlock(
|
|
82
|
+
function convertContentBlock(
|
|
83
|
+
block: ContentBlock,
|
|
84
|
+
toolNameByCallId: Map<string, string>,
|
|
85
|
+
): Part | null {
|
|
71
86
|
switch (block.type) {
|
|
72
87
|
case "text": {
|
|
73
88
|
const textBlock = block as TextContent;
|
|
@@ -101,6 +116,7 @@ function convertContentBlock(block: ContentBlock): Part | null {
|
|
|
101
116
|
const toolBlock = block as ToolUseContent;
|
|
102
117
|
const part: Part = {
|
|
103
118
|
functionCall: {
|
|
119
|
+
id: toolBlock.id,
|
|
104
120
|
name: toolBlock.name,
|
|
105
121
|
args: normalizeToolUseInput(toolBlock.input),
|
|
106
122
|
},
|
|
@@ -116,7 +132,7 @@ function convertContentBlock(block: ContentBlock): Part | null {
|
|
|
116
132
|
let responseContent: Record<string, unknown>;
|
|
117
133
|
|
|
118
134
|
if (typeof resultBlock.content === "string") {
|
|
119
|
-
responseContent = { result: resultBlock.content };
|
|
135
|
+
responseContent = { result: parseJsonStream(resultBlock.content) };
|
|
120
136
|
} else {
|
|
121
137
|
responseContent = {
|
|
122
138
|
result: serializeToolResultContent(resultBlock.content),
|
|
@@ -129,7 +145,10 @@ function convertContentBlock(block: ContentBlock): Part | null {
|
|
|
129
145
|
|
|
130
146
|
return {
|
|
131
147
|
functionResponse: {
|
|
132
|
-
|
|
148
|
+
id: resultBlock.tool_use_id,
|
|
149
|
+
name:
|
|
150
|
+
toolNameByCallId.get(resultBlock.tool_use_id) ??
|
|
151
|
+
resultBlock.tool_use_id,
|
|
133
152
|
response: responseContent,
|
|
134
153
|
},
|
|
135
154
|
};
|
|
@@ -118,7 +118,7 @@ export interface TokenConfig {
|
|
|
118
118
|
*/
|
|
119
119
|
export interface ReasoningConfig {
|
|
120
120
|
/** Reasoning effort level */
|
|
121
|
-
reasoningEffort?: "low" | "medium" | "high";
|
|
121
|
+
reasoningEffort?: "low" | "medium" | "high" | "xhigh";
|
|
122
122
|
/** Extended thinking budget in tokens */
|
|
123
123
|
thinkingBudgetTokens?: number;
|
|
124
124
|
/** Enable thinking with provider/model defaults when supported */
|
|
@@ -50,6 +50,8 @@ export interface ToolUseContent {
|
|
|
50
50
|
type: "tool_use";
|
|
51
51
|
/** Unique ID for this tool call */
|
|
52
52
|
id: string;
|
|
53
|
+
/** Provider-native call ID for this tool call (if available) */
|
|
54
|
+
call_id?: string;
|
|
53
55
|
/** Name of the tool being called */
|
|
54
56
|
name: string;
|
|
55
57
|
/** Arguments for the tool call */
|
|
@@ -57,7 +57,7 @@ export type AuthSettings = z.infer<typeof AuthSettingsSchema>;
|
|
|
57
57
|
/**
|
|
58
58
|
* Reasoning/thinking configuration
|
|
59
59
|
*/
|
|
60
|
-
const ReasoningLevelSchema = z.enum(["none", "low", "medium", "high"]);
|
|
60
|
+
const ReasoningLevelSchema = z.enum(["none", "low", "medium", "high", "xhigh"]);
|
|
61
61
|
|
|
62
62
|
export const ReasoningSettingsSchema = z.object({
|
|
63
63
|
/** Enable thinking with provider/model defaults when supported */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getOpenAIToolParams } from "../transform/openai-format";
|
|
2
3
|
import { ToolCallProcessor } from "./tool-processor";
|
|
3
4
|
|
|
4
5
|
describe("ToolCallProcessor", () => {
|
|
@@ -31,4 +32,144 @@ describe("ToolCallProcessor", () => {
|
|
|
31
32
|
expect(first[0].tool_call.function.arguments).toBe('{"commands":["ls');
|
|
32
33
|
expect(second[0].tool_call.function.arguments).toBe(' -la"]}');
|
|
33
34
|
});
|
|
35
|
+
|
|
36
|
+
it("preserves tool call id/name for interleaved parallel deltas", () => {
|
|
37
|
+
const processor = new ToolCallProcessor();
|
|
38
|
+
|
|
39
|
+
const firstChunk = [
|
|
40
|
+
{
|
|
41
|
+
index: 0,
|
|
42
|
+
id: "call_a",
|
|
43
|
+
function: { name: "read_file" },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
index: 1,
|
|
47
|
+
id: "call_b",
|
|
48
|
+
function: { name: "search_files" },
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const secondChunk = [
|
|
53
|
+
{
|
|
54
|
+
index: 1,
|
|
55
|
+
function: { arguments: '{"path":"src"}' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
index: 0,
|
|
59
|
+
function: { arguments: '{"path":"README.md"}' },
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const firstResult = processor.processToolCallDeltas(firstChunk, "resp_1");
|
|
64
|
+
const secondResult = processor.processToolCallDeltas(secondChunk, "resp_1");
|
|
65
|
+
|
|
66
|
+
// Current implementation emits tool call chunks once id+name are known,
|
|
67
|
+
// even before argument deltas arrive.
|
|
68
|
+
expect(firstResult).toHaveLength(2);
|
|
69
|
+
expect(secondResult).toHaveLength(2);
|
|
70
|
+
|
|
71
|
+
// Intentionally reversed from the setup chunk: output follows incoming
|
|
72
|
+
// argument-delta order while reconstruction remains index-safe.
|
|
73
|
+
const firstToolCall = secondResult[0].tool_call;
|
|
74
|
+
const secondToolCall = secondResult[1].tool_call;
|
|
75
|
+
|
|
76
|
+
expect(firstToolCall.function.id).toBe("call_b");
|
|
77
|
+
expect(firstToolCall.function.name).toBe("search_files");
|
|
78
|
+
expect(firstToolCall.function.arguments).toBe('{"path":"src"}');
|
|
79
|
+
|
|
80
|
+
expect(secondToolCall.function.id).toBe("call_a");
|
|
81
|
+
expect(secondToolCall.function.name).toBe("read_file");
|
|
82
|
+
expect(secondToolCall.function.arguments).toBe('{"path":"README.md"}');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("clears accumulated state on reset", () => {
|
|
86
|
+
const processor = new ToolCallProcessor();
|
|
87
|
+
|
|
88
|
+
const setupChunk = [
|
|
89
|
+
{
|
|
90
|
+
index: 0,
|
|
91
|
+
id: "call_reset",
|
|
92
|
+
function: { name: "read_file" },
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const argsChunk = [
|
|
97
|
+
{
|
|
98
|
+
index: 0,
|
|
99
|
+
function: { arguments: '{"path":"after-reset"}' },
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
expect(processor.processToolCallDeltas(setupChunk, "resp_1")).toHaveLength(
|
|
104
|
+
1,
|
|
105
|
+
);
|
|
106
|
+
processor.reset();
|
|
107
|
+
expect(processor.processToolCallDeltas(argsChunk, "resp_1")).toHaveLength(
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const newSetupChunk = [
|
|
112
|
+
{
|
|
113
|
+
index: 0,
|
|
114
|
+
id: "call_new",
|
|
115
|
+
function: { name: "write_file" },
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const newArgsChunk = [
|
|
120
|
+
{
|
|
121
|
+
index: 0,
|
|
122
|
+
function: { arguments: '{"path":"file.txt"}' },
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
processor.processToolCallDeltas(newSetupChunk, "resp_1"),
|
|
128
|
+
).toHaveLength(1);
|
|
129
|
+
expect(
|
|
130
|
+
processor.processToolCallDeltas(newArgsChunk, "resp_1"),
|
|
131
|
+
).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("getOpenAIToolParams", () => {
|
|
136
|
+
it("returns tools and tool_choice when tools are present", () => {
|
|
137
|
+
const tools = [
|
|
138
|
+
{
|
|
139
|
+
name: "read_file",
|
|
140
|
+
description: "",
|
|
141
|
+
inputSchema: { type: "object" },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const params = getOpenAIToolParams(tools);
|
|
146
|
+
|
|
147
|
+
expect(params.tools).toHaveLength(1);
|
|
148
|
+
expect(params.tool_choice).toBe("auto");
|
|
149
|
+
expect(params).not.toHaveProperty("parallel_tool_calls");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns empty object when tools are absent", () => {
|
|
153
|
+
const params = getOpenAIToolParams(undefined);
|
|
154
|
+
|
|
155
|
+
expect(params).toEqual({});
|
|
156
|
+
expect(params).not.toHaveProperty("parallel_tool_calls");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("supports strict option passthrough", () => {
|
|
160
|
+
const tools = [
|
|
161
|
+
{
|
|
162
|
+
name: "read_file",
|
|
163
|
+
description: "",
|
|
164
|
+
inputSchema: { type: "object" },
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const params = getOpenAIToolParams(tools, { strict: false });
|
|
169
|
+
|
|
170
|
+
expect(params.tools?.[0]).toMatchObject({
|
|
171
|
+
type: "function",
|
|
172
|
+
function: { strict: false },
|
|
173
|
+
});
|
|
174
|
+
});
|
|
34
175
|
});
|