alchemy 0.85.0 → 0.85.2
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/bin/alchemy.js +32 -23
- package/lib/cloudflare/account-api-token.d.ts +5 -0
- package/lib/cloudflare/account-api-token.d.ts.map +1 -1
- package/lib/cloudflare/account-api-token.js +13 -2
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/ai-crawler.d.ts +27 -0
- package/lib/cloudflare/ai-crawler.d.ts.map +1 -0
- package/lib/cloudflare/ai-crawler.js +75 -0
- package/lib/cloudflare/ai-crawler.js.map +1 -0
- package/lib/cloudflare/ai-search-token.d.ts +150 -0
- package/lib/cloudflare/ai-search-token.d.ts.map +1 -0
- package/lib/cloudflare/ai-search-token.js +171 -0
- package/lib/cloudflare/ai-search-token.js.map +1 -0
- package/lib/cloudflare/ai-search.d.ts +397 -0
- package/lib/cloudflare/ai-search.d.ts.map +1 -0
- package/lib/cloudflare/ai-search.js +307 -0
- package/lib/cloudflare/ai-search.js.map +1 -0
- package/lib/cloudflare/auth.d.ts +3 -2
- package/lib/cloudflare/auth.d.ts.map +1 -1
- package/lib/cloudflare/auth.js +17 -14
- package/lib/cloudflare/auth.js.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.js +4 -2
- package/lib/cloudflare/bucket-custom-domain.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +8 -2
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/index.d.ts +3 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +5 -0
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/permission-groups.js +6 -0
- package/lib/cloudflare/permission-groups.js.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.js +11 -5
- package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
- package/lib/cloudflare/vite/vite.d.ts.map +1 -1
- package/lib/cloudflare/vite/vite.js +2 -1
- package/lib/cloudflare/vite/vite.js.map +1 -1
- package/lib/docker/api.d.ts +33 -2
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +8 -4
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/container.d.ts +1 -1
- package/lib/docker/container.d.ts.map +1 -1
- package/lib/docker/container.js +244 -15
- package/lib/docker/container.js.map +1 -1
- package/lib/scope.d.ts +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +10 -5
- package/lib/scope.js.map +1 -1
- package/lib/test/bun.d.ts +17 -0
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +2 -1
- package/lib/test/bun.js.map +1 -1
- package/lib/test/vitest.d.ts +17 -0
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +4 -1
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/snake-to-camel.d.ts +21 -0
- package/lib/util/snake-to-camel.d.ts.map +1 -0
- package/lib/util/snake-to-camel.js +34 -0
- package/lib/util/snake-to-camel.js.map +1 -0
- package/package.json +1 -1
- package/src/cloudflare/account-api-token.ts +23 -6
- package/src/cloudflare/ai-crawler.ts +89 -0
- package/src/cloudflare/ai-search-token.ts +324 -0
- package/src/cloudflare/ai-search.ts +952 -0
- package/src/cloudflare/auth.ts +17 -14
- package/src/cloudflare/bucket-custom-domain.ts +4 -1
- package/src/cloudflare/bucket.ts +9 -2
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/permission-groups.ts +6 -0
- package/src/cloudflare/sveltekit/plugin.ts +17 -8
- package/src/cloudflare/vite/vite.ts +4 -2
- package/src/docker/api.ts +47 -4
- package/src/docker/container.ts +318 -18
- package/src/scope.ts +14 -5
- package/src/test/bun.ts +26 -1
- package/src/test/vitest.ts +34 -1
- package/src/util/snake-to-camel.ts +59 -0
- package/workers/tunnel-proxy.js +1 -1
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import type { Context } from "../context.ts";
|
|
2
|
+
import { Resource } from "../resource.ts";
|
|
3
|
+
import { logger } from "../util/logger.ts";
|
|
4
|
+
import { poll } from "../util/poll.ts";
|
|
5
|
+
import { sleep } from "../util/sleep.ts";
|
|
6
|
+
import {
|
|
7
|
+
snakeToCamelObjectDeep,
|
|
8
|
+
type SnakeToCamel,
|
|
9
|
+
} from "../util/snake-to-camel.ts";
|
|
10
|
+
import { AiSearchToken } from "./ai-search-token.ts";
|
|
11
|
+
import { CloudflareApiError, isCloudflareApiError } from "./api-error.ts";
|
|
12
|
+
import {
|
|
13
|
+
extractCloudflareResult,
|
|
14
|
+
type CloudflareApiErrorPayload,
|
|
15
|
+
} from "./api-response.ts";
|
|
16
|
+
import {
|
|
17
|
+
createCloudflareApi,
|
|
18
|
+
type CloudflareApi,
|
|
19
|
+
type CloudflareApiOptions,
|
|
20
|
+
} from "./api.ts";
|
|
21
|
+
import {
|
|
22
|
+
getBucket,
|
|
23
|
+
isBucket,
|
|
24
|
+
type R2Bucket,
|
|
25
|
+
type R2BucketJurisdiction,
|
|
26
|
+
} from "./bucket.ts";
|
|
27
|
+
import { deleteIndex } from "./vectorize-index.ts";
|
|
28
|
+
|
|
29
|
+
interface BaseAiSearchProps extends CloudflareApiOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Name of the AI Search instance
|
|
32
|
+
* @default `${app}-${stage}-${id}`
|
|
33
|
+
* @minLength 1
|
|
34
|
+
* @maxLength 32
|
|
35
|
+
*/
|
|
36
|
+
name?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Data source for indexing.
|
|
40
|
+
* Can be an R2Bucket directly, an R2 source config, or a web crawler config.
|
|
41
|
+
*/
|
|
42
|
+
source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Text generation model for AI responses
|
|
46
|
+
*
|
|
47
|
+
* @default "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
|
48
|
+
*/
|
|
49
|
+
aiSearchModel?: AiSearch.Model;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Embedding model for vectorization
|
|
53
|
+
*
|
|
54
|
+
* @default "@cf/baai/bge-m3"
|
|
55
|
+
*/
|
|
56
|
+
embeddingModel?: AiSearch.EmbeddingModel;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enable chunking of source documents
|
|
60
|
+
*
|
|
61
|
+
* @default true
|
|
62
|
+
*/
|
|
63
|
+
chunk?: boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Size of each chunk (minimum 64)
|
|
67
|
+
*
|
|
68
|
+
* @default 256
|
|
69
|
+
*/
|
|
70
|
+
chunkSize?: number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Overlap between chunks (0-30)
|
|
74
|
+
*
|
|
75
|
+
* @default 10
|
|
76
|
+
*/
|
|
77
|
+
chunkOverlap?: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Maximum search results (1-50)
|
|
81
|
+
*
|
|
82
|
+
* @default 10
|
|
83
|
+
*/
|
|
84
|
+
maxNumResults?: number;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Minimum match score (0-1)
|
|
88
|
+
*
|
|
89
|
+
* @default 0.4
|
|
90
|
+
*/
|
|
91
|
+
scoreThreshold?: number;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Enable result reranking
|
|
95
|
+
*
|
|
96
|
+
* @default false
|
|
97
|
+
*/
|
|
98
|
+
reranking?: boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reranking model
|
|
102
|
+
*
|
|
103
|
+
* @default "@cf/baai/bge-reranker-base"
|
|
104
|
+
*/
|
|
105
|
+
rerankingModel?: AiSearch.RerankingModel;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Enable query rewriting for better retrieval
|
|
109
|
+
*
|
|
110
|
+
* @default false
|
|
111
|
+
*/
|
|
112
|
+
rewriteQuery?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Query rewriting model
|
|
116
|
+
*
|
|
117
|
+
* @default "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
|
118
|
+
*/
|
|
119
|
+
rewriteModel?: AiSearch.Model;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Enable similarity caching
|
|
123
|
+
*
|
|
124
|
+
* @default false
|
|
125
|
+
*/
|
|
126
|
+
cache?: boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cache similarity threshold
|
|
130
|
+
*
|
|
131
|
+
* @default "close_enough"
|
|
132
|
+
*/
|
|
133
|
+
cacheThreshold?:
|
|
134
|
+
| "super_strict_match"
|
|
135
|
+
| "close_enough"
|
|
136
|
+
| "flexible_friend"
|
|
137
|
+
| "anything_goes";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Custom metadata
|
|
141
|
+
*/
|
|
142
|
+
metadata?: Record<string, unknown>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether to index the source documents when the AI Search instance is created
|
|
146
|
+
* @default true
|
|
147
|
+
*/
|
|
148
|
+
indexOnCreate?: boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether to delete the AI Search instance when removed from Alchemy
|
|
152
|
+
* @default true
|
|
153
|
+
*/
|
|
154
|
+
delete?: boolean;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether to adopt the AI Search instance if it already exists
|
|
158
|
+
* @default false
|
|
159
|
+
*/
|
|
160
|
+
adopt?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type AiSearchProps = BaseAiSearchProps &
|
|
164
|
+
(
|
|
165
|
+
| {
|
|
166
|
+
token?: AiSearchToken;
|
|
167
|
+
}
|
|
168
|
+
| {
|
|
169
|
+
tokenId: string;
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export interface AiSearchR2Source {
|
|
174
|
+
/**
|
|
175
|
+
* Source type
|
|
176
|
+
*/
|
|
177
|
+
type: "r2";
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* R2 bucket - can be bucket name string or R2Bucket resource
|
|
181
|
+
*/
|
|
182
|
+
bucket: string | R2Bucket;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Jurisdiction for the R2 bucket
|
|
186
|
+
* @default "default"
|
|
187
|
+
*/
|
|
188
|
+
jurisdiction?: R2BucketJurisdiction;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Prefix for included items from the R2 bucket
|
|
192
|
+
*/
|
|
193
|
+
prefix?: string;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Path patterns to include in the R2 bucket (up to 10 patterns).
|
|
197
|
+
* Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
|
|
198
|
+
*/
|
|
199
|
+
includePaths?: string[];
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Path patterns to exclude from the R2 bucket (up to 10 patterns).
|
|
203
|
+
* Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
|
|
204
|
+
*/
|
|
205
|
+
excludePaths?: string[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface AiSearchWebCrawlerSource {
|
|
209
|
+
/**
|
|
210
|
+
* Source type
|
|
211
|
+
*/
|
|
212
|
+
type: "web-crawler";
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Domain to crawl. Must be a domain that is onboarded to your Cloudflare account
|
|
216
|
+
* (added as a zone with active nameservers pointing to Cloudflare).
|
|
217
|
+
*
|
|
218
|
+
* Can be provided as just the domain (e.g., "docs.example.com") or with protocol
|
|
219
|
+
* (e.g., "https://docs.example.com") - the protocol will be stripped automatically.
|
|
220
|
+
*
|
|
221
|
+
* @example "docs.example.com"
|
|
222
|
+
* @example "https://example.com" // Protocol will be stripped
|
|
223
|
+
*/
|
|
224
|
+
domain: string;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Path patterns to include in crawling (up to 10 patterns).
|
|
228
|
+
* Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
|
|
229
|
+
*/
|
|
230
|
+
includePaths?: string[];
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Path patterns to exclude from crawling (up to 10 patterns).
|
|
234
|
+
* Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
|
|
235
|
+
*/
|
|
236
|
+
excludePaths?: string[];
|
|
237
|
+
|
|
238
|
+
parseOptions?: {
|
|
239
|
+
include_headers?: Record<string, string>;
|
|
240
|
+
include_images?: boolean;
|
|
241
|
+
specific_sitemaps?: string[];
|
|
242
|
+
use_browser_rendering?: boolean;
|
|
243
|
+
};
|
|
244
|
+
parseType?: "sitemap" | "feed-rss";
|
|
245
|
+
storeOptions?: {
|
|
246
|
+
storage_id: string;
|
|
247
|
+
jurisdiction?: R2BucketJurisdiction;
|
|
248
|
+
storage_type?: "r2";
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type AiSearch = SnakeToCamel<AiSearch.ApiResponse> & {
|
|
253
|
+
/**
|
|
254
|
+
* The name of the AI Search instance (this is an alias for the `id` property)
|
|
255
|
+
*/
|
|
256
|
+
name: string;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const AiSearch = Resource(
|
|
260
|
+
"cloudflare::AiSearch",
|
|
261
|
+
async function (
|
|
262
|
+
this: Context<AiSearch>,
|
|
263
|
+
id: string,
|
|
264
|
+
props: AiSearchProps,
|
|
265
|
+
): Promise<AiSearch> {
|
|
266
|
+
const api = await createCloudflareApi(props);
|
|
267
|
+
const adopt = props.adopt ?? this.scope.adopt;
|
|
268
|
+
|
|
269
|
+
const validateBucketSource = async (
|
|
270
|
+
bucket: R2Bucket | string,
|
|
271
|
+
jurisdiction: R2BucketJurisdiction = "default",
|
|
272
|
+
) => {
|
|
273
|
+
let name: string;
|
|
274
|
+
if (typeof bucket === "string") {
|
|
275
|
+
name = bucket;
|
|
276
|
+
} else {
|
|
277
|
+
if (this.scope.local && !bucket.dev?.remote) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
[
|
|
280
|
+
`AI Search "${id}" depends on an R2Bucket that is running locally, but AI Search requires the bucket to be deployed.`,
|
|
281
|
+
"Add `dev: { remote: true }` to the R2Bucket to use it with AI Search.",
|
|
282
|
+
].join("\n"),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
name = bucket.name;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await getBucket(api, name, { jurisdiction });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${error instanceof Error ? error.message : String(error)}`,
|
|
292
|
+
{ cause: error },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const normalizeSource = async (
|
|
297
|
+
source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource,
|
|
298
|
+
): Promise<
|
|
299
|
+
(AiSearchR2Source & { bucket: string }) | AiSearchWebCrawlerSource
|
|
300
|
+
> => {
|
|
301
|
+
if (isBucket(source)) {
|
|
302
|
+
await validateBucketSource(source, source.jurisdiction);
|
|
303
|
+
return {
|
|
304
|
+
type: "r2",
|
|
305
|
+
bucket: source.name,
|
|
306
|
+
jurisdiction: source.jurisdiction,
|
|
307
|
+
};
|
|
308
|
+
} else if (source.type === "r2" && isBucket(source.bucket)) {
|
|
309
|
+
await validateBucketSource(source.bucket, source.bucket.jurisdiction);
|
|
310
|
+
return {
|
|
311
|
+
...source,
|
|
312
|
+
type: "r2",
|
|
313
|
+
bucket: source.bucket.name,
|
|
314
|
+
jurisdiction: source.bucket.jurisdiction,
|
|
315
|
+
};
|
|
316
|
+
} else if (source.type === "web-crawler") {
|
|
317
|
+
await validateWebCrawlerSourceDomain(api, source.domain);
|
|
318
|
+
return source;
|
|
319
|
+
} else {
|
|
320
|
+
await validateBucketSource(source.bucket, source.jurisdiction);
|
|
321
|
+
return source as AiSearchR2Source & { bucket: string };
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const validateTokenId = (id: string): void => {
|
|
325
|
+
if (
|
|
326
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
327
|
+
id,
|
|
328
|
+
)
|
|
329
|
+
) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Invalid token ID: "${id}"\n` +
|
|
332
|
+
"The token ID must be a valid UUID for an AI Search service token.\n" +
|
|
333
|
+
"See https://developers.cloudflare.com/ai-search/get-started/api/#2-create-a-service-api-token",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const normalizeTokenId = async (): Promise<string> => {
|
|
338
|
+
if ("tokenId" in props) {
|
|
339
|
+
validateTokenId(props.tokenId);
|
|
340
|
+
return props.tokenId;
|
|
341
|
+
} else if (props.token) {
|
|
342
|
+
validateTokenId(props.token.tokenId);
|
|
343
|
+
return props.token.tokenId;
|
|
344
|
+
} else {
|
|
345
|
+
const token = await AiSearchToken("token", {
|
|
346
|
+
baseUrl: props.baseUrl,
|
|
347
|
+
profile: props.profile,
|
|
348
|
+
apiKey: props.apiKey,
|
|
349
|
+
apiToken: props.apiToken,
|
|
350
|
+
accountId: props.accountId,
|
|
351
|
+
email: props.email,
|
|
352
|
+
adopt: props.adopt,
|
|
353
|
+
delete: props.delete,
|
|
354
|
+
});
|
|
355
|
+
return token.tokenId;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (this.phase === "delete") {
|
|
360
|
+
if (props.delete !== false && this.output?.id) {
|
|
361
|
+
await deleteIndex(api, this.output.vectorizeName);
|
|
362
|
+
await deleteAiSearchInstance(api, this.output.id);
|
|
363
|
+
}
|
|
364
|
+
return this.destroy();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const name = props.name ?? this.scope.createPhysicalName(id, "-", 32);
|
|
368
|
+
if (name.length < 1 || name.length > 32) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`AI Search instance name must be 1-32 characters, got ${name.length} ("${name}")`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const [source, tokenId] = await Promise.all([
|
|
374
|
+
normalizeSource(props.source),
|
|
375
|
+
normalizeTokenId(),
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
const payload: AiSearch.ApiPayload = {
|
|
379
|
+
id: name,
|
|
380
|
+
source: source.type === "r2" ? source.bucket : source.domain,
|
|
381
|
+
type: source.type,
|
|
382
|
+
ai_search_model: props.aiSearchModel,
|
|
383
|
+
source_params: {
|
|
384
|
+
include_items: source.includePaths,
|
|
385
|
+
exclude_items: source.excludePaths,
|
|
386
|
+
...(source.type === "r2"
|
|
387
|
+
? {
|
|
388
|
+
r2_jurisdiction:
|
|
389
|
+
source.jurisdiction !== "default"
|
|
390
|
+
? source.jurisdiction
|
|
391
|
+
: undefined,
|
|
392
|
+
prefix: source.prefix,
|
|
393
|
+
}
|
|
394
|
+
: {
|
|
395
|
+
web_crawler: {
|
|
396
|
+
parse_type: source.parseType,
|
|
397
|
+
parse_options: source.parseOptions,
|
|
398
|
+
store_options: source.storeOptions,
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
embedding_model: props.embeddingModel,
|
|
403
|
+
chunk: props.chunk,
|
|
404
|
+
chunk_size: props.chunkSize,
|
|
405
|
+
chunk_overlap: props.chunkOverlap,
|
|
406
|
+
max_num_results: props.maxNumResults,
|
|
407
|
+
score_threshold: props.scoreThreshold,
|
|
408
|
+
reranking: props.reranking,
|
|
409
|
+
reranking_model: props.rerankingModel,
|
|
410
|
+
rewrite_query: props.rewriteQuery,
|
|
411
|
+
rewrite_model: props.rewriteModel,
|
|
412
|
+
cache: props.cache,
|
|
413
|
+
cache_threshold: props.cacheThreshold,
|
|
414
|
+
metadata: props.metadata,
|
|
415
|
+
token_id: tokenId,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
let instance: AiSearch.ApiResponse;
|
|
419
|
+
if (this.phase === "update" && this.output?.id) {
|
|
420
|
+
const replace =
|
|
421
|
+
"source" in this.output &&
|
|
422
|
+
(payload.type !== this.output.type ||
|
|
423
|
+
payload.source !== this.output.source);
|
|
424
|
+
// the development version of this resource had different properties, so check those to avoid an unnecessary replacement
|
|
425
|
+
const replaceLegacy =
|
|
426
|
+
"sourceType" in this.output &&
|
|
427
|
+
(payload.type !== this.output.sourceType ||
|
|
428
|
+
(payload.type === "r2" &&
|
|
429
|
+
"sourceBucket" in this.output &&
|
|
430
|
+
payload.source !== this.output.sourceBucket) ||
|
|
431
|
+
(payload.type === "web-crawler" &&
|
|
432
|
+
"sourceDomain" in this.output &&
|
|
433
|
+
payload.source !== this.output.sourceDomain));
|
|
434
|
+
if (replace || replaceLegacy) {
|
|
435
|
+
return this.replace(true);
|
|
436
|
+
}
|
|
437
|
+
instance = await updateAiSearchInstance(api, this.output.id, payload);
|
|
438
|
+
} else {
|
|
439
|
+
try {
|
|
440
|
+
instance = await createAiSearchInstance(api, payload);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
const isAlreadyExistsError =
|
|
443
|
+
error instanceof CloudflareApiError &&
|
|
444
|
+
error.status === 400 &&
|
|
445
|
+
(error.errorData as CloudflareApiErrorPayload[]).some(
|
|
446
|
+
(error) => error.code === 7022,
|
|
447
|
+
);
|
|
448
|
+
if (isAlreadyExistsError && adopt) {
|
|
449
|
+
instance = await getAiSearchInstance(api, name);
|
|
450
|
+
instance = await updateAiSearchInstance(api, instance.id, payload);
|
|
451
|
+
} else {
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (props.indexOnCreate !== false) {
|
|
456
|
+
await runAiSearchJob(api, instance.id, (message) =>
|
|
457
|
+
logger.task(id, {
|
|
458
|
+
prefix: "index",
|
|
459
|
+
prefixColor: "gray",
|
|
460
|
+
resource: id,
|
|
461
|
+
message,
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
...snakeToCamelObjectDeep(instance),
|
|
468
|
+
name: instance.id,
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Validate that a domain string is a valid domain format (not a URL).
|
|
475
|
+
* Throws a helpful error if the input looks like a URL.
|
|
476
|
+
*/
|
|
477
|
+
async function validateWebCrawlerSourceDomain(
|
|
478
|
+
api: CloudflareApi,
|
|
479
|
+
domain: string,
|
|
480
|
+
): Promise<void> {
|
|
481
|
+
if (domain.includes("://")) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Invalid domain format "${domain}". Provide just the domain (e.g., "docs.example.com"), not a URL. ` +
|
|
484
|
+
`For URL-based crawling, use AiCrawler instead of AiSearch.`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (domain.includes("/")) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Invalid domain format "${domain}". Provide just the domain without paths (e.g., "docs.example.com"). ` +
|
|
490
|
+
`Use includePaths to filter specific paths, or use AiCrawler for URL-based crawling.`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The Cloudflare dashboard uses this undocumented endpoint to validate domains.
|
|
495
|
+
// If we don't validate here, the create request fails with a 500 error.
|
|
496
|
+
const response = await api.post(
|
|
497
|
+
`/accounts/${api.accountId}/ai-search/domains`,
|
|
498
|
+
{
|
|
499
|
+
domain,
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
// The error message is more like an error code. This mapping is from the Cloudflare dashboard.
|
|
503
|
+
const errorMap = {
|
|
504
|
+
not_a_valid_domain: "Not a valid domain.",
|
|
505
|
+
invalid_domain:
|
|
506
|
+
"Invalid domain. The domain needs to belong to this account.",
|
|
507
|
+
fail_to_find_domain_info: "Failed to find domain information.",
|
|
508
|
+
missing_sitemap: "Sitemap not found. Please check your robots.txt.",
|
|
509
|
+
domain_not_owned_by_user: "The domain needs to belong to this account.",
|
|
510
|
+
forbidden_robots_txt:
|
|
511
|
+
"Failed to fetch robots.txt: The file is inaccessible.",
|
|
512
|
+
forbidden_sitemap:
|
|
513
|
+
"Failed to fetch your sitemap: The file is inaccessible.",
|
|
514
|
+
};
|
|
515
|
+
const json = (await response.json()) as {
|
|
516
|
+
success: boolean;
|
|
517
|
+
errors: Array<{ code: number; message: string }>;
|
|
518
|
+
};
|
|
519
|
+
if (json.success) return;
|
|
520
|
+
throw new Error(
|
|
521
|
+
[
|
|
522
|
+
`Failed to validate domain "${domain}" (${response.status}):`,
|
|
523
|
+
...json.errors.map(
|
|
524
|
+
(e) =>
|
|
525
|
+
`- [${e.code}] ${
|
|
526
|
+
e.message in errorMap
|
|
527
|
+
? errorMap[e.message as keyof typeof errorMap]
|
|
528
|
+
: e.message
|
|
529
|
+
}`,
|
|
530
|
+
),
|
|
531
|
+
"Learn more: https://developers.cloudflare.com/ai-search/configuration/data-source/website/",
|
|
532
|
+
].join("\n"),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export declare namespace AiSearch {
|
|
537
|
+
type Model =
|
|
538
|
+
| "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
|
539
|
+
| "@cf/meta/llama-3.1-8b-instruct-fast"
|
|
540
|
+
| "@cf/meta/llama-3.1-8b-instruct-fp8"
|
|
541
|
+
| "@cf/meta/llama-4-scout-17b-16e-instruct"
|
|
542
|
+
| "@cf/qwen/qwen3-30b-a3b-fp8"
|
|
543
|
+
| "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b"
|
|
544
|
+
| "@cf/moonshotai/kimi-k2-instruct"
|
|
545
|
+
| "anthropic/claude-3-7-sonnet"
|
|
546
|
+
| "anthropic/claude-sonnet-4"
|
|
547
|
+
| "anthropic/claude-opus-4"
|
|
548
|
+
| "anthropic/claude-3-5-haiku"
|
|
549
|
+
| "cerebras/qwen-3-235b-a22b-instruct"
|
|
550
|
+
| "cerebras/qwen-3-235b-a22b-thinking"
|
|
551
|
+
| "cerebras/llama-3.3-70b"
|
|
552
|
+
| "cerebras/llama-4-maverick-17b-128e-instruct"
|
|
553
|
+
| "cerebras/llama-4-scout-17b-16e-instruct"
|
|
554
|
+
| "cerebras/gpt-oss-120b"
|
|
555
|
+
| "google-ai-studio/gemini-2.5-flash"
|
|
556
|
+
| "google-ai-studio/gemini-2.5-pro"
|
|
557
|
+
| "grok/grok-4"
|
|
558
|
+
| "groq/llama-3.3-70b-versatile"
|
|
559
|
+
| "groq/llama-3.1-8b-instant"
|
|
560
|
+
| "openai/gpt-5"
|
|
561
|
+
| "openai/gpt-5-mini"
|
|
562
|
+
| "openai/gpt-5-nano"
|
|
563
|
+
| (string & {});
|
|
564
|
+
|
|
565
|
+
type EmbeddingModel =
|
|
566
|
+
| "@cf/qwen/qwen3-embedding-0.6b"
|
|
567
|
+
| "@cf/baai/bge-m3"
|
|
568
|
+
| "@cf/baai/bge-large-en-v1.5"
|
|
569
|
+
| "@cf/google/embeddinggemma-300m"
|
|
570
|
+
| "google-ai-studio/gemini-embedding-001"
|
|
571
|
+
| "openai/text-embedding-3-small"
|
|
572
|
+
| "openai/text-embedding-3-large"
|
|
573
|
+
| (string & {});
|
|
574
|
+
|
|
575
|
+
type RerankingModel = "@cf/baai/bge-reranker-base" | (string & {});
|
|
576
|
+
|
|
577
|
+
interface ApiPayload {
|
|
578
|
+
id: string;
|
|
579
|
+
source: string;
|
|
580
|
+
type: "r2" | "web-crawler";
|
|
581
|
+
ai_gateway_id?: string;
|
|
582
|
+
ai_search_model?: Model;
|
|
583
|
+
cache?: boolean;
|
|
584
|
+
cache_threshold?:
|
|
585
|
+
| "super_strict_match"
|
|
586
|
+
| "close_enough"
|
|
587
|
+
| "flexible_friend"
|
|
588
|
+
| "anything_goes";
|
|
589
|
+
chunk?: boolean;
|
|
590
|
+
chunk_overlap?: number;
|
|
591
|
+
chunk_size?: number;
|
|
592
|
+
custom_metadata?: Array<{
|
|
593
|
+
data_type: "text" | "number" | "boolean";
|
|
594
|
+
/**
|
|
595
|
+
* @minLength 1
|
|
596
|
+
* @maxLength 64
|
|
597
|
+
*/
|
|
598
|
+
field_name: string;
|
|
599
|
+
}>;
|
|
600
|
+
embedding_model?: EmbeddingModel;
|
|
601
|
+
hybrid_search_enabled?: boolean;
|
|
602
|
+
max_num_results?: number;
|
|
603
|
+
metadata?: {
|
|
604
|
+
created_from_aisearch_wizard?: boolean;
|
|
605
|
+
worker_domain?: string;
|
|
606
|
+
};
|
|
607
|
+
public_endpoint_params?: {
|
|
608
|
+
authorized_hosts?: string[];
|
|
609
|
+
chat_completions_endpoint?: {
|
|
610
|
+
disabled?: boolean;
|
|
611
|
+
};
|
|
612
|
+
enabled?: boolean;
|
|
613
|
+
mcp?: {
|
|
614
|
+
disabled?: boolean;
|
|
615
|
+
};
|
|
616
|
+
rate_limit?: {
|
|
617
|
+
/**
|
|
618
|
+
* Maximum: 3,600,000, Minimum: 60,000
|
|
619
|
+
*/
|
|
620
|
+
period_ms?: number;
|
|
621
|
+
/**
|
|
622
|
+
* Minimum: 1
|
|
623
|
+
*/
|
|
624
|
+
requests?: number;
|
|
625
|
+
technique?: "fixed" | "sliding";
|
|
626
|
+
};
|
|
627
|
+
search_endpoint?: {
|
|
628
|
+
disabled?: boolean;
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
reranking?: boolean;
|
|
632
|
+
reranking_model?: RerankingModel;
|
|
633
|
+
rewrite_model?: Model;
|
|
634
|
+
rewrite_query?: boolean;
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Maximum: 1, Minimum: 0, Default: 0.4
|
|
638
|
+
*/
|
|
639
|
+
score_threshold?: number;
|
|
640
|
+
source_params?: {
|
|
641
|
+
exclude_items?: string[];
|
|
642
|
+
include_items?: string[];
|
|
643
|
+
prefix?: string;
|
|
644
|
+
r2_jurisdiction?: string; // Default: "default"
|
|
645
|
+
web_crawler?: {
|
|
646
|
+
/**
|
|
647
|
+
* Default: {"parse_type":"sitemap"}
|
|
648
|
+
*/
|
|
649
|
+
parse_options?: {
|
|
650
|
+
include_headers?: Record<string, string>;
|
|
651
|
+
include_images?: boolean;
|
|
652
|
+
specific_sitemaps?: string[]; // Only valid when parse_type is 'sitemap'
|
|
653
|
+
use_browser_rendering?: boolean;
|
|
654
|
+
};
|
|
655
|
+
parse_type?: "sitemap" | "feed-rss"; // Default: "sitemap"
|
|
656
|
+
store_options?: {
|
|
657
|
+
storage_id: string;
|
|
658
|
+
r2_jurisdiction?: string; // Default: "default"
|
|
659
|
+
storage_type?: "r2";
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
};
|
|
663
|
+
token_id?: string;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
interface ApiResponse {
|
|
667
|
+
id: string;
|
|
668
|
+
account_id: string;
|
|
669
|
+
account_tag: string;
|
|
670
|
+
created_at: string;
|
|
671
|
+
internal_id: string;
|
|
672
|
+
modified_at: string;
|
|
673
|
+
source: string;
|
|
674
|
+
type: "r2" | "web-crawler";
|
|
675
|
+
vectorize_name: string;
|
|
676
|
+
ai_gateway_id?: string;
|
|
677
|
+
ai_search_model?: Model;
|
|
678
|
+
cache?: boolean; // default: true
|
|
679
|
+
cache_threshold?:
|
|
680
|
+
| "super_strict_match"
|
|
681
|
+
| "close_enough"
|
|
682
|
+
| "flexible_friend"
|
|
683
|
+
| "anything_goes"; // default: "close_enough"
|
|
684
|
+
chunk?: boolean; // default: true
|
|
685
|
+
chunk_overlap?: number; // maximum: 30, minimum: 0, default: 10
|
|
686
|
+
chunk_size?: number; // minimum: 64, default: 256
|
|
687
|
+
created_by?: string;
|
|
688
|
+
custom_metadata?: Array<{
|
|
689
|
+
data_type: "text" | "number" | "boolean";
|
|
690
|
+
field_name: string;
|
|
691
|
+
}>;
|
|
692
|
+
embedding_model?: EmbeddingModel;
|
|
693
|
+
enable?: boolean;
|
|
694
|
+
engine_version?: number; // default: 1
|
|
695
|
+
hybrid_search_enabled?: boolean;
|
|
696
|
+
last_activity?: string;
|
|
697
|
+
max_num_results?: number; // maximum: 50, minimum: 1, default: 10
|
|
698
|
+
metadata?: {
|
|
699
|
+
created_from_aisearch_wizard?: boolean;
|
|
700
|
+
worker_domain?: string;
|
|
701
|
+
};
|
|
702
|
+
modified_by?: string;
|
|
703
|
+
paused?: boolean;
|
|
704
|
+
public_endpoint_id?: string;
|
|
705
|
+
public_endpoint_params?: {
|
|
706
|
+
authorized_hosts?: string[];
|
|
707
|
+
chat_completions_endpoint?: {
|
|
708
|
+
disabled?: boolean;
|
|
709
|
+
};
|
|
710
|
+
enabled?: boolean;
|
|
711
|
+
mcp?: {
|
|
712
|
+
disabled?: boolean;
|
|
713
|
+
};
|
|
714
|
+
rate_limit?: {
|
|
715
|
+
period_ms?: number; // maximum: 3600000, minimum: 60000
|
|
716
|
+
requests?: number;
|
|
717
|
+
technique?: "fixed" | "sliding";
|
|
718
|
+
};
|
|
719
|
+
search_endpoint?: {
|
|
720
|
+
disabled?: boolean;
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
reranking?: boolean;
|
|
724
|
+
reranking_model?: RerankingModel;
|
|
725
|
+
rewrite_model?: Model;
|
|
726
|
+
rewrite_query?: boolean;
|
|
727
|
+
score_threshold?: number;
|
|
728
|
+
source_params?: {
|
|
729
|
+
exclude_items?: string[];
|
|
730
|
+
include_items?: string[];
|
|
731
|
+
prefix?: string;
|
|
732
|
+
r2_jurisdiction?: string; // default: "default"
|
|
733
|
+
web_crawler?: {
|
|
734
|
+
parse_options?: {
|
|
735
|
+
include_headers?: Record<string, string>;
|
|
736
|
+
include_images?: boolean;
|
|
737
|
+
specific_sitemaps?: string[]; // valid with 'sitemap' parse_type
|
|
738
|
+
use_browser_rendering?: boolean;
|
|
739
|
+
};
|
|
740
|
+
parse_type?: "sitemap" | "feed-rss"; // default: "sitemap"
|
|
741
|
+
store_options?: {
|
|
742
|
+
storage_id: string;
|
|
743
|
+
r2_jurisdiction?: string; // default: "default"
|
|
744
|
+
storage_type?: "r2";
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
};
|
|
748
|
+
status?: "waiting" | "ready" | "indexing" | "error";
|
|
749
|
+
summarization?: boolean;
|
|
750
|
+
summarization_model?:
|
|
751
|
+
| "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
|
752
|
+
| "@cf/meta/llama-3.1-8b-instruct-fast"
|
|
753
|
+
| "@cf/meta/llama-3.1-8b-instruct-fp8"
|
|
754
|
+
| "@cf/meta/llama-4-scout-17b-16e-instruct"
|
|
755
|
+
| "@cf/qwen/qwen3-30b-a3b-fp8"
|
|
756
|
+
| "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b"
|
|
757
|
+
| "@cf/moonshotai/kimi-k2-instruct"
|
|
758
|
+
| "anthropic/claude-3-7-sonnet"
|
|
759
|
+
| "anthropic/claude-sonnet-4"
|
|
760
|
+
| "anthropic/claude-opus-4"
|
|
761
|
+
| "anthropic/claude-3-5-haiku"
|
|
762
|
+
| "cerebras/qwen-3-235b-a22b-instruct"
|
|
763
|
+
| "cerebras/qwen-3-235b-a22b-thinking"
|
|
764
|
+
| "cerebras/llama-3.3-70b"
|
|
765
|
+
| "cerebras/llama-4-maverick-17b-128e-instruct"
|
|
766
|
+
| "cerebras/llama-4-scout-17b-16e-instruct"
|
|
767
|
+
| "cerebras/gpt-oss-120b"
|
|
768
|
+
| "google-ai-studio/gemini-2.5-flash"
|
|
769
|
+
| "google-ai-studio/gemini-2.5-pro"
|
|
770
|
+
| "grok/grok-4"
|
|
771
|
+
| "groq/llama-3.3-70b-versatile"
|
|
772
|
+
| "groq/llama-3.1-8b-instant"
|
|
773
|
+
| "openai/gpt-5"
|
|
774
|
+
| "openai/gpt-5-mini"
|
|
775
|
+
| "openai/gpt-5-nano"
|
|
776
|
+
| (string & {});
|
|
777
|
+
system_prompt_ai_search?: string;
|
|
778
|
+
system_prompt_index_summarization?: string;
|
|
779
|
+
system_prompt_rewrite_query?: string;
|
|
780
|
+
token_id?: string;
|
|
781
|
+
vectorize_active_namespace?: string;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function listAiSearchInstances(
|
|
786
|
+
api: CloudflareApi,
|
|
787
|
+
): Promise<AiSearch.ApiResponse[]> {
|
|
788
|
+
return await extractCloudflareResult<AiSearch.ApiResponse[]>(
|
|
789
|
+
"list AI Search instances",
|
|
790
|
+
api.get(`/accounts/${api.accountId}/ai-search/instances`),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export async function createAiSearchInstance(
|
|
795
|
+
api: CloudflareApi,
|
|
796
|
+
payload: AiSearch.ApiPayload,
|
|
797
|
+
): Promise<AiSearch.ApiResponse> {
|
|
798
|
+
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
799
|
+
`create AI Search instance "${payload.id}"`,
|
|
800
|
+
api.post(`/accounts/${api.accountId}/ai-search/instances`, payload),
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export async function getAiSearchInstance(
|
|
805
|
+
api: CloudflareApi,
|
|
806
|
+
id: string,
|
|
807
|
+
): Promise<AiSearch.ApiResponse> {
|
|
808
|
+
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
809
|
+
`get AI Search instance "${id}"`,
|
|
810
|
+
api.get(`/accounts/${api.accountId}/ai-search/instances/${id}`),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export async function updateAiSearchInstance(
|
|
815
|
+
api: CloudflareApi,
|
|
816
|
+
id: string,
|
|
817
|
+
payload: AiSearch.ApiPayload,
|
|
818
|
+
): Promise<AiSearch.ApiResponse> {
|
|
819
|
+
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
820
|
+
`update AI Search instance "${id}"`,
|
|
821
|
+
api.put(`/accounts/${api.accountId}/ai-search/instances/${id}`, payload),
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export async function deleteAiSearchInstance(
|
|
826
|
+
api: CloudflareApi,
|
|
827
|
+
id: string,
|
|
828
|
+
): Promise<void> {
|
|
829
|
+
try {
|
|
830
|
+
await extractCloudflareResult(
|
|
831
|
+
`delete AI Search instance "${id}"`,
|
|
832
|
+
api.delete(`/accounts/${api.accountId}/ai-search/instances/${id}`),
|
|
833
|
+
);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error instanceof CloudflareApiError && error.status === 404) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
interface AiSearchJobApiResponse {
|
|
843
|
+
id: string;
|
|
844
|
+
source: "user" | "schedule";
|
|
845
|
+
end_reason: string | null;
|
|
846
|
+
ended_at: string | null;
|
|
847
|
+
last_seen_at: string | null;
|
|
848
|
+
started_at: string | null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export async function listAiSearchJobs(
|
|
852
|
+
api: CloudflareApi,
|
|
853
|
+
aiSearchId: string,
|
|
854
|
+
): Promise<AiSearchJobApiResponse[]> {
|
|
855
|
+
return await extractCloudflareResult<AiSearchJobApiResponse[]>(
|
|
856
|
+
`list AI Search jobs for instance "${aiSearchId}"`,
|
|
857
|
+
api.get(
|
|
858
|
+
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
|
|
859
|
+
),
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export async function createAiSearchJob(
|
|
864
|
+
api: CloudflareApi,
|
|
865
|
+
aiSearchId: string,
|
|
866
|
+
): Promise<AiSearchJobApiResponse> {
|
|
867
|
+
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
868
|
+
`create AI Search job for instance "${aiSearchId}"`,
|
|
869
|
+
api.post(
|
|
870
|
+
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
|
|
871
|
+
{},
|
|
872
|
+
),
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export async function getAiSearchJob(
|
|
877
|
+
api: CloudflareApi,
|
|
878
|
+
aiSearchId: string,
|
|
879
|
+
jobId: string,
|
|
880
|
+
): Promise<AiSearchJobApiResponse> {
|
|
881
|
+
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
882
|
+
`get AI Search job "${jobId}" for instance "${aiSearchId}"`,
|
|
883
|
+
api.get(
|
|
884
|
+
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}`,
|
|
885
|
+
),
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
interface AiSearchJobLogItem {
|
|
890
|
+
id: number;
|
|
891
|
+
created_at: number;
|
|
892
|
+
message: string;
|
|
893
|
+
message_type: number;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export async function listAiSearchJobLogs(
|
|
897
|
+
api: CloudflareApi,
|
|
898
|
+
aiSearchId: string,
|
|
899
|
+
jobId: string,
|
|
900
|
+
): Promise<AiSearchJobLogItem[]> {
|
|
901
|
+
try {
|
|
902
|
+
return await extractCloudflareResult<AiSearchJobLogItem[]>(
|
|
903
|
+
`list AI Search job logs for job "${jobId}" for instance "${aiSearchId}"`,
|
|
904
|
+
api.get(
|
|
905
|
+
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
|
|
906
|
+
),
|
|
907
|
+
);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
if (
|
|
910
|
+
isCloudflareApiError(error, { code: 7002 }) // ai_search_not_found
|
|
911
|
+
) {
|
|
912
|
+
return [];
|
|
913
|
+
}
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export async function runAiSearchJob(
|
|
919
|
+
api: CloudflareApi,
|
|
920
|
+
aiSearchId: string,
|
|
921
|
+
log: (message: string) => void,
|
|
922
|
+
): Promise<void> {
|
|
923
|
+
log("Preparing to index...");
|
|
924
|
+
const job = await createAiSearchJob(api, aiSearchId);
|
|
925
|
+
let lastLogId = 0;
|
|
926
|
+
let done = false;
|
|
927
|
+
const resultPromise = poll({
|
|
928
|
+
description: `run AI Search job "${job.id}" for instance "${aiSearchId}"`,
|
|
929
|
+
fn: () => getAiSearchJob(api, aiSearchId, job.id),
|
|
930
|
+
predicate: (result) => result.ended_at !== null,
|
|
931
|
+
});
|
|
932
|
+
pollLogs();
|
|
933
|
+
|
|
934
|
+
const result = await resultPromise;
|
|
935
|
+
done = true;
|
|
936
|
+
log(`Sync completed: ${result.end_reason}`);
|
|
937
|
+
|
|
938
|
+
async function pollLogs() {
|
|
939
|
+
const logs = await listAiSearchJobLogs(api, aiSearchId, job.id);
|
|
940
|
+
for (let i = logs.length - 1; i >= 0; i--) {
|
|
941
|
+
const item = logs[i];
|
|
942
|
+
if (item.id > lastLogId) {
|
|
943
|
+
lastLogId = item.id;
|
|
944
|
+
log(item.message);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (!done) {
|
|
948
|
+
await sleep(3000);
|
|
949
|
+
await pollLogs();
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|