@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,121 @@
1
+ import * as modelProviderExports from "../../models/providers/index";
2
+ import type { ModelCollection } from "../../models/schemas/index";
3
+ import { BUILT_IN_PROVIDER, normalizeProviderId } from "../types/provider-ids";
4
+
5
+ const DEFAULT_FALLBACK_PROVIDER_IDS = [
6
+ BUILT_IN_PROVIDER.CLINE,
7
+ BUILT_IN_PROVIDER.ANTHROPIC,
8
+ BUILT_IN_PROVIDER.OPENAI_NATIVE,
9
+ BUILT_IN_PROVIDER.GEMINI,
10
+ BUILT_IN_PROVIDER.OPENROUTER,
11
+ ] as const;
12
+
13
+ function isModelCollection(value: unknown): value is ModelCollection {
14
+ if (!value || typeof value !== "object") {
15
+ return false;
16
+ }
17
+
18
+ const maybeCollection = value as Partial<ModelCollection>;
19
+ return (
20
+ typeof maybeCollection.provider === "object" &&
21
+ typeof maybeCollection.models === "object"
22
+ );
23
+ }
24
+
25
+ function dedupe(values: readonly string[]): string[] {
26
+ return [...new Set(values)];
27
+ }
28
+
29
+ function buildProviderEnvKeys(): Record<string, readonly string[]> {
30
+ const envKeysByProvider: Record<string, readonly string[]> = {};
31
+
32
+ for (const value of Object.values(modelProviderExports)) {
33
+ if (!isModelCollection(value)) {
34
+ continue;
35
+ }
36
+
37
+ const providerId = value.provider.id;
38
+ envKeysByProvider[providerId] = dedupe(value.provider.env ?? []);
39
+ }
40
+
41
+ return envKeysByProvider;
42
+ }
43
+
44
+ const ENV_KEYS_BY_PROVIDER = buildProviderEnvKeys();
45
+ const DEFAULT_FALLBACK_ENV_KEYS = dedupe(
46
+ DEFAULT_FALLBACK_PROVIDER_IDS.flatMap(
47
+ (providerId) => ENV_KEYS_BY_PROVIDER[providerId] ?? [],
48
+ ),
49
+ );
50
+
51
+ function readTrimmed(
52
+ env: Record<string, string | undefined>,
53
+ key: string,
54
+ ): string | undefined {
55
+ const value = env[key];
56
+ if (typeof value !== "string") {
57
+ return undefined;
58
+ }
59
+ const trimmed = value.trim();
60
+ return trimmed.length > 0 ? trimmed : undefined;
61
+ }
62
+
63
+ function resolveFromKeys(
64
+ keys: readonly string[],
65
+ env: Record<string, string | undefined>,
66
+ ): string | undefined {
67
+ for (const key of keys) {
68
+ const value = readTrimmed(env, key);
69
+ if (value) {
70
+ return value;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ export { normalizeProviderId };
77
+
78
+ export function getProviderEnvKeys(providerId: string): readonly string[] {
79
+ return ENV_KEYS_BY_PROVIDER[normalizeProviderId(providerId)] ?? [];
80
+ }
81
+
82
+ export function resolveApiKeyForProvider(
83
+ providerId: string,
84
+ explicitApiKey: string | undefined,
85
+ env: Record<string, string | undefined> = process.env,
86
+ ): string | undefined {
87
+ const normalizedProviderId = normalizeProviderId(providerId);
88
+ const explicit = explicitApiKey?.trim();
89
+ if (explicit) {
90
+ return explicit;
91
+ }
92
+
93
+ const providerKey = resolveFromKeys(
94
+ getProviderEnvKeys(normalizedProviderId),
95
+ env,
96
+ );
97
+ if (providerKey) {
98
+ return providerKey;
99
+ }
100
+
101
+ // LM Studio local runtime typically does not require auth.
102
+ if (normalizedProviderId === BUILT_IN_PROVIDER.LMSTUDIO) {
103
+ return "noop";
104
+ }
105
+
106
+ return resolveFromKeys(DEFAULT_FALLBACK_ENV_KEYS, env);
107
+ }
108
+
109
+ export function getMissingApiKeyError(providerId: string): string {
110
+ const expectedKeys = [
111
+ ...new Set([
112
+ ...getProviderEnvKeys(providerId),
113
+ ...DEFAULT_FALLBACK_ENV_KEYS,
114
+ ]),
115
+ ];
116
+ const keysMessage =
117
+ expectedKeys.length > 0
118
+ ? expectedKeys.join(", ")
119
+ : "provider-specific API key env var";
120
+ return `Missing API key for provider "${normalizeProviderId(providerId)}". Set apiKey explicitly or one of: ${keysMessage}.`;
121
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ApiStream, ProviderConfig } from "../types/index";
3
+ import { BaseHandler } from "./base";
4
+
5
+ class TestHandler extends BaseHandler {
6
+ getMessages(): unknown {
7
+ return [];
8
+ }
9
+
10
+ createMessage(): ApiStream {
11
+ throw new Error("not implemented");
12
+ }
13
+
14
+ public computeCost(
15
+ inputTokens: number,
16
+ outputTokens: number,
17
+ cacheReadTokens = 0,
18
+ ): number | undefined {
19
+ return this.calculateCost(inputTokens, outputTokens, cacheReadTokens);
20
+ }
21
+ }
22
+
23
+ describe("BaseHandler.calculateCost", () => {
24
+ it("uses known model pricing when modelInfo is not provided", () => {
25
+ const config: ProviderConfig = {
26
+ providerId: "anthropic",
27
+ modelId: "claude-sonnet-test",
28
+ apiKey: "test-key",
29
+ knownModels: {
30
+ "claude-sonnet-test": {
31
+ id: "claude-sonnet-test",
32
+ pricing: {
33
+ input: 3,
34
+ output: 15,
35
+ cacheRead: 0.3,
36
+ },
37
+ },
38
+ },
39
+ };
40
+ const handler = new TestHandler(config);
41
+
42
+ const cost = handler.computeCost(1_000_000, 1_000_000, 100_000);
43
+
44
+ expect(cost).toBeCloseTo(17.73, 6);
45
+ });
46
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Base Handler
3
+ *
4
+ * Abstract base class that provides common functionality for all handlers.
5
+ */
6
+
7
+ import { nanoid } from "nanoid";
8
+ import type {
9
+ ApiHandler,
10
+ ApiStream,
11
+ ApiStreamUsageChunk,
12
+ HandlerModelInfo,
13
+ ProviderConfig,
14
+ } from "../types";
15
+ import type { Message, ToolDefinition } from "../types/messages";
16
+ import type { ApiStreamChunk } from "../types/stream";
17
+
18
+ export const DEFAULT_REQUEST_HEADERS: Record<string, string> = {
19
+ "HTTP-Referer": "https://cline.bot",
20
+ "X-Title": "Cline",
21
+ "X-IS-MULTIROOT": "false",
22
+ "X-CLIENT-TYPE": "cline-sdk",
23
+ };
24
+
25
+ /**
26
+ * Base handler class with common functionality
27
+ */
28
+ export abstract class BaseHandler implements ApiHandler {
29
+ protected config: ProviderConfig;
30
+ protected abortController: AbortController | undefined;
31
+
32
+ constructor(config: ProviderConfig) {
33
+ this.config = config;
34
+ }
35
+
36
+ /**
37
+ * Convert Cline messages to provider-specific format
38
+ * Must be implemented by subclasses
39
+ */
40
+ abstract getMessages(systemPrompt: string, messages: Message[]): unknown;
41
+
42
+ /**
43
+ * Create a streaming message completion
44
+ * Must be implemented by subclasses
45
+ */
46
+ abstract createMessage(
47
+ systemPrompt: string,
48
+ messages: Message[],
49
+ tools?: ToolDefinition[],
50
+ ): ApiStream;
51
+
52
+ /**
53
+ * Get the current model configuration
54
+ * Can be overridden by subclasses for provider-specific logic
55
+ */
56
+ getModel(): HandlerModelInfo {
57
+ const modelId = this.config.modelId;
58
+ return {
59
+ id: modelId,
60
+ info: { ...(this.config.modelInfo ?? {}), id: modelId },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Get usage information (optional)
66
+ * Override in subclasses that support this
67
+ */
68
+ async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
69
+ return undefined;
70
+ }
71
+
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
+ 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;
86
+ if (configSignal.aborted) {
87
+ this.abortController.abort(configSignal.reason);
88
+ } else {
89
+ configSignal.addEventListener("abort", () => {
90
+ this.abortController?.abort(configSignal.reason);
91
+ });
92
+ }
93
+ }
94
+
95
+ return this.abortController.signal;
96
+ }
97
+
98
+ /**
99
+ * Abort the current request
100
+ */
101
+ abort(): void {
102
+ this.abortController?.abort();
103
+ }
104
+
105
+ /**
106
+ * Helper to calculate cost from usage
107
+ */
108
+ protected calculateCost(
109
+ inputTokens: number,
110
+ outputTokens: number,
111
+ cacheReadTokens = 0,
112
+ ): number | undefined {
113
+ const modelPricingSource =
114
+ this.config.modelInfo ??
115
+ (this.config.modelId
116
+ ? this.config.knownModels?.[this.config.modelId]
117
+ : undefined);
118
+ const pricing = modelPricingSource?.pricing;
119
+ if (!pricing?.input || !pricing?.output) {
120
+ return undefined;
121
+ }
122
+
123
+ const uncachedInputTokens = inputTokens - cacheReadTokens;
124
+ const inputCost = (uncachedInputTokens / 1_000_000) * pricing.input;
125
+ const outputCost = (outputTokens / 1_000_000) * pricing.output;
126
+ const cacheReadCost =
127
+ cacheReadTokens > 0
128
+ ? (cacheReadTokens / 1_000_000) * (pricing.cacheRead ?? 0)
129
+ : 0;
130
+
131
+ return inputCost + outputCost + cacheReadCost;
132
+ }
133
+
134
+ protected createResponseId(): string {
135
+ return nanoid();
136
+ }
137
+
138
+ protected withResponseId<T extends ApiStreamChunk>(
139
+ chunk: T,
140
+ responseId: string,
141
+ ): T {
142
+ return { ...chunk, id: responseId };
143
+ }
144
+
145
+ protected *withResponseIdForAll(
146
+ chunks: Iterable<ApiStreamChunk>,
147
+ responseId: string,
148
+ ): Generator<ApiStreamChunk> {
149
+ for (const chunk of chunks) {
150
+ yield this.withResponseId(chunk, responseId);
151
+ }
152
+ }
153
+
154
+ protected getRequestHeaders(): Record<string, string> {
155
+ return {
156
+ ...DEFAULT_REQUEST_HEADERS,
157
+ ...(this.config.headers ?? {}),
158
+ };
159
+ }
160
+ }
@@ -0,0 +1,390 @@
1
+ import { convertToolsToAnthropic } from "../transform/anthropic-format";
2
+ import {
3
+ type ApiStream,
4
+ type HandlerModelInfo,
5
+ type ProviderConfig,
6
+ supportsModelThinking,
7
+ } from "../types";
8
+ import type { Message, ToolDefinition } from "../types/messages";
9
+ import { retryStream } from "../utils/retry";
10
+ import { BaseHandler } from "./base";
11
+ import { createBedrockClient } from "./bedrock-client";
12
+
13
+ const CLAUDE_SONNET_1M_SUFFIX = ":1m";
14
+
15
+ type AiModule = {
16
+ streamText: (input: Record<string, unknown>) => {
17
+ fullStream?: AsyncIterable<{ type?: string; [key: string]: unknown }>;
18
+ textStream?: AsyncIterable<string>;
19
+ usage?: Promise<{
20
+ inputTokens?: number;
21
+ outputTokens?: number;
22
+ reasoningTokens?: number;
23
+ cachedInputTokens?: number;
24
+ [key: string]: unknown;
25
+ }>;
26
+ };
27
+ };
28
+
29
+ let cachedAiModule: AiModule | undefined;
30
+ const DEFAULT_THINKING_BUDGET_TOKENS = 1024;
31
+ const DEFAULT_REASONING_EFFORT = "medium" as const;
32
+
33
+ async function loadAiModule(): Promise<AiModule> {
34
+ if (cachedAiModule) {
35
+ return cachedAiModule;
36
+ }
37
+
38
+ const moduleName = "ai";
39
+ cachedAiModule = (await import(moduleName)) as AiModule;
40
+ return cachedAiModule;
41
+ }
42
+
43
+ type ModelMessagePart = Record<string, unknown>;
44
+ type ModelMessage = {
45
+ role: "system" | "user" | "assistant" | "tool";
46
+ content: string | ModelMessagePart[];
47
+ };
48
+
49
+ /**
50
+ * Handler for AWS Bedrock using AI SDK's Amazon Bedrock provider.
51
+ *
52
+ * This handler is async-lazy loaded via createHandlerAsync.
53
+ */
54
+ export class BedrockHandler extends BaseHandler {
55
+ private clientFactory: ((modelId: string) => unknown) | undefined;
56
+
57
+ private async ensureClientFactory(): Promise<(modelId: string) => unknown> {
58
+ if (!this.clientFactory) {
59
+ this.clientFactory = await createBedrockClient(
60
+ this.config,
61
+ this.getRequestHeaders(),
62
+ );
63
+ }
64
+ return this.clientFactory;
65
+ }
66
+
67
+ getModel(): HandlerModelInfo {
68
+ const modelId = this.config.modelId;
69
+ if (!modelId) {
70
+ throw new Error("Model ID is required. Set modelId in config.");
71
+ }
72
+
73
+ const modelInfo =
74
+ this.config.modelInfo ?? this.config.knownModels?.[modelId] ?? {};
75
+ return { id: modelId, info: { ...modelInfo, id: modelId } };
76
+ }
77
+
78
+ getMessages(systemPrompt: string, messages: Message[]): ModelMessage[] {
79
+ return toModelMessages(systemPrompt, messages);
80
+ }
81
+
82
+ async *createMessage(
83
+ systemPrompt: string,
84
+ messages: Message[],
85
+ tools?: ToolDefinition[],
86
+ ): ApiStream {
87
+ yield* retryStream(
88
+ () => this.createMessageInternal(systemPrompt, messages, tools),
89
+ { maxRetries: 4 },
90
+ );
91
+ }
92
+
93
+ private async *createMessageInternal(
94
+ systemPrompt: string,
95
+ messages: Message[],
96
+ tools?: ToolDefinition[],
97
+ ): ApiStream {
98
+ const ai = await loadAiModule();
99
+ const factory = await this.ensureClientFactory();
100
+ const responseId = this.createResponseId();
101
+ const abortSignal = this.getAbortSignal();
102
+ const model = this.getModel();
103
+
104
+ let modelId = model.id;
105
+ const providerOptions: Record<string, unknown> = {};
106
+ const bedrockOptions: Record<string, unknown> = {};
107
+
108
+ if (modelId.endsWith(CLAUDE_SONNET_1M_SUFFIX)) {
109
+ modelId = modelId.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length);
110
+ bedrockOptions.anthropicBeta = ["context-1m-2025-08-07"];
111
+ }
112
+
113
+ const thinkingSupported = supportsModelThinking(model.info);
114
+ const budgetTokens =
115
+ this.config.thinkingBudgetTokens ??
116
+ (this.config.thinking ? DEFAULT_THINKING_BUDGET_TOKENS : 0);
117
+ let reasoningEnabled = false;
118
+ if (
119
+ thinkingSupported &&
120
+ budgetTokens > 0 &&
121
+ modelId.includes("anthropic")
122
+ ) {
123
+ bedrockOptions.reasoningConfig = { type: "enabled", budgetTokens };
124
+ reasoningEnabled = true;
125
+ } else if (thinkingSupported && modelId.includes("amazon.nova")) {
126
+ const reasoningEffort =
127
+ this.config.reasoningEffort ??
128
+ (this.config.thinking ? DEFAULT_REASONING_EFFORT : undefined);
129
+ if (reasoningEffort) {
130
+ bedrockOptions.reasoningConfig = {
131
+ type: "enabled",
132
+ maxReasoningEffort: reasoningEffort,
133
+ };
134
+ reasoningEnabled = true;
135
+ }
136
+ }
137
+
138
+ if (Object.keys(bedrockOptions).length > 0) {
139
+ providerOptions.bedrock = bedrockOptions;
140
+ }
141
+
142
+ const stream = ai.streamText({
143
+ model: factory(modelId),
144
+ messages: this.getMessages(systemPrompt, messages),
145
+ tools: toAiSdkTools(tools),
146
+ maxTokens: model.info.maxTokens ?? undefined,
147
+ temperature: reasoningEnabled ? undefined : (model.info.temperature ?? 0),
148
+ providerOptions:
149
+ Object.keys(providerOptions).length > 0 ? providerOptions : undefined,
150
+ abortSignal,
151
+ });
152
+
153
+ let usageEmitted = false;
154
+
155
+ if (stream.fullStream) {
156
+ for await (const part of stream.fullStream) {
157
+ const partType = part.type;
158
+
159
+ if (partType === "text-delta") {
160
+ const text =
161
+ (part.textDelta as string | undefined) ??
162
+ (part.delta as string | undefined);
163
+ if (text) {
164
+ yield { type: "text", text, id: responseId };
165
+ }
166
+ continue;
167
+ }
168
+
169
+ if (partType === "reasoning-delta" || partType === "reasoning") {
170
+ const reasoning =
171
+ (part.textDelta as string | undefined) ??
172
+ (part.reasoning as string | undefined);
173
+ if (reasoning) {
174
+ yield { type: "reasoning", reasoning, id: responseId };
175
+ }
176
+ continue;
177
+ }
178
+
179
+ if (partType === "tool-call") {
180
+ const toolCallId =
181
+ (part.toolCallId as string | undefined) ??
182
+ (part.id as string | undefined);
183
+ const toolName =
184
+ (part.toolName as string | undefined) ??
185
+ (part.name as string | undefined);
186
+ const args = (part.args as Record<string, unknown> | undefined) ?? {};
187
+
188
+ yield {
189
+ type: "tool_calls",
190
+ id: responseId,
191
+ tool_call: {
192
+ call_id: toolCallId,
193
+ function: {
194
+ name: toolName,
195
+ arguments: args,
196
+ },
197
+ },
198
+ };
199
+ continue;
200
+ }
201
+
202
+ if (partType === "error") {
203
+ const message =
204
+ (part.error as Error | undefined)?.message ??
205
+ "Bedrock stream failed";
206
+ throw new Error(message);
207
+ }
208
+
209
+ if (partType === "finish") {
210
+ const usage =
211
+ (part.usage as Record<string, unknown> | undefined) ?? {};
212
+ const inputTokens = numberOrZero(usage.inputTokens);
213
+ const outputTokens = numberOrZero(usage.outputTokens);
214
+ const thoughtsTokenCount = numberOrZero(usage.reasoningTokens);
215
+ const cacheReadTokens = numberOrZero(usage.cachedInputTokens);
216
+
217
+ yield {
218
+ type: "usage",
219
+ inputTokens: inputTokens - cacheReadTokens,
220
+ outputTokens,
221
+ thoughtsTokenCount,
222
+ cacheReadTokens,
223
+ totalCost: this.calculateCost(
224
+ inputTokens,
225
+ outputTokens,
226
+ cacheReadTokens,
227
+ ),
228
+ id: responseId,
229
+ };
230
+ usageEmitted = true;
231
+ }
232
+ }
233
+ } else if (stream.textStream) {
234
+ for await (const text of stream.textStream) {
235
+ yield { type: "text", text, id: responseId };
236
+ }
237
+ }
238
+
239
+ if (!usageEmitted && stream.usage) {
240
+ const usage = await stream.usage;
241
+ const inputTokens = numberOrZero(usage.inputTokens);
242
+ const outputTokens = numberOrZero(usage.outputTokens);
243
+ const thoughtsTokenCount = numberOrZero(usage.reasoningTokens);
244
+ const cacheReadTokens = numberOrZero(usage.cachedInputTokens);
245
+
246
+ yield {
247
+ type: "usage",
248
+ inputTokens: inputTokens - cacheReadTokens,
249
+ outputTokens,
250
+ thoughtsTokenCount,
251
+ cacheReadTokens,
252
+ totalCost: this.calculateCost(
253
+ inputTokens,
254
+ outputTokens,
255
+ cacheReadTokens,
256
+ ),
257
+ id: responseId,
258
+ };
259
+ }
260
+
261
+ yield { type: "done", success: true, id: responseId };
262
+ }
263
+ }
264
+
265
+ export function createBedrockHandler(config: ProviderConfig): BedrockHandler {
266
+ return new BedrockHandler(config);
267
+ }
268
+
269
+ function numberOrZero(value: unknown): number {
270
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
271
+ }
272
+
273
+ function toAiSdkTools(
274
+ tools: ToolDefinition[] | undefined,
275
+ ): Record<string, unknown> | undefined {
276
+ if (!tools || tools.length === 0) {
277
+ return undefined;
278
+ }
279
+
280
+ // We keep the same schema shape used by Anthropic conversion.
281
+ const anthropicTools = convertToolsToAnthropic(tools);
282
+ return Object.fromEntries(
283
+ anthropicTools.map((tool) => [
284
+ tool.name,
285
+ {
286
+ description: tool.description,
287
+ inputSchema: tool.input_schema,
288
+ },
289
+ ]),
290
+ );
291
+ }
292
+
293
+ function toModelMessages(
294
+ systemPrompt: string,
295
+ messages: Message[],
296
+ ): ModelMessage[] {
297
+ const result: ModelMessage[] = [{ role: "system", content: systemPrompt }];
298
+ const toolNamesById = new Map<string, string>();
299
+
300
+ for (const message of messages) {
301
+ if (typeof message.content === "string") {
302
+ result.push({ role: message.role, content: message.content });
303
+ continue;
304
+ }
305
+
306
+ if (message.role === "assistant") {
307
+ const parts: ModelMessagePart[] = [];
308
+ for (const block of message.content) {
309
+ if (block.type === "text") {
310
+ parts.push({ type: "text", text: block.text });
311
+ continue;
312
+ }
313
+
314
+ if (block.type === "tool_use") {
315
+ toolNamesById.set(block.id, block.name);
316
+ parts.push({
317
+ type: "tool-call",
318
+ toolCallId: block.id,
319
+ toolName: block.name,
320
+ args: block.input,
321
+ });
322
+ }
323
+ }
324
+
325
+ if (parts.length > 0) {
326
+ result.push({ role: "assistant", content: parts });
327
+ }
328
+ continue;
329
+ }
330
+
331
+ // User message (can include text/image/tool_result blocks)
332
+ const userParts: ModelMessagePart[] = [];
333
+
334
+ for (const block of message.content) {
335
+ if (block.type === "text") {
336
+ userParts.push({ type: "text", text: block.text });
337
+ continue;
338
+ }
339
+
340
+ if (block.type === "image") {
341
+ userParts.push({
342
+ type: "image",
343
+ image: Buffer.from(block.data, "base64"),
344
+ mediaType: block.mediaType,
345
+ });
346
+ continue;
347
+ }
348
+
349
+ if (block.type === "tool_result") {
350
+ if (userParts.length > 0) {
351
+ result.push({
352
+ role: "user",
353
+ content: userParts.splice(0, userParts.length),
354
+ });
355
+ }
356
+
357
+ result.push({
358
+ role: "tool",
359
+ content: [
360
+ {
361
+ type: "tool-result",
362
+ toolCallId: block.tool_use_id,
363
+ toolName: toolNamesById.get(block.tool_use_id) ?? "tool",
364
+ output: serializeToolResult(block.content),
365
+ isError: block.is_error ?? false,
366
+ },
367
+ ],
368
+ });
369
+ }
370
+ }
371
+
372
+ if (userParts.length > 0) {
373
+ result.push({ role: "user", content: userParts });
374
+ }
375
+ }
376
+
377
+ return result;
378
+ }
379
+
380
+ function serializeToolResult(content: Message["content"] | string): string {
381
+ if (typeof content === "string") {
382
+ return content;
383
+ }
384
+
385
+ try {
386
+ return JSON.stringify(content);
387
+ } catch {
388
+ return String(content);
389
+ }
390
+ }