@clinebot/llms 0.0.5 → 0.0.7

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.
@@ -5,6 +5,7 @@
5
5
  * Supports prompt caching, extended thinking, and native tool calling.
6
6
  */
7
7
  import { Anthropic } from "@anthropic-ai/sdk";
8
+ import type { RawMessageStreamEvent } from "@anthropic-ai/sdk/resources";
8
9
  import { type ApiStream, type HandlerModelInfo, type ProviderConfig } from "../types";
9
10
  import type { Message, ToolDefinition } from "../types/messages";
10
11
  import { BaseHandler } from "./base";
@@ -18,7 +19,16 @@ export declare class AnthropicHandler extends BaseHandler {
18
19
  getMessages(_systemPrompt: string, messages: Message[]): Anthropic.MessageParam[];
19
20
  createMessage(systemPrompt: string, messages: Message[], tools?: ToolDefinition[]): ApiStream;
20
21
  private createMessageInternal;
21
- private processChunk;
22
+ protected processChunk(chunk: RawMessageStreamEvent, currentToolCall: {
23
+ id: string;
24
+ name: string;
25
+ arguments: string;
26
+ }, usageSnapshot: {
27
+ inputTokens: number;
28
+ outputTokens: number;
29
+ cacheReadTokens: number;
30
+ cacheWriteTokens: number;
31
+ }, responseId: string): Generator<import("../types").ApiStreamChunk>;
22
32
  }
23
33
  /**
24
34
  * Create an Anthropic handler
@@ -44,6 +44,7 @@ export declare abstract class BaseHandler implements ApiHandler {
44
44
  * Abort the current request
45
45
  */
46
46
  abort(): void;
47
+ setAbortSignal(signal: AbortSignal | undefined): void;
47
48
  /**
48
49
  * Helper to calculate cost from usage
49
50
  */
@@ -51,6 +51,10 @@ export interface ApiHandler {
51
51
  * Abort the current request (optional)
52
52
  */
53
53
  abort?(): void;
54
+ /**
55
+ * Update the abort signal used for subsequent requests (optional).
56
+ */
57
+ setAbortSignal?(signal: AbortSignal | undefined): void;
54
58
  }
55
59
  /**
56
60
  * Handler for simple single-turn completions
@@ -103,6 +103,8 @@ export interface Message {
103
103
  export interface MessageWithMetadata extends Message {
104
104
  /** Unique message ID */
105
105
  id?: string;
106
+ /** Additional message metadata for storage/history consumers */
107
+ metadata?: Record<string, unknown>;
106
108
  /** Provider ID used to generate this message */
107
109
  providerId?: string;
108
110
  /** Model ID used to generate this message */
@@ -11,7 +11,7 @@ interface ToolCallDelta {
11
11
  id?: string;
12
12
  function?: {
13
13
  name?: string;
14
- arguments?: string;
14
+ arguments?: unknown;
15
15
  };
16
16
  }
17
17
  /**
@@ -31,4 +31,5 @@ export declare class ToolCallProcessor {
31
31
  * Reset the processor state
32
32
  */
33
33
  reset(): void;
