alchemy 0.1.2 → 0.1.4
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 +8 -39
- package/package.json +12 -8
- package/src/$.ts +36 -0
- package/src/agent/ai.ts +215 -0
- package/src/{components/agent → agent}/anthropic.ts +6 -2
- package/src/agent/dependencies.ts +62 -0
- package/src/agent/file-context.ts +8 -0
- package/src/agent/index.ts +9 -0
- package/src/agent/model.ts +39 -0
- package/src/agent/module.ts +418 -0
- package/src/{components/agent → agent}/openai.ts +6 -0
- package/src/agent/program.md +38 -0
- package/src/agent/program.ts +37 -0
- package/src/agent/prompts.ts +20 -0
- package/src/alchemize.ts +37 -14
- package/src/apply.ts +25 -10
- package/src/{components/aws → aws}/bucket.ts +2 -2
- package/src/{components/aws → aws}/function.ts +2 -2
- package/src/{components/aws → aws}/policy.ts +2 -2
- package/src/{components/aws → aws}/queue.ts +1 -1
- package/src/{components/aws → aws}/role.ts +2 -2
- package/src/{components/aws → aws}/table.ts +2 -2
- package/src/destroy.ts +36 -12
- package/src/{components/esbuild.ts → esbuild.ts} +1 -1
- package/src/{components/fs.ts → fs.ts} +20 -18
- package/src/global.ts +7 -14
- package/src/index.ts +11 -2
- package/src/input.ts +1 -1
- package/src/markdown/design.ts +125 -0
- package/src/markdown/extract.ts +9 -0
- package/src/markdown/index.ts +3 -0
- package/src/{components/agent → markdown}/requirements.ts +53 -29
- package/src/output.ts +6 -0
- package/src/resource.ts +208 -75
- package/src/scope.ts +56 -0
- package/src/slug.ts +3 -0
- package/src/state.ts +66 -38
- package/src/typescript/check-omission.ts +47 -0
- package/src/typescript/debug-type-errors.ts +42 -0
- package/src/typescript/extract.ts +9 -0
- package/src/{components/agent → typescript}/index.ts +3 -1
- package/src/typescript/install-packages.ts +50 -0
- package/src/{components/agent → typescript}/package.ts +44 -27
- package/src/typescript/repair-omissions.ts +31 -0
- package/src/typescript/repair.ts +45 -0
- package/src/{components/agent → typescript}/tsconfig.ts +37 -17
- package/src/typescript/typescript.ts +217 -0
- package/src/typescript/validate.ts +73 -0
- package/test/scope.test.ts +67 -0
- package/src/components/agent/agent.ts +0 -45
- package/src/components/agent/model.ts +0 -22
- package/src/components/agent/typescript.ts +0 -177
- /package/src/{components/agent → agent}/scrape.ts +0 -0
- /package/src/{components/aws → aws}/index.ts +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# Alchemy
|
|
2
2
|
|
|
3
|
-
Alchemy is a JS-native, embeddable Infrastructure
|
|
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
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
AI is so damn good at CRUD, so good that we no longer need to shackle ourselves to heavy toolchains like Pulumi and Terraform. All of the pre-built components in [src/components](./alchemy/src/components) were generated in less than a few minutes. Use them if you want, or create your own. It's that easy.
|
|
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.
|
|
8
6
|
|
|
9
7
|
[](./alchemy.gif)
|
|
10
8
|
|
|
@@ -19,10 +17,13 @@ AI is so damn good at CRUD, so good that we no longer need to shackle ourselves
|
|
|
19
17
|
- **No service** - state files are stored locally in your project and can be easily inspected, modified, checked into your repo, etc.
|
|
20
18
|
- **No strong opinions** - structure your codebase however you want, store state anywhere - we don't care!
|
|
21
19
|
|
|
22
|
-
#
|
|
20
|
+
# Examples
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
27
|
|
|
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.
|
|
28
29
|
|
|
@@ -321,35 +322,3 @@ export default {
|
|
|
321
322
|
|
|
322
323
|
> [!NOTE]
|
|
323
324
|
> See [global.ts](./alchemy/src/global.ts).
|
|
324
|
-
|
|
325
|
-
## Philosophy and comparison with existing IaC frameworks
|
|
326
|
-
|
|
327
|
-
I built alchemy after years of working with every other option. IaC is non-negotiable in my opinion, and has been one of my favorite technologies as a developer.
|
|
328
|
-
|
|
329
|
-
I started with CloudFormation (since I worked at Amazon) and hated that. Fuck deploying JSON to a slow service, am I right? The CDK was a huge upgrade on that (yay for TypeScript) but it is limited by the CloudFormation service, which is slow to _change_ and slow to _use_, and has very strong opinions on how and where it should be used.
|
|
330
|
-
|
|
331
|
-
For example, the CDK is written in a "meta" language called JSII. Which is a subset of TypeScript designed to be compiled to Java, Python, etc. This means you're not free to use all TypeScript features (which are ideal for configuration as code). It's also coupled to synchronous I/O. It is damn near impossible to do anything async, making it slow and clunky. Finally, the CDK is still CJS and doesn't emit ESM. Taking a dependency on the CDK will 10x the size of your bundle and destroy your DX.
|
|
332
|
-
|
|
333
|
-
Later, I moved on to Pulumi, which was a nice change of pace since it runs locally and is much faster. You can cancel a deployment by hitting Ctrl+C (hallelujah!). It also supported more than just AWS, which is useful for managing all your resources, such as Stripe, GitHub or another provider like Cloudflare.
|
|
334
|
-
|
|
335
|
-
However, Pulumi is largely a wrapper around Terraform and relies on complicated "providers" implemented with Go, running in a separate process. It can't run anywhere (like the browser). Different languages work better/worse than others and always have "sharp edge" limitations and gotchas. Custom Resources are possible but more of an afterthought and a PITA to implement.
|
|
336
|
-
|
|
337
|
-
I've used Terraform when I've been forced into it. It's ... ok ... but I don't love it. It's a custom DSL and a heavy toolchain for what, calling a few CRUD APIs? Way overkill. Every time I think about implementing a custom resource for Terraform, I just can't bring myself to do it. Let me just write a function, please!
|
|
338
|
-
|
|
339
|
-
Lately, I've been using SST because I've been doing a ton of web development. At first, I really liked SST, especially because of its local development experience. `sst dev` gives you live deploy, a TUI multiplexer and a proxy from the cloud to your local code. This is great for building web apps in AWS.
|
|
340
|
-
|
|
341
|
-
But, as my app grew, SST's bugs ate away at me. I got blocked by broken resources that have race conditions and it was impossible to workaround. I also wanted to deploy a nested app (another `sst.config.ts`) which didn't gel well with the generated `sst-env.d.ts` files.
|
|
342
|
-
|
|
343
|
-
Again, I was let down by the complexity and opinions of my chosen IaC framework. And for what? To help me call a few CRUD APIs? Honestly, it just seems insane how far we've drifted away from simplicity in this area. This became even more apparent as I started using Cursor more and more to write code.
|
|
344
|
-
|
|
345
|
-
You see, I've found myself doing more frontend than I've ever done before, and I'm not a good frontend developer. So, I relied on Cursor to write most of the code for me and (as many others have experienced) it totally blew my mind 🤯. Cursor is just really, really, really good at TypeScript, React and Tailwind. I've built a functioning and (if i don't say so myself) good looking SPA. It's been a blast.
|
|
346
|
-
|
|
347
|
-
But, this got me thinking ... you know what else Cursor is really great at? Perhaps even better at? Interacting with CRUD APIs, that's what! While there's a ton of frontend training data for LLMs, there's just as much (if not more) training data for CRUD lifecycle operations. There's also all of Terraform, Pulumi, SST and the AWS CDK's training data. All. Of. It.
|
|
348
|
-
|
|
349
|
-
Long story short, I discovered that Cursor can pretty much one-shot the implementation of Resources. All of the resources in [src/components](./alchemy/src/components) are entirely generated on-demand (1 minute time investment, tops). When I run into a bug, I just explained it to Cursor who fixed it immediately.
|
|
350
|
-
|
|
351
|
-
This is a game changer. It means we don't need tools like Terraform to build suites of "provider" libraries for us. We just need the engine - the bit that tracks state and decides what to create/update/delete. The rest can be generated at near zero cost.
|
|
352
|
-
|
|
353
|
-
This story is ultimately why I built Alchemy - I just wanted total control and freedom over how I deploy my resources. And, I wanted it to be in TypeScript, so I can use its amazing type system. Annnnd, I wanted it to be embeddable, so I can use it anywhere, maybe even in a React app.
|
|
354
|
-
|
|
355
|
-
"Reactive IaC", anyone?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,22 +8,26 @@
|
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
|
-
"./agent": "./src/
|
|
12
|
-
"./
|
|
13
|
-
"./
|
|
14
|
-
"./
|
|
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"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
|
-
"@types/bun": "latest"
|
|
19
|
+
"@types/bun": "latest",
|
|
20
|
+
"@types/diff": "^5.0.0"
|
|
18
21
|
},
|
|
19
22
|
"peerDependencies": {
|
|
20
|
-
"@ai-sdk/openai": "^1.
|
|
23
|
+
"@ai-sdk/openai": "^1.1.9",
|
|
21
24
|
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
22
25
|
"@aws-sdk/client-iam": "^3.0.0",
|
|
23
26
|
"@aws-sdk/client-lambda": "^3.0.0",
|
|
24
27
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
25
28
|
"@aws-sdk/client-sqs": "^3.0.1",
|
|
26
|
-
"ai": "^4.
|
|
29
|
+
"ai": "^4.1.16",
|
|
30
|
+
"diff": "^7.0.0",
|
|
27
31
|
"jszip": "^3.0.0",
|
|
28
32
|
"esbuild": "^0.24.2",
|
|
29
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
|
+
}
|
package/src/agent/ai.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LanguageModelV1 } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
AnthropicModel,
|
|
5
|
+
type AnthropicModelId,
|
|
6
|
+
isAnthropicModel,
|
|
7
|
+
} from "./anthropic";
|
|
8
|
+
import { type OpenAIChatModelId, OpenAIModel, isOpenAIModel } from "./openai";
|
|
9
|
+
|
|
10
|
+
export type ModelId = OpenAIChatModelId | AnthropicModelId;
|
|
11
|
+
|
|
12
|
+
export const ModelId = z.union([OpenAIModel, AnthropicModel]);
|
|
13
|
+
|
|
14
|
+
const modelCache = new Map<ModelId, Promise<LanguageModelV1>>();
|
|
15
|
+
|
|
16
|
+
export async function resolveModel(modelId: ModelId): Promise<LanguageModelV1> {
|
|
17
|
+
const cached = modelCache.get(modelId);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return await cached;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const client = (async () => {
|
|
23
|
+
let client: LanguageModelV1;
|
|
24
|
+
if (isOpenAIModel(modelId)) {
|
|
25
|
+
const { openai } = await import("@ai-sdk/openai");
|
|
26
|
+
client = openai(modelId as OpenAIChatModelId);
|
|
27
|
+
} else if (isAnthropicModel(modelId)) {
|
|
28
|
+
const { anthropic } = await import("@ai-sdk/anthropic");
|
|
29
|
+
client = anthropic(modelId as AnthropicModelId);
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(`Unsupported model ID: ${modelId}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return client;
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
modelCache.set(modelId, client);
|
|
38
|
+
return client;
|
|
39
|
+
}
|