@clinebot/llms 0.0.0
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 +198 -0
- package/dist/config-browser.d.ts +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/models/generated-access.d.ts +4 -0
- package/dist/models/generated-provider-loaders.d.ts +13 -0
- package/dist/models/generated.d.ts +14 -0
- package/dist/models/index.d.ts +43 -0
- package/dist/models/models-dev-catalog.d.ts +32 -0
- package/dist/models/providers/aihubmix.d.ts +5 -0
- package/dist/models/providers/anthropic.d.ts +53 -0
- package/dist/models/providers/asksage.d.ts +5 -0
- package/dist/models/providers/baseten.d.ts +5 -0
- package/dist/models/providers/bedrock.d.ts +7 -0
- package/dist/models/providers/cerebras.d.ts +7 -0
- package/dist/models/providers/claude-code.d.ts +4 -0
- package/dist/models/providers/cline.d.ts +34 -0
- package/dist/models/providers/deepseek.d.ts +8 -0
- package/dist/models/providers/dify.d.ts +5 -0
- package/dist/models/providers/doubao.d.ts +7 -0
- package/dist/models/providers/fireworks.d.ts +8 -0
- package/dist/models/providers/gemini.d.ts +9 -0
- package/dist/models/providers/groq.d.ts +8 -0
- package/dist/models/providers/hicap.d.ts +5 -0
- package/dist/models/providers/huawei-cloud-maas.d.ts +5 -0
- package/dist/models/providers/huggingface.d.ts +6 -0
- package/dist/models/providers/index.d.ts +45 -0
- package/dist/models/providers/litellm.d.ts +5 -0
- package/dist/models/providers/lmstudio.d.ts +5 -0
- package/dist/models/providers/minimax.d.ts +7 -0
- package/dist/models/providers/mistral.d.ts +5 -0
- package/dist/models/providers/moonshot.d.ts +7 -0
- package/dist/models/providers/nebius.d.ts +7 -0
- package/dist/models/providers/nous-research.d.ts +7 -0
- package/dist/models/providers/oca.d.ts +9 -0
- package/dist/models/providers/ollama.d.ts +5 -0
- package/dist/models/providers/openai-codex.d.ts +10 -0
- package/dist/models/providers/openai.d.ts +9 -0
- package/dist/models/providers/opencode.d.ts +10 -0
- package/dist/models/providers/openrouter.d.ts +7 -0
- package/dist/models/providers/qwen-code.d.ts +7 -0
- package/dist/models/providers/qwen.d.ts +7 -0
- package/dist/models/providers/requesty.d.ts +6 -0
- package/dist/models/providers/sambanova.d.ts +7 -0
- package/dist/models/providers/sapaicore.d.ts +7 -0
- package/dist/models/providers/together.d.ts +8 -0
- package/dist/models/providers/vercel-ai-gateway.d.ts +5 -0
- package/dist/models/providers/vertex.d.ts +7 -0
- package/dist/models/providers/xai.d.ts +8 -0
- package/dist/models/providers/zai.d.ts +7 -0
- package/dist/models/query.d.ts +181 -0
- package/dist/models/registry.d.ts +123 -0
- package/dist/models/schemas/index.d.ts +7 -0
- package/dist/models/schemas/model.d.ts +340 -0
- package/dist/models/schemas/query.d.ts +191 -0
- package/dist/providers/handlers/ai-sdk-community.d.ts +46 -0
- package/dist/providers/handlers/ai-sdk-provider-base.d.ts +32 -0
- package/dist/providers/handlers/anthropic-base.d.ts +26 -0
- package/dist/providers/handlers/asksage.d.ts +12 -0
- package/dist/providers/handlers/auth.d.ts +5 -0
- package/dist/providers/handlers/base.d.ts +55 -0
- package/dist/providers/handlers/bedrock-base.d.ts +23 -0
- package/dist/providers/handlers/bedrock-client.d.ts +4 -0
- package/dist/providers/handlers/community-sdk.d.ts +97 -0
- package/dist/providers/handlers/fetch-base.d.ts +18 -0
- package/dist/providers/handlers/gemini-base.d.ts +25 -0
- package/dist/providers/handlers/index.d.ts +19 -0
- package/dist/providers/handlers/openai-base.d.ts +54 -0
- package/dist/providers/handlers/openai-responses.d.ts +64 -0
- package/dist/providers/handlers/providers.d.ts +43 -0
- package/dist/providers/handlers/r1-base.d.ts +62 -0
- package/dist/providers/handlers/registry.d.ts +106 -0
- package/dist/providers/handlers/vertex.d.ts +32 -0
- package/dist/providers/index.d.ts +100 -0
- package/dist/providers/public.browser.d.ts +2 -0
- package/dist/providers/public.d.ts +3 -0
- package/dist/providers/shared/openai-compatible.d.ts +10 -0
- package/dist/providers/transform/ai-sdk-community-format.d.ts +9 -0
- package/dist/providers/transform/anthropic-format.d.ts +24 -0
- package/dist/providers/transform/content-format.d.ts +3 -0
- package/dist/providers/transform/gemini-format.d.ts +19 -0
- package/dist/providers/transform/index.d.ts +10 -0
- package/dist/providers/transform/openai-format.d.ts +36 -0
- package/dist/providers/transform/r1-format.d.ts +26 -0
- package/dist/providers/types/config.d.ts +261 -0
- package/dist/providers/types/handler.d.ts +71 -0
- package/dist/providers/types/index.d.ts +11 -0
- package/dist/providers/types/messages.d.ts +139 -0
- package/dist/providers/types/model-info.d.ts +32 -0
- package/dist/providers/types/provider-ids.d.ts +63 -0
- package/dist/providers/types/settings.d.ts +308 -0
- package/dist/providers/types/stream.d.ts +106 -0
- package/dist/providers/utils/index.d.ts +7 -0
- package/dist/providers/utils/retry.d.ts +38 -0
- package/dist/providers/utils/stream-processor.d.ts +110 -0
- package/dist/providers/utils/tool-processor.d.ts +34 -0
- package/dist/sdk.d.ts +18 -0
- package/dist/types.d.ts +60 -0
- package/package.json +66 -0
- package/src/catalog.ts +20 -0
- package/src/config-browser.ts +11 -0
- package/src/config.ts +49 -0
- package/src/index.browser.ts +9 -0
- package/src/index.ts +10 -0
- package/src/live-providers.test.ts +137 -0
- package/src/models/generated-access.ts +41 -0
- package/src/models/generated-provider-loaders.ts +166 -0
- package/src/models/generated.ts +11997 -0
- package/src/models/index.ts +271 -0
- package/src/models/models-dev-catalog.test.ts +161 -0
- package/src/models/models-dev-catalog.ts +161 -0
- package/src/models/providers/aihubmix.ts +19 -0
- package/src/models/providers/anthropic.ts +60 -0
- package/src/models/providers/asksage.ts +19 -0
- package/src/models/providers/baseten.ts +21 -0
- package/src/models/providers/bedrock.ts +30 -0
- package/src/models/providers/cerebras.ts +24 -0
- package/src/models/providers/claude-code.ts +51 -0
- package/src/models/providers/cline.ts +25 -0
- package/src/models/providers/deepseek.ts +33 -0
- package/src/models/providers/dify.ts +17 -0
- package/src/models/providers/doubao.ts +33 -0
- package/src/models/providers/fireworks.ts +34 -0
- package/src/models/providers/gemini.ts +43 -0
- package/src/models/providers/groq.ts +33 -0
- package/src/models/providers/hicap.ts +18 -0
- package/src/models/providers/huawei-cloud-maas.ts +18 -0
- package/src/models/providers/huggingface.ts +22 -0
- package/src/models/providers/index.ts +162 -0
- package/src/models/providers/litellm.ts +19 -0
- package/src/models/providers/lmstudio.ts +22 -0
- package/src/models/providers/minimax.ts +34 -0
- package/src/models/providers/mistral.ts +19 -0
- package/src/models/providers/moonshot.ts +34 -0
- package/src/models/providers/nebius.ts +24 -0
- package/src/models/providers/nous-research.ts +21 -0
- package/src/models/providers/oca.ts +30 -0
- package/src/models/providers/ollama.ts +18 -0
- package/src/models/providers/openai-codex.ts +30 -0
- package/src/models/providers/openai.ts +43 -0
- package/src/models/providers/opencode.ts +28 -0
- package/src/models/providers/openrouter.ts +24 -0
- package/src/models/providers/qwen-code.ts +33 -0
- package/src/models/providers/qwen.ts +34 -0
- package/src/models/providers/requesty.ts +23 -0
- package/src/models/providers/sambanova.ts +23 -0
- package/src/models/providers/sapaicore.ts +34 -0
- package/src/models/providers/together.ts +35 -0
- package/src/models/providers/vercel-ai-gateway.ts +23 -0
- package/src/models/providers/vertex.ts +36 -0
- package/src/models/providers/xai.ts +34 -0
- package/src/models/providers/zai.ts +25 -0
- package/src/models/query.ts +407 -0
- package/src/models/registry.ts +511 -0
- package/src/models/schemas/index.ts +62 -0
- package/src/models/schemas/model.ts +308 -0
- package/src/models/schemas/query.ts +336 -0
- package/src/providers/browser.ts +4 -0
- package/src/providers/handlers/ai-sdk-community.ts +226 -0
- package/src/providers/handlers/ai-sdk-provider-base.ts +193 -0
- package/src/providers/handlers/anthropic-base.ts +372 -0
- package/src/providers/handlers/asksage.test.ts +103 -0
- package/src/providers/handlers/asksage.ts +138 -0
- package/src/providers/handlers/auth.test.ts +19 -0
- package/src/providers/handlers/auth.ts +121 -0
- package/src/providers/handlers/base.test.ts +46 -0
- package/src/providers/handlers/base.ts +160 -0
- package/src/providers/handlers/bedrock-base.ts +390 -0
- package/src/providers/handlers/bedrock-client.ts +100 -0
- package/src/providers/handlers/codex.test.ts +123 -0
- package/src/providers/handlers/community-sdk.test.ts +288 -0
- package/src/providers/handlers/community-sdk.ts +392 -0
- package/src/providers/handlers/fetch-base.ts +68 -0
- package/src/providers/handlers/gemini-base.ts +302 -0
- package/src/providers/handlers/index.ts +67 -0
- package/src/providers/handlers/openai-base.ts +277 -0
- package/src/providers/handlers/openai-responses.ts +598 -0
- package/src/providers/handlers/providers.test.ts +120 -0
- package/src/providers/handlers/providers.ts +563 -0
- package/src/providers/handlers/r1-base.ts +280 -0
- package/src/providers/handlers/registry.ts +185 -0
- package/src/providers/handlers/vertex.test.ts +124 -0
- package/src/providers/handlers/vertex.ts +292 -0
- package/src/providers/index.ts +534 -0
- package/src/providers/public.browser.ts +20 -0
- package/src/providers/public.ts +51 -0
- package/src/providers/shared/openai-compatible.ts +63 -0
- package/src/providers/transform/ai-sdk-community-format.test.ts +73 -0
- package/src/providers/transform/ai-sdk-community-format.ts +115 -0
- package/src/providers/transform/anthropic-format.ts +218 -0
- package/src/providers/transform/content-format.ts +34 -0
- package/src/providers/transform/format-conversion.test.ts +310 -0
- package/src/providers/transform/gemini-format.ts +167 -0
- package/src/providers/transform/index.ts +22 -0
- package/src/providers/transform/openai-format.ts +247 -0
- package/src/providers/transform/r1-format.ts +287 -0
- package/src/providers/types/config.ts +388 -0
- package/src/providers/types/handler.ts +87 -0
- package/src/providers/types/index.ts +120 -0
- package/src/providers/types/messages.ts +158 -0
- package/src/providers/types/model-info.test.ts +57 -0
- package/src/providers/types/model-info.ts +65 -0
- package/src/providers/types/provider-ids.test.ts +12 -0
- package/src/providers/types/provider-ids.ts +89 -0
- package/src/providers/types/settings.test.ts +49 -0
- package/src/providers/types/settings.ts +533 -0
- package/src/providers/types/stream.ts +117 -0
- package/src/providers/utils/index.ts +27 -0
- package/src/providers/utils/retry.test.ts +140 -0
- package/src/providers/utils/retry.ts +188 -0
- package/src/providers/utils/stream-processor.test.ts +232 -0
- package/src/providers/utils/stream-processor.ts +472 -0
- package/src/providers/utils/tool-processor.test.ts +34 -0
- package/src/providers/utils/tool-processor.ts +111 -0
- package/src/sdk.ts +264 -0
- package/src/types.ts +79 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream Response Processor
|
|
3
|
+
*
|
|
4
|
+
* Processes ApiStreamChunks and assembles them into Cline's message content format.
|
|
5
|
+
* This provides a clean interface between the streaming API and Cline's storage format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseJsonStream } from "@clinebot/shared";
|
|
9
|
+
import { JSONParser } from "@streamparser/json";
|
|
10
|
+
import type {
|
|
11
|
+
ApiStreamChunk,
|
|
12
|
+
ApiStreamReasoningChunk,
|
|
13
|
+
ApiStreamTextChunk,
|
|
14
|
+
ApiStreamToolCallsChunk,
|
|
15
|
+
ApiStreamUsageChunk,
|
|
16
|
+
} from "../types/stream";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Output Types (Cline Storage Format)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface ReasoningDetailParam {
|
|
23
|
+
type: "reasoning.text" | string;
|
|
24
|
+
text: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
format: "anthropic-claude-v1" | string;
|
|
27
|
+
index: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AssistantTextBlock {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
call_id?: string;
|
|
34
|
+
reasoning_details?: ReasoningDetailParam[];
|
|
35
|
+
signature?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AssistantToolUseBlock {
|
|
39
|
+
type: "tool_use";
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
input: Record<string, unknown>;
|
|
43
|
+
call_id?: string;
|
|
44
|
+
reasoning_details?: ReasoningDetailParam[];
|
|
45
|
+
signature?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AssistantThinkingBlock {
|
|
49
|
+
type: "thinking";
|
|
50
|
+
thinking: string;
|
|
51
|
+
signature: string;
|
|
52
|
+
call_id?: string;
|
|
53
|
+
summary?: ReasoningDetailParam[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AssistantRedactedThinkingBlock {
|
|
57
|
+
type: "redacted_thinking";
|
|
58
|
+
data: string;
|
|
59
|
+
call_id?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AssistantContentBlock =
|
|
63
|
+
| AssistantTextBlock
|
|
64
|
+
| AssistantToolUseBlock
|
|
65
|
+
| AssistantThinkingBlock
|
|
66
|
+
| AssistantRedactedThinkingBlock;
|
|
67
|
+
|
|
68
|
+
export interface UsageInfo {
|
|
69
|
+
inputTokens: number;
|
|
70
|
+
outputTokens: number;
|
|
71
|
+
cacheWriteTokens?: number;
|
|
72
|
+
cacheReadTokens?: number;
|
|
73
|
+
thoughtsTokenCount?: number;
|
|
74
|
+
totalCost?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ProcessedResponse {
|
|
78
|
+
content: AssistantContentBlock[];
|
|
79
|
+
usage?: UsageInfo;
|
|
80
|
+
responseId?: string;
|
|
81
|
+
incompleteReason?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Internal State
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
interface PendingToolUse {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
rawInput: string;
|
|
92
|
+
parsedInput?: Record<string, unknown>;
|
|
93
|
+
signature?: string;
|
|
94
|
+
callId: string;
|
|
95
|
+
parser: JSONParser;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface PendingReasoning {
|
|
99
|
+
thinking: string;
|
|
100
|
+
signature: string;
|
|
101
|
+
details: ReasoningDetailParam[];
|
|
102
|
+
redactedBlocks: AssistantRedactedThinkingBlock[];
|
|
103
|
+
callId?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Stream Response Processor
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Processes streaming API responses and assembles content blocks.
|
|
112
|
+
*
|
|
113
|
+
* Usage:
|
|
114
|
+
* ```ts
|
|
115
|
+
* const processor = new StreamResponseProcessor()
|
|
116
|
+
* for await (const chunk of apiStream) {
|
|
117
|
+
* const partial = processor.process(chunk)
|
|
118
|
+
* // Use partial for live updates
|
|
119
|
+
* }
|
|
120
|
+
* const final = processor.finalize()
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export class StreamResponseProcessor {
|
|
124
|
+
private text = "";
|
|
125
|
+
private textSignature?: string;
|
|
126
|
+
private toolUses = new Map<string, PendingToolUse>();
|
|
127
|
+
private reasoning: PendingReasoning | null = null;
|
|
128
|
+
private usage: UsageInfo | null = null;
|
|
129
|
+
private responseId?: string;
|
|
130
|
+
private incompleteReason?: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Process a single stream chunk and return current partial state.
|
|
134
|
+
* Call this for each chunk to get live updates.
|
|
135
|
+
*/
|
|
136
|
+
process(chunk: ApiStreamChunk): ProcessedResponse {
|
|
137
|
+
switch (chunk.type) {
|
|
138
|
+
case "text":
|
|
139
|
+
this.processText(chunk);
|
|
140
|
+
break;
|
|
141
|
+
case "reasoning":
|
|
142
|
+
this.processReasoning(chunk);
|
|
143
|
+
break;
|
|
144
|
+
case "tool_calls":
|
|
145
|
+
this.processToolCall(chunk);
|
|
146
|
+
break;
|
|
147
|
+
case "usage":
|
|
148
|
+
this.processUsage(chunk);
|
|
149
|
+
break;
|
|
150
|
+
case "done":
|
|
151
|
+
this.incompleteReason = chunk.incompleteReason;
|
|
152
|
+
if (chunk.id) this.responseId = chunk.id;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return this.getPartialResponse();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Finalize and return the complete response.
|
|
161
|
+
* Call this after all chunks have been processed.
|
|
162
|
+
*/
|
|
163
|
+
finalize(): ProcessedResponse {
|
|
164
|
+
const content: AssistantContentBlock[] = [];
|
|
165
|
+
|
|
166
|
+
// Add thinking block if present
|
|
167
|
+
if (
|
|
168
|
+
this.reasoning &&
|
|
169
|
+
(this.reasoning.thinking || this.reasoning.details.length)
|
|
170
|
+
) {
|
|
171
|
+
content.push({
|
|
172
|
+
type: "thinking",
|
|
173
|
+
thinking: this.reasoning.thinking,
|
|
174
|
+
signature: this.reasoning.signature,
|
|
175
|
+
call_id: this.reasoning.callId,
|
|
176
|
+
summary: this.reasoning.details.length
|
|
177
|
+
? this.reasoning.details
|
|
178
|
+
: undefined,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Add any redacted thinking blocks
|
|
182
|
+
content.push(...this.reasoning.redactedBlocks);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add text if present
|
|
186
|
+
if (this.text) {
|
|
187
|
+
content.push({
|
|
188
|
+
type: "text",
|
|
189
|
+
text: this.text,
|
|
190
|
+
signature: this.textSignature,
|
|
191
|
+
reasoning_details: this.reasoning?.details.length
|
|
192
|
+
? this.reasoning.details
|
|
193
|
+
: undefined,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add finalized tool uses
|
|
198
|
+
for (const pending of this.toolUses.values()) {
|
|
199
|
+
if (!pending.name) continue;
|
|
200
|
+
|
|
201
|
+
const input = this.finalizeToolInput(pending);
|
|
202
|
+
content.push({
|
|
203
|
+
type: "tool_use",
|
|
204
|
+
id: pending.id,
|
|
205
|
+
name: pending.name,
|
|
206
|
+
input,
|
|
207
|
+
call_id: pending.callId,
|
|
208
|
+
signature: pending.signature,
|
|
209
|
+
reasoning_details: this.reasoning?.details.length
|
|
210
|
+
? this.reasoning.details
|
|
211
|
+
: undefined,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
content,
|
|
217
|
+
usage: this.usage ?? undefined,
|
|
218
|
+
responseId: this.responseId,
|
|
219
|
+
incompleteReason: this.incompleteReason,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get current partial response for live streaming updates.
|
|
225
|
+
*/
|
|
226
|
+
getPartialResponse(): ProcessedResponse {
|
|
227
|
+
const content: AssistantContentBlock[] = [];
|
|
228
|
+
|
|
229
|
+
// Add partial thinking
|
|
230
|
+
if (
|
|
231
|
+
this.reasoning &&
|
|
232
|
+
(this.reasoning.thinking || this.reasoning.details.length)
|
|
233
|
+
) {
|
|
234
|
+
content.push({
|
|
235
|
+
type: "thinking",
|
|
236
|
+
thinking: this.reasoning.thinking,
|
|
237
|
+
signature: this.reasoning.signature,
|
|
238
|
+
call_id: this.reasoning.callId,
|
|
239
|
+
summary: this.reasoning.details.length
|
|
240
|
+
? this.reasoning.details
|
|
241
|
+
: undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Add partial text
|
|
246
|
+
if (this.text) {
|
|
247
|
+
content.push({
|
|
248
|
+
type: "text",
|
|
249
|
+
text: this.text,
|
|
250
|
+
signature: this.textSignature,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add partial tool uses
|
|
255
|
+
for (const pending of this.toolUses.values()) {
|
|
256
|
+
if (!pending.name) continue;
|
|
257
|
+
|
|
258
|
+
const input =
|
|
259
|
+
pending.parsedInput ?? this.extractPartialJson(pending.rawInput);
|
|
260
|
+
content.push({
|
|
261
|
+
type: "tool_use",
|
|
262
|
+
id: pending.id,
|
|
263
|
+
name: pending.name,
|
|
264
|
+
input,
|
|
265
|
+
call_id: pending.callId,
|
|
266
|
+
signature: pending.signature,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
content,
|
|
272
|
+
usage: this.usage ?? undefined,
|
|
273
|
+
responseId: this.responseId,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reset processor state for reuse.
|
|
279
|
+
*/
|
|
280
|
+
reset(): void {
|
|
281
|
+
this.text = "";
|
|
282
|
+
this.textSignature = undefined;
|
|
283
|
+
this.toolUses.clear();
|
|
284
|
+
this.reasoning = null;
|
|
285
|
+
this.usage = null;
|
|
286
|
+
this.responseId = undefined;
|
|
287
|
+
this.incompleteReason = undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Private Methods
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
private processText(chunk: ApiStreamTextChunk): void {
|
|
295
|
+
this.text += chunk.text;
|
|
296
|
+
if (chunk.signature) this.textSignature = chunk.signature;
|
|
297
|
+
if (chunk.id) this.responseId = chunk.id;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private processReasoning(chunk: ApiStreamReasoningChunk): void {
|
|
301
|
+
if (!this.reasoning) {
|
|
302
|
+
this.reasoning = {
|
|
303
|
+
thinking: "",
|
|
304
|
+
signature: "",
|
|
305
|
+
details: [],
|
|
306
|
+
redactedBlocks: [],
|
|
307
|
+
callId: chunk.id,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (chunk.reasoning) {
|
|
312
|
+
this.reasoning.thinking += chunk.reasoning;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (chunk.signature) {
|
|
316
|
+
this.reasoning.signature = chunk.signature;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (chunk.details) {
|
|
320
|
+
const details = Array.isArray(chunk.details)
|
|
321
|
+
? chunk.details
|
|
322
|
+
: [chunk.details];
|
|
323
|
+
for (const detail of details) {
|
|
324
|
+
if (this.isReasoningDetail(detail)) {
|
|
325
|
+
this.reasoning.details.push(detail);
|
|
326
|
+
// Extract signature from details if not set at top level
|
|
327
|
+
if (!this.reasoning.signature && detail.signature) {
|
|
328
|
+
this.reasoning.signature = detail.signature;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (chunk.redacted_data) {
|
|
335
|
+
this.reasoning.redactedBlocks.push({
|
|
336
|
+
type: "redacted_thinking",
|
|
337
|
+
data: chunk.redacted_data,
|
|
338
|
+
call_id: chunk.id ?? this.reasoning.callId,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (chunk.id) this.responseId = chunk.id;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private processToolCall(chunk: ApiStreamToolCallsChunk): void {
|
|
346
|
+
const tc = chunk.tool_call;
|
|
347
|
+
const fn = tc.function;
|
|
348
|
+
const id = fn.id ?? tc.call_id ?? "";
|
|
349
|
+
|
|
350
|
+
if (!id) return;
|
|
351
|
+
|
|
352
|
+
let pending = this.toolUses.get(id);
|
|
353
|
+
if (!pending) {
|
|
354
|
+
pending = this.createPendingToolUse(id, tc.call_id ?? id);
|
|
355
|
+
this.toolUses.set(id, pending);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (fn.name) {
|
|
359
|
+
pending.name = fn.name;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (chunk.signature) {
|
|
363
|
+
pending.signature = chunk.signature;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (fn.arguments) {
|
|
367
|
+
const args =
|
|
368
|
+
typeof fn.arguments === "string"
|
|
369
|
+
? fn.arguments
|
|
370
|
+
: JSON.stringify(fn.arguments);
|
|
371
|
+
pending.rawInput += args;
|
|
372
|
+
try {
|
|
373
|
+
pending.parser.write(args);
|
|
374
|
+
} catch {
|
|
375
|
+
// Expected during streaming - parser may not have complete JSON
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (chunk.id) this.responseId = chunk.id;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private processUsage(chunk: ApiStreamUsageChunk): void {
|
|
383
|
+
this.usage = {
|
|
384
|
+
inputTokens: chunk.inputTokens,
|
|
385
|
+
outputTokens: chunk.outputTokens,
|
|
386
|
+
cacheWriteTokens: chunk.cacheWriteTokens,
|
|
387
|
+
cacheReadTokens: chunk.cacheReadTokens,
|
|
388
|
+
thoughtsTokenCount: chunk.thoughtsTokenCount,
|
|
389
|
+
totalCost: chunk.totalCost,
|
|
390
|
+
};
|
|
391
|
+
if (chunk.id) this.responseId = chunk.id;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private createPendingToolUse(id: string, callId: string): PendingToolUse {
|
|
395
|
+
const pending: PendingToolUse = {
|
|
396
|
+
id,
|
|
397
|
+
name: "",
|
|
398
|
+
rawInput: "",
|
|
399
|
+
parsedInput: undefined,
|
|
400
|
+
signature: undefined,
|
|
401
|
+
callId,
|
|
402
|
+
parser: new JSONParser(),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
pending.parser.onValue = (info: { stack: unknown[]; value?: unknown }) => {
|
|
406
|
+
if (
|
|
407
|
+
info.stack.length === 0 &&
|
|
408
|
+
info.value &&
|
|
409
|
+
typeof info.value === "object"
|
|
410
|
+
) {
|
|
411
|
+
pending.parsedInput = info.value as Record<string, unknown>;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
pending.parser.onError = () => {};
|
|
415
|
+
|
|
416
|
+
return pending;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private finalizeToolInput(pending: PendingToolUse): Record<string, unknown> {
|
|
420
|
+
if (pending.parsedInput) {
|
|
421
|
+
return pending.parsedInput;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (pending.rawInput) {
|
|
425
|
+
const parsed = parseJsonStream(pending.rawInput);
|
|
426
|
+
if (this.isRecord(parsed)) {
|
|
427
|
+
return parsed;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return this.extractPartialJson(pending.rawInput);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Extract fields from incomplete JSON during streaming.
|
|
438
|
+
*/
|
|
439
|
+
private extractPartialJson(partial: string): Record<string, unknown> {
|
|
440
|
+
const result: Record<string, unknown> = {};
|
|
441
|
+
const pattern = /"(\w+)":\s*"((?:[^"\\]|\\.)*)(?:")?/g;
|
|
442
|
+
|
|
443
|
+
for (const match of partial.matchAll(pattern)) {
|
|
444
|
+
result[match[1]] = this.unescapeString(match[2]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private unescapeString(str: string): string {
|
|
451
|
+
return str
|
|
452
|
+
.replace(/\\n/g, "\n")
|
|
453
|
+
.replace(/\\t/g, "\t")
|
|
454
|
+
.replace(/\\r/g, "\r")
|
|
455
|
+
.replace(/\\"/g, '"')
|
|
456
|
+
.replace(/\\\\/g, "\\");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private isReasoningDetail(value: unknown): value is ReasoningDetailParam {
|
|
460
|
+
return (
|
|
461
|
+
typeof value === "object" &&
|
|
462
|
+
value !== null &&
|
|
463
|
+
"type" in value &&
|
|
464
|
+
"text" in value &&
|
|
465
|
+
typeof (value as ReasoningDetailParam).text === "string"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private isRecord(value: unknown): value is Record<string, unknown> {
|
|
470
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ToolCallProcessor } from "./tool-processor";
|
|
3
|
+
|
|
4
|
+
describe("ToolCallProcessor", () => {
|
|
5
|
+
it("emits delta arguments (not cumulative) so downstream can accumulate once", () => {
|
|
6
|
+
const processor = new ToolCallProcessor();
|
|
7
|
+
|
|
8
|
+
const first = processor.processToolCallDeltas(
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
index: 0,
|
|
12
|
+
id: "call_1",
|
|
13
|
+
function: { name: "run_commands", arguments: '{"commands":["ls' },
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
"resp_1",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const second = processor.processToolCallDeltas(
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
index: 0,
|
|
23
|
+
function: { arguments: ' -la"]}' },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
"resp_1",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(first).toHaveLength(1);
|
|
30
|
+
expect(second).toHaveLength(1);
|
|
31
|
+
expect(first[0].tool_call.function.arguments).toBe('{"commands":["ls');
|
|
32
|
+
expect(second[0].tool_call.function.arguments).toBe(' -la"]}');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Call Processor
|
|
3
|
+
*
|
|
4
|
+
* Handles incremental assembly of tool calls from streaming responses.
|
|
5
|
+
* Tool calls can arrive in chunks that need to be accumulated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ApiStreamToolCall,
|
|
10
|
+
ApiStreamToolCallsChunk,
|
|
11
|
+
} from "../types/stream";
|
|
12
|
+
|
|
13
|
+
// Re-export to satisfy linter (type is used in generator return type)
|
|
14
|
+
export type { ApiStreamToolCallsChunk };
|
|
15
|
+
|
|
16
|
+
interface ToolCallDelta {
|
|
17
|
+
index: number;
|
|
18
|
+
id?: string;
|
|
19
|
+
function?: {
|
|
20
|
+
name?: string;
|
|
21
|
+
arguments?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Processes tool call deltas from streaming API responses
|
|
27
|
+
*/
|
|
28
|
+
export class ToolCallProcessor {
|
|
29
|
+
private toolCalls: Map<
|
|
30
|
+
number,
|
|
31
|
+
{
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
arguments: string;
|
|
35
|
+
}
|
|
36
|
+
> = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Process tool call deltas and yield complete tool calls
|
|
40
|
+
*/
|
|
41
|
+
processToolCallDeltas(
|
|
42
|
+
deltas: ToolCallDelta[],
|
|
43
|
+
responseId: string,
|
|
44
|
+
): ApiStreamToolCallsChunk[] {
|
|
45
|
+
const results: ApiStreamToolCallsChunk[] = [];
|
|
46
|
+
for (const delta of deltas) {
|
|
47
|
+
const { index, id } = delta;
|
|
48
|
+
const fn = delta.function;
|
|
49
|
+
|
|
50
|
+
// Get or create tool call state
|
|
51
|
+
let toolCall = this.toolCalls.get(index);
|
|
52
|
+
if (!toolCall) {
|
|
53
|
+
toolCall = { id: "", name: "", arguments: "" };
|
|
54
|
+
this.toolCalls.set(index, toolCall);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update with delta values
|
|
58
|
+
if (id) {
|
|
59
|
+
toolCall.id = id;
|
|
60
|
+
}
|
|
61
|
+
if (fn?.name) {
|
|
62
|
+
toolCall.name = fn.name;
|
|
63
|
+
}
|
|
64
|
+
const deltaArguments = fn?.arguments ?? "";
|
|
65
|
+
if (deltaArguments) {
|
|
66
|
+
toolCall.arguments += deltaArguments;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add the current state as a chunk
|
|
70
|
+
// This allows streaming of partial tool calls
|
|
71
|
+
if (toolCall.id && toolCall.name) {
|
|
72
|
+
results.push({
|
|
73
|
+
type: "tool_calls",
|
|
74
|
+
id: responseId,
|
|
75
|
+
tool_call: {
|
|
76
|
+
call_id: toolCall.id,
|
|
77
|
+
function: {
|
|
78
|
+
id: toolCall.id,
|
|
79
|
+
name: toolCall.name,
|
|
80
|
+
// Emit only the current delta arguments; the agent layer already
|
|
81
|
+
// performs accumulation for streaming tool arguments.
|
|
82
|
+
arguments: deltaArguments,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all accumulated tool calls
|
|
93
|
+
*/
|
|
94
|
+
getToolCalls(): ApiStreamToolCall[] {
|
|
95
|
+
return Array.from(this.toolCalls.values()).map((tc) => ({
|
|
96
|
+
call_id: tc.id,
|
|
97
|
+
function: {
|
|
98
|
+
id: tc.id,
|
|
99
|
+
name: tc.name,
|
|
100
|
+
arguments: tc.arguments,
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reset the processor state
|
|
107
|
+
*/
|
|
108
|
+
reset(): void {
|
|
109
|
+
this.toolCalls.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|