34
+ private normalizeArgumentsDelta;
34
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clinebot/llms",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -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: 1773940480693,
17
+ version: 1774031158558,
18
18
  providers: {
19
19
  aihubmix: {
20
20
  "claude-sonnet-4-6": {
@@ -5296,6 +5296,47 @@ export const GENERATED_PROVIDER_MODELS: {
5296
5296
  },
5297
5297
  releaseDate: "2026-03-18",
5298
5298
  },
5299
+ "openai/gpt-5.4-mini": {
5300
+ id: "openai/gpt-5.4-mini",
5301
+ name: "GPT-5.4 Mini",
5302
+ contextWindow: 400000,
5303
+ maxTokens: 128000,
5304
+ capabilities: [
5305
+ "images",
5306
+ "files",
5307
+ "tools",
5308
+ "reasoning",
5309
+ "structured_output",
5310
+ "temperature",
5311
+ ],
5312
+ pricing: {
5313
+ input: 7.5e-7,
5314
+ output: 0.0000045,
5315
+ cacheRead: 7.5e-8,
5316
+ cacheWrite: 0,
5317
+ },
5318
+ releaseDate: "2026-03-17",
5319
+ },
5320
+ "openai/gpt-5.4-nano": {
5321
+ id: "openai/gpt-5.4-nano",
5322
+ name: "GPT-5.4 Nano",
5323
+ contextWindow: 400000,
5324
+ maxTokens: 128000,
5325
+ capabilities: [
5326
+ "images",
5327
+ "files",
5328
+ "tools",
5329
+ "structured_output",
5330
+ "temperature",
5331
+ ],
5332
+ pricing: {
5333
+ input: 2e-7,
5334
+ output: 0.00000125,
5335
+ cacheRead: 2e-8,
5336
+ cacheWrite: 0,
5337
+ },
5338
+ releaseDate: "2026-03-17",
5339
+ },
5299
5340
  "x-ai/grok-4.20-beta": {
5300
5341
  id: "x-ai/grok-4.20-beta",
5301
5342
  name: "Grok 4.20 Beta",
@@ -9194,6 +9235,20 @@ export const GENERATED_PROVIDER_MODELS: {
9194
9235
  },
9195
9236
  releaseDate: "2026-03-18",
9196
9237
  },
9238
+ "xiaomi/mimo-v2-pro": {
9239
+ id: "xiaomi/mimo-v2-pro",
9240
+ name: "MiMo V2 Pro",
9241
+ contextWindow: 1000000,
9242
+ maxTokens: 128000,
9243
+ capabilities: ["tools", "reasoning", "temperature"],
9244
+ pricing: {
9245
+ input: 1,
9246
+ output: 3,
9247
+ cacheRead: 0.19999999999999998,
9248
+ cacheWrite: 0,
9249
+ },
9250
+ releaseDate: "2026-03-18",
9251
+ },
9197
9252
  "openai/gpt-5.4-mini": {
9198
9253
  id: "openai/gpt-5.4-mini",
9199
9254
  name: "GPT 5.4 Mini",
@@ -7,10 +7,26 @@
7
7
 
8
8
  import { getGeneratedModelsForProvider } from "../generated-access";
9
9
  import type { ModelCollection, ModelInfo } from "../schemas/index";
10
- import { OPENAI_MODELS } from "./openai";
10
+
11
+ function removeCustomToolCapability(model: ModelInfo): ModelInfo {
12
+ if (!model.capabilities?.includes("tools")) {
13
+ return model;
14
+ }
15
+
16
+ return {
17
+ ...model,
18
+ capabilities: model.capabilities.filter(
19
+ (capability) => capability !== "tools",
20
+ ),
21
+ };
22
+ }
11
23
 
12
24
  export const OPENAI_CODEX_MODELS: Record<string, ModelInfo> =
13
- getGeneratedModelsForProvider("openai");
25
+ Object.fromEntries(
26
+ Object.entries(getGeneratedModelsForProvider("openai")).map(
27
+ ([modelId, model]) => [modelId, removeCustomToolCapability(model)],
28
+ ),
29
+ );
14
30
 
15
31
  export const OPENAI_CODEX_DEFAULT_MODEL =
16
32
  Object.keys(OPENAI_CODEX_MODELS)[0] ?? "gpt-5.3-codex";
@@ -26,5 +42,5 @@ export const OPENAI_CODEX_PROVIDER: ModelCollection = {
26
42
  defaultModelId: OPENAI_CODEX_DEFAULT_MODEL,
27
43
  capabilities: ["reasoning", "oauth"],
28
44
  },
29
- models: OPENAI_MODELS,
45
+ models: OPENAI_CODEX_MODELS,
30
46
  };
@@ -203,7 +203,7 @@ export class AnthropicHandler extends BaseHandler {
203
203
  yield { type: "done", success: true, id: responseId };
204
204
  }
205
205
 
206
- private *processChunk(
206
+ protected *processChunk(
207
207
  chunk: RawMessageStreamEvent,
208
208
  currentToolCall: { id: string; name: string; arguments: string },
209
209
  usageSnapshot: {
@@ -332,11 +332,13 @@ export class AnthropicHandler extends BaseHandler {
332
332
  case "content_block_stop": {
333
333
  // If we have a tool call, yield it
334
334
  if (currentToolCall.id) {
335
- let parsedArgs: Record<string, unknown>;
335
+ let parsedArgs: string | Record<string, unknown>;
336
336
  try {
337
337
  parsedArgs = JSON.parse(currentToolCall.arguments || "{}");
338
338
  } catch {
339
- parsedArgs = {};
339
+ // Preserve the raw JSON fragment so downstream can classify it
340
+ // as an invalid tool call instead of silently turning it into {}.
341
+ parsedArgs = currentToolCall.arguments;
340
342
  }
341
343
 
342
344
  yield {
@@ -102,6 +102,13 @@ export abstract class BaseHandler implements ApiHandler {
102
102
  this.abortController?.abort();
103
103
  }
104
104
 
105
+ setAbortSignal(signal: AbortSignal | undefined): void {
106
+ this.config.abortSignal = signal;
107
+ if (signal?.aborted) {
108
+ this.abortController?.abort(signal.reason);
109
+ }
110
+ }
111
+
105
112
  /**
106
113
  * Helper to calculate cost from usage
107
114
  */
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { OPENAI_CODEX_PROVIDER } from "../../models/providers/openai-codex";
2
3
  import { CodexHandler } from "./community-sdk";
3
4
 
4
5
  const streamTextSpy = vi.fn();
@@ -120,4 +121,40 @@ describe("CodexHandler", () => {
120
121
  "sk-test-key",
121
122
  );
122
123
  });
124
+
125
+ it("does not surface Codex native tool calls as local tool calls", async () => {
126
+ streamTextSpy.mockReturnValue({
127
+ fullStream: makeStreamParts([
128
+ {
129
+ type: "tool-call",
130
+ toolCallId: "codex-call-1",
131
+ toolName: "read_file",
132
+ args: { path: "README.md" },
133
+ },
134
+ {
135
+ type: "finish",
136
+ usage: { inputTokens: 8, outputTokens: 3 },
137
+ },
138
+ ]),
139
+ });
140
+
141
+ const handler = new CodexHandler({
142
+ providerId: "openai-codex",
143
+ modelId: "gpt-5.3-codex",
144
+ });
145
+
146
+ const chunks: Array<Record<string, unknown>> = [];
147
+ for await (const chunk of handler.createMessage("System", [
148
+ { role: "user", content: "Hi" },
149
+ ])) {
150
+ chunks.push(chunk as unknown as Record<string, unknown>);
151
+ }
152
+
153
+ expect(chunks.map((chunk) => chunk.type)).toEqual(["usage", "done"]);
154
+ });
155
+
156
+ it("does not advertise custom tool capability for Codex models", () => {
157
+ const model = OPENAI_CODEX_PROVIDER.models["gpt-5.3-codex"];
158
+ expect(model?.capabilities).not.toContain("tools");
159
+ });
123
160
  });
@@ -145,7 +145,6 @@ export class CodexHandler extends AiSdkProviderHandler {
145
145
  > {
146
146
  return {
147
147
  reasoningTypes: ["reasoning-delta", "reasoning"],
148
- enableToolCalls: true,
149
148
  toolCallArgsOrder: ["args", "input"],
150
149
  };
151
150
  }
@@ -62,6 +62,11 @@ export interface ApiHandler {
62
62
  * Abort the current request (optional)
63
63
  */
64
64
  abort?(): void;
65
+
66
+ /**
67
+ * Update the abort signal used for subsequent requests (optional).
68
+ */
69
+ setAbortSignal?(signal: AbortSignal | undefined): void;
65
70
  }
66
71
 
67
72
  /**
@@ -121,6 +121,8 @@ export interface Message {
121
121
  export interface MessageWithMetadata extends Message {
122
122
  /** Unique message ID */
123
123
  id?: string;
124
+ /** Additional message metadata for storage/history consumers */
125
+ metadata?: Record<string, unknown>;
124
126
  /** Provider ID used to generate this message */
125
127
  providerId?: string;
126
128
  /** Model ID used to generate this message */
@@ -33,6 +33,66 @@ describe("ToolCallProcessor", () => {
33
33
  expect(second[0].tool_call.function.arguments).toBe(' -la"]}');
34
34
  });
35
35
 
36
+ it("normalizes cumulative argument snapshots into deltas", () => {
37
+ const processor = new ToolCallProcessor();
38
+
39
+ const first = processor.processToolCallDeltas(
40
+ [
41
+ {
42
+ index: 0,
43
+ id: "call_1",
44
+ function: { name: "editor", arguments: '{"command":"create"' },
45
+ },
46
+ ],
47
+ "resp_1",
48
+ );
49
+
50
+ const second = processor.processToolCallDeltas(
51
+ [
52
+ {
53
+ index: 0,
54
+ function: {
55
+ arguments: '{"command":"create","path":"/tmp/file.txt"}',
56
+ },
57
+ },
58
+ ],
59
+ "resp_1",
60
+ );
61
+
62
+ expect(first).toHaveLength(1);
63
+ expect(second).toHaveLength(1);
64
+ expect(first[0].tool_call.function.arguments).toBe('{"command":"create"');
65
+ expect(second[0].tool_call.function.arguments).toBe(
66
+ ',"path":"/tmp/file.txt"}',
67
+ );
68
+ });
69
+
70
+ it("serializes object-shaped arguments instead of concatenating [object Object]", () => {
71
+ const processor = new ToolCallProcessor();
72
+
73
+ const result = processor.processToolCallDeltas(
74
+ [
75
+ {
76
+ index: 0,
77
+ id: "call_1",
78
+ function: {
79
+ name: "editor",
80
+ arguments: {
81
+ command: "create",
82
+ path: "/tmp/file.txt",
83
+ },
84
+ },
85
+ },
86
+ ],
87
+ "resp_1",
88
+ );
89
+
90
+ expect(result).toHaveLength(1);
91
+ expect(result[0].tool_call.function.arguments).toBe(
92
+ '{"command":"create","path":"/tmp/file.txt"}',
93
+ );
94
+ });
95
+
36
96
  it("preserves tool call id/name for interleaved parallel deltas", () => {
37
97
  const processor = new ToolCallProcessor();
38
98
 
@@ -18,7 +18,7 @@ interface ToolCallDelta {
18
18
  id?: string;
19
19
  function?: {
20
20
  name?: string;
21
- arguments?: string;
21
+ arguments?: unknown;
22
22
  };
23
23
  }
24
24
 
@@ -61,7 +61,11 @@ export class ToolCallProcessor {
61
61
  if (fn?.name) {
62
62
  toolCall.name = fn.name;
63
63
  }
64
- const deltaArguments = fn?.arguments ?? "";
64
+ const rawArguments = fn?.arguments;
65
+ const deltaArguments = this.normalizeArgumentsDelta(
66
+ toolCall.arguments,
67
+ rawArguments,
68
+ );
65
69
  if (deltaArguments) {
66
70
  toolCall.arguments += deltaArguments;
67
71
  }
@@ -108,4 +112,35 @@ export class ToolCallProcessor {
108
112
  reset(): void {
109
113
  this.toolCalls.clear();
110
114
  }
115
+
116
+ private normalizeArgumentsDelta(
117
+ accumulatedArguments: string,
118
+ rawArguments: unknown,
119
+ ): string {
120
+ if (rawArguments == null) {
121
+ return "";
122
+ }
123
+
124
+ const nextArguments =
125
+ typeof rawArguments === "string"
126
+ ? rawArguments
127
+ : JSON.stringify(rawArguments);
128
+
129
+ if (!nextArguments) {
130
+ return "";
131
+ }
132
+
133
+ // Some OpenAI-compatible providers emit cumulative argument snapshots
134
+ // instead of true deltas. Convert those snapshots back into a suffix so
135
+ // downstream accumulation only happens once.
136
+ if (
137
+ accumulatedArguments &&
138
+ nextArguments.length >= accumulatedArguments.length &&
139
+ nextArguments.startsWith(accumulatedArguments)
140
+ ) {
141
+ return nextArguments.slice(accumulatedArguments.length);
142
+ }
143
+
144
+ return nextArguments;
145
+ }
111
146
  }