alchemy 0.92.2 → 0.93.1
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 +2 -2
- package/lib/cloudflare/ai-search-namespace.d.ts +150 -0
- package/lib/cloudflare/ai-search-namespace.d.ts.map +1 -0
- package/lib/cloudflare/ai-search-namespace.js +278 -0
- package/lib/cloudflare/ai-search-namespace.js.map +1 -0
- package/lib/cloudflare/ai-search-token.d.ts +11 -1
- package/lib/cloudflare/ai-search-token.d.ts.map +1 -1
- package/lib/cloudflare/ai-search-token.js +35 -7
- package/lib/cloudflare/ai-search-token.js.map +1 -1
- package/lib/cloudflare/ai-search.d.ts +123 -18
- package/lib/cloudflare/ai-search.d.ts.map +1 -1
- package/lib/cloudflare/ai-search.js +222 -68
- package/lib/cloudflare/ai-search.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +29 -2
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +4 -2
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/container.d.ts +79 -6
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +130 -9
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/custom-domain.d.ts +14 -0
- package/lib/cloudflare/custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/custom-domain.js +3 -0
- package/lib/cloudflare/custom-domain.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +43 -0
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +31 -13
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/website.d.ts +4 -0
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +15 -1
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +25 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +6 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +3 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +32 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +4 -1
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +8 -2
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +1 -1
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +4 -44
- package/lib/docker/image.js.map +1 -1
- package/lib/docker/registry.d.ts +11 -0
- package/lib/docker/registry.d.ts.map +1 -0
- package/lib/docker/registry.js +41 -0
- package/lib/docker/registry.js.map +1 -0
- package/package.json +3 -3
- package/src/cloudflare/ai-search-namespace.ts +435 -0
- package/src/cloudflare/ai-search-token.ts +43 -9
- package/src/cloudflare/ai-search.ts +334 -73
- package/src/cloudflare/bindings.ts +33 -0
- package/src/cloudflare/bound.ts +87 -74
- package/src/cloudflare/container.ts +242 -18
- package/src/cloudflare/custom-domain.ts +25 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +50 -1
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +58 -34
- package/src/cloudflare/website.ts +19 -1
- package/src/cloudflare/worker-metadata.ts +25 -1
- package/src/cloudflare/worker.ts +9 -0
- package/src/cloudflare/wrangler.json.ts +32 -0
- package/src/docker/api.ts +11 -2
- package/src/docker/image.ts +5 -56
- package/src/docker/registry.ts +62 -0
- package/workers/cloudflare-state-store.js +57 -57
- package/workers/tunnel-proxy.js +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
|
-
import { Resource } from "../resource.ts";
|
|
2
|
+
import { Resource, ResourceKind } from "../resource.ts";
|
|
3
3
|
import { logger } from "../util/logger.ts";
|
|
4
4
|
import { poll } from "../util/poll.ts";
|
|
5
5
|
import { sleep } from "../util/sleep.ts";
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
snakeToCamelObjectDeep,
|
|
8
8
|
type SnakeToCamel,
|
|
9
9
|
} from "../util/snake-to-camel.ts";
|
|
10
|
+
import {
|
|
11
|
+
type AiSearchNamespace,
|
|
12
|
+
isAiSearchNamespace,
|
|
13
|
+
} from "./ai-search-namespace.ts";
|
|
10
14
|
import { AiSearchToken } from "./ai-search-token.ts";
|
|
11
15
|
import { CloudflareApiError, isCloudflareApiError } from "./api-error.ts";
|
|
12
16
|
import {
|
|
@@ -37,9 +41,49 @@ interface BaseAiSearchProps extends CloudflareApiOptions {
|
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Data source for indexing.
|
|
40
|
-
*
|
|
44
|
+
*
|
|
45
|
+
* Accepts three forms:
|
|
46
|
+
* - **R2Bucket (shorthand)**: pass an `R2Bucket` resource directly for
|
|
47
|
+
* default indexing. `prefix`, `includePaths`, and `excludePaths` cannot
|
|
48
|
+
* be set in this form — use the full R2 config form below.
|
|
49
|
+
* - **R2 config**: `{ type: "r2", bucket, prefix?, includePaths?, excludePaths?, jurisdiction? }`.
|
|
50
|
+
* - **Web crawler**: `{ type: "web-crawler", domain, ... }`.
|
|
51
|
+
*
|
|
52
|
+
* When omitted, creates a built-in storage instance for manual file uploads
|
|
53
|
+
* (via the Items API or the AI Search binding).
|
|
54
|
+
*/
|
|
55
|
+
source?: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The namespace this instance belongs to.
|
|
59
|
+
* Can be a namespace name string or an AiSearchNamespace resource.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Single-instance Worker bindings (`bindings: { MY: aiSearch }`) can only
|
|
63
|
+
* bind instances in the `default` namespace. To bind instances in a
|
|
64
|
+
* non-default namespace, use an `AiSearchNamespace` binding instead and
|
|
65
|
+
* access the instance via `env.NS.get(name)`.
|
|
66
|
+
*
|
|
67
|
+
* Changing `namespace` on an existing instance triggers a replace
|
|
68
|
+
* (delete + create) because namespaces are immutable on the Cloudflare
|
|
69
|
+
* side.
|
|
70
|
+
*
|
|
71
|
+
* @default "default"
|
|
72
|
+
*/
|
|
73
|
+
namespace?: string | AiSearchNamespace;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Controls which storage backends are used during indexing.
|
|
77
|
+
* Defaults to vector-only. Set both `vector` and `keyword` to `true` for hybrid search.
|
|
41
78
|
*/
|
|
42
|
-
|
|
79
|
+
indexMethod?: { vector?: boolean; keyword?: boolean };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fusion method for combining vector and keyword results.
|
|
83
|
+
*
|
|
84
|
+
* @default "rrf"
|
|
85
|
+
*/
|
|
86
|
+
fusionMethod?: "max" | "rrf";
|
|
43
87
|
|
|
44
88
|
/**
|
|
45
89
|
* Text generation model for AI responses
|
|
@@ -142,7 +186,8 @@ interface BaseAiSearchProps extends CloudflareApiOptions {
|
|
|
142
186
|
metadata?: Record<string, unknown>;
|
|
143
187
|
|
|
144
188
|
/**
|
|
145
|
-
* Whether to index the source documents when the AI Search instance is created
|
|
189
|
+
* Whether to index the source documents when the AI Search instance is created.
|
|
190
|
+
* Only applicable when a source is provided.
|
|
146
191
|
* @default true
|
|
147
192
|
*/
|
|
148
193
|
indexOnCreate?: boolean;
|
|
@@ -249,13 +294,73 @@ export interface AiSearchWebCrawlerSource {
|
|
|
249
294
|
};
|
|
250
295
|
}
|
|
251
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Type guard for AiSearch
|
|
299
|
+
*/
|
|
300
|
+
export function isAiSearch(resource: unknown): resource is AiSearch {
|
|
301
|
+
return (
|
|
302
|
+
typeof resource === "object" &&
|
|
303
|
+
resource !== null &&
|
|
304
|
+
(resource as any)[ResourceKind] === "cloudflare::AiSearch"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
252
308
|
export type AiSearch = SnakeToCamel<AiSearch.ApiResponse> & {
|
|
253
309
|
/**
|
|
254
|
-
* The name
|
|
310
|
+
* The instance name on the Cloudflare side. Equal to `id`. This is what
|
|
311
|
+
* gets emitted as `instance_name` in single-instance `ai_search` bindings.
|
|
255
312
|
*/
|
|
256
313
|
name: string;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The namespace this instance belongs to.
|
|
317
|
+
*
|
|
318
|
+
* Optional for backwards compatibility with state files that predate
|
|
319
|
+
* namespace support; at write-time this is always populated (defaults to
|
|
320
|
+
* `"default"` when the user did not specify a namespace).
|
|
321
|
+
*/
|
|
322
|
+
namespace?: string;
|
|
257
323
|
};
|
|
258
324
|
|
|
325
|
+
/**
|
|
326
|
+
* An AI Search instance: a managed search index with optional built-in
|
|
327
|
+
* storage and optional external data source (R2 or web crawler).
|
|
328
|
+
*
|
|
329
|
+
* @see https://developers.cloudflare.com/ai-search/
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ## Built-in storage (no source)
|
|
333
|
+
*
|
|
334
|
+
* Creates an instance whose content is uploaded directly via the items API.
|
|
335
|
+
*
|
|
336
|
+
* ```ts
|
|
337
|
+
* const kb = await AiSearch("knowledge-base", {
|
|
338
|
+
* name: "knowledge-base",
|
|
339
|
+
* });
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ## R2-backed instance
|
|
344
|
+
*
|
|
345
|
+
* ```ts
|
|
346
|
+
* const bucket = await R2Bucket("docs");
|
|
347
|
+
* const search = await AiSearch("docs-search", {
|
|
348
|
+
* name: "docs-search",
|
|
349
|
+
* source: bucket,
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ## Instance in a custom namespace
|
|
355
|
+
*
|
|
356
|
+
* ```ts
|
|
357
|
+
* const ns = await AiSearchNamespace("tenants", { name: "tenants" });
|
|
358
|
+
* const search = await AiSearch("tenant-a", {
|
|
359
|
+
* name: "tenant-a",
|
|
360
|
+
* namespace: ns,
|
|
361
|
+
* });
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
259
364
|
export const AiSearch = Resource(
|
|
260
365
|
"cloudflare::AiSearch",
|
|
261
366
|
async function (
|
|
@@ -263,9 +368,20 @@ export const AiSearch = Resource(
|
|
|
263
368
|
id: string,
|
|
264
369
|
props: AiSearchProps,
|
|
265
370
|
): Promise<AiSearch> {
|
|
266
|
-
const api = await createCloudflareApi(props);
|
|
267
371
|
const adopt = props.adopt ?? this.scope.adopt;
|
|
268
372
|
|
|
373
|
+
// Resolve namespace: AiSearchNamespace resource → string, default to "default"
|
|
374
|
+
const namespace = resolveNamespace(props.namespace);
|
|
375
|
+
|
|
376
|
+
// NOTE: AI Search is an always-remote binding (no Miniflare-native
|
|
377
|
+
// implementation). `alchemy dev` wires the worker binding via
|
|
378
|
+
// `remote-binding-proxy`, which requires the instance to actually exist
|
|
379
|
+
// on Cloudflare at preview-token creation time — a locally mocked
|
|
380
|
+
// resource would cause the Worker deploy to fail with error 10360
|
|
381
|
+
// ("instance … not found"). Follow the same pattern as Vectorize and
|
|
382
|
+
// skip the `scope.local` mock branch entirely.
|
|
383
|
+
const api = await createCloudflareApi(props);
|
|
384
|
+
|
|
269
385
|
const validateBucketSource = async (
|
|
270
386
|
bucket: R2Bucket | string,
|
|
271
387
|
jurisdiction: R2BucketJurisdiction = "default",
|
|
@@ -288,16 +404,25 @@ export const AiSearch = Resource(
|
|
|
288
404
|
await getBucket(api, name, { jurisdiction });
|
|
289
405
|
} catch (error) {
|
|
290
406
|
throw new Error(
|
|
291
|
-
`Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${
|
|
407
|
+
`Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${
|
|
408
|
+
error instanceof Error ? error.message : String(error)
|
|
409
|
+
}`,
|
|
292
410
|
{ cause: error },
|
|
293
411
|
);
|
|
294
412
|
}
|
|
295
413
|
};
|
|
296
414
|
const normalizeSource = async (
|
|
297
|
-
source:
|
|
415
|
+
source:
|
|
416
|
+
| R2Bucket
|
|
417
|
+
| AiSearchR2Source
|
|
418
|
+
| AiSearchWebCrawlerSource
|
|
419
|
+
| undefined,
|
|
298
420
|
): Promise<
|
|
299
|
-
(AiSearchR2Source & { bucket: string })
|
|
421
|
+
| (AiSearchR2Source & { bucket: string })
|
|
422
|
+
| AiSearchWebCrawlerSource
|
|
423
|
+
| undefined
|
|
300
424
|
> => {
|
|
425
|
+
if (!source) return undefined;
|
|
301
426
|
if (isBucket(source)) {
|
|
302
427
|
await validateBucketSource(source, source.jurisdiction);
|
|
303
428
|
return {
|
|
@@ -334,14 +459,34 @@ export const AiSearch = Resource(
|
|
|
334
459
|
);
|
|
335
460
|
}
|
|
336
461
|
};
|
|
337
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Only resolve token for R2 sources. Web-crawler and built-in storage
|
|
464
|
+
* instances do not require a token.
|
|
465
|
+
*/
|
|
466
|
+
const normalizeTokenId = async (
|
|
467
|
+
source:
|
|
468
|
+
| (AiSearchR2Source & { bucket: string })
|
|
469
|
+
| AiSearchWebCrawlerSource
|
|
470
|
+
| undefined,
|
|
471
|
+
): Promise<string | undefined> => {
|
|
472
|
+
// Token only needed for R2 sources
|
|
473
|
+
if (!source || source.type !== "r2") {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
338
476
|
if ("tokenId" in props) {
|
|
339
477
|
validateTokenId(props.tokenId);
|
|
340
478
|
return props.tokenId;
|
|
341
|
-
} else if (props.token) {
|
|
479
|
+
} else if ("token" in props && props.token) {
|
|
342
480
|
validateTokenId(props.token.tokenId);
|
|
343
481
|
return props.token.tokenId;
|
|
344
482
|
} else {
|
|
483
|
+
// Auto-created tokens are an implementation detail of this resource —
|
|
484
|
+
// `adopt: true` so updates reuse a prior auto-created token with the
|
|
485
|
+
// same stable child id. We forward `delete: false` when the user
|
|
486
|
+
// preserves the instance, because Cloudflare rejects token deletion
|
|
487
|
+
// while the token is still referenced by the preserved instance
|
|
488
|
+
// (error 7076 `token_in_use_by_instances`). When the instance IS
|
|
489
|
+
// being deleted normally, the token deletes with it.
|
|
345
490
|
const token = await AiSearchToken("token", {
|
|
346
491
|
baseUrl: props.baseUrl,
|
|
347
492
|
profile: props.profile,
|
|
@@ -349,8 +494,8 @@ export const AiSearch = Resource(
|
|
|
349
494
|
apiToken: props.apiToken,
|
|
350
495
|
accountId: props.accountId,
|
|
351
496
|
email: props.email,
|
|
352
|
-
adopt:
|
|
353
|
-
delete:
|
|
497
|
+
adopt: true,
|
|
498
|
+
...(props.delete === false ? { delete: false } : {}),
|
|
354
499
|
});
|
|
355
500
|
return token.tokenId;
|
|
356
501
|
}
|
|
@@ -358,8 +503,11 @@ export const AiSearch = Resource(
|
|
|
358
503
|
|
|
359
504
|
if (this.phase === "delete") {
|
|
360
505
|
if (props.delete !== false && this.output?.id) {
|
|
361
|
-
|
|
362
|
-
|
|
506
|
+
if (this.output.vectorizeName) {
|
|
507
|
+
await deleteIndex(api, this.output.vectorizeName);
|
|
508
|
+
}
|
|
509
|
+
const deleteNs = this.output.namespace ?? "default";
|
|
510
|
+
await deleteAiSearchInstance(api, deleteNs, this.output.id);
|
|
363
511
|
}
|
|
364
512
|
return this.destroy();
|
|
365
513
|
}
|
|
@@ -370,35 +518,40 @@ export const AiSearch = Resource(
|
|
|
370
518
|
`AI Search instance name must be 1-32 characters, got ${name.length} ("${name}")`,
|
|
371
519
|
);
|
|
372
520
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
]);
|
|
521
|
+
|
|
522
|
+
const source = await normalizeSource(props.source);
|
|
523
|
+
const tokenId = await normalizeTokenId(source);
|
|
377
524
|
|
|
378
525
|
const payload: AiSearch.ApiPayload = {
|
|
379
526
|
id: name,
|
|
380
|
-
source: source
|
|
381
|
-
|
|
527
|
+
source: source
|
|
528
|
+
? source.type === "r2"
|
|
529
|
+
? source.bucket
|
|
530
|
+
: source.domain
|
|
531
|
+
: undefined,
|
|
532
|
+
type: source?.type,
|
|
382
533
|
ai_search_model: props.aiSearchModel,
|
|
383
|
-
source_params:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
534
|
+
source_params: source
|
|
535
|
+
? {
|
|
536
|
+
include_items: source.includePaths,
|
|
537
|
+
exclude_items: source.excludePaths,
|
|
538
|
+
...(source.type === "r2"
|
|
539
|
+
? {
|
|
540
|
+
r2_jurisdiction:
|
|
541
|
+
source.jurisdiction !== "default"
|
|
542
|
+
? source.jurisdiction
|
|
543
|
+
: undefined,
|
|
544
|
+
prefix: source.prefix,
|
|
545
|
+
}
|
|
546
|
+
: {
|
|
547
|
+
web_crawler: {
|
|
548
|
+
parse_type: source.parseType,
|
|
549
|
+
parse_options: source.parseOptions,
|
|
550
|
+
store_options: source.storeOptions,
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
}
|
|
554
|
+
: undefined,
|
|
402
555
|
embedding_model: props.embeddingModel,
|
|
403
556
|
chunk: props.chunk,
|
|
404
557
|
chunk_size: props.chunkSize,
|
|
@@ -413,6 +566,8 @@ export const AiSearch = Resource(
|
|
|
413
566
|
cache_threshold: props.cacheThreshold,
|
|
414
567
|
metadata: props.metadata,
|
|
415
568
|
token_id: tokenId,
|
|
569
|
+
index_method: props.indexMethod,
|
|
570
|
+
fusion_method: props.fusionMethod,
|
|
416
571
|
};
|
|
417
572
|
|
|
418
573
|
let instance: AiSearch.ApiResponse;
|
|
@@ -431,29 +586,73 @@ export const AiSearch = Resource(
|
|
|
431
586
|
(payload.type === "web-crawler" &&
|
|
432
587
|
"sourceDomain" in this.output &&
|
|
433
588
|
payload.source !== this.output.sourceDomain));
|
|
434
|
-
|
|
589
|
+
// Namespace is immutable: moving an instance between namespaces must
|
|
590
|
+
// replace (delete old, create new) rather than attempt an in-place
|
|
591
|
+
// update against a non-existent `PUT /namespaces/{new}/instances/{id}`.
|
|
592
|
+
// Default to "default" for legacy state files that predate the
|
|
593
|
+
// namespace prop.
|
|
594
|
+
const currentNamespace =
|
|
595
|
+
("namespace" in this.output && this.output.namespace) || "default";
|
|
596
|
+
const namespaceChanged = currentNamespace !== namespace;
|
|
597
|
+
if (replace || replaceLegacy || namespaceChanged) {
|
|
435
598
|
return this.replace(true);
|
|
436
599
|
}
|
|
437
|
-
instance = await updateAiSearchInstance(
|
|
600
|
+
instance = await updateAiSearchInstance(
|
|
601
|
+
api,
|
|
602
|
+
namespace,
|
|
603
|
+
this.output.id,
|
|
604
|
+
payload,
|
|
605
|
+
);
|
|
438
606
|
} else {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
607
|
+
// Pre-flight adopt: if `adopt: true` and an instance already exists,
|
|
608
|
+
// adopt it directly via GET→PUT. This avoids racing on 400/7022 error
|
|
609
|
+
// codes and the fragile `errorData as CloudflareApiErrorPayload[]` cast.
|
|
610
|
+
if (adopt) {
|
|
611
|
+
const existing = await getAiSearchInstance(api, namespace, name).catch(
|
|
612
|
+
(e: unknown) => {
|
|
613
|
+
if (e instanceof CloudflareApiError && e.status === 404) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
throw e;
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
if (existing) {
|
|
620
|
+
instance = await updateAiSearchInstance(
|
|
621
|
+
api,
|
|
622
|
+
namespace,
|
|
623
|
+
existing.id,
|
|
624
|
+
payload,
|
|
447
625
|
);
|
|
448
|
-
if (isAlreadyExistsError && adopt) {
|
|
449
|
-
instance = await getAiSearchInstance(api, name);
|
|
450
|
-
instance = await updateAiSearchInstance(api, instance.id, payload);
|
|
451
626
|
} else {
|
|
627
|
+
instance = await createAiSearchInstance(api, namespace, payload);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
try {
|
|
631
|
+
instance = await createAiSearchInstance(api, namespace, payload);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
// Wrap "already exists" errors with a clearer message pointing at
|
|
634
|
+
// the adoption path (AGENTS.md convention).
|
|
635
|
+
const errorData = Array.isArray(
|
|
636
|
+
(error as CloudflareApiError | undefined)?.errorData,
|
|
637
|
+
)
|
|
638
|
+
? ((error as CloudflareApiError)
|
|
639
|
+
.errorData as CloudflareApiErrorPayload[])
|
|
640
|
+
: [];
|
|
641
|
+
const isAlreadyExistsError =
|
|
642
|
+
error instanceof CloudflareApiError &&
|
|
643
|
+
error.status === 400 &&
|
|
644
|
+
errorData.some((e) => e.code === 7022);
|
|
645
|
+
if (isAlreadyExistsError) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`AI Search instance "${name}" already exists in namespace "${namespace}". Use \`adopt: true\` to adopt it.`,
|
|
648
|
+
{ cause: error },
|
|
649
|
+
);
|
|
650
|
+
}
|
|
452
651
|
throw error;
|
|
453
652
|
}
|
|
454
653
|
}
|
|
455
|
-
if (props.indexOnCreate !== false) {
|
|
456
|
-
await runAiSearchJob(api, instance.id, (message) =>
|
|
654
|
+
if (props.indexOnCreate !== false && source) {
|
|
655
|
+
await runAiSearchJob(api, namespace, instance.id, (message) =>
|
|
457
656
|
logger.task(id, {
|
|
458
657
|
prefix: "index",
|
|
459
658
|
prefixColor: "gray",
|
|
@@ -461,15 +660,33 @@ export const AiSearch = Resource(
|
|
|
461
660
|
message,
|
|
462
661
|
}),
|
|
463
662
|
);
|
|
663
|
+
} else if (props.indexOnCreate === true && !source) {
|
|
664
|
+
logger.warn(
|
|
665
|
+
`AI Search "${id}": \`indexOnCreate: true\` has no effect because no \`source\` was provided.`,
|
|
666
|
+
);
|
|
464
667
|
}
|
|
465
668
|
}
|
|
466
669
|
return {
|
|
467
670
|
...snakeToCamelObjectDeep(instance),
|
|
468
671
|
name: instance.id,
|
|
672
|
+
// The API response may include a `namespace` field; explicitly use the
|
|
673
|
+
// resolved local value to ensure the output always matches the prop
|
|
674
|
+
// (and defaults to "default" when unspecified).
|
|
675
|
+
namespace,
|
|
469
676
|
};
|
|
470
677
|
},
|
|
471
678
|
);
|
|
472
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Resolve a namespace prop to a string name.
|
|
682
|
+
*/
|
|
683
|
+
function resolveNamespace(ns: string | AiSearchNamespace | undefined): string {
|
|
684
|
+
if (!ns) return "default";
|
|
685
|
+
if (typeof ns === "string") return ns;
|
|
686
|
+
if (isAiSearchNamespace(ns)) return ns.namespace;
|
|
687
|
+
return "default";
|
|
688
|
+
}
|
|
689
|
+
|
|
473
690
|
/**
|
|
474
691
|
* Validate that a domain string is a valid domain format (not a URL).
|
|
475
692
|
* Throws a helpful error if the input looks like a URL.
|
|
@@ -576,8 +793,8 @@ export declare namespace AiSearch {
|
|
|
576
793
|
|
|
577
794
|
interface ApiPayload {
|
|
578
795
|
id: string;
|
|
579
|
-
source
|
|
580
|
-
type
|
|
796
|
+
source?: string;
|
|
797
|
+
type?: "r2" | "web-crawler";
|
|
581
798
|
ai_gateway_id?: string;
|
|
582
799
|
ai_search_model?: Model;
|
|
583
800
|
cache?: boolean;
|
|
@@ -599,6 +816,8 @@ export declare namespace AiSearch {
|
|
|
599
816
|
}>;
|
|
600
817
|
embedding_model?: EmbeddingModel;
|
|
601
818
|
hybrid_search_enabled?: boolean;
|
|
819
|
+
index_method?: { vector?: boolean; keyword?: boolean };
|
|
820
|
+
fusion_method?: "max" | "rrf";
|
|
602
821
|
max_num_results?: number;
|
|
603
822
|
metadata?: {
|
|
604
823
|
created_from_aisearch_wizard?: boolean;
|
|
@@ -670,9 +889,10 @@ export declare namespace AiSearch {
|
|
|
670
889
|
created_at: string;
|
|
671
890
|
internal_id: string;
|
|
672
891
|
modified_at: string;
|
|
673
|
-
source
|
|
674
|
-
type
|
|
892
|
+
source?: string;
|
|
893
|
+
type?: "r2" | "web-crawler";
|
|
675
894
|
vectorize_name: string;
|
|
895
|
+
namespace?: string;
|
|
676
896
|
ai_gateway_id?: string;
|
|
677
897
|
ai_search_model?: Model;
|
|
678
898
|
cache?: boolean; // default: true
|
|
@@ -693,6 +913,8 @@ export declare namespace AiSearch {
|
|
|
693
913
|
enable?: boolean;
|
|
694
914
|
engine_version?: number; // default: 1
|
|
695
915
|
hybrid_search_enabled?: boolean;
|
|
916
|
+
index_method?: { vector?: boolean; keyword?: boolean };
|
|
917
|
+
fusion_method?: "max" | "rrf";
|
|
696
918
|
last_activity?: string;
|
|
697
919
|
max_num_results?: number; // maximum: 50, minimum: 1, default: 10
|
|
698
920
|
metadata?: {
|
|
@@ -782,54 +1004,71 @@ export declare namespace AiSearch {
|
|
|
782
1004
|
}
|
|
783
1005
|
}
|
|
784
1006
|
|
|
1007
|
+
// ─── Namespace-scoped API Helper Functions ───────────────────────────────────
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Base path for namespace-scoped instance operations
|
|
1011
|
+
*/
|
|
1012
|
+
function aiSearchInstanceBasePath(
|
|
1013
|
+
api: CloudflareApi,
|
|
1014
|
+
namespace: string,
|
|
1015
|
+
): string {
|
|
1016
|
+
return `/accounts/${api.accountId}/ai-search/namespaces/${namespace}/instances`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
785
1019
|
export async function listAiSearchInstances(
|
|
786
1020
|
api: CloudflareApi,
|
|
1021
|
+
namespace = "default",
|
|
787
1022
|
): Promise<AiSearch.ApiResponse[]> {
|
|
788
1023
|
return await extractCloudflareResult<AiSearch.ApiResponse[]>(
|
|
789
1024
|
"list AI Search instances",
|
|
790
|
-
api.get(
|
|
1025
|
+
api.get(aiSearchInstanceBasePath(api, namespace)),
|
|
791
1026
|
);
|
|
792
1027
|
}
|
|
793
1028
|
|
|
794
1029
|
export async function createAiSearchInstance(
|
|
795
1030
|
api: CloudflareApi,
|
|
1031
|
+
namespace: string,
|
|
796
1032
|
payload: AiSearch.ApiPayload,
|
|
797
1033
|
): Promise<AiSearch.ApiResponse> {
|
|
798
1034
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
799
1035
|
`create AI Search instance "${payload.id}"`,
|
|
800
|
-
api.post(
|
|
1036
|
+
api.post(aiSearchInstanceBasePath(api, namespace), payload),
|
|
801
1037
|
);
|
|
802
1038
|
}
|
|
803
1039
|
|
|
804
1040
|
export async function getAiSearchInstance(
|
|
805
1041
|
api: CloudflareApi,
|
|
1042
|
+
namespace: string,
|
|
806
1043
|
id: string,
|
|
807
1044
|
): Promise<AiSearch.ApiResponse> {
|
|
808
1045
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
809
1046
|
`get AI Search instance "${id}"`,
|
|
810
|
-
api.get(
|
|
1047
|
+
api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
811
1048
|
);
|
|
812
1049
|
}
|
|
813
1050
|
|
|
814
1051
|
export async function updateAiSearchInstance(
|
|
815
1052
|
api: CloudflareApi,
|
|
1053
|
+
namespace: string,
|
|
816
1054
|
id: string,
|
|
817
1055
|
payload: AiSearch.ApiPayload,
|
|
818
1056
|
): Promise<AiSearch.ApiResponse> {
|
|
819
1057
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
820
1058
|
`update AI Search instance "${id}"`,
|
|
821
|
-
api.put(
|
|
1059
|
+
api.put(`${aiSearchInstanceBasePath(api, namespace)}/${id}`, payload),
|
|
822
1060
|
);
|
|
823
1061
|
}
|
|
824
1062
|
|
|
825
1063
|
export async function deleteAiSearchInstance(
|
|
826
1064
|
api: CloudflareApi,
|
|
1065
|
+
namespace: string,
|
|
827
1066
|
id: string,
|
|
828
1067
|
): Promise<void> {
|
|
829
1068
|
try {
|
|
830
1069
|
await extractCloudflareResult(
|
|
831
1070
|
`delete AI Search instance "${id}"`,
|
|
832
|
-
api.delete(
|
|
1071
|
+
api.delete(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
833
1072
|
);
|
|
834
1073
|
} catch (error) {
|
|
835
1074
|
if (error instanceof CloudflareApiError && error.status === 404) {
|
|
@@ -837,8 +1076,27 @@ export async function deleteAiSearchInstance(
|
|
|
837
1076
|
}
|
|
838
1077
|
throw error;
|
|
839
1078
|
}
|
|
1079
|
+
|
|
1080
|
+
// Cloudflare's DELETE returns 204 before the instance fully disappears
|
|
1081
|
+
// from the backing services. A subsequent GET can return the stale row
|
|
1082
|
+
// for a bounded window — same-colo is invalidated immediately via the
|
|
1083
|
+
// edge cache, cross-colo is bounded by a 60s KV TTL.
|
|
1084
|
+
//
|
|
1085
|
+
// Actively wait here so the destroy phase presents a strongly-
|
|
1086
|
+
// consistent "instance is gone" guarantee to its callers (child token
|
|
1087
|
+
// delete, user-facing teardown assertions, dependent resources).
|
|
1088
|
+
await poll({
|
|
1089
|
+
description: `wait for AI Search instance "${id}" deletion to propagate`,
|
|
1090
|
+
fn: () => api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
1091
|
+
predicate: (res) => res.status === 404,
|
|
1092
|
+
initialDelay: 500,
|
|
1093
|
+
maxDelay: 5000,
|
|
1094
|
+
timeout: 90_000,
|
|
1095
|
+
});
|
|
840
1096
|
}
|
|
841
1097
|
|
|
1098
|
+
// ─── Job API Helper Functions ────────────────────────────────────────────────
|
|
1099
|
+
|
|
842
1100
|
interface AiSearchJobApiResponse {
|
|
843
1101
|
id: string;
|
|
844
1102
|
source: "user" | "schedule";
|
|
@@ -850,24 +1108,24 @@ interface AiSearchJobApiResponse {
|
|
|
850
1108
|
|
|
851
1109
|
export async function listAiSearchJobs(
|
|
852
1110
|
api: CloudflareApi,
|
|
1111
|
+
namespace: string,
|
|
853
1112
|
aiSearchId: string,
|
|
854
1113
|
): Promise<AiSearchJobApiResponse[]> {
|
|
855
1114
|
return await extractCloudflareResult<AiSearchJobApiResponse[]>(
|
|
856
1115
|
`list AI Search jobs for instance "${aiSearchId}"`,
|
|
857
|
-
api.get(
|
|
858
|
-
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
|
|
859
|
-
),
|
|
1116
|
+
api.get(`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`),
|
|
860
1117
|
);
|
|
861
1118
|
}
|
|
862
1119
|
|
|
863
1120
|
export async function createAiSearchJob(
|
|
864
1121
|
api: CloudflareApi,
|
|
1122
|
+
namespace: string,
|
|
865
1123
|
aiSearchId: string,
|
|
866
1124
|
): Promise<AiSearchJobApiResponse> {
|
|
867
1125
|
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
868
1126
|
`create AI Search job for instance "${aiSearchId}"`,
|
|
869
1127
|
api.post(
|
|
870
|
-
|
|
1128
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`,
|
|
871
1129
|
{},
|
|
872
1130
|
),
|
|
873
1131
|
);
|
|
@@ -875,13 +1133,14 @@ export async function createAiSearchJob(
|
|
|
875
1133
|
|
|
876
1134
|
export async function getAiSearchJob(
|
|
877
1135
|
api: CloudflareApi,
|
|
1136
|
+
namespace: string,
|
|
878
1137
|
aiSearchId: string,
|
|
879
1138
|
jobId: string,
|
|
880
1139
|
): Promise<AiSearchJobApiResponse> {
|
|
881
1140
|
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
882
1141
|
`get AI Search job "${jobId}" for instance "${aiSearchId}"`,
|
|
883
1142
|
api.get(
|
|
884
|
-
|
|
1143
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}`,
|
|
885
1144
|
),
|
|
886
1145
|
);
|
|
887
1146
|
}
|
|
@@ -895,6 +1154,7 @@ interface AiSearchJobLogItem {
|
|
|
895
1154
|
|
|
896
1155
|
export async function listAiSearchJobLogs(
|
|
897
1156
|
api: CloudflareApi,
|
|
1157
|
+
namespace: string,
|
|
898
1158
|
aiSearchId: string,
|
|
899
1159
|
jobId: string,
|
|
900
1160
|
): Promise<AiSearchJobLogItem[]> {
|
|
@@ -902,7 +1162,7 @@ export async function listAiSearchJobLogs(
|
|
|
902
1162
|
return await extractCloudflareResult<AiSearchJobLogItem[]>(
|
|
903
1163
|
`list AI Search job logs for job "${jobId}" for instance "${aiSearchId}"`,
|
|
904
1164
|
api.get(
|
|
905
|
-
|
|
1165
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
|
|
906
1166
|
),
|
|
907
1167
|
);
|
|
908
1168
|
} catch (error) {
|
|
@@ -917,16 +1177,17 @@ export async function listAiSearchJobLogs(
|
|
|
917
1177
|
|
|
918
1178
|
export async function runAiSearchJob(
|
|
919
1179
|
api: CloudflareApi,
|
|
1180
|
+
namespace: string,
|
|
920
1181
|
aiSearchId: string,
|
|
921
1182
|
log: (message: string) => void,
|
|
922
1183
|
): Promise<void> {
|
|
923
1184
|
log("Preparing to index...");
|
|
924
|
-
const job = await createAiSearchJob(api, aiSearchId);
|
|
1185
|
+
const job = await createAiSearchJob(api, namespace, aiSearchId);
|
|
925
1186
|
let lastLogId = 0;
|
|
926
1187
|
let done = false;
|
|
927
1188
|
const resultPromise = poll({
|
|
928
1189
|
description: `run AI Search job "${job.id}" for instance "${aiSearchId}"`,
|
|
929
|
-
fn: () => getAiSearchJob(api, aiSearchId, job.id),
|
|
1190
|
+
fn: () => getAiSearchJob(api, namespace, aiSearchId, job.id),
|
|
930
1191
|
predicate: (result) => result.ended_at !== null,
|
|
931
1192
|
});
|
|
932
1193
|
pollLogs();
|
|
@@ -936,7 +1197,7 @@ export async function runAiSearchJob(
|
|
|
936
1197
|
log(`Sync completed: ${result.end_reason}`);
|
|
937
1198
|
|
|
938
1199
|
async function pollLogs() {
|
|
939
|
-
const logs = await listAiSearchJobLogs(api, aiSearchId, job.id);
|
|
1200
|
+
const logs = await listAiSearchJobLogs(api, namespace, aiSearchId, job.id);
|
|
940
1201
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
941
1202
|
const item = logs[i];
|
|
942
1203
|
if (item.id > lastLogId) {
|