@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.
- package/dist/index.browser.d.ts +2 -2
- package/dist/index.browser.js +40 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -12
- package/dist/providers/handlers/anthropic-base.d.ts +11 -1
- package/dist/providers/handlers/base.d.ts +2 -27
- package/dist/providers/transform/openai-format.d.ts +1 -1
- package/dist/providers/types/config.d.ts +6 -0
- package/dist/providers/types/messages.d.ts +2 -0
- package/dist/providers/utils/tool-processor.d.ts +2 -1
- package/package.json +2 -1
- package/src/index.browser.ts +2 -2
- package/src/index.ts +2 -2
- package/src/models/generated.ts +15 -1
- package/src/models/providers/openai-codex.ts +19 -3
- package/src/models/providers/vercel-ai-gateway.ts +1 -1
- package/src/providers/handlers/anthropic-base.ts +19 -6
- package/src/providers/handlers/base.test.ts +60 -1
- package/src/providers/handlers/base.ts +83 -54
- package/src/providers/handlers/bedrock-base.ts +1 -1
- package/src/providers/handlers/codex.test.ts +37 -0
- package/src/providers/handlers/community-sdk.ts +0 -1
- package/src/providers/handlers/gemini-base.test.ts +40 -0
- package/src/providers/handlers/gemini-base.ts +16 -1
- package/src/providers/handlers/openai-base.ts +55 -11
- package/src/providers/handlers/vertex.ts +1 -1
- package/src/providers/transform/format-conversion.test.ts +26 -0
- package/src/providers/transform/openai-format.ts +50 -7
- package/src/providers/types/config.ts +8 -0
- package/src/providers/types/messages.ts +2 -0
- package/src/providers/utils/tool-processor.test.ts +60 -0
- 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
|
-
|
|
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?:
|
|
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.
|
|
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",
|
package/src/index.browser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { defineLlmsConfig, loadLlmsConfigFromFile } from "./config-browser";
|
|
2
|
-
export * as
|
|
3
|
-
export * as
|
|
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
|
|
3
|
-
export * as
|
|
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,
|
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: 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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
121
|
-
this.config.modelInfo ??
|
|
122
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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 ??
|
|
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
|
});
|