@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Message } from "../types/messages";
|
|
3
|
+
import type { ApiStreamChunk } from "../types/stream";
|
|
4
|
+
import { OpenAIResponsesHandler } from "./openai-responses";
|
|
5
|
+
|
|
6
|
+
class TestOpenAIResponsesHandler extends OpenAIResponsesHandler {
|
|
7
|
+
private readonly functionCallMetadataByItemId = new Map<
|
|
8
|
+
string,
|
|
9
|
+
{ callId?: string; name?: string }
|
|
10
|
+
>();
|
|
11
|
+
|
|
12
|
+
processChunkForTest(chunk: any, responseId = "resp_1"): ApiStreamChunk[] {
|
|
13
|
+
return [
|
|
14
|
+
...this.processResponseChunk(
|
|
15
|
+
chunk,
|
|
16
|
+
{ id: "gpt-5.4", capabilities: ["tools"] },
|
|
17
|
+
responseId,
|
|
18
|
+
this.functionCallMetadataByItemId,
|
|
19
|
+
),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("OpenAIResponsesHandler", () => {
|
|
25
|
+
it("converts tool_use/tool_result message history into Responses input items", () => {
|
|
26
|
+
const handler = new TestOpenAIResponsesHandler({
|
|
27
|
+
providerId: "openai-native",
|
|
28
|
+
modelId: "gpt-5.4",
|
|
29
|
+
apiKey: "test-key",
|
|
30
|
+
baseUrl: "https://example.com",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const messages: Message[] = [
|
|
34
|
+
{ role: "user", content: [{ type: "text", text: "Run pwd" }] },
|
|
35
|
+
{
|
|
36
|
+
role: "assistant",
|
|
37
|
+
content: [
|
|
38
|
+
{ type: "text", text: "Running command..." },
|
|
39
|
+
{
|
|
40
|
+
type: "tool_use",
|
|
41
|
+
id: "fc_1",
|
|
42
|
+
call_id: "call_1",
|
|
43
|
+
name: "run_commands",
|
|
44
|
+
input: { commands: ["pwd"] },
|
|
45
|
+
},
|
|
46
|
+
{ type: "text", text: "Waiting for output" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
role: "user",
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "tool_result",
|
|
54
|
+
tool_use_id: "call_1",
|
|
55
|
+
content: "/tmp/workspace",
|
|
56
|
+
},
|
|
57
|
+
{ type: "text", text: "continue" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const input = handler.getMessages("system", messages);
|
|
63
|
+
|
|
64
|
+
expect(input).toEqual([
|
|
65
|
+
{
|
|
66
|
+
type: "message",
|
|
67
|
+
role: "user",
|
|
68
|
+
content: [{ type: "input_text", text: "Run pwd" }],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "message",
|
|
72
|
+
role: "assistant",
|
|
73
|
+
content: [{ type: "output_text", text: "Running command..." }],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "function_call",
|
|
77
|
+
call_id: "call_1",
|
|
78
|
+
name: "run_commands",
|
|
79
|
+
arguments: '{"commands":["pwd"]}',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "message",
|
|
83
|
+
role: "assistant",
|
|
84
|
+
content: [{ type: "output_text", text: "Waiting for output" }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: "function_call_output",
|
|
88
|
+
call_id: "call_1",
|
|
89
|
+
output: "/tmp/workspace",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "message",
|
|
93
|
+
role: "user",
|
|
94
|
+
content: [{ type: "input_text", text: "continue" }],
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("falls back to tool_use id when call_id is unavailable", () => {
|
|
100
|
+
const handler = new TestOpenAIResponsesHandler({
|
|
101
|
+
providerId: "openai-native",
|
|
102
|
+
modelId: "gpt-5.4",
|
|
103
|
+
apiKey: "test-key",
|
|
104
|
+
baseUrl: "https://example.com",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const messages: Message[] = [
|
|
108
|
+
{
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "tool_use",
|
|
113
|
+
id: "fc_123",
|
|
114
|
+
name: "search_codebase",
|
|
115
|
+
input: { pattern: "history" },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
role: "user",
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "tool_result",
|
|
124
|
+
tool_use_id: "fc_123",
|
|
125
|
+
content: "found",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const input = handler.getMessages("system", messages);
|
|
132
|
+
expect(input).toEqual([
|
|
133
|
+
{
|
|
134
|
+
type: "function_call",
|
|
135
|
+
call_id: "fc_123",
|
|
136
|
+
name: "search_codebase",
|
|
137
|
+
arguments: '{"pattern":"history"}',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "function_call_output",
|
|
141
|
+
call_id: "fc_123",
|
|
142
|
+
output: "found",
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("does not map function-call item ids to tool names", () => {
|
|
148
|
+
const handler = new TestOpenAIResponsesHandler({
|
|
149
|
+
providerId: "openai-native",
|
|
150
|
+
modelId: "gpt-5.4",
|
|
151
|
+
apiKey: "test-key",
|
|
152
|
+
baseUrl: "https://example.com",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const itemId = "fc_03aad4ff6c019bed0069ba5e9ad030819f8b2b06c5ac013811";
|
|
156
|
+
|
|
157
|
+
const addedChunks = handler.processChunkForTest({
|
|
158
|
+
type: "response.output_item.added",
|
|
159
|
+
item: {
|
|
160
|
+
type: "function_call",
|
|
161
|
+
id: itemId,
|
|
162
|
+
call_id: "call_1",
|
|
163
|
+
name: "run_commands",
|
|
164
|
+
arguments: "{}",
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const deltaChunks = handler.processChunkForTest({
|
|
168
|
+
type: "response.function_call_arguments.delta",
|
|
169
|
+
item_id: itemId,
|
|
170
|
+
delta: '{"commands":["pwd"]',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(addedChunks).toHaveLength(1);
|
|
174
|
+
expect(deltaChunks).toHaveLength(1);
|
|
175
|
+
expect(deltaChunks[0]).toMatchObject({
|
|
176
|
+
type: "tool_calls",
|
|
177
|
+
tool_call: {
|
|
178
|
+
call_id: "call_1",
|
|
179
|
+
function: {
|
|
180
|
+
id: itemId,
|
|
181
|
+
name: "run_commands",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("leaves tool name undefined for argument deltas without metadata", () => {
|
|
188
|
+
const handler = new TestOpenAIResponsesHandler({
|
|
189
|
+
providerId: "openai-native",
|
|
190
|
+
modelId: "gpt-5.4",
|
|
191
|
+
apiKey: "test-key",
|
|
192
|
+
baseUrl: "https://example.com",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const itemId = "fc_unknown";
|
|
196
|
+
const deltaChunks = handler.processChunkForTest({
|
|
197
|
+
type: "response.function_call_arguments.delta",
|
|
198
|
+
item_id: itemId,
|
|
199
|
+
delta: '{"x":1}',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(deltaChunks).toHaveLength(1);
|
|
203
|
+
expect(deltaChunks[0]).toMatchObject({
|
|
204
|
+
type: "tool_calls",
|
|
205
|
+
tool_call: {
|
|
206
|
+
function: {
|
|
207
|
+
id: itemId,
|
|
208
|
+
name: undefined,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -12,90 +12,34 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import OpenAI from "openai";
|
|
15
|
+
import {
|
|
16
|
+
normalizeToolUseInput,
|
|
17
|
+
serializeToolResultContent,
|
|
18
|
+
} from "../transform/content-format";
|
|
15
19
|
import type {
|
|
16
20
|
ApiStream,
|
|
17
21
|
HandlerModelInfo,
|
|
18
22
|
ModelInfo,
|
|
19
23
|
ProviderConfig,
|
|
20
24
|
} from "../types";
|
|
21
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
ContentBlock,
|
|
27
|
+
Message,
|
|
28
|
+
ToolDefinition,
|
|
29
|
+
ToolUseContent,
|
|
30
|
+
} from "../types/messages";
|
|
22
31
|
import { retryStream } from "../utils/retry";
|
|
23
32
|
import { getMissingApiKeyError, resolveApiKeyForProvider } from "./auth";
|
|
24
33
|
import { BaseHandler } from "./base";
|
|
25
34
|
|
|
26
35
|
const DEFAULT_REASONING_EFFORT = "medium" as const;
|
|
27
36
|
|
|
28
|
-
function normalizeStrictToolSchema(
|
|
29
|
-
schema: unknown,
|
|
30
|
-
options?: { stripFormat?: boolean },
|
|
31
|
-
): unknown {
|
|
32
|
-
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
33
|
-
return schema;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const normalized = { ...(schema as Record<string, unknown>) };
|
|
37
|
-
if (options?.stripFormat && "format" in normalized) {
|
|
38
|
-
delete normalized.format;
|
|
39
|
-
}
|
|
40
|
-
const type = normalized.type;
|
|
41
|
-
|
|
42
|
-
if (type === "object") {
|
|
43
|
-
if (!Object.hasOwn(normalized, "additionalProperties")) {
|
|
44
|
-
normalized.additionalProperties = false;
|
|
45
|
-
}
|
|
46
|
-
const properties = normalized.properties;
|
|
47
|
-
if (
|
|
48
|
-
properties &&
|
|
49
|
-
typeof properties === "object" &&
|
|
50
|
-
!Array.isArray(properties)
|
|
51
|
-
) {
|
|
52
|
-
const nextProperties: Record<string, unknown> = {};
|
|
53
|
-
for (const [key, value] of Object.entries(
|
|
54
|
-
properties as Record<string, unknown>,
|
|
55
|
-
)) {
|
|
56
|
-
nextProperties[key] = normalizeStrictToolSchema(value, options);
|
|
57
|
-
}
|
|
58
|
-
normalized.properties = nextProperties;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (Array.isArray(normalized.anyOf)) {
|
|
63
|
-
normalized.anyOf = normalized.anyOf.map((item) =>
|
|
64
|
-
normalizeStrictToolSchema(item, options),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
if (Array.isArray(normalized.oneOf)) {
|
|
68
|
-
normalized.oneOf = normalized.oneOf.map((item) =>
|
|
69
|
-
normalizeStrictToolSchema(item, options),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
if (Array.isArray(normalized.allOf)) {
|
|
73
|
-
normalized.allOf = normalized.allOf.map((item) =>
|
|
74
|
-
normalizeStrictToolSchema(item, options),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
if (normalized.not && typeof normalized.not === "object") {
|
|
78
|
-
normalized.not = normalizeStrictToolSchema(normalized.not, options);
|
|
79
|
-
}
|
|
80
|
-
if (normalized.items) {
|
|
81
|
-
if (Array.isArray(normalized.items)) {
|
|
82
|
-
normalized.items = normalized.items.map((item) =>
|
|
83
|
-
normalizeStrictToolSchema(item, options),
|
|
84
|
-
);
|
|
85
|
-
} else {
|
|
86
|
-
normalized.items = normalizeStrictToolSchema(normalized.items, options);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return normalized;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
37
|
/**
|
|
94
38
|
* Convert tool definitions to Responses API format
|
|
95
39
|
*/
|
|
96
40
|
function convertToolsToResponsesFormat(
|
|
97
41
|
tools?: ToolDefinition[],
|
|
98
|
-
|
|
42
|
+
_options?: { stripFormat?: boolean },
|
|
99
43
|
) {
|
|
100
44
|
if (!tools?.length) return undefined;
|
|
101
45
|
|
|
@@ -103,8 +47,7 @@ function convertToolsToResponsesFormat(
|
|
|
103
47
|
type: "function" as const,
|
|
104
48
|
name: tool.name,
|
|
105
49
|
description: tool.description,
|
|
106
|
-
parameters:
|
|
107
|
-
strict: true, // Responses API defaults to strict mode
|
|
50
|
+
parameters: tool.inputSchema,
|
|
108
51
|
}));
|
|
109
52
|
}
|
|
110
53
|
|
|
@@ -112,41 +55,103 @@ function convertToolsToResponsesFormat(
|
|
|
112
55
|
* Convert messages to Responses API input format
|
|
113
56
|
*/
|
|
114
57
|
function convertToResponsesInput(messages: Message[]) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
58
|
+
type ResponsesInputItem =
|
|
59
|
+
| {
|
|
60
|
+
type: "message";
|
|
61
|
+
role: "user" | "assistant";
|
|
62
|
+
content: Array<{ type: "input_text" | "output_text"; text: string }>;
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
type: "function_call";
|
|
66
|
+
call_id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
arguments: string;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: "function_call_output";
|
|
72
|
+
call_id: string;
|
|
73
|
+
output: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const input: ResponsesInputItem[] = [];
|
|
77
|
+
|
|
78
|
+
const toText = (
|
|
79
|
+
role: "user" | "assistant",
|
|
80
|
+
contentBlocks: Array<{ type: "text"; text: string }>,
|
|
81
|
+
) => {
|
|
82
|
+
const textContent = contentBlocks.map((block) => block.text).join("\n");
|
|
83
|
+
if (!textContent) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
input.push({
|
|
87
|
+
type: "message",
|
|
88
|
+
role,
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: role === "user" ? "input_text" : "output_text",
|
|
92
|
+
text: textContent,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isTextBlock = (
|
|
99
|
+
block: ContentBlock,
|
|
100
|
+
): block is { type: "text"; text: string } => block.type === "text";
|
|
101
|
+
|
|
102
|
+
const assistantToolUseCallId = (block: ToolUseContent): string =>
|
|
103
|
+
block.call_id?.trim() || block.id;
|
|
123
104
|
|
|
124
105
|
for (const msg of messages) {
|
|
125
|
-
if (msg.role
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
106
|
+
if (msg.role !== "user" && msg.role !== "assistant") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!Array.isArray(msg.content)) {
|
|
111
|
+
if (msg.content) {
|
|
112
|
+
toText(msg.role, [{ type: "text", text: msg.content }]);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let bufferedText: Array<{ type: "text"; text: string }> = [];
|
|
118
|
+
const flushText = () => {
|
|
119
|
+
if (bufferedText.length === 0) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
toText(msg.role, bufferedText);
|
|
123
|
+
bufferedText = [];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const block of msg.content) {
|
|
127
|
+
if (isTextBlock(block)) {
|
|
128
|
+
bufferedText.push(block);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (msg.role === "assistant" && block.type === "tool_use") {
|
|
133
|
+
flushText();
|
|
134
|
+
const toolUseBlock = block as ToolUseContent;
|
|
135
|
+
input.push({
|
|
136
|
+
type: "function_call",
|
|
137
|
+
call_id: assistantToolUseCallId(toolUseBlock),
|
|
138
|
+
name: toolUseBlock.name,
|
|
139
|
+
arguments: JSON.stringify(normalizeToolUseInput(toolUseBlock.input)),
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (msg.role === "user" && block.type === "tool_result") {
|
|
145
|
+
flushText();
|
|
140
146
|
input.push({
|
|
141
|
-
type: "
|
|
142
|
-
|
|
143
|
-
content
|
|
144
|
-
msg.role === "user"
|
|
145
|
-
? [{ type: "input_text", text: textContent }]
|
|
146
|
-
: [{ type: "output_text", text: textContent }],
|
|
147
|
+
type: "function_call_output",
|
|
148
|
+
call_id: block.tool_use_id,
|
|
149
|
+
output: serializeToolResultContent(block.content),
|
|
147
150
|
});
|
|
148
151
|
}
|
|
149
152
|
}
|
|
153
|
+
|
|
154
|
+
flushText();
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
return input;
|
|
@@ -253,6 +258,10 @@ export class OpenAIResponsesHandler extends BaseHandler {
|
|
|
253
258
|
const abortSignal = this.getAbortSignal();
|
|
254
259
|
const fallbackResponseId = this.createResponseId();
|
|
255
260
|
let resolvedResponseId: string | undefined;
|
|
261
|
+
const functionCallMetadataByItemId = new Map<
|
|
262
|
+
string,
|
|
263
|
+
{ callId?: string; name?: string }
|
|
264
|
+
>();
|
|
256
265
|
|
|
257
266
|
// Convert messages to Responses API input format
|
|
258
267
|
const input = this.getMessages(systemPrompt, messages);
|
|
@@ -362,6 +371,7 @@ export class OpenAIResponsesHandler extends BaseHandler {
|
|
|
362
371
|
chunk,
|
|
363
372
|
modelInfo,
|
|
364
373
|
resolvedResponseId ?? fallbackResponseId,
|
|
374
|
+
functionCallMetadataByItemId,
|
|
365
375
|
);
|
|
366
376
|
}
|
|
367
377
|
}
|
|
@@ -373,12 +383,20 @@ export class OpenAIResponsesHandler extends BaseHandler {
|
|
|
373
383
|
chunk: any,
|
|
374
384
|
_modelInfo: ModelInfo,
|
|
375
385
|
responseId: string,
|
|
386
|
+
functionCallMetadataByItemId: Map<
|
|
387
|
+
string,
|
|
388
|
+
{ callId?: string; name?: string }
|
|
389
|
+
>,
|
|
376
390
|
): Generator<import("../types").ApiStreamChunk> {
|
|
377
391
|
// Handle different event types from Responses API
|
|
378
392
|
switch (chunk.type) {
|
|
379
393
|
case "response.output_item.added": {
|
|
380
394
|
const item = chunk.item;
|
|
381
395
|
if (item.type === "function_call" && item.id) {
|
|
396
|
+
functionCallMetadataByItemId.set(item.id, {
|
|
397
|
+
callId: item.call_id,
|
|
398
|
+
name: item.name,
|
|
399
|
+
});
|
|
382
400
|
yield {
|
|
383
401
|
type: "tool_calls",
|
|
384
402
|
id: item.id || responseId,
|
|
@@ -406,6 +424,12 @@ export class OpenAIResponsesHandler extends BaseHandler {
|
|
|
406
424
|
case "response.output_item.done": {
|
|
407
425
|
const item = chunk.item;
|
|
408
426
|
if (item.type === "function_call") {
|
|
427
|
+
if (item.id) {
|
|
428
|
+
functionCallMetadataByItemId.set(item.id, {
|
|
429
|
+
callId: item.call_id,
|
|
430
|
+
name: item.name,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
409
433
|
yield {
|
|
410
434
|
type: "tool_calls",
|
|
411
435
|
id: item.id || responseId,
|
|
@@ -476,28 +500,36 @@ export class OpenAIResponsesHandler extends BaseHandler {
|
|
|
476
500
|
break;
|
|
477
501
|
|
|
478
502
|
case "response.function_call_arguments.delta":
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
503
|
+
{
|
|
504
|
+
const meta = chunk.item_id
|
|
505
|
+
? functionCallMetadataByItemId.get(chunk.item_id)
|
|
506
|
+
: undefined;
|
|
507
|
+
yield {
|
|
508
|
+
type: "tool_calls",
|
|
509
|
+
id: chunk.item_id || responseId,
|
|
510
|
+
tool_call: {
|
|
511
|
+
call_id: meta?.callId,
|
|
512
|
+
function: {
|
|
513
|
+
id: chunk.item_id,
|
|
514
|
+
name: meta?.name,
|
|
515
|
+
arguments: chunk.delta,
|
|
516
|
+
},
|
|
487
517
|
},
|
|
488
|
-
}
|
|
489
|
-
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
490
520
|
break;
|
|
491
521
|
|
|
492
522
|
case "response.function_call_arguments.done":
|
|
493
|
-
if (chunk.item_id && chunk.
|
|
523
|
+
if (chunk.item_id && chunk.arguments) {
|
|
524
|
+
const meta = functionCallMetadataByItemId.get(chunk.item_id);
|
|
494
525
|
yield {
|
|
495
526
|
type: "tool_calls",
|
|
496
527
|
id: chunk.item_id || responseId,
|
|
497
528
|
tool_call: {
|
|
529
|
+
call_id: chunk.call_id ?? meta?.callId,
|
|
498
530
|
function: {
|
|
499
531
|
id: chunk.item_id,
|
|
500
|
-
name: chunk.name,
|
|
532
|
+
name: chunk.name ?? meta?.name,
|
|
501
533
|
arguments: chunk.arguments,
|
|
502
534
|
},
|
|
503
535
|
},
|
|
@@ -155,8 +155,9 @@ export class R1BaseHandler extends BaseHandler {
|
|
|
155
155
|
};
|
|
156
156
|
|
|
157
157
|
// Add max tokens if configured
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
const maxTokens = modelInfo.maxTokens ?? this.config.maxOutputTokens;
|
|
159
|
+
if (maxTokens) {
|
|
160
|
+
requestOptions.max_completion_tokens = maxTokens;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
// Only set temperature for non-reasoner models
|
|
@@ -241,7 +241,7 @@ export class VertexHandler extends BaseHandler {
|
|
|
241
241
|
promptCacheOn,
|
|
242
242
|
}),
|
|
243
243
|
tools: toAiSdkTools(tools),
|
|
244
|
-
maxTokens: model.info.maxTokens ?? 8192,
|
|
244
|
+
maxTokens: model.info.maxTokens ?? this.config.maxOutputTokens ?? 8192,
|
|
245
245
|
temperature: reasoningOn ? undefined : 0,
|
|
246
246
|
providerOptions:
|
|
247
247
|
Object.keys(providerOptions).length > 0 ? providerOptions : undefined,
|
|
@@ -56,9 +56,12 @@ describe("format conversion", () => {
|
|
|
56
56
|
|
|
57
57
|
const gemini = convertToGeminiMessages(messages) as any[];
|
|
58
58
|
expect(gemini[0]?.parts?.[0]?.text).toBe(fileText);
|
|
59
|
+
expect(gemini[1]?.parts?.[0]?.functionCall?.id).toBe("call_1");
|
|
59
60
|
expect(gemini[2]?.parts?.[0]?.functionResponse?.response?.result).toBe(
|
|
60
61
|
fileText,
|
|
61
62
|
);
|
|
63
|
+
expect(gemini[2]?.parts?.[0]?.functionResponse?.id).toBe("call_1");
|
|
64
|
+
expect(gemini[2]?.parts?.[0]?.functionResponse?.name).toBe("read_file");
|
|
62
65
|
|
|
63
66
|
const anthropic = convertToAnthropicMessages(messages) as any[];
|
|
64
67
|
expect(anthropic[0]?.content?.[0]).toMatchObject({
|
|
@@ -104,6 +107,7 @@ describe("format conversion", () => {
|
|
|
104
107
|
const assistant = gemini[1] as any;
|
|
105
108
|
expect(assistant.role).toBe("model");
|
|
106
109
|
expect(assistant.parts[0].functionCall.name).toBe("run_commands");
|
|
110
|
+
expect(assistant.parts[0].functionCall.id).toBe("call_1");
|
|
107
111
|
expect(assistant.parts[0].thoughtSignature).toBe("sig-a");
|
|
108
112
|
expect(assistant.parts[1].thought).toBe(true);
|
|
109
113
|
expect(assistant.parts[1].thoughtSignature).toBe("sig-think");
|
|
@@ -111,6 +115,56 @@ describe("format conversion", () => {
|
|
|
111
115
|
expect(assistant.parts[2].thoughtSignature).toBe("sig-text");
|
|
112
116
|
});
|
|
113
117
|
|
|
118
|
+
it("maps out-of-order gemini tool results by call id", () => {
|
|
119
|
+
const messages: Message[] = [
|
|
120
|
+
{ role: "user", content: "check both" },
|
|
121
|
+
{
|
|
122
|
+
role: "assistant",
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "tool_use",
|
|
126
|
+
id: "call_1",
|
|
127
|
+
name: "read_file",
|
|
128
|
+
input: { path: "a.ts" },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "tool_use",
|
|
132
|
+
id: "call_2",
|
|
133
|
+
name: "search_files",
|
|
134
|
+
input: { query: "TODO" },
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
role: "user",
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "tool_result",
|
|
143
|
+
tool_use_id: "call_2",
|
|
144
|
+
content: '{"matches":1}',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "tool_result",
|
|
148
|
+
tool_use_id: "call_1",
|
|
149
|
+
content: '{"text":"ok"}',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const gemini = convertToGeminiMessages(messages) as any[];
|
|
156
|
+
expect(gemini[2]?.parts?.[0]?.functionResponse).toMatchObject({
|
|
157
|
+
id: "call_2",
|
|
158
|
+
name: "search_files",
|
|
159
|
+
response: { result: { matches: 1 } },
|
|
160
|
+
});
|
|
161
|
+
expect(gemini[2]?.parts?.[1]?.functionResponse).toMatchObject({
|
|
162
|
+
id: "call_1",
|
|
163
|
+
name: "read_file",
|
|
164
|
+
response: { result: { text: "ok" } },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
114
168
|
it("converts multiple tool_result blocks for openai without dropping any", () => {
|
|
115
169
|
const messages: Message[] = [
|
|
116
170
|
{ role: "user", content: "check both" },
|