@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.
Files changed (219) hide show
  1. package/README.md +198 -0
  2. package/dist/config-browser.d.ts +3 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/index.browser.d.ts +4 -0
  5. package/dist/index.browser.js +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +7 -0
  8. package/dist/models/generated-access.d.ts +4 -0
  9. package/dist/models/generated-provider-loaders.d.ts +13 -0
  10. package/dist/models/generated.d.ts +14 -0
  11. package/dist/models/index.d.ts +43 -0
  12. package/dist/models/models-dev-catalog.d.ts +32 -0
  13. package/dist/models/providers/aihubmix.d.ts +5 -0
  14. package/dist/models/providers/anthropic.d.ts +53 -0
  15. package/dist/models/providers/asksage.d.ts +5 -0
  16. package/dist/models/providers/baseten.d.ts +5 -0
  17. package/dist/models/providers/bedrock.d.ts +7 -0
  18. package/dist/models/providers/cerebras.d.ts +7 -0
  19. package/dist/models/providers/claude-code.d.ts +4 -0
  20. package/dist/models/providers/cline.d.ts +34 -0
  21. package/dist/models/providers/deepseek.d.ts +8 -0
  22. package/dist/models/providers/dify.d.ts +5 -0
  23. package/dist/models/providers/doubao.d.ts +7 -0
  24. package/dist/models/providers/fireworks.d.ts +8 -0
  25. package/dist/models/providers/gemini.d.ts +9 -0
  26. package/dist/models/providers/groq.d.ts +8 -0
  27. package/dist/models/providers/hicap.d.ts +5 -0
  28. package/dist/models/providers/huawei-cloud-maas.d.ts +5 -0
  29. package/dist/models/providers/huggingface.d.ts +6 -0
  30. package/dist/models/providers/index.d.ts +45 -0
  31. package/dist/models/providers/litellm.d.ts +5 -0
  32. package/dist/models/providers/lmstudio.d.ts +5 -0
  33. package/dist/models/providers/minimax.d.ts +7 -0
  34. package/dist/models/providers/mistral.d.ts +5 -0
  35. package/dist/models/providers/moonshot.d.ts +7 -0
  36. package/dist/models/providers/nebius.d.ts +7 -0
  37. package/dist/models/providers/nous-research.d.ts +7 -0
  38. package/dist/models/providers/oca.d.ts +9 -0
  39. package/dist/models/providers/ollama.d.ts +5 -0
  40. package/dist/models/providers/openai-codex.d.ts +10 -0
  41. package/dist/models/providers/openai.d.ts +9 -0
  42. package/dist/models/providers/opencode.d.ts +10 -0
  43. package/dist/models/providers/openrouter.d.ts +7 -0
  44. package/dist/models/providers/qwen-code.d.ts +7 -0
  45. package/dist/models/providers/qwen.d.ts +7 -0
  46. package/dist/models/providers/requesty.d.ts +6 -0
  47. package/dist/models/providers/sambanova.d.ts +7 -0
  48. package/dist/models/providers/sapaicore.d.ts +7 -0
  49. package/dist/models/providers/together.d.ts +8 -0
  50. package/dist/models/providers/vercel-ai-gateway.d.ts +5 -0
  51. package/dist/models/providers/vertex.d.ts +7 -0
  52. package/dist/models/providers/xai.d.ts +8 -0
  53. package/dist/models/providers/zai.d.ts +7 -0
  54. package/dist/models/query.d.ts +181 -0
  55. package/dist/models/registry.d.ts +123 -0
  56. package/dist/models/schemas/index.d.ts +7 -0
  57. package/dist/models/schemas/model.d.ts +340 -0
  58. package/dist/models/schemas/query.d.ts +191 -0
  59. package/dist/providers/handlers/ai-sdk-community.d.ts +46 -0
  60. package/dist/providers/handlers/ai-sdk-provider-base.d.ts +32 -0
  61. package/dist/providers/handlers/anthropic-base.d.ts +26 -0
  62. package/dist/providers/handlers/asksage.d.ts +12 -0
  63. package/dist/providers/handlers/auth.d.ts +5 -0
  64. package/dist/providers/handlers/base.d.ts +55 -0
  65. package/dist/providers/handlers/bedrock-base.d.ts +23 -0
  66. package/dist/providers/handlers/bedrock-client.d.ts +4 -0
  67. package/dist/providers/handlers/community-sdk.d.ts +97 -0
  68. package/dist/providers/handlers/fetch-base.d.ts +18 -0
  69. package/dist/providers/handlers/gemini-base.d.ts +25 -0
  70. package/dist/providers/handlers/index.d.ts +19 -0
  71. package/dist/providers/handlers/openai-base.d.ts +54 -0
  72. package/dist/providers/handlers/openai-responses.d.ts +64 -0
  73. package/dist/providers/handlers/providers.d.ts +43 -0
  74. package/dist/providers/handlers/r1-base.d.ts +62 -0
  75. package/dist/providers/handlers/registry.d.ts +106 -0
  76. package/dist/providers/handlers/vertex.d.ts +32 -0
  77. package/dist/providers/index.d.ts +100 -0
  78. package/dist/providers/public.browser.d.ts +2 -0
  79. package/dist/providers/public.d.ts +3 -0
  80. package/dist/providers/shared/openai-compatible.d.ts +10 -0
  81. package/dist/providers/transform/ai-sdk-community-format.d.ts +9 -0
  82. package/dist/providers/transform/anthropic-format.d.ts +24 -0
  83. package/dist/providers/transform/content-format.d.ts +3 -0
  84. package/dist/providers/transform/gemini-format.d.ts +19 -0
  85. package/dist/providers/transform/index.d.ts +10 -0
  86. package/dist/providers/transform/openai-format.d.ts +36 -0
  87. package/dist/providers/transform/r1-format.d.ts +26 -0
  88. package/dist/providers/types/config.d.ts +261 -0
  89. package/dist/providers/types/handler.d.ts +71 -0
  90. package/dist/providers/types/index.d.ts +11 -0
  91. package/dist/providers/types/messages.d.ts +139 -0
  92. package/dist/providers/types/model-info.d.ts +32 -0
  93. package/dist/providers/types/provider-ids.d.ts +63 -0
  94. package/dist/providers/types/settings.d.ts +308 -0
  95. package/dist/providers/types/stream.d.ts +106 -0
  96. package/dist/providers/utils/index.d.ts +7 -0
  97. package/dist/providers/utils/retry.d.ts +38 -0
  98. package/dist/providers/utils/stream-processor.d.ts +110 -0
  99. package/dist/providers/utils/tool-processor.d.ts +34 -0
  100. package/dist/sdk.d.ts +18 -0
  101. package/dist/types.d.ts +60 -0
  102. package/package.json +66 -0
  103. package/src/catalog.ts +20 -0
  104. package/src/config-browser.ts +11 -0
  105. package/src/config.ts +49 -0
  106. package/src/index.browser.ts +9 -0
  107. package/src/index.ts +10 -0
  108. package/src/live-providers.test.ts +137 -0
  109. package/src/models/generated-access.ts +41 -0
  110. package/src/models/generated-provider-loaders.ts +166 -0
  111. package/src/models/generated.ts +11997 -0
  112. package/src/models/index.ts +271 -0
  113. package/src/models/models-dev-catalog.test.ts +161 -0
  114. package/src/models/models-dev-catalog.ts +161 -0
  115. package/src/models/providers/aihubmix.ts +19 -0
  116. package/src/models/providers/anthropic.ts +60 -0
  117. package/src/models/providers/asksage.ts +19 -0
  118. package/src/models/providers/baseten.ts +21 -0
  119. package/src/models/providers/bedrock.ts +30 -0
  120. package/src/models/providers/cerebras.ts +24 -0
  121. package/src/models/providers/claude-code.ts +51 -0
  122. package/src/models/providers/cline.ts +25 -0
  123. package/src/models/providers/deepseek.ts +33 -0
  124. package/src/models/providers/dify.ts +17 -0
  125. package/src/models/providers/doubao.ts +33 -0
  126. package/src/models/providers/fireworks.ts +34 -0
  127. package/src/models/providers/gemini.ts +43 -0
  128. package/src/models/providers/groq.ts +33 -0
  129. package/src/models/providers/hicap.ts +18 -0
  130. package/src/models/providers/huawei-cloud-maas.ts +18 -0
  131. package/src/models/providers/huggingface.ts +22 -0
  132. package/src/models/providers/index.ts +162 -0
  133. package/src/models/providers/litellm.ts +19 -0
  134. package/src/models/providers/lmstudio.ts +22 -0
  135. package/src/models/providers/minimax.ts +34 -0
  136. package/src/models/providers/mistral.ts +19 -0
  137. package/src/models/providers/moonshot.ts +34 -0
  138. package/src/models/providers/nebius.ts +24 -0
  139. package/src/models/providers/nous-research.ts +21 -0
  140. package/src/models/providers/oca.ts +30 -0
  141. package/src/models/providers/ollama.ts +18 -0
  142. package/src/models/providers/openai-codex.ts +30 -0
  143. package/src/models/providers/openai.ts +43 -0
  144. package/src/models/providers/opencode.ts +28 -0
  145. package/src/models/providers/openrouter.ts +24 -0
  146. package/src/models/providers/qwen-code.ts +33 -0
  147. package/src/models/providers/qwen.ts +34 -0
  148. package/src/models/providers/requesty.ts +23 -0
  149. package/src/models/providers/sambanova.ts +23 -0
  150. package/src/models/providers/sapaicore.ts +34 -0
  151. package/src/models/providers/together.ts +35 -0
  152. package/src/models/providers/vercel-ai-gateway.ts +23 -0
  153. package/src/models/providers/vertex.ts +36 -0
  154. package/src/models/providers/xai.ts +34 -0
  155. package/src/models/providers/zai.ts +25 -0
  156. package/src/models/query.ts +407 -0
  157. package/src/models/registry.ts +511 -0
  158. package/src/models/schemas/index.ts +62 -0
  159. package/src/models/schemas/model.ts +308 -0
  160. package/src/models/schemas/query.ts +336 -0
  161. package/src/providers/browser.ts +4 -0
  162. package/src/providers/handlers/ai-sdk-community.ts +226 -0
  163. package/src/providers/handlers/ai-sdk-provider-base.ts +193 -0
  164. package/src/providers/handlers/anthropic-base.ts +372 -0
  165. package/src/providers/handlers/asksage.test.ts +103 -0
  166. package/src/providers/handlers/asksage.ts +138 -0
  167. package/src/providers/handlers/auth.test.ts +19 -0
  168. package/src/providers/handlers/auth.ts +121 -0
  169. package/src/providers/handlers/base.test.ts +46 -0
  170. package/src/providers/handlers/base.ts +160 -0
  171. package/src/providers/handlers/bedrock-base.ts +390 -0
  172. package/src/providers/handlers/bedrock-client.ts +100 -0
  173. package/src/providers/handlers/codex.test.ts +123 -0
  174. package/src/providers/handlers/community-sdk.test.ts +288 -0
  175. package/src/providers/handlers/community-sdk.ts +392 -0
  176. package/src/providers/handlers/fetch-base.ts +68 -0
  177. package/src/providers/handlers/gemini-base.ts +302 -0
  178. package/src/providers/handlers/index.ts +67 -0
  179. package/src/providers/handlers/openai-base.ts +277 -0
  180. package/src/providers/handlers/openai-responses.ts +598 -0
  181. package/src/providers/handlers/providers.test.ts +120 -0
  182. package/src/providers/handlers/providers.ts +563 -0
  183. package/src/providers/handlers/r1-base.ts +280 -0
  184. package/src/providers/handlers/registry.ts +185 -0
  185. package/src/providers/handlers/vertex.test.ts +124 -0
  186. package/src/providers/handlers/vertex.ts +292 -0
  187. package/src/providers/index.ts +534 -0
  188. package/src/providers/public.browser.ts +20 -0
  189. package/src/providers/public.ts +51 -0
  190. package/src/providers/shared/openai-compatible.ts +63 -0
  191. package/src/providers/transform/ai-sdk-community-format.test.ts +73 -0
  192. package/src/providers/transform/ai-sdk-community-format.ts +115 -0
  193. package/src/providers/transform/anthropic-format.ts +218 -0
  194. package/src/providers/transform/content-format.ts +34 -0
  195. package/src/providers/transform/format-conversion.test.ts +310 -0
  196. package/src/providers/transform/gemini-format.ts +167 -0
  197. package/src/providers/transform/index.ts +22 -0
  198. package/src/providers/transform/openai-format.ts +247 -0
  199. package/src/providers/transform/r1-format.ts +287 -0
  200. package/src/providers/types/config.ts +388 -0
  201. package/src/providers/types/handler.ts +87 -0
  202. package/src/providers/types/index.ts +120 -0
  203. package/src/providers/types/messages.ts +158 -0
  204. package/src/providers/types/model-info.test.ts +57 -0
  205. package/src/providers/types/model-info.ts +65 -0
  206. package/src/providers/types/provider-ids.test.ts +12 -0
  207. package/src/providers/types/provider-ids.ts +89 -0
  208. package/src/providers/types/settings.test.ts +49 -0
  209. package/src/providers/types/settings.ts +533 -0
  210. package/src/providers/types/stream.ts +117 -0
  211. package/src/providers/utils/index.ts +27 -0
  212. package/src/providers/utils/retry.test.ts +140 -0
  213. package/src/providers/utils/retry.ts +188 -0
  214. package/src/providers/utils/stream-processor.test.ts +232 -0
  215. package/src/providers/utils/stream-processor.ts +472 -0
  216. package/src/providers/utils/tool-processor.test.ts +34 -0
  217. package/src/providers/utils/tool-processor.ts +111 -0
  218. package/src/sdk.ts +264 -0
  219. package/src/types.ts +79 -0
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Anthropic Base Handler
3
+ *
4
+ * Handler for Anthropic's API using the official SDK.
5
+ * Supports prompt caching, extended thinking, and native tool calling.
6
+ */
7
+
8
+ import { Anthropic } from "@anthropic-ai/sdk";
9
+ import type {
10
+ Tool as AnthropicTool,
11
+ RawMessageStreamEvent,
12
+ } from "@anthropic-ai/sdk/resources";
13
+ import {
14
+ convertToAnthropicMessages,
15
+ convertToolsToAnthropic,
16
+ } from "../transform/anthropic-format";
17
+ import {
18
+ type ApiStream,
19
+ type HandlerModelInfo,
20
+ hasModelCapability,
21
+ type ProviderConfig,
22
+ supportsModelThinking,
23
+ } from "../types";
24
+ import type { Message, ToolDefinition } from "../types/messages";
25
+ import { retryStream } from "../utils/retry";
26
+ import { getMissingApiKeyError, resolveApiKeyForProvider } from "./auth";
27
+ import { BaseHandler } from "./base";
28
+
29
+ const DEFAULT_THINKING_BUDGET_TOKENS = 1024;
30
+ const THINKING_DEBUG_ENV = "CLINE_DEBUG_THINKING";
31
+
32
+ function isThinkingDebugEnabled(): boolean {
33
+ const raw = process.env[THINKING_DEBUG_ENV];
34
+ if (!raw) {
35
+ return false;
36
+ }
37
+ const normalized = raw.trim().toLowerCase();
38
+ return normalized === "1" || normalized === "true" || normalized === "yes";
39
+ }
40
+
41
+ /**
42
+ * Handler for Anthropic's API
43
+ */
44
+ export class AnthropicHandler extends BaseHandler {
45
+ private client: Anthropic | undefined;
46
+
47
+ private ensureClient(): Anthropic {
48
+ if (!this.client) {
49
+ const apiKey = resolveApiKeyForProvider(
50
+ this.config.providerId,
51
+ this.config.apiKey,
52
+ );
53
+ if (!apiKey) {
54
+ throw new Error(getMissingApiKeyError(this.config.providerId));
55
+ }
56
+
57
+ this.client = new Anthropic({
58
+ apiKey,
59
+ baseURL: this.config.baseUrl || undefined,
60
+ defaultHeaders: this.getRequestHeaders(),
61
+ });
62
+ }
63
+ return this.client;
64
+ }
65
+
66
+ getModel(): HandlerModelInfo {
67
+ const modelId = this.config.modelId;
68
+ const knownModels = this.config.knownModels ?? {};
69
+ const fallbackModel = knownModels[modelId] ?? {};
70
+ const modelInfo = this.config.modelInfo ?? fallbackModel;
71
+
72
+ return { id: modelId, info: { ...modelInfo, id: modelId } };
73
+ }
74
+
75
+ getMessages(
76
+ _systemPrompt: string,
77
+ messages: Message[],
78
+ ): Anthropic.MessageParam[] {
79
+ const supportsPromptCache = hasModelCapability(
80
+ this.getModel().info,
81
+ "prompt-cache",
82
+ );
83
+ return convertToAnthropicMessages(
84
+ messages,
85
+ supportsPromptCache,
86
+ ) as Anthropic.MessageParam[];
87
+ }
88
+
89
+ async *createMessage(
90
+ systemPrompt: string,
91
+ messages: Message[],
92
+ tools?: ToolDefinition[],
93
+ ): ApiStream {
94
+ yield* retryStream(() =>
95
+ this.createMessageInternal(systemPrompt, messages, tools),
96
+ );
97
+ }
98
+
99
+ private async *createMessageInternal(
100
+ systemPrompt: string,
101
+ messages: Message[],
102
+ tools?: ToolDefinition[],
103
+ ): ApiStream {
104
+ const client = this.ensureClient();
105
+ const model = this.getModel();
106
+ const abortSignal = this.getAbortSignal();
107
+ const responseId = this.createResponseId();
108
+
109
+ const thinkingSupported = supportsModelThinking(model.info);
110
+ const requestedBudget =
111
+ this.config.thinkingBudgetTokens ??
112
+ (this.config.thinking ? DEFAULT_THINKING_BUDGET_TOKENS : 0);
113
+ const budgetTokens =
114
+ thinkingSupported && requestedBudget > 0 ? requestedBudget : 0;
115
+ const nativeToolsOn = tools && tools.length > 0;
116
+ const supportsPromptCache = hasModelCapability(model.info, "prompt-cache");
117
+ const reasoningOn = thinkingSupported && budgetTokens > 0;
118
+ const debugThinking = isThinkingDebugEnabled();
119
+ const debugChunkCounts: Record<string, number> = {};
120
+ const countChunk = (type: string): void => {
121
+ debugChunkCounts[type] = (debugChunkCounts[type] ?? 0) + 1;
122
+ };
123
+
124
+ if (debugThinking) {
125
+ console.error(
126
+ `[thinking-debug][anthropic][request] model=${model.id} thinkingFlag=${this.config.thinking === true} supportsModelThinking=${thinkingSupported} requestedBudget=${requestedBudget} effectiveBudget=${budgetTokens} reasoningOn=${reasoningOn} promptCache=${supportsPromptCache}`,
127
+ );
128
+ }
129
+
130
+ // Convert messages
131
+ const anthropicMessages = this.getMessages(systemPrompt, messages);
132
+
133
+ // Convert tools
134
+ const anthropicTools: AnthropicTool[] | undefined = nativeToolsOn
135
+ ? convertToolsToAnthropic(tools)
136
+ : undefined;
137
+
138
+ // Request options with abort signal
139
+ const requestOptions = { signal: abortSignal };
140
+
141
+ // Create the request
142
+ const stream = await client.messages.create(
143
+ {
144
+ model: model.id,
145
+ thinking: reasoningOn
146
+ ? { type: "enabled", budget_tokens: budgetTokens }
147
+ : undefined,
148
+ max_tokens: model.info.maxTokens || 8192,
149
+ temperature: reasoningOn ? undefined : 0,
150
+ system: supportsPromptCache
151
+ ? [
152
+ {
153
+ text: systemPrompt,
154
+ type: "text",
155
+ cache_control: { type: "ephemeral" },
156
+ },
157
+ ]
158
+ : [{ text: systemPrompt, type: "text" }],
159
+ messages: anthropicMessages as Anthropic.MessageParam[],
160
+ stream: true,
161
+ tools: anthropicTools,
162
+ tool_choice:
163
+ nativeToolsOn && !reasoningOn ? { type: "auto" } : undefined,
164
+ },
165
+ requestOptions,
166
+ );
167
+
168
+ // Track tool call state
169
+ const currentToolCall = { id: "", name: "", arguments: "" };
170
+ const usageSnapshot = {
171
+ inputTokens: 0,
172
+ outputTokens: 0,
173
+ cacheReadTokens: 0,
174
+ cacheWriteTokens: 0,
175
+ };
176
+
177
+ for await (const chunk of stream) {
178
+ if (debugThinking) {
179
+ countChunk(`event:${chunk.type}`);
180
+ if (chunk.type === "content_block_start") {
181
+ countChunk(
182
+ `content_block_start:${chunk.content_block?.type ?? "unknown"}`,
183
+ );
184
+ } else if (chunk.type === "content_block_delta") {
185
+ countChunk(`content_block_delta:${chunk.delta?.type ?? "unknown"}`);
186
+ }
187
+ }
188
+ yield* this.withResponseIdForAll(
189
+ this.processChunk(chunk, currentToolCall, usageSnapshot, responseId),
190
+ responseId,
191
+ );
192
+ }
193
+
194
+ if (debugThinking) {
195
+ const summary = Object.entries(debugChunkCounts)
196
+ .map(([key, count]) => `${key}=${count}`)
197
+ .sort()
198
+ .join(" ");
199
+ console.error(`[thinking-debug][anthropic][stream] ${summary}`);
200
+ }
201
+
202
+ // Yield done chunk to indicate streaming completed successfully
203
+ yield { type: "done", success: true, id: responseId };
204
+ }
205
+
206
+ private *processChunk(
207
+ chunk: RawMessageStreamEvent,
208
+ currentToolCall: { id: string; name: string; arguments: string },
209
+ usageSnapshot: {
210
+ inputTokens: number;
211
+ outputTokens: number;
212
+ cacheReadTokens: number;
213
+ cacheWriteTokens: number;
214
+ },
215
+ responseId: string,
216
+ ): Generator<import("../types").ApiStreamChunk> {
217
+ switch (chunk.type) {
218
+ case "message_start": {
219
+ const usage = chunk.message.usage;
220
+ usageSnapshot.inputTokens = usage.input_tokens || 0;
221
+ usageSnapshot.outputTokens = usage.output_tokens || 0;
222
+ usageSnapshot.cacheWriteTokens =
223
+ (usage as any).cache_creation_input_tokens || 0;
224
+ usageSnapshot.cacheReadTokens =
225
+ (usage as any).cache_read_input_tokens || 0;
226
+ yield {
227
+ type: "usage",
228
+ inputTokens: usageSnapshot.inputTokens,
229
+ outputTokens: usageSnapshot.outputTokens,
230
+ cacheWriteTokens: usageSnapshot.cacheWriteTokens,
231
+ cacheReadTokens: usageSnapshot.cacheReadTokens,
232
+ totalCost: this.calculateCost(
233
+ usageSnapshot.inputTokens,
234
+ usageSnapshot.outputTokens,
235
+ usageSnapshot.cacheReadTokens,
236
+ ),
237
+ id: responseId,
238
+ };
239
+ break;
240
+ }
241
+
242
+ case "message_delta": {
243
+ usageSnapshot.outputTokens =
244
+ chunk.usage.output_tokens || usageSnapshot.outputTokens;
245
+ yield {
246
+ type: "usage",
247
+ inputTokens: usageSnapshot.inputTokens,
248
+ outputTokens: usageSnapshot.outputTokens,
249
+ cacheWriteTokens: usageSnapshot.cacheWriteTokens,
250
+ cacheReadTokens: usageSnapshot.cacheReadTokens,
251
+ totalCost: this.calculateCost(
252
+ usageSnapshot.inputTokens,
253
+ usageSnapshot.outputTokens,
254
+ usageSnapshot.cacheReadTokens,
255
+ ),
256
+ id: responseId,
257
+ };
258
+ break;
259
+ }
260
+
261
+ case "content_block_start": {
262
+ const block = chunk.content_block;
263
+ switch (block.type) {
264
+ case "thinking":
265
+ yield {
266
+ type: "reasoning",
267
+ reasoning:
268
+ typeof (block as { thinking?: unknown }).thinking === "string"
269
+ ? ((block as { thinking: string }).thinking ?? "")
270
+ : "",
271
+ signature:
272
+ typeof (block as { signature?: unknown }).signature === "string"
273
+ ? ((block as { signature: string }).signature ?? undefined)
274
+ : undefined,
275
+ id: responseId,
276
+ };
277
+ break;
278
+ case "redacted_thinking":
279
+ yield {
280
+ type: "reasoning",
281
+ reasoning: "",
282
+ redacted_data:
283
+ typeof (block as { data?: unknown }).data === "string"
284
+ ? ((block as { data: string }).data ?? undefined)
285
+ : undefined,
286
+ id: responseId,
287
+ };
288
+ break;
289
+ case "text":
290
+ yield { type: "text", text: "", id: responseId };
291
+ break;
292
+ case "tool_use":
293
+ currentToolCall.id = block.id;
294
+ currentToolCall.name = block.name;
295
+ currentToolCall.arguments = "";
296
+ break;
297
+ }
298
+ break;
299
+ }
300
+
301
+ case "content_block_delta": {
302
+ const delta = chunk.delta;
303
+ switch (delta.type) {
304
+ case "thinking_delta":
305
+ yield {
306
+ type: "reasoning",
307
+ reasoning: delta.thinking,
308
+ id: responseId,
309
+ };
310
+ break;
311
+ case "signature_delta":
312
+ yield {
313
+ type: "reasoning",
314
+ reasoning: "",
315
+ signature:
316
+ typeof (delta as { signature?: unknown }).signature === "string"
317
+ ? ((delta as { signature: string }).signature ?? undefined)
318
+ : undefined,
319
+ id: responseId,
320
+ };
321
+ break;
322
+ case "text_delta":
323
+ yield { type: "text", text: delta.text, id: responseId };
324
+ break;
325
+ case "input_json_delta":
326
+ currentToolCall.arguments += delta.partial_json;
327
+ break;
328
+ }
329
+ break;
330
+ }
331
+
332
+ case "content_block_stop": {
333
+ // If we have a tool call, yield it
334
+ if (currentToolCall.id) {
335
+ let parsedArgs: Record<string, unknown>;
336
+ try {
337
+ parsedArgs = JSON.parse(currentToolCall.arguments || "{}");
338
+ } catch {
339
+ parsedArgs = {};
340
+ }
341
+
342
+ yield {
343
+ type: "tool_calls",
344
+ id: responseId,
345
+ tool_call: {
346
+ call_id: currentToolCall.id,
347
+ function: {
348
+ name: currentToolCall.name,
349
+ arguments: parsedArgs,
350
+ },
351
+ },
352
+ };
353
+
354
+ // Reset tool call state
355
+ currentToolCall.id = "";
356
+ currentToolCall.name = "";
357
+ currentToolCall.arguments = "";
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Create an Anthropic handler
367
+ */
368
+ export function createAnthropicHandler(
369
+ config: ProviderConfig,
370
+ ): AnthropicHandler {
371
+ return new AnthropicHandler(config);
372
+ }
@@ -0,0 +1,103 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createHandler } from "../index";
3
+ import type { ApiStreamChunk } from "../types";
4
+ import { AskSageHandler } from "./asksage";
5
+
6
+ vi.mock("./auth", async () => {
7
+ const actual = await vi.importActual("./auth");
8
+ return {
9
+ ...(actual as object),
10
+ resolveApiKeyForProvider: (_providerId: string, explicitApiKey?: string) =>
11
+ explicitApiKey?.trim() || undefined,
12
+ };
13
+ });
14
+
15
+ describe("AskSageHandler", () => {
16
+ beforeEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it("formats request payload and emits text/usage/done chunks", async () => {
21
+ const fetchMock = vi.fn(async () => ({
22
+ ok: true,
23
+ json: async () => ({
24
+ message: "final answer",
25
+ tool_responses: [{ name: "search", ok: true }],
26
+ usage: {
27
+ model_tokens: {
28
+ prompt_tokens: 123,
29
+ completion_tokens: 45,
30
+ total_tokens: 168,
31
+ },
32
+ asksage_tokens: 17.5,
33
+ },
34
+ }),
35
+ }));
36
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
37
+
38
+ const handler = new AskSageHandler({
39
+ providerId: "asksage",
40
+ modelId: "gpt-4o",
41
+ apiKey: "ask-key",
42
+ });
43
+
44
+ const chunks: ApiStreamChunk[] = [];
45
+ for await (const chunk of handler.createMessage("system prompt", [
46
+ { role: "user", content: [{ type: "text", text: "hello" }] },
47
+ { role: "assistant", content: "hi there" },
48
+ ])) {
49
+ chunks.push(chunk);
50
+ }
51
+
52
+ expect(fetchMock).toHaveBeenCalledTimes(1);
53
+ const [url, init] = fetchMock.mock.calls[0] as unknown as [
54
+ string,
55
+ RequestInit & { body?: string },
56
+ ];
57
+ expect(url).toBe("https://api.asksage.ai/server/query");
58
+ expect(init.method).toBe("POST");
59
+ expect(init.headers).toMatchObject({
60
+ "Content-Type": "application/json",
61
+ "x-access-tokens": "ask-key",
62
+ });
63
+ expect(JSON.parse(init.body ?? "{}")).toEqual({
64
+ system_prompt: "system prompt",
65
+ message: [
66
+ { user: "me", message: "hello" },
67
+ { user: "gpt", message: "hi there" },
68
+ ],
69
+ model: "gpt-4o",
70
+ dataset: "none",
71
+ usage: true,
72
+ });
73
+
74
+ expect(chunks.map((chunk) => chunk.type)).toEqual([
75
+ "text",
76
+ "text",
77
+ "usage",
78
+ "done",
79
+ ]);
80
+ });
81
+
82
+ it("is used by createHandler for built-in asksage provider id", () => {
83
+ const handler = createHandler({
84
+ providerId: "asksage",
85
+ modelId: "gpt-4o",
86
+ apiKey: "ask-key",
87
+ });
88
+ expect(handler).toBeInstanceOf(AskSageHandler);
89
+ });
90
+
91
+ it("throws when API key is missing", async () => {
92
+ const handler = new AskSageHandler({
93
+ providerId: "asksage",
94
+ modelId: "gpt-4o",
95
+ });
96
+
97
+ await expect(async () => {
98
+ for await (const _chunk of handler.createMessage("system", [])) {
99
+ // noop
100
+ }
101
+ }).rejects.toThrow("AskSage API key is required");
102
+ });
103
+ });
@@ -0,0 +1,138 @@
1
+ import type { ApiStream, HandlerModelInfo, ProviderConfig } from "../types";
2
+ import type { ContentBlock, Message } from "../types/messages";
3
+ import { resolveApiKeyForProvider } from "./auth";
4
+ import { FetchBaseHandler } from "./fetch-base";
5
+
6
+ export const DEFAULT_ASKSAGE_BASE_URL = "https://api.asksage.ai/server";
7
+ const DEFAULT_ASKSAGE_MODEL_ID = "gpt-4o";
8
+
9
+ type AskSageRequest = {
10
+ system_prompt: string;
11
+ message: Array<{
12
+ user: "gpt" | "me";
13
+ message: string;
14
+ }>;
15
+ model: string;
16
+ dataset: "none";
17
+ usage: boolean;
18
+ };
19
+
20
+ type AskSageUsage = {
21
+ model_tokens: {
22
+ completion_tokens: number;
23
+ prompt_tokens: number;
24
+ total_tokens: number;
25
+ };
26
+ asksage_tokens: number;
27
+ };
28
+
29
+ type AskSageResponse = {
30
+ message?: string;
31
+ usage?: AskSageUsage | null;
32
+ tool_responses?: unknown[];
33
+ };
34
+
35
+ export class AskSageHandler extends FetchBaseHandler {
36
+ protected getDefaultBaseUrl(): string {
37
+ return DEFAULT_ASKSAGE_BASE_URL;
38
+ }
39
+
40
+ getModel(): HandlerModelInfo {
41
+ const modelId = this.config.modelId?.trim() || DEFAULT_ASKSAGE_MODEL_ID;
42
+ const modelInfo = this.config.modelInfo ??
43
+ this.config.knownModels?.[modelId] ?? {
44
+ id: modelId,
45
+ capabilities: ["tools"],
46
+ };
47
+ return { id: modelId, info: { ...modelInfo, id: modelId } };
48
+ }
49
+
50
+ protected getJsonHeaders(
51
+ extra?: Record<string, string>,
52
+ ): Record<string, string> {
53
+ const apiKey = resolveApiKeyForProvider(
54
+ this.config.providerId,
55
+ this.config.apiKey,
56
+ );
57
+ if (!apiKey) {
58
+ throw new Error("AskSage API key is required");
59
+ }
60
+ return super.getJsonHeaders({
61
+ "x-access-tokens": apiKey,
62
+ ...(extra ?? {}),
63
+ });
64
+ }
65
+
66
+ protected async *createMessageWithFetch(
67
+ systemPrompt: string,
68
+ messages: Message[],
69
+ ): ApiStream {
70
+ const responseId = this.createResponseId();
71
+ const { id: modelId } = this.getModel();
72
+
73
+ const payload: AskSageRequest = {
74
+ system_prompt: systemPrompt,
75
+ message: messages.map((message) => ({
76
+ user: message.role === "assistant" ? "gpt" : "me",
77
+ message: this.serializeMessageContent(message.content),
78
+ })),
79
+ model: modelId,
80
+ dataset: "none",
81
+ usage: true,
82
+ };
83
+
84
+ let result: AskSageResponse;
85
+ try {
86
+ result = await this.fetchJson<AskSageResponse>("/query", {
87
+ method: "POST",
88
+ body: payload,
89
+ });
90
+ } catch (error) {
91
+ const details = error instanceof Error ? error.message : String(error);
92
+ throw new Error(`AskSage request failed: ${details}`);
93
+ }
94
+
95
+ for (const toolResponse of result.tool_responses ?? []) {
96
+ yield {
97
+ type: "text",
98
+ text: `[Tool Response: ${JSON.stringify(toolResponse)}]\n`,
99
+ id: responseId,
100
+ };
101
+ }
102
+
103
+ const text = result.message?.trim();
104
+ if (!text) {
105
+ throw new Error("AskSage request failed: no content in response");
106
+ }
107
+
108
+ yield { type: "text", text, id: responseId };
109
+
110
+ if (result.usage) {
111
+ yield {
112
+ type: "usage",
113
+ inputTokens: result.usage.model_tokens.prompt_tokens,
114
+ outputTokens: result.usage.model_tokens.completion_tokens,
115
+ cacheReadTokens: 0,
116
+ cacheWriteTokens: 0,
117
+ totalCost: result.usage.asksage_tokens,
118
+ id: responseId,
119
+ };
120
+ }
121
+
122
+ yield { type: "done", success: true, id: responseId };
123
+ }
124
+
125
+ private serializeMessageContent(content: string | ContentBlock[]): string {
126
+ if (typeof content === "string") {
127
+ return content;
128
+ }
129
+ return content
130
+ .map((block) => ("text" in block ? block.text : ""))
131
+ .join("")
132
+ .trim();
133
+ }
134
+ }
135
+
136
+ export function createAskSageHandler(config: ProviderConfig): AskSageHandler {
137
+ return new AskSageHandler(config);
138
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveApiKeyForProvider } from "./auth";
3
+
4
+ describe("resolveApiKeyForProvider", () => {
5
+ it("returns noop for lmstudio when no key is provided", () => {
6
+ const apiKey = resolveApiKeyForProvider("lmstudio", undefined, {});
7
+ expect(apiKey).toBe("noop");
8
+ });
9
+
10
+ it("prefers explicit api keys over provider defaults", () => {
11
+ const apiKey = resolveApiKeyForProvider("lmstudio", "real-key", {});
12
+ expect(apiKey).toBe("real-key");
13
+ });
14
+
15
+ it("does not apply lmstudio fallback to zai", () => {
16
+ const apiKey = resolveApiKeyForProvider("zai", undefined, {});
17
+ expect(apiKey).toBeUndefined();
18
+ });
19
+ });