@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.
@@ -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: string | Array<{
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): Generator<import("../types").ApiStreamChunk>;
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,10 +1,10 @@
1
1
  {
2
2
  "name": "@clinebot/llms",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
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": {
7
- "@clinebot/shared": "workspace:*",
8
8
  "@ai-sdk/amazon-bedrock": "^4.0.67",
9
9
  "@ai-sdk/google-vertex": "^4.0.74",
10
10
  "@ai-sdk/mistral": "^3.0.24",
@@ -17,7 +17,7 @@
17
17
  "ai-sdk-provider-codex-cli": "^1.1.0",
18
18
  "ai-sdk-provider-opencode-sdk": "^2.1.2",
19
19
  "dify-ai-provider": "^1.1.0",
20
- "nanoid": "^5.1.6",
20
+ "nanoid": "^5.1.7",
21
21
  "openai": "^6.25.0",
22
22
  "zod": "^4.3.6"
23
23
  },
@@ -43,7 +43,6 @@
43
43
  "types": "./dist/index.browser.d.ts"
44
44
  }
45
45
  },
46
- "description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
47
46
  "files": [
48
47
  "dist",
49
48
  "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
- liveDescribe("live provider smoke test", () => {
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
 
@@ -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: 1773798019500,
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: 200000,
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: 200000,
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: 200000,
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: 200000,
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: 200000,
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: 200000,
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: 200000,
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: 200000,
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's multimodal AI models",
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 || 8192,
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 ?? undefined,
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 args = Object.entries(functionCall.args ?? {}).filter(
207
- ([, val]) => !!val,
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: responseId,
217
+ id: callId,
216
218
  name: functionCall.name,
217
- arguments: functionCall.args as Record<string, unknown>,
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
- if (modelInfo.maxTokens) {
154
- requestOptions.max_completion_tokens = modelInfo.maxTokens;
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