alchemy 0.92.2 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy.js +1 -1
- 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 +281 -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 +239 -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 +117 -2
- package/lib/cloudflare/container.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 +37 -0
- package/lib/cloudflare/miniflare/build-worker-options.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/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/package.json +2 -2
- package/src/cloudflare/ai-search-namespace.ts +439 -0
- package/src/cloudflare/ai-search-token.ts +43 -9
- package/src/cloudflare/ai-search.ts +353 -73
- package/src/cloudflare/bindings.ts +33 -0
- package/src/cloudflare/bound.ts +87 -74
- package/src/cloudflare/container.ts +228 -11
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +39 -0
- package/src/cloudflare/worker-metadata.ts +25 -1
- package/src/cloudflare/wrangler.json.ts +32 -0
- package/src/docker/api.ts +11 -2
- 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.
|
|
78
|
+
*/
|
|
79
|
+
indexMethod?: { vector?: boolean; keyword?: boolean };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fusion method for combining vector and keyword results.
|
|
83
|
+
*
|
|
84
|
+
* @default "rrf"
|
|
41
85
|
*/
|
|
42
|
-
|
|
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,39 @@ 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
|
+
if (this.scope.local) {
|
|
377
|
+
// Local development mode — return a mock shape so `alchemy dev` does
|
|
378
|
+
// not hit the real Cloudflare API. Matches the AiSearchNamespace
|
|
379
|
+
// local-mode pattern and satisfies AGENTS.md requirements.
|
|
380
|
+
const name =
|
|
381
|
+
props.name ??
|
|
382
|
+
this.output?.name ??
|
|
383
|
+
this.scope.createPhysicalName(id, "-", 32);
|
|
384
|
+
const now = new Date().toISOString();
|
|
385
|
+
// `AiSearch` is `SnakeToCamel<ApiResponse>` intersected with extras —
|
|
386
|
+
// all non-required fields are optional, so we only populate what's
|
|
387
|
+
// needed. No `as unknown as` escape hatch required.
|
|
388
|
+
const mock: AiSearch = {
|
|
389
|
+
id: name,
|
|
390
|
+
name,
|
|
391
|
+
namespace,
|
|
392
|
+
accountId: "local",
|
|
393
|
+
accountTag: "local",
|
|
394
|
+
createdAt: now,
|
|
395
|
+
internalId: name,
|
|
396
|
+
modifiedAt: now,
|
|
397
|
+
vectorizeName: "",
|
|
398
|
+
};
|
|
399
|
+
return mock;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const api = await createCloudflareApi(props);
|
|
403
|
+
|
|
269
404
|
const validateBucketSource = async (
|
|
270
405
|
bucket: R2Bucket | string,
|
|
271
406
|
jurisdiction: R2BucketJurisdiction = "default",
|
|
@@ -288,16 +423,25 @@ export const AiSearch = Resource(
|
|
|
288
423
|
await getBucket(api, name, { jurisdiction });
|
|
289
424
|
} catch (error) {
|
|
290
425
|
throw new Error(
|
|
291
|
-
`Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${
|
|
426
|
+
`Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${
|
|
427
|
+
error instanceof Error ? error.message : String(error)
|
|
428
|
+
}`,
|
|
292
429
|
{ cause: error },
|
|
293
430
|
);
|
|
294
431
|
}
|
|
295
432
|
};
|
|
296
433
|
const normalizeSource = async (
|
|
297
|
-
source:
|
|
434
|
+
source:
|
|
435
|
+
| R2Bucket
|
|
436
|
+
| AiSearchR2Source
|
|
437
|
+
| AiSearchWebCrawlerSource
|
|
438
|
+
| undefined,
|
|
298
439
|
): Promise<
|
|
299
|
-
(AiSearchR2Source & { bucket: string })
|
|
440
|
+
| (AiSearchR2Source & { bucket: string })
|
|
441
|
+
| AiSearchWebCrawlerSource
|
|
442
|
+
| undefined
|
|
300
443
|
> => {
|
|
444
|
+
if (!source) return undefined;
|
|
301
445
|
if (isBucket(source)) {
|
|
302
446
|
await validateBucketSource(source, source.jurisdiction);
|
|
303
447
|
return {
|
|
@@ -334,14 +478,34 @@ export const AiSearch = Resource(
|
|
|
334
478
|
);
|
|
335
479
|
}
|
|
336
480
|
};
|
|
337
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Only resolve token for R2 sources. Web-crawler and built-in storage
|
|
483
|
+
* instances do not require a token.
|
|
484
|
+
*/
|
|
485
|
+
const normalizeTokenId = async (
|
|
486
|
+
source:
|
|
487
|
+
| (AiSearchR2Source & { bucket: string })
|
|
488
|
+
| AiSearchWebCrawlerSource
|
|
489
|
+
| undefined,
|
|
490
|
+
): Promise<string | undefined> => {
|
|
491
|
+
// Token only needed for R2 sources
|
|
492
|
+
if (!source || source.type !== "r2") {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
338
495
|
if ("tokenId" in props) {
|
|
339
496
|
validateTokenId(props.tokenId);
|
|
340
497
|
return props.tokenId;
|
|
341
|
-
} else if (props.token) {
|
|
498
|
+
} else if ("token" in props && props.token) {
|
|
342
499
|
validateTokenId(props.token.tokenId);
|
|
343
500
|
return props.token.tokenId;
|
|
344
501
|
} else {
|
|
502
|
+
// Auto-created tokens are an implementation detail of this resource —
|
|
503
|
+
// `adopt: true` so updates reuse a prior auto-created token with the
|
|
504
|
+
// same stable child id. We forward `delete: false` when the user
|
|
505
|
+
// preserves the instance, because Cloudflare rejects token deletion
|
|
506
|
+
// while the token is still referenced by the preserved instance
|
|
507
|
+
// (error 7076 `token_in_use_by_instances`). When the instance IS
|
|
508
|
+
// being deleted normally, the token deletes with it.
|
|
345
509
|
const token = await AiSearchToken("token", {
|
|
346
510
|
baseUrl: props.baseUrl,
|
|
347
511
|
profile: props.profile,
|
|
@@ -349,8 +513,8 @@ export const AiSearch = Resource(
|
|
|
349
513
|
apiToken: props.apiToken,
|
|
350
514
|
accountId: props.accountId,
|
|
351
515
|
email: props.email,
|
|
352
|
-
adopt:
|
|
353
|
-
delete:
|
|
516
|
+
adopt: true,
|
|
517
|
+
...(props.delete === false ? { delete: false } : {}),
|
|
354
518
|
});
|
|
355
519
|
return token.tokenId;
|
|
356
520
|
}
|
|
@@ -358,8 +522,11 @@ export const AiSearch = Resource(
|
|
|
358
522
|
|
|
359
523
|
if (this.phase === "delete") {
|
|
360
524
|
if (props.delete !== false && this.output?.id) {
|
|
361
|
-
|
|
362
|
-
|
|
525
|
+
if (this.output.vectorizeName) {
|
|
526
|
+
await deleteIndex(api, this.output.vectorizeName);
|
|
527
|
+
}
|
|
528
|
+
const deleteNs = this.output.namespace ?? "default";
|
|
529
|
+
await deleteAiSearchInstance(api, deleteNs, this.output.id);
|
|
363
530
|
}
|
|
364
531
|
return this.destroy();
|
|
365
532
|
}
|
|
@@ -370,35 +537,40 @@ export const AiSearch = Resource(
|
|
|
370
537
|
`AI Search instance name must be 1-32 characters, got ${name.length} ("${name}")`,
|
|
371
538
|
);
|
|
372
539
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
]);
|
|
540
|
+
|
|
541
|
+
const source = await normalizeSource(props.source);
|
|
542
|
+
const tokenId = await normalizeTokenId(source);
|
|
377
543
|
|
|
378
544
|
const payload: AiSearch.ApiPayload = {
|
|
379
545
|
id: name,
|
|
380
|
-
source: source
|
|
381
|
-
|
|
546
|
+
source: source
|
|
547
|
+
? source.type === "r2"
|
|
548
|
+
? source.bucket
|
|
549
|
+
: source.domain
|
|
550
|
+
: undefined,
|
|
551
|
+
type: source?.type,
|
|
382
552
|
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
|
-
|
|
553
|
+
source_params: source
|
|
554
|
+
? {
|
|
555
|
+
include_items: source.includePaths,
|
|
556
|
+
exclude_items: source.excludePaths,
|
|
557
|
+
...(source.type === "r2"
|
|
558
|
+
? {
|
|
559
|
+
r2_jurisdiction:
|
|
560
|
+
source.jurisdiction !== "default"
|
|
561
|
+
? source.jurisdiction
|
|
562
|
+
: undefined,
|
|
563
|
+
prefix: source.prefix,
|
|
564
|
+
}
|
|
565
|
+
: {
|
|
566
|
+
web_crawler: {
|
|
567
|
+
parse_type: source.parseType,
|
|
568
|
+
parse_options: source.parseOptions,
|
|
569
|
+
store_options: source.storeOptions,
|
|
570
|
+
},
|
|
571
|
+
}),
|
|
572
|
+
}
|
|
573
|
+
: undefined,
|
|
402
574
|
embedding_model: props.embeddingModel,
|
|
403
575
|
chunk: props.chunk,
|
|
404
576
|
chunk_size: props.chunkSize,
|
|
@@ -413,6 +585,8 @@ export const AiSearch = Resource(
|
|
|
413
585
|
cache_threshold: props.cacheThreshold,
|
|
414
586
|
metadata: props.metadata,
|
|
415
587
|
token_id: tokenId,
|
|
588
|
+
index_method: props.indexMethod,
|
|
589
|
+
fusion_method: props.fusionMethod,
|
|
416
590
|
};
|
|
417
591
|
|
|
418
592
|
let instance: AiSearch.ApiResponse;
|
|
@@ -431,29 +605,73 @@ export const AiSearch = Resource(
|
|
|
431
605
|
(payload.type === "web-crawler" &&
|
|
432
606
|
"sourceDomain" in this.output &&
|
|
433
607
|
payload.source !== this.output.sourceDomain));
|
|
434
|
-
|
|
608
|
+
// Namespace is immutable: moving an instance between namespaces must
|
|
609
|
+
// replace (delete old, create new) rather than attempt an in-place
|
|
610
|
+
// update against a non-existent `PUT /namespaces/{new}/instances/{id}`.
|
|
611
|
+
// Default to "default" for legacy state files that predate the
|
|
612
|
+
// namespace prop.
|
|
613
|
+
const currentNamespace =
|
|
614
|
+
("namespace" in this.output && this.output.namespace) || "default";
|
|
615
|
+
const namespaceChanged = currentNamespace !== namespace;
|
|
616
|
+
if (replace || replaceLegacy || namespaceChanged) {
|
|
435
617
|
return this.replace(true);
|
|
436
618
|
}
|
|
437
|
-
instance = await updateAiSearchInstance(
|
|
619
|
+
instance = await updateAiSearchInstance(
|
|
620
|
+
api,
|
|
621
|
+
namespace,
|
|
622
|
+
this.output.id,
|
|
623
|
+
payload,
|
|
624
|
+
);
|
|
438
625
|
} else {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
626
|
+
// Pre-flight adopt: if `adopt: true` and an instance already exists,
|
|
627
|
+
// adopt it directly via GET→PUT. This avoids racing on 400/7022 error
|
|
628
|
+
// codes and the fragile `errorData as CloudflareApiErrorPayload[]` cast.
|
|
629
|
+
if (adopt) {
|
|
630
|
+
const existing = await getAiSearchInstance(api, namespace, name).catch(
|
|
631
|
+
(e: unknown) => {
|
|
632
|
+
if (e instanceof CloudflareApiError && e.status === 404) {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
throw e;
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
if (existing) {
|
|
639
|
+
instance = await updateAiSearchInstance(
|
|
640
|
+
api,
|
|
641
|
+
namespace,
|
|
642
|
+
existing.id,
|
|
643
|
+
payload,
|
|
447
644
|
);
|
|
448
|
-
if (isAlreadyExistsError && adopt) {
|
|
449
|
-
instance = await getAiSearchInstance(api, name);
|
|
450
|
-
instance = await updateAiSearchInstance(api, instance.id, payload);
|
|
451
645
|
} else {
|
|
646
|
+
instance = await createAiSearchInstance(api, namespace, payload);
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
try {
|
|
650
|
+
instance = await createAiSearchInstance(api, namespace, payload);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
// Wrap "already exists" errors with a clearer message pointing at
|
|
653
|
+
// the adoption path (AGENTS.md convention).
|
|
654
|
+
const errorData = Array.isArray(
|
|
655
|
+
(error as CloudflareApiError | undefined)?.errorData,
|
|
656
|
+
)
|
|
657
|
+
? ((error as CloudflareApiError)
|
|
658
|
+
.errorData as CloudflareApiErrorPayload[])
|
|
659
|
+
: [];
|
|
660
|
+
const isAlreadyExistsError =
|
|
661
|
+
error instanceof CloudflareApiError &&
|
|
662
|
+
error.status === 400 &&
|
|
663
|
+
errorData.some((e) => e.code === 7022);
|
|
664
|
+
if (isAlreadyExistsError) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`AI Search instance "${name}" already exists in namespace "${namespace}". Use \`adopt: true\` to adopt it.`,
|
|
667
|
+
{ cause: error },
|
|
668
|
+
);
|
|
669
|
+
}
|
|
452
670
|
throw error;
|
|
453
671
|
}
|
|
454
672
|
}
|
|
455
|
-
if (props.indexOnCreate !== false) {
|
|
456
|
-
await runAiSearchJob(api, instance.id, (message) =>
|
|
673
|
+
if (props.indexOnCreate !== false && source) {
|
|
674
|
+
await runAiSearchJob(api, namespace, instance.id, (message) =>
|
|
457
675
|
logger.task(id, {
|
|
458
676
|
prefix: "index",
|
|
459
677
|
prefixColor: "gray",
|
|
@@ -461,15 +679,33 @@ export const AiSearch = Resource(
|
|
|
461
679
|
message,
|
|
462
680
|
}),
|
|
463
681
|
);
|
|
682
|
+
} else if (props.indexOnCreate === true && !source) {
|
|
683
|
+
logger.warn(
|
|
684
|
+
`AI Search "${id}": \`indexOnCreate: true\` has no effect because no \`source\` was provided.`,
|
|
685
|
+
);
|
|
464
686
|
}
|
|
465
687
|
}
|
|
466
688
|
return {
|
|
467
689
|
...snakeToCamelObjectDeep(instance),
|
|
468
690
|
name: instance.id,
|
|
691
|
+
// The API response may include a `namespace` field; explicitly use the
|
|
692
|
+
// resolved local value to ensure the output always matches the prop
|
|
693
|
+
// (and defaults to "default" when unspecified).
|
|
694
|
+
namespace,
|
|
469
695
|
};
|
|
470
696
|
},
|
|
471
697
|
);
|
|
472
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Resolve a namespace prop to a string name.
|
|
701
|
+
*/
|
|
702
|
+
function resolveNamespace(ns: string | AiSearchNamespace | undefined): string {
|
|
703
|
+
if (!ns) return "default";
|
|
704
|
+
if (typeof ns === "string") return ns;
|
|
705
|
+
if (isAiSearchNamespace(ns)) return ns.namespace;
|
|
706
|
+
return "default";
|
|
707
|
+
}
|
|
708
|
+
|
|
473
709
|
/**
|
|
474
710
|
* Validate that a domain string is a valid domain format (not a URL).
|
|
475
711
|
* Throws a helpful error if the input looks like a URL.
|
|
@@ -576,8 +812,8 @@ export declare namespace AiSearch {
|
|
|
576
812
|
|
|
577
813
|
interface ApiPayload {
|
|
578
814
|
id: string;
|
|
579
|
-
source
|
|
580
|
-
type
|
|
815
|
+
source?: string;
|
|
816
|
+
type?: "r2" | "web-crawler";
|
|
581
817
|
ai_gateway_id?: string;
|
|
582
818
|
ai_search_model?: Model;
|
|
583
819
|
cache?: boolean;
|
|
@@ -599,6 +835,8 @@ export declare namespace AiSearch {
|
|
|
599
835
|
}>;
|
|
600
836
|
embedding_model?: EmbeddingModel;
|
|
601
837
|
hybrid_search_enabled?: boolean;
|
|
838
|
+
index_method?: { vector?: boolean; keyword?: boolean };
|
|
839
|
+
fusion_method?: "max" | "rrf";
|
|
602
840
|
max_num_results?: number;
|
|
603
841
|
metadata?: {
|
|
604
842
|
created_from_aisearch_wizard?: boolean;
|
|
@@ -670,9 +908,10 @@ export declare namespace AiSearch {
|
|
|
670
908
|
created_at: string;
|
|
671
909
|
internal_id: string;
|
|
672
910
|
modified_at: string;
|
|
673
|
-
source
|
|
674
|
-
type
|
|
911
|
+
source?: string;
|
|
912
|
+
type?: "r2" | "web-crawler";
|
|
675
913
|
vectorize_name: string;
|
|
914
|
+
namespace?: string;
|
|
676
915
|
ai_gateway_id?: string;
|
|
677
916
|
ai_search_model?: Model;
|
|
678
917
|
cache?: boolean; // default: true
|
|
@@ -693,6 +932,8 @@ export declare namespace AiSearch {
|
|
|
693
932
|
enable?: boolean;
|
|
694
933
|
engine_version?: number; // default: 1
|
|
695
934
|
hybrid_search_enabled?: boolean;
|
|
935
|
+
index_method?: { vector?: boolean; keyword?: boolean };
|
|
936
|
+
fusion_method?: "max" | "rrf";
|
|
696
937
|
last_activity?: string;
|
|
697
938
|
max_num_results?: number; // maximum: 50, minimum: 1, default: 10
|
|
698
939
|
metadata?: {
|
|
@@ -782,54 +1023,71 @@ export declare namespace AiSearch {
|
|
|
782
1023
|
}
|
|
783
1024
|
}
|
|
784
1025
|
|
|
1026
|
+
// ─── Namespace-scoped API Helper Functions ───────────────────────────────────
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Base path for namespace-scoped instance operations
|
|
1030
|
+
*/
|
|
1031
|
+
function aiSearchInstanceBasePath(
|
|
1032
|
+
api: CloudflareApi,
|
|
1033
|
+
namespace: string,
|
|
1034
|
+
): string {
|
|
1035
|
+
return `/accounts/${api.accountId}/ai-search/namespaces/${namespace}/instances`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
785
1038
|
export async function listAiSearchInstances(
|
|
786
1039
|
api: CloudflareApi,
|
|
1040
|
+
namespace = "default",
|
|
787
1041
|
): Promise<AiSearch.ApiResponse[]> {
|
|
788
1042
|
return await extractCloudflareResult<AiSearch.ApiResponse[]>(
|
|
789
1043
|
"list AI Search instances",
|
|
790
|
-
api.get(
|
|
1044
|
+
api.get(aiSearchInstanceBasePath(api, namespace)),
|
|
791
1045
|
);
|
|
792
1046
|
}
|
|
793
1047
|
|
|
794
1048
|
export async function createAiSearchInstance(
|
|
795
1049
|
api: CloudflareApi,
|
|
1050
|
+
namespace: string,
|
|
796
1051
|
payload: AiSearch.ApiPayload,
|
|
797
1052
|
): Promise<AiSearch.ApiResponse> {
|
|
798
1053
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
799
1054
|
`create AI Search instance "${payload.id}"`,
|
|
800
|
-
api.post(
|
|
1055
|
+
api.post(aiSearchInstanceBasePath(api, namespace), payload),
|
|
801
1056
|
);
|
|
802
1057
|
}
|
|
803
1058
|
|
|
804
1059
|
export async function getAiSearchInstance(
|
|
805
1060
|
api: CloudflareApi,
|
|
1061
|
+
namespace: string,
|
|
806
1062
|
id: string,
|
|
807
1063
|
): Promise<AiSearch.ApiResponse> {
|
|
808
1064
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
809
1065
|
`get AI Search instance "${id}"`,
|
|
810
|
-
api.get(
|
|
1066
|
+
api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
811
1067
|
);
|
|
812
1068
|
}
|
|
813
1069
|
|
|
814
1070
|
export async function updateAiSearchInstance(
|
|
815
1071
|
api: CloudflareApi,
|
|
1072
|
+
namespace: string,
|
|
816
1073
|
id: string,
|
|
817
1074
|
payload: AiSearch.ApiPayload,
|
|
818
1075
|
): Promise<AiSearch.ApiResponse> {
|
|
819
1076
|
return await extractCloudflareResult<AiSearch.ApiResponse>(
|
|
820
1077
|
`update AI Search instance "${id}"`,
|
|
821
|
-
api.put(
|
|
1078
|
+
api.put(`${aiSearchInstanceBasePath(api, namespace)}/${id}`, payload),
|
|
822
1079
|
);
|
|
823
1080
|
}
|
|
824
1081
|
|
|
825
1082
|
export async function deleteAiSearchInstance(
|
|
826
1083
|
api: CloudflareApi,
|
|
1084
|
+
namespace: string,
|
|
827
1085
|
id: string,
|
|
828
1086
|
): Promise<void> {
|
|
829
1087
|
try {
|
|
830
1088
|
await extractCloudflareResult(
|
|
831
1089
|
`delete AI Search instance "${id}"`,
|
|
832
|
-
api.delete(
|
|
1090
|
+
api.delete(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
833
1091
|
);
|
|
834
1092
|
} catch (error) {
|
|
835
1093
|
if (error instanceof CloudflareApiError && error.status === 404) {
|
|
@@ -837,8 +1095,27 @@ export async function deleteAiSearchInstance(
|
|
|
837
1095
|
}
|
|
838
1096
|
throw error;
|
|
839
1097
|
}
|
|
1098
|
+
|
|
1099
|
+
// Cloudflare's DELETE returns 204 before the instance fully disappears
|
|
1100
|
+
// from the backing services. A subsequent GET can return the stale row
|
|
1101
|
+
// for a bounded window — same-colo is invalidated immediately via the
|
|
1102
|
+
// edge cache, cross-colo is bounded by a 60s KV TTL.
|
|
1103
|
+
//
|
|
1104
|
+
// Actively wait here so the destroy phase presents a strongly-
|
|
1105
|
+
// consistent "instance is gone" guarantee to its callers (child token
|
|
1106
|
+
// delete, user-facing teardown assertions, dependent resources).
|
|
1107
|
+
await poll({
|
|
1108
|
+
description: `wait for AI Search instance "${id}" deletion to propagate`,
|
|
1109
|
+
fn: () => api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
|
|
1110
|
+
predicate: (res) => res.status === 404,
|
|
1111
|
+
initialDelay: 500,
|
|
1112
|
+
maxDelay: 5000,
|
|
1113
|
+
timeout: 90_000,
|
|
1114
|
+
});
|
|
840
1115
|
}
|
|
841
1116
|
|
|
1117
|
+
// ─── Job API Helper Functions ────────────────────────────────────────────────
|
|
1118
|
+
|
|
842
1119
|
interface AiSearchJobApiResponse {
|
|
843
1120
|
id: string;
|
|
844
1121
|
source: "user" | "schedule";
|
|
@@ -850,24 +1127,24 @@ interface AiSearchJobApiResponse {
|
|
|
850
1127
|
|
|
851
1128
|
export async function listAiSearchJobs(
|
|
852
1129
|
api: CloudflareApi,
|
|
1130
|
+
namespace: string,
|
|
853
1131
|
aiSearchId: string,
|
|
854
1132
|
): Promise<AiSearchJobApiResponse[]> {
|
|
855
1133
|
return await extractCloudflareResult<AiSearchJobApiResponse[]>(
|
|
856
1134
|
`list AI Search jobs for instance "${aiSearchId}"`,
|
|
857
|
-
api.get(
|
|
858
|
-
`/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
|
|
859
|
-
),
|
|
1135
|
+
api.get(`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`),
|
|
860
1136
|
);
|
|
861
1137
|
}
|
|
862
1138
|
|
|
863
1139
|
export async function createAiSearchJob(
|
|
864
1140
|
api: CloudflareApi,
|
|
1141
|
+
namespace: string,
|
|
865
1142
|
aiSearchId: string,
|
|
866
1143
|
): Promise<AiSearchJobApiResponse> {
|
|
867
1144
|
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
868
1145
|
`create AI Search job for instance "${aiSearchId}"`,
|
|
869
1146
|
api.post(
|
|
870
|
-
|
|
1147
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`,
|
|
871
1148
|
{},
|
|
872
1149
|
),
|
|
873
1150
|
);
|
|
@@ -875,13 +1152,14 @@ export async function createAiSearchJob(
|
|
|
875
1152
|
|
|
876
1153
|
export async function getAiSearchJob(
|
|
877
1154
|
api: CloudflareApi,
|
|
1155
|
+
namespace: string,
|
|
878
1156
|
aiSearchId: string,
|
|
879
1157
|
jobId: string,
|
|
880
1158
|
): Promise<AiSearchJobApiResponse> {
|
|
881
1159
|
return await extractCloudflareResult<AiSearchJobApiResponse>(
|
|
882
1160
|
`get AI Search job "${jobId}" for instance "${aiSearchId}"`,
|
|
883
1161
|
api.get(
|
|
884
|
-
|
|
1162
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}`,
|
|
885
1163
|
),
|
|
886
1164
|
);
|
|
887
1165
|
}
|
|
@@ -895,6 +1173,7 @@ interface AiSearchJobLogItem {
|
|
|
895
1173
|
|
|
896
1174
|
export async function listAiSearchJobLogs(
|
|
897
1175
|
api: CloudflareApi,
|
|
1176
|
+
namespace: string,
|
|
898
1177
|
aiSearchId: string,
|
|
899
1178
|
jobId: string,
|
|
900
1179
|
): Promise<AiSearchJobLogItem[]> {
|
|
@@ -902,7 +1181,7 @@ export async function listAiSearchJobLogs(
|
|
|
902
1181
|
return await extractCloudflareResult<AiSearchJobLogItem[]>(
|
|
903
1182
|
`list AI Search job logs for job "${jobId}" for instance "${aiSearchId}"`,
|
|
904
1183
|
api.get(
|
|
905
|
-
|
|
1184
|
+
`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
|
|
906
1185
|
),
|
|
907
1186
|
);
|
|
908
1187
|
} catch (error) {
|
|
@@ -917,16 +1196,17 @@ export async function listAiSearchJobLogs(
|
|
|
917
1196
|
|
|
918
1197
|
export async function runAiSearchJob(
|
|
919
1198
|
api: CloudflareApi,
|
|
1199
|
+
namespace: string,
|
|
920
1200
|
aiSearchId: string,
|
|
921
1201
|
log: (message: string) => void,
|
|
922
1202
|
): Promise<void> {
|
|
923
1203
|
log("Preparing to index...");
|
|
924
|
-
const job = await createAiSearchJob(api, aiSearchId);
|
|
1204
|
+
const job = await createAiSearchJob(api, namespace, aiSearchId);
|
|
925
1205
|
let lastLogId = 0;
|
|
926
1206
|
let done = false;
|
|
927
1207
|
const resultPromise = poll({
|
|
928
1208
|
description: `run AI Search job "${job.id}" for instance "${aiSearchId}"`,
|
|
929
|
-
fn: () => getAiSearchJob(api, aiSearchId, job.id),
|
|
1209
|
+
fn: () => getAiSearchJob(api, namespace, aiSearchId, job.id),
|
|
930
1210
|
predicate: (result) => result.ended_at !== null,
|
|
931
1211
|
});
|
|
932
1212
|
pollLogs();
|
|
@@ -936,7 +1216,7 @@ export async function runAiSearchJob(
|
|
|
936
1216
|
log(`Sync completed: ${result.end_reason}`);
|
|
937
1217
|
|
|
938
1218
|
async function pollLogs() {
|
|
939
|
-
const logs = await listAiSearchJobLogs(api, aiSearchId, job.id);
|
|
1219
|
+
const logs = await listAiSearchJobLogs(api, namespace, aiSearchId, job.id);
|
|
940
1220
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
941
1221
|
const item = logs[i];
|
|
942
1222
|
if (item.id > lastLogId) {
|