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.
Files changed (82) hide show
  1. package/bin/alchemy.js +2 -2
  2. package/lib/cloudflare/ai-search-namespace.d.ts +150 -0
  3. package/lib/cloudflare/ai-search-namespace.d.ts.map +1 -0
  4. package/lib/cloudflare/ai-search-namespace.js +278 -0
  5. package/lib/cloudflare/ai-search-namespace.js.map +1 -0
  6. package/lib/cloudflare/ai-search-token.d.ts +11 -1
  7. package/lib/cloudflare/ai-search-token.d.ts.map +1 -1
  8. package/lib/cloudflare/ai-search-token.js +35 -7
  9. package/lib/cloudflare/ai-search-token.js.map +1 -1
  10. package/lib/cloudflare/ai-search.d.ts +123 -18
  11. package/lib/cloudflare/ai-search.d.ts.map +1 -1
  12. package/lib/cloudflare/ai-search.js +222 -68
  13. package/lib/cloudflare/ai-search.js.map +1 -1
  14. package/lib/cloudflare/bindings.d.ts +29 -2
  15. package/lib/cloudflare/bindings.d.ts.map +1 -1
  16. package/lib/cloudflare/bindings.js.map +1 -1
  17. package/lib/cloudflare/bound.d.ts +4 -2
  18. package/lib/cloudflare/bound.d.ts.map +1 -1
  19. package/lib/cloudflare/container.d.ts +79 -6
  20. package/lib/cloudflare/container.d.ts.map +1 -1
  21. package/lib/cloudflare/container.js +130 -9
  22. package/lib/cloudflare/container.js.map +1 -1
  23. package/lib/cloudflare/custom-domain.d.ts +14 -0
  24. package/lib/cloudflare/custom-domain.d.ts.map +1 -1
  25. package/lib/cloudflare/custom-domain.js +3 -0
  26. package/lib/cloudflare/custom-domain.js.map +1 -1
  27. package/lib/cloudflare/index.d.ts +1 -0
  28. package/lib/cloudflare/index.d.ts.map +1 -1
  29. package/lib/cloudflare/index.js +1 -0
  30. package/lib/cloudflare/index.js.map +1 -1
  31. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  32. package/lib/cloudflare/miniflare/build-worker-options.js +43 -0
  33. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  34. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  35. package/lib/cloudflare/miniflare/remote-binding-proxy.js +31 -13
  36. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  37. package/lib/cloudflare/website.d.ts +4 -0
  38. package/lib/cloudflare/website.d.ts.map +1 -1
  39. package/lib/cloudflare/website.js +15 -1
  40. package/lib/cloudflare/website.js.map +1 -1
  41. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  42. package/lib/cloudflare/worker-metadata.js +25 -1
  43. package/lib/cloudflare/worker-metadata.js.map +1 -1
  44. package/lib/cloudflare/worker.d.ts +6 -0
  45. package/lib/cloudflare/worker.d.ts.map +1 -1
  46. package/lib/cloudflare/worker.js +3 -0
  47. package/lib/cloudflare/worker.js.map +1 -1
  48. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  49. package/lib/cloudflare/wrangler.json.js +32 -0
  50. package/lib/cloudflare/wrangler.json.js.map +1 -1
  51. package/lib/docker/api.d.ts +4 -1
  52. package/lib/docker/api.d.ts.map +1 -1
  53. package/lib/docker/api.js +8 -2
  54. package/lib/docker/api.js.map +1 -1
  55. package/lib/docker/image.d.ts +1 -1
  56. package/lib/docker/image.d.ts.map +1 -1
  57. package/lib/docker/image.js +4 -44
  58. package/lib/docker/image.js.map +1 -1
  59. package/lib/docker/registry.d.ts +11 -0
  60. package/lib/docker/registry.d.ts.map +1 -0
  61. package/lib/docker/registry.js +41 -0
  62. package/lib/docker/registry.js.map +1 -0
  63. package/package.json +3 -3
  64. package/src/cloudflare/ai-search-namespace.ts +435 -0
  65. package/src/cloudflare/ai-search-token.ts +43 -9
  66. package/src/cloudflare/ai-search.ts +334 -73
  67. package/src/cloudflare/bindings.ts +33 -0
  68. package/src/cloudflare/bound.ts +87 -74
  69. package/src/cloudflare/container.ts +242 -18
  70. package/src/cloudflare/custom-domain.ts +25 -1
  71. package/src/cloudflare/index.ts +1 -0
  72. package/src/cloudflare/miniflare/build-worker-options.ts +50 -1
  73. package/src/cloudflare/miniflare/remote-binding-proxy.ts +58 -34
  74. package/src/cloudflare/website.ts +19 -1
  75. package/src/cloudflare/worker-metadata.ts +25 -1
  76. package/src/cloudflare/worker.ts +9 -0
  77. package/src/cloudflare/wrangler.json.ts +32 -0
  78. package/src/docker/api.ts +11 -2
  79. package/src/docker/image.ts +5 -56
  80. package/src/docker/registry.ts +62 -0
  81. package/workers/cloudflare-state-store.js +57 -57
  82. 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
- * Can be an R2Bucket directly, an R2 source config, or a web crawler config.
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
- source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
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 of the AI Search instance (this is an alias for the `id` property)
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}": ${error instanceof Error ? error.message : String(error)}`,
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: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource,
415
+ source:
416
+ | R2Bucket
417
+ | AiSearchR2Source
418
+ | AiSearchWebCrawlerSource
419
+ | undefined,
298
420
  ): Promise<
299
- (AiSearchR2Source & { bucket: string }) | AiSearchWebCrawlerSource
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
- const normalizeTokenId = async (): Promise<string> => {
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: props.adopt,
353
- delete: props.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
- await deleteIndex(api, this.output.vectorizeName);
362
- await deleteAiSearchInstance(api, this.output.id);
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
- const [source, tokenId] = await Promise.all([
374
- normalizeSource(props.source),
375
- normalizeTokenId(),
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.type === "r2" ? source.bucket : source.domain,
381
- type: source.type,
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
- 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
- },
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
- if (replace || replaceLegacy) {
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(api, this.output.id, payload);
600
+ instance = await updateAiSearchInstance(
601
+ api,
602
+ namespace,
603
+ this.output.id,
604
+ payload,
605
+ );
438
606
  } 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,
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: string;
580
- type: "r2" | "web-crawler";
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: string;
674
- type: "r2" | "web-crawler";
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(`/accounts/${api.accountId}/ai-search/instances`),
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(`/accounts/${api.accountId}/ai-search/instances`, payload),
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(`/accounts/${api.accountId}/ai-search/instances/${id}`),
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(`/accounts/${api.accountId}/ai-search/instances/${id}`, payload),
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(`/accounts/${api.accountId}/ai-search/instances/${id}`),
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
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
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
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}`,
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
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
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) {