@clinebot/llms 0.0.0 → 0.0.1
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 -3
- 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
|
@@ -17,14 +17,23 @@ import { BaseHandler } from "./base";
|
|
|
17
17
|
/**
|
|
18
18
|
* Convert messages to Responses API input format
|
|
19
19
|
*/
|
|
20
|
-
declare function convertToResponsesInput(messages: Message[]): {
|
|
20
|
+
declare function convertToResponsesInput(messages: Message[]): ({
|
|
21
21
|
type: "message";
|
|
22
22
|
role: "user" | "assistant";
|
|
23
|
-
content:
|
|
23
|
+
content: Array<{
|
|
24
24
|
type: "input_text" | "output_text";
|
|
25
25
|
text: string;
|
|
26
26
|
}>;
|
|
27
|
-
}
|
|
27
|
+
} | {
|
|
28
|
+
type: "function_call";
|
|
29
|
+
call_id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
arguments: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "function_call_output";
|
|
34
|
+
call_id: string;
|
|
35
|
+
output: string;
|
|
36
|
+
})[];
|
|
28
37
|
/**
|
|
29
38
|
* Handler for OpenAI Responses API
|
|
30
39
|
*
|
|
@@ -54,7 +63,10 @@ export declare class OpenAIResponsesHandler extends BaseHandler {
|
|
|
54
63
|
/**
|
|
55
64
|
* Process a single chunk from the Responses API stream
|
|
56
65
|
*/
|
|
57
|
-
protected processResponseChunk(chunk: any, _modelInfo: ModelInfo, responseId: string
|
|
66
|
+
protected processResponseChunk(chunk: any, _modelInfo: ModelInfo, responseId: string, functionCallMetadataByItemId: Map<string, {
|
|
67
|
+
callId?: string;
|
|
68
|
+
name?: string;
|
|
69
|
+
}>): Generator<import("../types").ApiStreamChunk>;
|
|
58
70
|
private getApiResponseId;
|
|
59
71
|
}
|
|
60
72
|
/**
|
|
@@ -73,7 +73,7 @@ export interface TokenConfig {
|
|
|
73
73
|
*/
|
|
74
74
|
export interface ReasoningConfig {
|
|
75
75
|
/** Reasoning effort level */
|
|
76
|
-
reasoningEffort?: "low" | "medium" | "high";
|
|
76
|
+
reasoningEffort?: "low" | "medium" | "high" | "xhigh";
|
|
77
77
|
/** Extended thinking budget in tokens */
|
|
78
78
|
thinkingBudgetTokens?: number;
|
|
79
79
|
/** Enable thinking with provider/model defaults when supported */
|
|
@@ -45,6 +45,8 @@ export interface ToolUseContent {
|
|
|
45
45
|
type: "tool_use";
|
|
46
46
|
/** Unique ID for this tool call */
|
|
47
47
|
id: string;
|
|
48
|
+
/** Provider-native call ID for this tool call (if available) */
|
|
49
|
+
call_id?: string;
|
|
48
50
|
/** Name of the tool being called */
|
|
49
51
|
name: string;
|
|
50
52
|
/** Arguments for the tool call */
|
|
@@ -27,6 +27,7 @@ export declare const ReasoningSettingsSchema: z.ZodObject<{
|
|
|
27
27
|
low: "low";
|
|
28
28
|
high: "high";
|
|
29
29
|
medium: "medium";
|
|
30
|
+
xhigh: "xhigh";
|
|
30
31
|
none: "none";
|
|
31
32
|
}>>;
|
|
32
33
|
budgetTokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -136,6 +137,7 @@ export declare const ProviderSettingsSchema: z.ZodObject<{
|
|
|
136
137
|
low: "low";
|
|
137
138
|
high: "high";
|
|
138
139
|
medium: "medium";
|
|
140
|
+
xhigh: "xhigh";
|
|
139
141
|
none: "none";
|
|
140
142
|
}>>;
|
|
141
143
|
budgetTokens: z.ZodOptional<z.ZodNumber>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/llms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
|
|
4
5
|
"main": "./dist/index.js",
|
|
5
6
|
"module": "./dist/index.js",
|
|
6
7
|
"dependencies": {
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
"ai-sdk-provider-codex-cli": "^1.1.0",
|
|
18
19
|
"ai-sdk-provider-opencode-sdk": "^2.1.2",
|
|
19
20
|
"dify-ai-provider": "^1.1.0",
|
|
20
|
-
"nanoid": "^5.1.
|
|
21
|
+
"nanoid": "^5.1.7",
|
|
21
22
|
"openai": "^6.25.0",
|
|
22
23
|
"zod": "^4.3.6"
|
|
23
24
|
},
|
|
@@ -43,7 +44,6 @@
|
|
|
43
44
|
"types": "./dist/index.browser.d.ts"
|
|
44
45
|
}
|
|
45
46
|
},
|
|
46
|
-
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
49
|
"src"
|
|
@@ -25,8 +25,6 @@ const PROVIDER_TIMEOUT_MS = Number(
|
|
|
25
25
|
process.env.LLMS_LIVE_PROVIDER_TIMEOUT_MS ?? "90000",
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
const liveDescribe = LIVE_TEST_ENABLED ? describe : describe.skip;
|
|
29
|
-
|
|
30
28
|
function requireProvidersFilePath(): string {
|
|
31
29
|
const filePath = process.env[PROVIDERS_FILE_ENV];
|
|
32
30
|
if (!filePath) {
|
|
@@ -108,8 +106,11 @@ async function runPrompt(target: ProviderTarget): Promise<void> {
|
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
describe("live provider smoke test", () => {
|
|
112
110
|
it("reads configured providers from json and reports providers with failed responses", async () => {
|
|
111
|
+
if (!LIVE_TEST_ENABLED) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
113
114
|
const filePath = requireProvidersFilePath();
|
|
114
115
|
const targets = loadProviderTargets(filePath);
|
|
115
116
|
|
package/src/models/generated.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
14
14
|
version: number;
|
|
15
15
|
providers: Record<string, Record<string, ModelInfo>>;
|
|
16
16
|
} = {
|
|
17
|
-
version:
|
|
17
|
+
version: 1773862738565,
|
|
18
18
|
providers: {
|
|
19
19
|
aihubmix: {
|
|
20
20
|
"claude-sonnet-4-6": {
|
|
@@ -1124,7 +1124,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1124
1124
|
"anthropic.claude-sonnet-4-6": {
|
|
1125
1125
|
id: "anthropic.claude-sonnet-4-6",
|
|
1126
1126
|
name: "Claude Sonnet 4.6",
|
|
1127
|
-
contextWindow:
|
|
1127
|
+
contextWindow: 1000000,
|
|
1128
1128
|
maxTokens: 64000,
|
|
1129
1129
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1130
1130
|
pricing: {
|
|
@@ -1138,7 +1138,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1138
1138
|
"eu.anthropic.claude-sonnet-4-6": {
|
|
1139
1139
|
id: "eu.anthropic.claude-sonnet-4-6",
|
|
1140
1140
|
name: "Claude Sonnet 4.6 (EU)",
|
|
1141
|
-
contextWindow:
|
|
1141
|
+
contextWindow: 1000000,
|
|
1142
1142
|
maxTokens: 64000,
|
|
1143
1143
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1144
1144
|
pricing: {
|
|
@@ -1152,7 +1152,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1152
1152
|
"global.anthropic.claude-sonnet-4-6": {
|
|
1153
1153
|
id: "global.anthropic.claude-sonnet-4-6",
|
|
1154
1154
|
name: "Claude Sonnet 4.6 (Global)",
|
|
1155
|
-
contextWindow:
|
|
1155
|
+
contextWindow: 1000000,
|
|
1156
1156
|
maxTokens: 64000,
|
|
1157
1157
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1158
1158
|
pricing: {
|
|
@@ -1180,7 +1180,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1180
1180
|
"us.anthropic.claude-sonnet-4-6": {
|
|
1181
1181
|
id: "us.anthropic.claude-sonnet-4-6",
|
|
1182
1182
|
name: "Claude Sonnet 4.6 (US)",
|
|
1183
|
-
contextWindow:
|
|
1183
|
+
contextWindow: 1000000,
|
|
1184
1184
|
maxTokens: 64000,
|
|
1185
1185
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1186
1186
|
pricing: {
|
|
@@ -1222,7 +1222,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1222
1222
|
"anthropic.claude-opus-4-6-v1": {
|
|
1223
1223
|
id: "anthropic.claude-opus-4-6-v1",
|
|
1224
1224
|
name: "Claude Opus 4.6",
|
|
1225
|
-
contextWindow:
|
|
1225
|
+
contextWindow: 1000000,
|
|
1226
1226
|
maxTokens: 128000,
|
|
1227
1227
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1228
1228
|
pricing: {
|
|
@@ -1236,7 +1236,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1236
1236
|
"eu.anthropic.claude-opus-4-6-v1": {
|
|
1237
1237
|
id: "eu.anthropic.claude-opus-4-6-v1",
|
|
1238
1238
|
name: "Claude Opus 4.6 (EU)",
|
|
1239
|
-
contextWindow:
|
|
1239
|
+
contextWindow: 1000000,
|
|
1240
1240
|
maxTokens: 128000,
|
|
1241
1241
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1242
1242
|
pricing: {
|
|
@@ -1250,7 +1250,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1250
1250
|
"global.anthropic.claude-opus-4-6-v1": {
|
|
1251
1251
|
id: "global.anthropic.claude-opus-4-6-v1",
|
|
1252
1252
|
name: "Claude Opus 4.6 (Global)",
|
|
1253
|
-
contextWindow:
|
|
1253
|
+
contextWindow: 1000000,
|
|
1254
1254
|
maxTokens: 128000,
|
|
1255
1255
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1256
1256
|
pricing: {
|
|
@@ -1264,7 +1264,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
1264
1264
|
"us.anthropic.claude-opus-4-6-v1": {
|
|
1265
1265
|
id: "us.anthropic.claude-opus-4-6-v1",
|
|
1266
1266
|
name: "Claude Opus 4.6 (US)",
|
|
1267
|
-
contextWindow:
|
|
1267
|
+
contextWindow: 1000000,
|
|
1268
1268
|
maxTokens: 128000,
|
|
1269
1269
|
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
1270
1270
|
pricing: {
|
|
@@ -3434,6 +3434,34 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
3434
3434
|
},
|
|
3435
3435
|
},
|
|
3436
3436
|
minimax: {
|
|
3437
|
+
"MiniMax-M2.7": {
|
|
3438
|
+
id: "MiniMax-M2.7",
|
|
3439
|
+
name: "MiniMax-M2.7",
|
|
3440
|
+
contextWindow: 204800,
|
|
3441
|
+
maxTokens: 131072,
|
|
3442
|
+
capabilities: ["tools", "reasoning", "temperature"],
|
|
3443
|
+
pricing: {
|
|
3444
|
+
input: 0.3,
|
|
3445
|
+
output: 1.2,
|
|
3446
|
+
cacheRead: 0.06,
|
|
3447
|
+
cacheWrite: 0.375,
|
|
3448
|
+
},
|
|
3449
|
+
releaseDate: "2026-03-18",
|
|
3450
|
+
},
|
|
3451
|
+
"MiniMax-M2.7-highspeed": {
|
|
3452
|
+
id: "MiniMax-M2.7-highspeed",
|
|
3453
|
+
name: "MiniMax-M2.7-highspeed",
|
|
3454
|
+
contextWindow: 204800,
|
|
3455
|
+
maxTokens: 131072,
|
|
3456
|
+
capabilities: ["tools", "reasoning", "temperature"],
|
|
3457
|
+
pricing: {
|
|
3458
|
+
input: 0.6,
|
|
3459
|
+
output: 2.4,
|
|
3460
|
+
cacheRead: 0.06,
|
|
3461
|
+
cacheWrite: 0.375,
|
|
3462
|
+
},
|
|
3463
|
+
releaseDate: "2026-03-18",
|
|
3464
|
+
},
|
|
3437
3465
|
"MiniMax-M2.5-highspeed": {
|
|
3438
3466
|
id: "MiniMax-M2.5-highspeed",
|
|
3439
3467
|
name: "MiniMax-M2.5-highspeed",
|
|
@@ -5201,6 +5229,20 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
5201
5229
|
},
|
|
5202
5230
|
},
|
|
5203
5231
|
openrouter: {
|
|
5232
|
+
"minimax/minimax-m2.7": {
|
|
5233
|
+
id: "minimax/minimax-m2.7",
|
|
5234
|
+
name: "MiniMax M2.7",
|
|
5235
|
+
contextWindow: 204800,
|
|
5236
|
+
maxTokens: 131072,
|
|
5237
|
+
capabilities: ["tools", "reasoning", "temperature"],
|
|
5238
|
+
pricing: {
|
|
5239
|
+
input: 0.3,
|
|
5240
|
+
output: 1.2,
|
|
5241
|
+
cacheRead: 0.06,
|
|
5242
|
+
cacheWrite: 0.375,
|
|
5243
|
+
},
|
|
5244
|
+
releaseDate: "2026-03-18",
|
|
5245
|
+
},
|
|
5204
5246
|
"x-ai/grok-4.20-beta": {
|
|
5205
5247
|
id: "x-ai/grok-4.20-beta",
|
|
5206
5248
|
name: "Grok 4.20 Beta",
|
|
@@ -9071,6 +9113,34 @@ export const GENERATED_PROVIDER_MODELS: {
|
|
|
9071
9113
|
},
|
|
9072
9114
|
},
|
|
9073
9115
|
"vercel-ai-gateway": {
|
|
9116
|
+
"minimax/minimax-m2.7": {
|
|
9117
|
+
id: "minimax/minimax-m2.7",
|
|
9118
|
+
name: "Minimax M2.7",
|
|
9119
|
+
contextWindow: 204800,
|
|
9120
|
+
maxTokens: 131000,
|
|
9121
|
+
capabilities: ["images", "files", "tools", "reasoning", "temperature"],
|
|
9122
|
+
pricing: {
|
|
9123
|
+
input: 0.3,
|
|
9124
|
+
output: 1.2,
|
|
9125
|
+
cacheRead: 0.06,
|
|
9126
|
+
cacheWrite: 0.375,
|
|
9127
|
+
},
|
|
9128
|
+
releaseDate: "2026-03-18",
|
|
9129
|
+
},
|
|
9130
|
+
"minimax/minimax-m2.7-highspeed": {
|
|
9131
|
+
id: "minimax/minimax-m2.7-highspeed",
|
|
9132
|
+
name: "MiniMax M2.7 High Speed",
|
|
9133
|
+
contextWindow: 204800,
|
|
9134
|
+
maxTokens: 131100,
|
|
9135
|
+
capabilities: ["images", "tools", "reasoning", "temperature"],
|
|
9136
|
+
pricing: {
|
|
9137
|
+
input: 0.6,
|
|
9138
|
+
output: 2.4,
|
|
9139
|
+
cacheRead: 0.06,
|
|
9140
|
+
cacheWrite: 0.375,
|
|
9141
|
+
},
|
|
9142
|
+
releaseDate: "2026-03-18",
|
|
9143
|
+
},
|
|
9074
9144
|
"openai/gpt-5.4-mini": {
|
|
9075
9145
|
id: "openai/gpt-5.4-mini",
|
|
9076
9146
|
name: "GPT 5.4 Mini",
|
|
@@ -15,7 +15,7 @@ export const GEMINI_PROVIDER: ModelCollection = {
|
|
|
15
15
|
provider: {
|
|
16
16
|
id: "gemini",
|
|
17
17
|
name: "Google Gemini",
|
|
18
|
-
description: "Google
|
|
18
|
+
description: "Google Gemini API",
|
|
19
19
|
protocol: "gemini",
|
|
20
20
|
baseUrl: "https://generativelanguage.googleapis.com",
|
|
21
21
|
defaultModelId: GEMINI_DEFAULT_MODEL,
|
|
@@ -145,7 +145,7 @@ export class AnthropicHandler extends BaseHandler {
|
|
|
145
145
|
thinking: reasoningOn
|
|
146
146
|
? { type: "enabled", budget_tokens: budgetTokens }
|
|
147
147
|
: undefined,
|
|
148
|
-
max_tokens: model.info.maxTokens
|
|
148
|
+
max_tokens: model.info.maxTokens ?? this.config.maxOutputTokens ?? 8192,
|
|
149
149
|
temperature: reasoningOn ? undefined : 0,
|
|
150
150
|
system: supportsPromptCache
|
|
151
151
|
? [
|
|
@@ -143,7 +143,7 @@ export class BedrockHandler extends BaseHandler {
|
|
|
143
143
|
model: factory(modelId),
|
|
144
144
|
messages: this.getMessages(systemPrompt, messages),
|
|
145
145
|
tools: toAiSdkTools(tools),
|
|
146
|
-
maxTokens: model.info.maxTokens ??
|
|
146
|
+
maxTokens: model.info.maxTokens ?? this.config.maxOutputTokens ?? 8192,
|
|
147
147
|
temperature: reasoningEnabled ? undefined : (model.info.temperature ?? 0),
|
|
148
148
|
providerOptions:
|
|
149
149
|
Object.keys(providerOptions).length > 0 ? providerOptions : undefined,
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ToolDefinition } from "../types/messages";
|
|
3
|
+
import type { ApiStreamChunk } from "../types/stream";
|
|
4
|
+
|
|
5
|
+
const generateContentStreamSpy = vi.fn();
|
|
6
|
+
const googleGenAIConstructorSpy = vi.fn();
|
|
7
|
+
|
|
8
|
+
vi.mock("@google/genai", () => {
|
|
9
|
+
class GoogleGenAI {
|
|
10
|
+
models = {
|
|
11
|
+
generateContentStream: generateContentStreamSpy,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
constructor(config: unknown) {
|
|
15
|
+
googleGenAIConstructorSpy(config);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
GoogleGenAI,
|
|
21
|
+
FunctionCallingConfigMode: { AUTO: "AUTO" },
|
|
22
|
+
ThinkingLevel: { HIGH: "HIGH", LOW: "LOW" },
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import { GeminiHandler } from "./gemini-base";
|
|
27
|
+
|
|
28
|
+
function createAsyncIterable<T>(items: T[]): AsyncIterable<T> {
|
|
29
|
+
return {
|
|
30
|
+
async *[Symbol.asyncIterator]() {
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
yield item;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function collectChunks(stream: AsyncIterable<ApiStreamChunk>) {
|
|
39
|
+
const chunks: ApiStreamChunk[] = [];
|
|
40
|
+
for await (const chunk of stream) {
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
}
|
|
43
|
+
return chunks;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("GeminiHandler", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("preserves per-call ids for parallel function calls and keeps falsy args", async () => {
|
|
52
|
+
generateContentStreamSpy.mockResolvedValue(
|
|
53
|
+
createAsyncIterable([
|
|
54
|
+
{
|
|
55
|
+
candidates: [
|
|
56
|
+
{
|
|
57
|
+
content: {
|
|
58
|
+
parts: [
|
|
59
|
+
{
|
|
60
|
+
functionCall: {
|
|
61
|
+
id: "call_a",
|
|
62
|
+
name: "power_disco_ball",
|
|
63
|
+
args: { power: false },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
functionCall: {
|
|
68
|
+
id: "call_b",
|
|
69
|
+
name: "dim_lights",
|
|
70
|
+
args: { brightness: 0 },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
usageMetadata: {
|
|
78
|
+
promptTokenCount: 10,
|
|
79
|
+
candidatesTokenCount: 4,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handler = new GeminiHandler({
|
|
86
|
+
providerId: "gemini",
|
|
87
|
+
modelId: "gemini-2.5-flash",
|
|
88
|
+
apiKey: "test-key",
|
|
89
|
+
modelInfo: {
|
|
90
|
+
id: "gemini-2.5-flash",
|
|
91
|
+
contextWindow: 1_000_000,
|
|
92
|
+
maxTokens: 8192,
|
|
93
|
+
temperature: 1,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const tools: ToolDefinition[] = [
|
|
98
|
+
{
|
|
99
|
+
name: "power_disco_ball",
|
|
100
|
+
description: "toggle disco ball power",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: { power: { type: "boolean" } },
|
|
104
|
+
required: ["power"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "dim_lights",
|
|
109
|
+
description: "set light brightness",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: { brightness: { type: "number" } },
|
|
113
|
+
required: ["brightness"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const chunks = await collectChunks(
|
|
119
|
+
handler.createMessage(
|
|
120
|
+
"You are helpful.",
|
|
121
|
+
[{ role: "user", content: "start" }],
|
|
122
|
+
tools,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const toolChunks = chunks.filter((chunk) => chunk.type === "tool_calls");
|
|
127
|
+
expect(toolChunks).toHaveLength(2);
|
|
128
|
+
expect(toolChunks[0]).toMatchObject({
|
|
129
|
+
tool_call: {
|
|
130
|
+
call_id: "call_a",
|
|
131
|
+
function: {
|
|
132
|
+
id: "call_a",
|
|
133
|
+
name: "power_disco_ball",
|
|
134
|
+
arguments: { power: false },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
expect(toolChunks[1]).toMatchObject({
|
|
139
|
+
tool_call: {
|
|
140
|
+
call_id: "call_b",
|
|
141
|
+
function: {
|
|
142
|
+
id: "call_b",
|
|
143
|
+
name: "dim_lights",
|
|
144
|
+
arguments: { brightness: 0 },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("generates distinct fallback ids when Gemini omits functionCall.id", async () => {
|
|
151
|
+
generateContentStreamSpy.mockResolvedValue(
|
|
152
|
+
createAsyncIterable([
|
|
153
|
+
{
|
|
154
|
+
candidates: [
|
|
155
|
+
{
|
|
156
|
+
content: {
|
|
157
|
+
parts: [
|
|
158
|
+
{
|
|
159
|
+
functionCall: {
|
|
160
|
+
name: "read_file",
|
|
161
|
+
args: { path: "a.ts" },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
functionCall: {
|
|
166
|
+
name: "search_files",
|
|
167
|
+
args: { query: "TODO" },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
usageMetadata: {
|
|
175
|
+
promptTokenCount: 5,
|
|
176
|
+
candidatesTokenCount: 3,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
]),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const handler = new GeminiHandler({
|
|
183
|
+
providerId: "gemini",
|
|
184
|
+
modelId: "gemini-2.5-flash",
|
|
185
|
+
apiKey: "test-key",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const chunks = await collectChunks(
|
|
189
|
+
handler.createMessage(
|
|
190
|
+
"System",
|
|
191
|
+
[{ role: "user", content: "go" }],
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
name: "read_file",
|
|
195
|
+
description: "read file",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: { path: { type: "string" } },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "search_files",
|
|
203
|
+
description: "search files",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: { query: { type: "string" } },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const toolChunks = chunks.filter((chunk) => chunk.type === "tool_calls");
|
|
214
|
+
expect(toolChunks).toHaveLength(2);
|
|
215
|
+
const firstId = toolChunks[0].tool_call.call_id;
|
|
216
|
+
const secondId = toolChunks[1].tool_call.call_id;
|
|
217
|
+
expect(firstId).toBeTruthy();
|
|
218
|
+
expect(secondId).toBeTruthy();
|
|
219
|
+
expect(firstId).not.toBe(secondId);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -138,6 +138,7 @@ export class GeminiHandler extends BaseHandler {
|
|
|
138
138
|
abortSignal,
|
|
139
139
|
systemInstruction: systemPrompt,
|
|
140
140
|
temperature: info.temperature ?? 1,
|
|
141
|
+
maxOutputTokens: info.maxTokens ?? this.config.maxOutputTokens,
|
|
141
142
|
};
|
|
142
143
|
|
|
143
144
|
// Add thinking config only when explicitly requested and supported.
|
|
@@ -176,6 +177,7 @@ export class GeminiHandler extends BaseHandler {
|
|
|
176
177
|
let outputTokens = 0;
|
|
177
178
|
let cacheReadTokens = 0;
|
|
178
179
|
let thoughtsTokenCount = 0;
|
|
180
|
+
let syntheticToolCallIndex = 0;
|
|
179
181
|
|
|
180
182
|
for await (const chunk of result) {
|
|
181
183
|
// Handle content parts
|
|
@@ -203,18 +205,19 @@ export class GeminiHandler extends BaseHandler {
|
|
|
203
205
|
if (part.functionCall) {
|
|
204
206
|
// Tool call
|
|
205
207
|
const functionCall = part.functionCall;
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (args.length > 0) {
|
|
208
|
+
const callId =
|
|
209
|
+
functionCall.id ??
|
|
210
|
+
`${responseId}_tool_${syntheticToolCallIndex++}`;
|
|
211
|
+
if (functionCall.name) {
|
|
211
212
|
yield {
|
|
212
213
|
type: "tool_calls",
|
|
213
214
|
tool_call: {
|
|
215
|
+
call_id: callId,
|
|
214
216
|
function: {
|
|
215
|
-
id:
|
|
217
|
+
id: callId,
|
|
216
218
|
name: functionCall.name,
|
|
217
|
-
arguments:
|
|
219
|
+
arguments:
|
|
220
|
+
(functionCall.args as Record<string, unknown>) ?? {},
|
|
218
221
|
},
|
|
219
222
|
},
|
|
220
223
|
id: responseId,
|
|
@@ -150,8 +150,9 @@ export class OpenAIBaseHandler extends BaseHandler {
|
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
// Add max tokens if configured
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const maxTokens = modelInfo.maxTokens ?? this.config.maxOutputTokens;
|
|
154
|
+
if (maxTokens) {
|
|
155
|
+
requestOptions.max_completion_tokens = maxTokens;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Add temperature if not a reasoning model
|