@clinebot/llms 0.0.6 → 0.0.10

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.
Files changed (32) hide show
  1. package/dist/index.browser.d.ts +2 -2
  2. package/dist/index.browser.js +40 -1
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +12 -12
  5. package/dist/providers/handlers/anthropic-base.d.ts +11 -1
  6. package/dist/providers/handlers/base.d.ts +2 -27
  7. package/dist/providers/transform/openai-format.d.ts +1 -1
  8. package/dist/providers/types/config.d.ts +6 -0
  9. package/dist/providers/types/messages.d.ts +2 -0
  10. package/dist/providers/utils/tool-processor.d.ts +2 -1
  11. package/package.json +2 -1
  12. package/src/index.browser.ts +2 -2
  13. package/src/index.ts +2 -2
  14. package/src/models/generated.ts +15 -1
  15. package/src/models/providers/openai-codex.ts +19 -3
  16. package/src/models/providers/vercel-ai-gateway.ts +1 -1
  17. package/src/providers/handlers/anthropic-base.ts +19 -6
  18. package/src/providers/handlers/base.test.ts +60 -1
  19. package/src/providers/handlers/base.ts +83 -54
  20. package/src/providers/handlers/bedrock-base.ts +1 -1
  21. package/src/providers/handlers/codex.test.ts +37 -0
  22. package/src/providers/handlers/community-sdk.ts +0 -1
  23. package/src/providers/handlers/gemini-base.test.ts +40 -0
  24. package/src/providers/handlers/gemini-base.ts +16 -1
  25. package/src/providers/handlers/openai-base.ts +55 -11
  26. package/src/providers/handlers/vertex.ts +1 -1
  27. package/src/providers/transform/format-conversion.test.ts +26 -0
  28. package/src/providers/transform/openai-format.ts +50 -7
  29. package/src/providers/types/config.ts +8 -0
  30. package/src/providers/types/messages.ts +2 -0
  31. package/src/providers/utils/tool-processor.test.ts +60 -0
  32. package/src/providers/utils/tool-processor.ts +37 -2
@@ -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
@@ -13,41 +13,16 @@ export declare const DEFAULT_REQUEST_HEADERS: Record<string, string>;
13
13
  export declare abstract class BaseHandler implements ApiHandler {
14
14
  protected config: ProviderConfig;
15
15
  protected abortController: AbortController | undefined;
16
+ private abortSignalSequence;
16
17
  constructor(config: ProviderConfig);
17
- /**
18
- * Convert Cline messages to provider-specific format
19
- * Must be implemented by subclasses
20
- */
21
18
  abstract getMessages(systemPrompt: string, messages: Message[]): unknown;
22
- /**
23
- * Create a streaming message completion
24
- * Must be implemented by subclasses
25
- */
26
19
  abstract createMessage(systemPrompt: string, messages: Message[], tools?: ToolDefinition[]): ApiStream;
27
- /**
28
- * Get the current model configuration
29
- * Can be overridden by subclasses for provider-specific logic
30
- */
31
20
  getModel(): HandlerModelInfo;
32
- /**
33
- * Get usage information (optional)
34
- * Override in subclasses that support this
35
- */
36
21
  getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined>;
37
- /**
38
- * Get the abort signal for the current request
39
- * Creates a new AbortController if one doesn't exist or was already aborted
40
- * Combines with config.abortSignal if provided
41
- */
42
22
  protected getAbortSignal(): AbortSignal;
43
- /**
44
- * Abort the current request
45
- */
46
23
  abort(): void;
47
24
  setAbortSignal(signal: AbortSignal | undefined): void;
48
- /**
49
- * Helper to calculate cost from usage
50
- */
25
+ private logAbort;
51
26
  protected calculateCost(inputTokens: number, outputTokens: number, cacheReadTokens?: number): number | undefined;
52
27
  protected createResponseId(): string;
53
28
  protected withResponseId<T extends ApiStreamChunk>(chunk: T, responseId: string): T;
@@ -9,7 +9,7 @@ type OpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
9
9
  /**
10
10
  * Convert messages to OpenAI format
11
11
  */
12
- export declare function convertToOpenAIMessages(messages: Message[]): OpenAIMessage[];
12
+ export declare function convertToOpenAIMessages(messages: Message[], enableCaching?: boolean): OpenAIMessage[];
13
13
  /**
14
14
  * Convert tool definitions to OpenAI format
15
15
  */
@@ -187,6 +187,10 @@ export interface ProviderOptions {
187
187
  /** Runtime model catalog refresh configuration */
188
188
  modelCatalog?: ModelCatalogConfig;
189
189
  }
190
+ /**
191
+ * Provider-specific options that don't fit other categories
192
+ */
193
+ import type { BasicLogger } from "@clinebot/shared";
190
194
  /**
191
195
  * Runtime model catalog refresh options
192
196
  */
@@ -219,6 +223,8 @@ export interface ProviderConfig extends AuthConfig, EndpointConfig, ModelConfig,
219
223
  onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: unknown) => void;
