alchemy 0.1.1 → 0.1.3

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 (54) hide show
  1. package/README.md +324 -0
  2. package/package.json +15 -8
  3. package/src/$.ts +36 -0
  4. package/src/agent/ai.ts +215 -0
  5. package/src/{components/agent → agent}/anthropic.ts +6 -2
  6. package/src/agent/dependencies.ts +62 -0
  7. package/src/agent/file-context.ts +8 -0
  8. package/src/agent/index.ts +9 -0
  9. package/src/agent/model.ts +39 -0
  10. package/src/agent/module.ts +418 -0
  11. package/src/{components/agent → agent}/openai.ts +6 -0
  12. package/src/agent/program.md +38 -0
  13. package/src/agent/program.ts +37 -0
  14. package/src/agent/prompts.ts +20 -0
  15. package/src/alchemize.ts +45 -14
  16. package/src/apply.ts +25 -10
  17. package/src/{components/aws → aws}/bucket.ts +2 -2
  18. package/src/{components/aws → aws}/function.ts +2 -2
  19. package/src/{components/aws → aws}/policy.ts +2 -2
  20. package/src/{components/aws → aws}/queue.ts +1 -1
  21. package/src/{components/aws → aws}/role.ts +2 -2
  22. package/src/{components/aws → aws}/table.ts +2 -2
  23. package/src/destroy.ts +36 -12
  24. package/src/{components/esbuild.ts → esbuild.ts} +1 -1
  25. package/src/{components/fs.ts → fs.ts} +20 -18
  26. package/src/global.ts +7 -14
  27. package/src/index.ts +11 -2
  28. package/src/input.ts +1 -1
  29. package/src/markdown/design.ts +125 -0
  30. package/src/markdown/extract.ts +9 -0
  31. package/src/markdown/index.ts +3 -0
  32. package/src/{components/agent → markdown}/requirements.ts +53 -29
  33. package/src/output.ts +6 -0
  34. package/src/resource.ts +208 -75
  35. package/src/scope.ts +56 -0
  36. package/src/slug.ts +3 -0
  37. package/src/state.ts +66 -38
  38. package/src/typescript/check-omission.ts +47 -0
  39. package/src/typescript/debug-type-errors.ts +42 -0
  40. package/src/typescript/extract.ts +9 -0
  41. package/src/{components/agent → typescript}/index.ts +3 -1
  42. package/src/typescript/install-packages.ts +50 -0
  43. package/src/{components/agent → typescript}/package.ts +44 -27
  44. package/src/typescript/repair-omissions.ts +31 -0
  45. package/src/typescript/repair.ts +45 -0
  46. package/src/{components/agent → typescript}/tsconfig.ts +37 -17
  47. package/src/typescript/typescript.ts +217 -0
  48. package/src/typescript/validate.ts +73 -0
  49. package/test/scope.test.ts +67 -0
  50. package/src/components/agent/agent.ts +0 -45
  51. package/src/components/agent/model.ts +0 -22
  52. package/src/components/agent/typescript.ts +0 -177
  53. /package/src/{components/agent → agent}/scrape.ts +0 -0
  54. /package/src/{components/aws → aws}/index.ts +0 -0