220
224
  /** AbortSignal for cancelling requests */
221
225
  abortSignal?: AbortSignal;
226
+ /** Optional runtime logger for provider-level diagnostics */
227
+ logger?: BasicLogger;
222
228
  /** Codex CLI-specific options */
223
229
  codex?: CodexConfig;
224
230
  /** Claude Code-specific options */
@@ -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,10 +1,11 @@
1
1
  {
2
2
  "name": "@clinebot/llms",
3
- "version": "0.0.6",
3
+ "version": "0.0.10",
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",
7
7
  "dependencies": {
8
+ "@clinebot/shared": "0.0.10",
8
9
  "@ai-sdk/amazon-bedrock": "^4.0.67",
9
10
  "@ai-sdk/google-vertex": "^4.0.74",
10
11
  "@ai-sdk/mistral": "^3.0.24",
@@ -1,6 +1,6 @@
1
1
  export { defineLlmsConfig, loadLlmsConfigFromFile } from "./config-browser";
2
- export * as models from "./models/index";
3
- export * as providers from "./providers/public.browser";
2
+ export * as LlmsModels from "./models/index";
3
+ export * as LlmsProviders from "./providers/public.browser";
4
4
  export type {
5
5
  CustomProviderConfig,
6
6
  LlmsConfig,
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { defineLlmsConfig } from "./config";
2
- export * as models from "./models/index";
3
- export * as providers from "./providers/public";
2
+ export * as LlmsModels from "./models/index";
3
+ export * as LlmsProviders from "./providers/public";
4
4
  export { createLlmsSdk } from "./sdk";
5
5
  export type {
6
6
  CustomProviderConfig,
@@ -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: 1773975012193,
17
+ version: 1774031158558,
18
18
  providers: {
19
19
  aihubmix: {
20
20
  "claude-sonnet-4-6": {
@@ -9235,6 +9235,20 @@ export const GENERATED_PROVIDER_MODELS: {
9235
9235
  },
9236
9236
  releaseDate: "2026-03-18",
9237
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
+ },
9238
9252
  "openai/gpt-5.4-mini": {
9239
9253
  id: "openai/gpt-5.4-mini",
9240
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
  };
@@ -14,7 +14,7 @@ export const VERCEL_AI_GATEWAY_PROVIDER: ModelCollection = {
14
14
  name: "Vercel AI Gateway",
15
15
  description: "Vercel's AI gateway service",
16
16
  protocol: "openai-chat",
17
- baseUrl: "https://ai-gateway.vercel.app/v1",
17
+ baseUrl: "https://ai-gateway.vercel.sh/v1",
18
18
  defaultModelId: Object.keys(VERCEL_AI_GATEWAY_MODELS)[0],
19
19
  capabilities: ["reasoning"],
20
20
  env: ["AI_GATEWAY_API_KEY"],
@@ -145,7 +145,8 @@ 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 ?? this.config.maxOutputTokens ?? 8192,
148
+ max_tokens:
149
+ model.info.maxTokens ?? this.config.maxOutputTokens ?? 128_000,
149
150
  temperature: reasoningOn ? undefined : 0,
150
151
  system: supportsPromptCache
151
152
  ? [
@@ -173,6 +174,7 @@ export class AnthropicHandler extends BaseHandler {
173
174
  cacheReadTokens: 0,
174
175
  cacheWriteTokens: 0,
175
176
  };
177
+ let stopReason: string | null = null;
176
178
 
177
179
  for await (const chunk of stream) {
178
180
  if (debugThinking) {
@@ -185,6 +187,11 @@ export class AnthropicHandler extends BaseHandler {
185
187
  countChunk(`content_block_delta:${chunk.delta?.type ?? "unknown"}`);
186
188
  }
187
189
  }
190
+ if (chunk.type === "message_delta") {
191
+ stopReason =
192
+ (chunk as { delta?: { stop_reason?: string } }).delta?.stop_reason ??
193
+ stopReason;
194
+ }
188
195
  yield* this.withResponseIdForAll(
189
196
  this.processChunk(chunk, currentToolCall, usageSnapshot, responseId),
190
197
  responseId,
@@ -199,11 +206,15 @@ export class AnthropicHandler extends BaseHandler {
199
206
  console.error(`[thinking-debug][anthropic][stream] ${summary}`);
200
207
  }
201
208
 
202
- // Yield done chunk to indicate streaming completed successfully
203
- yield { type: "done", success: true, id: responseId };
209
+ yield {
210
+ type: "done",
211
+ success: true,
212
+ id: responseId,
213
+ incompleteReason: stopReason === "max_tokens" ? "max_tokens" : undefined,
214
+ };
204
215
  }
205
216
 
206
- private *processChunk(
217
+ protected *processChunk(
207
218
  chunk: RawMessageStreamEvent,
208
219
  currentToolCall: { id: string; name: string; arguments: string },
209
220
  usageSnapshot: {
@@ -332,11 +343,13 @@ export class AnthropicHandler extends BaseHandler {
332
343
  case "content_block_stop": {
333
344
  // If we have a tool call, yield it
334
345
  if (currentToolCall.id) {
335
- let parsedArgs: Record<string, unknown>;
346
+ let parsedArgs: string | Record<string, unknown>;
336
347
  try {
337
348
  parsedArgs = JSON.parse(currentToolCall.arguments || "{}");
338
349
  } catch {
339
- parsedArgs = {};
350
+ // Preserve the raw JSON fragment so downstream can classify it
351
+ // as an invalid tool call instead of silently turning it into {}.
352
+ parsedArgs = currentToolCall.arguments;
340
353
  }
341
354
 
342
355
  yield {
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import type { ApiStream, ProviderConfig } from "../types/index";
3
3
  import { BaseHandler } from "./base";
4
4
 
@@ -18,6 +18,10 @@ class TestHandler extends BaseHandler {
18
18
  ): number | undefined {
19
19
  return this.calculateCost(inputTokens, outputTokens, cacheReadTokens);
20
20
  }
21
+
22
+ public exposeAbortSignal(): AbortSignal {
23
+ return this.getAbortSignal();
24
+ }
21
25
  }
22
26
 
23
27
  describe("BaseHandler.calculateCost", () => {
@@ -44,3 +48,58 @@ describe("BaseHandler.calculateCost", () => {
44
48
  expect(cost).toBeCloseTo(17.73, 6);
45
49
  });
46
50
  });
51
+
52
+ describe("BaseHandler abort signal wiring", () => {
53
+ it("does not let a stale request signal abort a newer request", () => {
54
+ const logger = {
55
+ debug: vi.fn(),
56
+ warn: vi.fn(),
57
+ };
58
+ const request1 = new AbortController();
59
+ const handler = new TestHandler({
60
+ providerId: "openrouter",
61
+ modelId: "mock-model",
62
+ apiKey: "test-key",
63
+ baseUrl: "https://example.com/v1",
64
+ abortSignal: request1.signal,
65
+ logger,
66
+ });
67
+
68
+ const signal1 = handler.exposeAbortSignal();
69
+ expect(signal1.aborted).toBe(false);
70
+
71
+ const request2 = new AbortController();
72
+ handler.setAbortSignal(request2.signal);
73
+ const signal2 = handler.exposeAbortSignal();
74
+ expect(signal2).not.toBe(signal1);
75
+ expect(signal2.aborted).toBe(false);
76
+
77
+ request1.abort(new Error("stale timeout"));
78
+
79
+ expect(signal1.aborted).toBe(true);
80
+ expect(signal2.aborted).toBe(false);
81
+ expect(logger.warn).toHaveBeenCalledWith(
82
+ "Provider request abort signal fired",
83
+ expect.objectContaining({
84
+ reason: expect.objectContaining({ message: "stale timeout" }),
85
+ }),
86
+ );
87
+ });
88
+
89
+ it("creates a fresh controller for each request", () => {
90
+ const handler = new TestHandler({
91
+ providerId: "openrouter",
92
+ modelId: "mock-model",
93
+ apiKey: "test-key",
94
+ baseUrl: "https://example.com/v1",
95
+ abortSignal: new AbortController().signal,
96
+ });
97
+
98
+ const signal1 = handler.exposeAbortSignal();
99
+ const signal2 = handler.exposeAbortSignal();
100
+
101
+ expect(signal2).not.toBe(signal1);
102
+ expect(signal1.aborted).toBe(false);
103
+ expect(signal2.aborted).toBe(false);
104
+ });
105
+ });
@@ -22,37 +22,44 @@ export const DEFAULT_REQUEST_HEADERS: Record<string, string> = {
22
22
  "X-CLIENT-TYPE": "cline-sdk",
23
23
  };
24
24
 
25
+ const controllerIds = new WeakMap<AbortController, string>();
26
+ let controllerIdCounter = 0;
27
+
28
+ function getControllerId(controller: AbortController): string {
29
+ let id = controllerIds.get(controller);
30
+ if (!id) {
31
+ id = `abort_${++controllerIdCounter}`;
32
+ controllerIds.set(controller, id);
33
+ }
34
+ return id;
35
+ }
36
+
37
+ function serializeAbortReason(reason: unknown): unknown {
38
+ return reason instanceof Error
39
+ ? { name: reason.name, message: reason.message }
40
+ : reason;
41
+ }
42
+
25
43
  /**
26
44
  * Base handler class with common functionality
27
45
  */
28
46
  export abstract class BaseHandler implements ApiHandler {
29
47
  protected config: ProviderConfig;
30
48
  protected abortController: AbortController | undefined;
49
+ private abortSignalSequence = 0;
31
50
 
32
51
  constructor(config: ProviderConfig) {
33
52
  this.config = config;
34
53
  }
35
54
 
36
- /**
37
- * Convert Cline messages to provider-specific format
38
- * Must be implemented by subclasses
39
- */
40
55
  abstract getMessages(systemPrompt: string, messages: Message[]): unknown;
41
56
 
42
- /**
43
- * Create a streaming message completion
44
- * Must be implemented by subclasses
45
- */
46
57
  abstract createMessage(
47
58
  systemPrompt: string,
48
59
  messages: Message[],
49
60
  tools?: ToolDefinition[],
50
61
  ): ApiStream;
51
62
 
52
- /**
53
- * Get the current model configuration
54
- * Can be overridden by subclasses for provider-specific logic
55
- */
56
63
  getModel(): HandlerModelInfo {
57
64
  const modelId = this.config.modelId;
58
65
  return {
@@ -61,43 +68,55 @@ export abstract class BaseHandler implements ApiHandler {
61
68
  };
62
69
  }
63
70
 
64
- /**
65
- * Get usage information (optional)
66
- * Override in subclasses that support this
67
- */
68
71
  async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
69
72
  return undefined;
70
73
  }
71
74
 
72
- /**
73
- * Get the abort signal for the current request
74
- * Creates a new AbortController if one doesn't exist or was already aborted
75
- * Combines with config.abortSignal if provided
76
- */
77
75
  protected getAbortSignal(): AbortSignal {
78
- // Create a new controller if needed
79
- if (!this.abortController || this.abortController.signal.aborted) {
80
- this.abortController = new AbortController();
81
- }
82
-
83
- // If a signal was provided in config, chain it
84
- if (this.config.abortSignal) {
85
- const configSignal = this.config.abortSignal;
76
+ const controller = new AbortController();
77
+ this.abortController = controller;
78
+ controller.signal.addEventListener(
79
+ "abort",
80
+ () => {
81
+ if (this.abortController === controller) {
82
+ this.abortController = undefined;
83
+ }
84
+ },
85
+ { once: true },
86
+ );
87
+
88
+ const configSignal = this.config.abortSignal;
89
+ if (configSignal) {
86
90
  if (configSignal.aborted) {
87
- this.abortController.abort(configSignal.reason);
91
+ this.logAbort("debug", "Provider request inherited aborted signal", {
92
+ controllerId: getControllerId(controller),
93
+ reason: serializeAbortReason(configSignal.reason),
94
+ });
95
+ controller.abort(configSignal.reason);
88
96
  } else {
89
- configSignal.addEventListener("abort", () => {
90
- this.abortController?.abort(configSignal.reason);
97
+ const signalId = ++this.abortSignalSequence;
98
+ configSignal.addEventListener(
99
+ "abort",
100
+ () => {
101
+ this.logAbort("warn", "Provider request abort signal fired", {
102
+ controllerId: getControllerId(controller),
103
+ signalId,
104
+ reason: serializeAbortReason(configSignal.reason),
105
+ });
106
+ controller.abort(configSignal.reason);
107
+ },
108
+ { once: true },
109
+ );
110
+ this.logAbort("debug", "Provider request attached abort signal", {
111
+ controllerId: getControllerId(controller),
112
+ signalId,
91
113
  });
92
114
  }
93
115
  }
94
116
 
95
- return this.abortController.signal;
117
+ return controller.signal;
96
118
  }
97
119
 
98
- /**
99
- * Abort the current request
100
- */
101
120
  abort(): void {
102
121
  this.abortController?.abort();
103
122
  }
@@ -105,37 +124,47 @@ export abstract class BaseHandler implements ApiHandler {
105
124
  setAbortSignal(signal: AbortSignal | undefined): void {
106
125
  this.config.abortSignal = signal;
107
126
  if (signal?.aborted) {
127
+ this.logAbort("debug", "Provider handler received pre-aborted signal", {
128
+ controllerId: this.abortController
129
+ ? getControllerId(this.abortController)
130
+ : undefined,
131
+ reason: serializeAbortReason(signal.reason),
132
+ });
108
133
  this.abortController?.abort(signal.reason);
109
134
  }
110
135
  }
111
136
 
112
- /**
113
- * Helper to calculate cost from usage
114
- */
137
+ private logAbort(
138
+ level: "debug" | "warn",
139
+ message: string,
140
+ metadata?: Record<string, unknown>,
141
+ ): void {
142
+ this.config.logger?.[level]?.(message, {
143
+ providerId: this.config.providerId,
144
+ modelId: this.config.modelId,
145
+ ...metadata,
146
+ });
147
+ }
148
+
115
149
  protected calculateCost(
116
150
  inputTokens: number,
117
151
  outputTokens: number,
118
152
  cacheReadTokens = 0,
119
153
  ): number | undefined {
120
- const modelPricingSource =
121
- this.config.modelInfo ??
122
- (this.config.modelId
123
- ? this.config.knownModels?.[this.config.modelId]
124
- : undefined);
125
- const pricing = modelPricingSource?.pricing;
154
+ const pricing = (
155
+ this.config.modelInfo ?? this.config.knownModels?.[this.config.modelId]
156
+ )?.pricing;
126
157
  if (!pricing?.input || !pricing?.output) {
127
158
  return undefined;
128
159
  }
129
160
 
130
- const uncachedInputTokens = inputTokens - cacheReadTokens;
131
- const inputCost = (uncachedInputTokens / 1_000_000) * pricing.input;
132
- const outputCost = (outputTokens / 1_000_000) * pricing.output;
133
- const cacheReadCost =
134
- cacheReadTokens > 0
161
+ return (
162
+ ((inputTokens - cacheReadTokens) / 1_000_000) * pricing.input +
163
+ (outputTokens / 1_000_000) * pricing.output +
164
+ (cacheReadTokens > 0
135
165
  ? (cacheReadTokens / 1_000_000) * (pricing.cacheRead ?? 0)
136
- : 0;
137
-
138
- return inputCost + outputCost + cacheReadCost;
166
+ : 0)
167
+ );
139
168
  }
140
169
 
141
170
  protected createResponseId(): string {
@@ -154,7 +183,7 @@ export abstract class BaseHandler implements ApiHandler {
154
183
  responseId: string,
155
184
  ): Generator<ApiStreamChunk> {
156
185
  for (const chunk of chunks) {
157
- yield this.withResponseId(chunk, responseId);
186
+ yield { ...chunk, id: responseId };
158
187
  }
159
188
  }
160
189
 
@@ -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 ?? this.config.maxOutputTokens ?? 8192,
146
+ maxTokens: model.info.maxTokens ?? this.config.maxOutputTokens ?? 128_000,
147
147
  temperature: reasoningEnabled ? undefined : (model.info.temperature ?? 0),
148
148
  providerOptions:
149
149
  Object.keys(providerOptions).length > 0 ? providerOptions : undefined,
@@ -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
  }