package/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # Alchemy
2
+
3
+ Alchemy is a JS-native, embeddable library for the materialization of resource graphs. From Infrastructure-as-Code (IaC) to code generation, Alchemy provides the fundamental building blocks for modeling resources that are Created, Updated and Deleted automatically.
4
+
5
+ Unlike similar tools like Pulumi, Terraform, and CloudFormation, Alchemy is implemented in pure ESM-native TypeScript with zero dependencies. It can run in any JavaScript runtime, including the browser.
6
+
7
+ [![Demo](./alchemy.gif)](./alchemy.gif)
8
+
9
+ # Features
10
+
11
+ - **JS-native** - no second language, toolchains, dependencies, processes, services, etc. to lug around.
12
+ - **ESM-native** - built exclusively on ESM, with a slight preference for modern JS runtimes like Bun.
13
+ - **Embeddable** - runs in any JavaScript/TypeScript environment, including the browser!
14
+ - **Extensible** - implement your own resources with a simple function.
15
+ - **AI-first** - alchemy actively encourages you to use LLMs to create/copy/fork/modify resources to fit your needs. No more waiting around for a provider to be implemented, just do it yourself in a few minutes.
16
+ - **No dependencies** - the `alchemy` core package has 0 required dependencies.
17
+ - **No service** - state files are stored locally in your project and can be easily inspected, modified, checked into your repo, etc.
18
+ - **No strong opinions** - structure your codebase however you want, store state anywhere - we don't care!
19
+
20
+ # Examples
21
+
22
+ - LLM-based code generation of all 200+ AWS CloudFormation resources: [examples/generate-aws-cfn/gen.ts](./examples/generate-aws-cfn/gen.ts)
23
+ - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/deploy-aws/app.ts](./examples/deploy-aws/)
24
+ - Generate a TODO application from a Markdown document: [examples/markdown-program/index.md](./examples/markdown-program/)
25
+
26
+ # Getting Started
27
+
28
+ An alchemy "app" (if you want to call it that) is just an ordinary TypeScript or JavaScript script. Once you've installed the `alchemy` package, you can start using it however you want.
29
+
30
+ ```bash
31
+ # I recommend bun, but you can use any JavaScript runtime.
32
+ bun add alchemy
33
+ ```
34
+
35
+ Usually, you'll want to create a script where you'll define your resources.
36
+
37
+ ```ts
38
+ import { Role } from "alchemy/aws";
39
+
40
+ const role = new Role("my-role", {
41
+ name: "my-role",
42
+ //..
43
+ });
44
+ ```
45
+
46
+ Then, call `alchemize` at the end to trigger the Create/Update/Delete lifecycle.
47
+
48
+ ```ts
49
+ await alchemize();
50
+ ```
51
+
52
+ Finally, run your script.
53
+
54
+ ```sh
55
+ bun ./my-app.ts
56
+ ```
57
+
58
+ You'll notice some files show up in your repo:
59
+
60
+ ```
61
+ .alchemy/
62
+ - sam/
63
+ - my-role.json
64
+ ```
65
+
66
+ Go ahead, click on one and take a look. Here's how my role looks:
67
+
68
+ ```jsonc
69
+ {
70
+ "provider": "iam::Role",
71
+ "data": {},
72
+ "deps": [],
73
+ "status": "updated",
74
+ "output": {
75
+ "roleName": "alchemy-api-lambda-role"
76
+ // ..
77
+ },
78
+ "inputs": [
79
+ {
80
+ "roleName": "alchemy-api-lambda-role",
81
+ "assumeRolePolicy": {
82
+ "Version": "2012-10-17"
83
+ // ..
84
+ }
85
+ }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ > [!TIP]
91
+ > Alchemy goes to great effort to be fully transparent. State is just a JSON file, nothing more. You can inspect it, modify it, commit it to your repo, etc.
92
+
93
+ ## `alchemize`
94
+
95
+ Calling `alchemize` will create new resources, update modified resources and then delete "orphaned" resources at the end.
96
+
97
+ This is equivalent to `terraform apply`, `pulumi up`, `cdk deploy`, etc. except really fast and in your control.
98
+
99
+ ## Creating a Resource Provider
100
+
101
+ Adding new resources is the whole point of Alchemy, and is therefore very simple.
102
+
103
+ A Resource provider is just a function with a globally unique name, e.g. `dynamo::Table`, and an implementation of the Create, Update, Delete lifecycle operations.
104
+
105
+ E.g. below we show what a simple `dynamo::Table` provider might look like.
106
+
107
+ > [!NOTE]
108
+ > See [table.ts](./alchemy/src/components/aws/table.ts) for the full implementation.
109
+
110
+ ```ts
111
+ interface TableInputs {
112
+ name: string;
113
+ //..
114
+ }
115
+ interface TableOutput {
116
+ tableArn: string;
117
+ }
118
+ class Table extends Resource(
119
+ "dynamo::Table",
120
+ async (ctx: Context<TableOutput>, inputs: TableInputs) => {
121
+ if (ctx.event === "create") {
122
+ // create logic
123
+ } else if (ctx.event === "update") {
124
+ // update logic
125
+ } else if (ctx.event === "delete") {
126
+ // delete logic
127
+ }
128
+ // ..
129
+ return output;
130
+ }
131
+ ) {}
132
+ ```
133
+
134
+ > [!TIP]
135
+ > Use Cursor or an LLM like Claude/OpenAI to generate the implementation of your resource. I think you'll be pleasantly surprised at how well it works, especially if you provide the API reference docs in your context.
136
+
137
+ That's it! Now you can instantiate tables in your app.
138
+
139
+ ```ts
140
+ const table = new Table("items", {
141
+ name: "items",
142
+ //..
143
+ });
144
+
145
+ table.tableArn; // Output<string>
146
+ ```
147
+
148
+ You may have noticed the odd pattern of extending the result of a function call, `extends Resource(..)`. This is called the "mix-in" pattern and is optional. You are free to just use a `const` instead:
149
+
150
+ ```ts
151
+ const Table = Resource("dynamo::Table", async (ctx, inputs) => {
152
+ //..
153
+ });
154
+
155
+ const table = new Table("items", {
156
+ name: "items",
157
+ //..
158
+ });
159
+ ```
160
+
161
+ > [!TIP]
162
+ > I recommend using a class so each resource has a type, e.g. `Table`, and a place to add helper/utility methods. Totally optional, though. Knock yourself out.
163
+
164
+ ## Lazy Outputs
165
+
166
+ A Resource is evaluated lazily (not when you call `new`), so you can't immediately access the value of a property like `tableArn` or `functionArn`. Instead you reference properties using the `Output<T>` interface.
167
+
168
+ ```ts
169
+ const table = new Table("my-table", { .. });
170
+
171
+ const tableArn = table.tableArn; // Output<string>
172
+ ```
173
+
174
+ Outputs can be chained explicitly and implicitly:
175
+
176
+ 1. _Explicitly_ with `.apply`
177
+
178
+ ```ts
179
+ const tableArn: Output<string> = table.tableArn.apply((arn: string) =>
180
+ // do something with the arn string value
181
+ arn.replace("table", "arn:aws:dynamodb:")
182
+ );
183
+ ```
184
+
185
+ 2. _Implicitly_ chained
186
+
187
+ ```ts
188
+ const tableArn = table.attributeDefinitions[0].attributeName; // Output<string>
189
+ // equivalent to:
190
+ // table.apply(t => t.attributeDefinitions[0].attributeName);
191
+ ```
192
+
193
+ > [!NOTE]
194
+ > Output is inspired by Pulumi, with a little extra added sugar.
195
+
196
+ ## `apply` and `destroy`
197
+
198
+ Calling `alchemize` is optional. Any object in your graph (`Resource` or `Output<T>`) can be "applied" or "destroyed" individually and programmatically.
199
+
200
+ Say, you've got some two resources, a `Role` and a `Function`.
201
+
202
+ ```ts
203
+ const role = new Role("my-role", {
204
+ name: "my-role",
205
+ //..
206
+ });
207
+
208
+ const func = new Function("my-function", {
209
+ name: "my-function",
210
+ role: role.roleArn,
211
+ //..
212
+ });
213
+ ```
214
+
215
+ Each of these Resources is known as a "sub-graph".
216
+
217
+ In this case we have `Role` (a 1-node graph, `Role`), and `Function` (a 2-node graph, `Role → Function`).
218
+
219
+ Each sub-graph can be "applied" or "destroyed" individually using the `apply` and `destroy` functions:
220
+
221
+ ```ts
222
+ import { apply, destroy } from "alchemy";
223
+
224
+ // will create Role and then Function (in that order)
225
+ const { functionArn } = await apply(func);
226
+
227
+ // you can destroy it right after if you want ☠️
228
+ await destroy(func); // will delete just the Function
229
+
230
+ // destroy deletes the resource and any downstream dependencies
231
+ // so, if you want to delete Role AND Function, you should call destroy(role)
232
+ await destroy(role); // will delete Role and then Function
233
+ ```
234
+
235
+ ## Destroying the app
236
+
237
+ To destroy the whole app (aka. the whole graph), you can call `alchemize` with the `mode: "destroy"` option. This will delete all resources in the specified or default stage.
238
+
239
+ ```ts
240
+ await alchemize({ mode: "destroy", stage: <optional> });
241
+ ```
242
+
243
+ > [!TIP]
244
+ > Alchemy is designed to have the minimum number of opinions as possible. This "embeddable" design is so that you can implement your own tools around Alchemy, e.g. a CLI or UI, instead of being stuck with a specific tool.
245
+ >
246
+ > ```ts
247
+ > await alchemize({
248
+ > // decide the mode/stage however you want, e.g. a CLI parser
249
+ > mode: process.argv[2] === "destroy" ? "destroy" : "up",
250
+ > stage: process.argv[3],
251
+ > });
252
+ > ```
253
+
254
+ ## "Stage" and State
255
+
256
+ > [!NOTE]
257
+ > Stage is inspired by [SST](https://sst.dev)'s stage concept.
258
+
259
+ Alchemy supports a "stage" concept to help isolate different environments from each other. E.g. a `"user"` or `"dev"` or `"prod"` stage.
260
+
261
+ By default, the stage is assumed to be your user name (a sensible default for local development).
262
+
263
+ To override the stage, you have three options:
264
+
265
+ 1. Pass the `stage` option to `alchemize`/`apply`/`destroy` (recommended)
266
+
267
+ ```ts
268
+ // alchemize the entire app
269
+ await alchemize({ stage: "production" });
270
+
271
+ // apply a single resource
272
+ await apply(func, { stage: "production" });
273
+ ```
274
+
275
+ 2. Config in `./alchemy.ts` (up to you if you want to have a global config). See the [alchemy.ts section](#global-values-and-the-alchemy-ts-config-file) for more details.
276
+
277
+ ```ts
278
+ export default {
279
+ defaultStage: "production",
280
+ };
281
+ ```
282
+
283
+ 3. Set the `ALCHEMY_STAGE` environment variable (not recommended, but available as an escape hatch)
284
+
285
+ ```sh
286
+ ALCHEMY_STAGE=production bun ./my-app.ts
287
+ ```
288
+
289
+ Each Resource "provider" can access the stage it's being deployed to via the `ctx.stage` property.
290
+
291
+ ```ts
292
+ class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
293
+ ctx.stage; // "production"
294
+ });
295
+ ```
296
+
297
+ > [!CAUTION]
298
+ > It is up to you to ensure that the physical names of resources don't conflict - alchemy does not (yet) offer any help or opinions here. You must decide on physical names, but you're free to add name generation logic to your resources if you so desire.
299
+ >
300
+ > ```ts
301
+ > class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
302
+ > const tableName = `${ctx.stage}-${inputs.tableName}`;
303
+ >
304
+ > // ..
305
+ > });
306
+ > ```
307
+
308
+ ## Global values and the `alchemy.ts` config file.
309
+
310
+ Alchemy looks for a `${cwd}/alchemy.ts` file and imports it if it finds it. This can be useful for emulating SST's `sst.config.ts` file as a convention for global configuration.
311
+
312
+ It supports overriding the `defaultStage` (instead of defaulting to your username) and providing a custom `stateStore` (instead of writing to the local file system).
313
+
314
+ ```ts
315
+ import type { Config } from "alchemy";
316
+
317
+ export default {
318
+ defaultStage: "dev",
319
+ stateStore: myCustomStateStore,
320
+ } satisfies Config;
321
+ ```
322
+
323
+ > [!NOTE]
324
+ > See [global.ts](./alchemy/src/global.ts).
package/package.json CHANGED
@@ -1,26 +1,33 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
+ "scripts": {
7
+ "publish": "cp ../README.md ./README.md && npm publish && rm ./README.md"
8
+ },
6
9
  "exports": {
7
10
  ".": "./src/index.ts",
8
- "./agent": "./src/components/agent/index.ts",
9
- "./fs": "./src/components/fs/index.ts",
10
- "./aws": "./src/components/aws/index.ts",
11
- "./esbuild": "./src/components/esbuild.ts"
11
+ "./agent": "./src/agent/index.ts",
12
+ "./aws": "./src/aws/index.ts",
13
+ "./esbuild": "./src/esbuild.ts",
14
+ "./markdown": "./src/markdown/index.ts",
15
+ "./fs": "./src/fs.ts",
16
+ "./typescript": "./src/typescript/index.ts"
12
17
  },
13
18
  "devDependencies": {
14
- "@types/bun": "latest"
19
+ "@types/bun": "latest",
20
+ "@types/diff": "^5.0.0"
15
21
  },
16
22
  "peerDependencies": {
17
- "@ai-sdk/openai": "^1.0.0",
23
+ "@ai-sdk/openai": "^1.1.9",
18
24
  "@aws-sdk/client-dynamodb": "^3.0.0",
19
25
  "@aws-sdk/client-iam": "^3.0.0",
20
26
  "@aws-sdk/client-lambda": "^3.0.0",
21
27
  "@aws-sdk/client-s3": "^3.0.0",
22
28
  "@aws-sdk/client-sqs": "^3.0.1",
23
- "ai": "^4.0.0",
29
+ "ai": "^4.1.16",
30
+ "diff": "^7.0.0",
24
31
  "jszip": "^3.0.0",
25
32
  "esbuild": "^0.24.2",
26
33
  "turndown": "^7.2.0",
package/src/$.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Input } from "./input";
2
+ import { type Output, isOutput } from "./output";
3
+
4
+ type Primitive = string | number | boolean;
5
+ type _Input = Primitive | Output<Primitive>;
6
+
7
+ export function $(
8
+ template: TemplateStringsArray,
9
+ ...args: _Input[]
10
+ ): Input<string> {
11
+ const inputs = args.reduce<Output<Primitive[]> | Primitive[]>((acc, arg) => {
12
+ if (isOutput(arg)) {
13
+ if (arg === null) {
14
+ return acc;
15
+ } else {
16
+ return arg.apply((arg) => [...acc, arg]);
17
+ }
18
+ } else if (isOutput(acc)) {
19
+ return acc.apply((acc) => [...acc, arg]);
20
+ } else {
21
+ return [...acc, arg];
22
+ }
23
+ }, null as any);
24
+
25
+ function interpolate(inputs: Primitive[]) {
26
+ return template.reduce((acc, str, i) => {
27
+ return acc + str + (inputs[i] ?? "");
28
+ }, "");
29
+ }
30
+
31
+ if (isOutput(inputs)) {
32
+ return inputs.apply(interpolate);
33
+ } else {
34
+ return interpolate(inputs);
35
+ }
36
+ }
@@ -0,0 +1,215 @@
1
+ import {
2
+ type GenerateObjectResult,
3
+ type LanguageModelV1,
4
+ generateObject as _generateObject,
5
+ generateText as _generateText,
6
+ } from "ai";
7
+ import type { z } from "zod";
8
+ import { isOpenAIModel } from "./openai";
9
+
10
+ // OpenAI error types
11
+ interface OpenAIError extends Error {
12
+ status?: number;
13
+ code?: string;
14
+ type?: string;
15
+ }
16
+
17
+ /**
18
+ * Rate-limited wrapper around generateText
19
+ */
20
+ export const generateText: typeof _generateText = async (options) => {
21
+ return withRateLimit(() => _generateText(options), options);
22
+ };
23
+
24
+ /**
25
+ * Rate-limited wrapper around generateObject
26
+ */
27
+ export const generateObject: typeof _generateObject = (async <OBJECT>(
28
+ options: Parameters<typeof _generateObject>[0] & {
29
+ schema: z.Schema<OBJECT>;
30
+ },
31
+ ): Promise<GenerateObjectResult<OBJECT>> => {
32
+ return withRateLimit(() => _generateObject<OBJECT>(options as any), options);
33
+ }) as any;
34
+
35
+ /**
36
+ * A simple async semaphore implementation for rate limiting
37
+ */
38
+ class AsyncSemaphore {
39
+ private permits: number;
40
+ private queue: Array<() => void> = [];
41
+
42
+ constructor(permits: number) {
43
+ this.permits = permits;
44
+ }
45
+
46
+ async acquire(): Promise<void> {
47
+ if (this.permits > 0) {
48
+ this.permits--;
49
+ return;
50
+ }
51
+
52
+ return new Promise<void>((resolve) => {
53
+ this.queue.push(resolve);
54
+ });
55
+ }
56
+
57
+ release(): void {
58
+ if (this.queue.length > 0) {
59
+ const next = this.queue.shift();
60
+ next?.();
61
+ } else {
62
+ this.permits++;
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Configuration for rate limiting and retries
69
+ */
70
+ export interface RateLimitConfig {
71
+ /**
72
+ * Maximum number of concurrent requests
73
+ * @default 3
74
+ */
75
+ maxConcurrent?: number;
76
+
77
+ /**
78
+ * Base delay for exponential backoff (in milliseconds)
79
+ * @default 1000
80
+ */
81
+ baseDelay?: number;
82
+
83
+ /**
84
+ * Maximum number of retries
85
+ * @default 3
86
+ */
87
+ maxRetries?: number;
88
+
89
+ /**
90
+ * Maximum delay for exponential backoff (in milliseconds)
91
+ * @default 30000
92
+ */
93
+ maxDelay?: number;
94
+
95
+ /**
96
+ * Jitter factor for randomizing delays (between 0 and 1)
97
+ * @default 0.1
98
+ */
99
+ jitter?: number;
100
+ }
101
+
102
+ // Global semaphore instances per client configuration
103
+ const defaultConfig: Required<RateLimitConfig> = {
104
+ maxConcurrent: 3,
105
+ baseDelay: 1000,
106
+ maxRetries: 10,
107
+ maxDelay: 30000,
108
+ jitter: 0.1,
109
+ };
110
+
111
+ // WeakMap to store semaphores per client
112
+ const clientSemaphores = new Map<"openai" | "anthropic", AsyncSemaphore>();
113
+
114
+ /**
115
+ * Gets or creates a semaphore for a specific client
116
+ */
117
+ function getSemaphore(client: "openai" | "anthropic"): AsyncSemaphore {
118
+ let semaphore = clientSemaphores.get(client);
119
+ if (!semaphore) {
120
+ semaphore = new AsyncSemaphore(defaultConfig.maxConcurrent);
121
+ clientSemaphores.set(client, semaphore);
122
+ }
123
+ return semaphore;
124
+ }
125
+
126
+ /**
127
+ * Calculates the delay for exponential backoff with jitter
128
+ */
129
+ function calculateBackoffDelay(
130
+ attempt: number,
131
+ { baseDelay, maxDelay, jitter }: Required<RateLimitConfig>,
132
+ ): number {
133
+ const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
134
+ const jitterAmount = exponentialDelay * jitter;
135
+ return exponentialDelay + (Math.random() * 2 - 1) * jitterAmount;
136
+ }
137
+
138
+ /**
139
+ * Checks if an error is a rate limit related error
140
+ */
141
+ function isRateLimitError(error: any): boolean {
142
+ if (error.url === "https://api.anthropic.com/v1/messages") {
143
+ return error.status === 429;
144
+ }
145
+
146
+ const openAIError = error as OpenAIError;
147
+
148
+ // Check for specific OpenAI rate limit error codes
149
+ if (openAIError.code) {
150
+ return [
151
+ "rate_limit_exceeded",
152
+ "insufficient_quota",
153
+ "tokens_quota_exceeded",
154
+ "requests_quota_exceeded",
155
+ ].includes(openAIError.code);
156
+ }
157
+
158
+ // Check for HTTP 429 status
159
+ if (openAIError.status === 429) return true;
160
+
161
+ // Check error types
162
+ if (openAIError.type) {
163
+ return ["tokens", "requests", "rate_limit", "capacity"].includes(
164
+ openAIError.type.toLowerCase(),
165
+ );
166
+ }
167
+
168
+ // Fallback to message content check
169
+ return (
170
+ openAIError.message.toLowerCase().includes("rate") ||
171
+ openAIError.message.toLowerCase().includes("quota") ||
172
+ openAIError.message.toLowerCase().includes("capacity") ||
173
+ openAIError.message.toLowerCase().includes("throttle")
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Wraps an async function with rate limiting and exponential backoff
179
+ */
180
+ async function withRateLimit<T>(
181
+ fn: () => Promise<T>,
182
+ options: { model: LanguageModelV1 },
183
+ config: RateLimitConfig = {},
184
+ ): Promise<T> {
185
+ const finalConfig = { ...defaultConfig, ...config };
186
+ const client = options.model;
187
+ const semaphore = getSemaphore(
188
+ isOpenAIModel(client.modelId) ? "openai" : "anthropic",
189
+ );
190
+ for (let attempt = 0; attempt <= finalConfig.maxRetries; attempt++) {
191
+ try {
192
+ await semaphore.acquire();
193
+ const result = await fn();
194
+ return result;
195
+ } catch (error) {
196
+ if (attempt === finalConfig.maxRetries) {
197
+ throw error;
198
+ }
199
+
200
+ if (!isRateLimitError(error)) {
201
+ throw error;
202
+ }
203
+
204
+ const delay = calculateBackoffDelay(attempt, finalConfig);
205
+ console.log(
206
+ `Rate limit error, retrying... (attempt ${attempt + 1}/${finalConfig.maxRetries + 1}, waiting ${Math.round(delay)}ms)`,
207
+ );
208
+ await new Promise((resolve) => setTimeout(resolve, delay));
209
+ } finally {
210
+ semaphore.release();
211
+ }
212
+ }
213
+
214
+ throw new Error("Unexpected: Should not reach this point");
215
+ }
@@ -1,14 +1,16 @@
1
+ import { z } from "zod";
2
+
1
3
  /**
2
4
  * @see https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-table
3
5
  */
4
6
  export const AnthropicModels = [
5
7
  // Claude 3.5 Sonnet models
6
- "claude-3-5-sonnet",
8
+ "claude-3-5-sonnet-latest",
7
9
  "claude-3-5-sonnet-20241022",
8
10
  "claude-3-5-sonnet-20240620",
9
11
 
10
12
  // Claude 3.5 Haiku models
11
- "claude-3-5-haiku",
13
+ "claude-3-5-haiku-latest",
12
14
  "claude-3-5-haiku-20241022",
13
15
 
14
16
  // Claude 3 models
@@ -20,6 +22,8 @@ export const AnthropicModels = [
20
22
  "claude-3-haiku-20240307",
21
23
  ] as const;
22
24
 
25
+ export const AnthropicModel = z.enum(AnthropicModels);
26
+
23
27
  export type AnthropicModelId = (typeof AnthropicModels)[number] | (string & {});
24
28
 
25
29
  export function isAnthropicModel(model: string): model is AnthropicModelId {
@@ -0,0 +1,62 @@
1
+ import type { CoreMessage } from "ai";
2
+ import type { FileContext } from "./file-context";
3
+
4
+ export function dependenciesAsMessages(
5
+ dependencies: FileContext[] | undefined,
6
+ ): CoreMessage[] {
7
+ if (!dependencies?.length) return [];
8
+
9
+ const fileTree = generateFileTree(dependencies);
10
+
11
+ return [
12
+ {
13
+ role: "user" as const,
14
+ content: [
15
+ `Here are some relevant upstream files that you may need to reference.`,
16
+ `Existing file structure:`,
17
+ fileTree,
18
+ `File contents:`,
19
+ dependencies
20
+ .flatMap((dep) =>
21
+ dep.content ? [`// ${dep.path}\n${dep.content}`] : [],
22
+ )
23
+ .join("\n\n"),
24
+ ].join("\n"),
25
+ },
26
+ {
27
+ role: "assistant" as const,
28
+ content: "Thanks, I'll refer to those where relevant.",
29
+ },
30
+ ];
31
+ }
32
+
33
+ function generateFileTree(dependencies: FileContext[]): string {
34
+ const tree: { [key: string]: boolean } = {};
35
+
36
+ // Sort dependencies by path for consistent ordering
37
+ dependencies.sort((a, b) => a.path.localeCompare(b.path));
38
+
39
+ for (const dep of dependencies) {
40
+ const parts = dep.path.split("/");
41
+ let currentPath = "";
42
+
43
+ for (let i = 0; i < parts.length; i++) {
44
+ const part = parts[i];
45
+ const newPath = currentPath ? `${currentPath}/${part}` : part;
46
+ tree[newPath] = true;
47
+ currentPath = newPath;
48
+ }
49
+ }
50
+
51
+ const paths = Object.keys(tree);
52
+ let result = "";
53
+
54
+ for (const path of paths) {
55
+ const depth = path.split("/").length - 1;
56
+ const prefix = " ".repeat(depth);
57
+ const name = path.split("/").pop()!;
58
+ result += `${prefix}${depth > 0 ? "└─ " : ""}${name}\n`;
59
+ }
60
+
61
+ return result;
62
+ }