From 0847a1d3e124ed202b10678282e915201032bcdb Mon Sep 17 00:00:00 2001 From: syn Date: Thu, 9 Jul 2026 14:57:18 -0500 Subject: [PATCH 1/5] feat(session-ingest): add dark-launched direct path --- services/session-ingest/.dev.vars.example | 3 + services/session-ingest/src/env.ts | 12 +- .../src/ingest/bounded-stream-reader.test.ts | 99 + .../src/ingest/bounded-stream-reader.ts | 55 + .../src/ingest/direct-ingest-rollout.test.ts | 108 + .../src/ingest/direct-ingest-rollout.ts | 90 + .../src/ingest/direct-ingest.ts | 409 ++ .../session-ingest/src/ingest/metadata.ts | 4 +- .../src/ingest/stage-and-enqueue.ts | 20 +- .../session-ingest/src/routes/api.test.ts | 438 +- services/session-ingest/src/routes/api.ts | 31 +- .../session-ingest/worker-configuration.d.ts | 4238 +++++++++++++++-- services/session-ingest/wrangler.jsonc | 5 + 13 files changed, 4961 insertions(+), 551 deletions(-) create mode 100644 services/session-ingest/src/ingest/bounded-stream-reader.test.ts create mode 100644 services/session-ingest/src/ingest/bounded-stream-reader.ts create mode 100644 services/session-ingest/src/ingest/direct-ingest-rollout.test.ts create mode 100644 services/session-ingest/src/ingest/direct-ingest-rollout.ts create mode 100644 services/session-ingest/src/ingest/direct-ingest.ts diff --git a/services/session-ingest/.dev.vars.example b/services/session-ingest/.dev.vars.example index e69de29bb2..6bb45db558 100644 --- a/services/session-ingest/.dev.vars.example +++ b/services/session-ingest/.dev.vars.example @@ -0,0 +1,3 @@ +DIRECT_INGEST_PERCENT="0" +DIRECT_INGEST_USER_IDS="" +DIRECT_INGEST_MAX_BYTES="4194304" diff --git a/services/session-ingest/src/env.ts b/services/session-ingest/src/env.ts index 911355d2be..6bc8f942a8 100644 --- a/services/session-ingest/src/env.ts +++ b/services/session-ingest/src/env.ts @@ -1,7 +1,17 @@ import type { NotificationsBinding } from './notifications-binding.js'; import type { O11YBinding } from './o11y-binding.js'; -export type Env = Omit & { +export type Env = Omit< + Cloudflare.Env, + | 'O11Y' + | 'NOTIFICATIONS' + | 'DIRECT_INGEST_PERCENT' + | 'DIRECT_INGEST_USER_IDS' + | 'DIRECT_INGEST_MAX_BYTES' +> & { O11Y: O11YBinding; NOTIFICATIONS: NotificationsBinding; + DIRECT_INGEST_PERCENT: string; + DIRECT_INGEST_USER_IDS: string; + DIRECT_INGEST_MAX_BYTES: string; }; diff --git a/services/session-ingest/src/ingest/bounded-stream-reader.test.ts b/services/session-ingest/src/ingest/bounded-stream-reader.test.ts new file mode 100644 index 0000000000..15f7f22052 --- /dev/null +++ b/services/session-ingest/src/ingest/bounded-stream-reader.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { readBoundedStream } from './bounded-stream-reader'; + +function streamFromChunks(chunks: number[][], cancel = vi.fn()) { + let nextChunk = 0; + return { + stream: new ReadableStream( + { + pull(controller) { + const chunk = chunks[nextChunk]; + if (chunk === undefined) { + controller.close(); + return; + } + nextChunk += 1; + controller.enqueue(Uint8Array.from(chunk)); + }, + cancel, + }, + { highWaterMark: 0 } + ), + cancel, + }; +} + +describe('readBoundedStream', () => { + it('accepts bytes exactly at both limits', async () => { + const { stream, cancel } = streamFromChunks([ + [1, 2], + [3, 4], + ]); + + await expect(readBoundedStream(stream, 4, 4)).resolves.toEqual({ + ok: true, + bytes: Uint8Array.from([1, 2, 3, 4]), + }); + expect(cancel).not.toHaveBeenCalled(); + }); + + it('rejects actual bytes over the declared size', async () => { + const { stream } = streamFromChunks([[1, 2, 3]]); + + await expect(readBoundedStream(stream, 2, 10)).resolves.toEqual({ + ok: false, + reason: 'too_large', + limit: 'declared_bytes', + }); + }); + + it('rejects a declaration over the configured cap without consuming bytes', async () => { + let pulled = false; + const cancel = vi.fn(); + const stream = new ReadableStream({ + pull(controller) { + pulled = true; + controller.enqueue(Uint8Array.from([1])); + }, + cancel, + }); + + await expect(readBoundedStream(stream, 11, 10)).resolves.toEqual({ + ok: false, + reason: 'too_large', + limit: 'configured_cap', + }); + expect(pulled).toBe(false); + expect(cancel).toHaveBeenCalledOnce(); + }); + + it('detects overflow accumulated across multiple chunks', async () => { + const { stream, cancel } = streamFromChunks([[1, 2], [3, 4], [5]]); + + await expect(readBoundedStream(stream, 4, 10)).resolves.toEqual({ + ok: false, + reason: 'too_large', + limit: 'declared_bytes', + }); + expect(cancel).toHaveBeenCalledOnce(); + }); + + it('cancels on actual-byte overflow and releases the reader lock', async () => { + const { stream, cancel } = streamFromChunks([[1, 2, 3]]); + + await readBoundedStream(stream, 2, 10); + + expect(cancel).toHaveBeenCalledOnce(); + expect(() => stream.getReader()).not.toThrow(); + }); + + it('preserves every byte and chunk ordering', async () => { + const { stream } = streamFromChunks([[0, 255], [], [17, 42, 128]]); + + await expect(readBoundedStream(stream, 5, 10)).resolves.toEqual({ + ok: true, + bytes: Uint8Array.from([0, 255, 17, 42, 128]), + }); + }); +}); diff --git a/services/session-ingest/src/ingest/bounded-stream-reader.ts b/services/session-ingest/src/ingest/bounded-stream-reader.ts new file mode 100644 index 0000000000..66424f4c32 --- /dev/null +++ b/services/session-ingest/src/ingest/bounded-stream-reader.ts @@ -0,0 +1,55 @@ +export type BoundedStreamReadResult = + | { ok: true; bytes: Uint8Array } + | { ok: false; reason: 'too_large'; limit: 'declared_bytes' | 'configured_cap' }; + +export async function readBoundedStream( + stream: ReadableStream, + declaredBytes: number, + configuredCap: number +): Promise { + const reader = stream.getReader(); + + try { + if (declaredBytes > configuredCap) { + await cancelReader(reader); + return { ok: false, reason: 'too_large', limit: 'configured_cap' }; + } + + const chunks: Uint8Array[] = []; + let totalBytes = 0; + + while (true) { + const result = await reader.read(); + if (result.done) { + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return { ok: true, bytes }; + } + + totalBytes += result.value.byteLength; + if (totalBytes > declaredBytes || totalBytes > configuredCap) { + await cancelReader(reader); + return { + ok: false, + reason: 'too_large', + limit: totalBytes > configuredCap ? 'configured_cap' : 'declared_bytes', + }; + } + chunks.push(result.value); + } + } finally { + reader.releaseLock(); + } +} + +async function cancelReader(reader: ReadableStreamDefaultReader): Promise { + try { + await reader.cancel('stream exceeded ingest byte limit'); + } catch { + // The typed overflow result remains authoritative if cancellation itself fails. + } +} diff --git a/services/session-ingest/src/ingest/direct-ingest-rollout.test.ts b/services/session-ingest/src/ingest/direct-ingest-rollout.test.ts new file mode 100644 index 0000000000..2066f9e6bd --- /dev/null +++ b/services/session-ingest/src/ingest/direct-ingest-rollout.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; + +import { + getDirectIngestUserBucket, + parseDirectIngestConfig, + selectDirectIngestUser, + type DirectIngestConfig, +} from './direct-ingest-rollout'; + +function validInput() { + return { + DIRECT_INGEST_PERCENT: '25', + DIRECT_INGEST_MAX_BYTES: '4194304', + DIRECT_INGEST_USER_IDS: '', + }; +} + +function config(overrides: Partial = {}): DirectIngestConfig { + return { percent: 25, maxBytes: 4_194_304, userIds: new Set(), ...overrides }; +} + +describe('parseDirectIngestConfig', () => { + it.each([undefined, '', ' ', '-1', '1.5', '01', '101', 'Infinity', '25 '])( + 'fails closed for invalid percent %j', + percent => { + expect(parseDirectIngestConfig({ ...validInput(), DIRECT_INGEST_PERCENT: percent })).toEqual({ + ok: false, + reason: 'invalid_percent', + }); + } + ); + + it.each([undefined, '', ' ', '0', '-1', '1.5', '01', '4194305', 'Infinity', '9007199254740992'])( + 'fails closed for invalid max bytes %j', + maxBytes => { + expect( + parseDirectIngestConfig({ ...validInput(), DIRECT_INGEST_MAX_BYTES: maxBytes }) + ).toEqual({ ok: false, reason: 'invalid_max_bytes' }); + } + ); + + it('accepts percent boundaries and the supported direct body cap', () => { + expect( + parseDirectIngestConfig({ + ...validInput(), + DIRECT_INGEST_PERCENT: '0', + DIRECT_INGEST_MAX_BYTES: '4194304', + }) + ).toMatchObject({ ok: true, config: { percent: 0, maxBytes: 4194304 } }); + + expect( + parseDirectIngestConfig({ ...validInput(), DIRECT_INGEST_PERCENT: '100' }) + ).toMatchObject({ ok: true, config: { percent: 100 } }); + }); + + it('trims comma-separated user IDs and removes empty and duplicate values', () => { + const result = parseDirectIngestConfig({ + ...validInput(), + DIRECT_INGEST_USER_IDS: ' user-a, user-b ,,user-a, ', + }); + + expect(result.ok && [...result.config.userIds]).toEqual(['user-a', 'user-b']); + }); + + it('fails closed when the user ID setting is missing', () => { + expect(parseDirectIngestConfig({ ...validInput(), DIRECT_INGEST_USER_IDS: undefined })).toEqual( + { ok: false, reason: 'invalid_user_ids' } + ); + }); +}); + +describe('selectDirectIngestUser', () => { + it('selects no percentage users at zero percent', async () => { + await expect(selectDirectIngestUser(config({ percent: 0 }), 'user-a')).resolves.toEqual({ + selected: false, + reason: 'not_selected', + bucket: null, + }); + }); + + it('selects every user at 100 percent', async () => { + await expect(selectDirectIngestUser(config({ percent: 100 }), 'user-a')).resolves.toEqual({ + selected: true, + reason: 'percentage', + bucket: null, + }); + }); + + it('allows an allowlisted user at zero percent', async () => { + await expect( + selectDirectIngestUser(config({ percent: 0, userIds: new Set(['user-a']) }), 'user-a') + ).resolves.toEqual({ selected: true, reason: 'allowlist', bucket: null }); + }); + + it('deterministically selects from the SHA-256 user bucket', async () => { + const bucket = 63; + + await expect(getDirectIngestUserBucket('stable-user')).resolves.toBe(63); + await expect(getDirectIngestUserBucket('usr_test')).resolves.toBe(84); + await expect(getDirectIngestUserBucket('rollout-user-a')).resolves.toBe(13); + await expect( + selectDirectIngestUser(config({ percent: bucket + 1 }), 'stable-user') + ).resolves.toEqual({ selected: true, reason: 'percentage', bucket }); + await expect( + selectDirectIngestUser(config({ percent: bucket }), 'stable-user') + ).resolves.toEqual({ selected: false, reason: 'not_selected', bucket }); + }); +}); diff --git a/services/session-ingest/src/ingest/direct-ingest-rollout.ts b/services/session-ingest/src/ingest/direct-ingest-rollout.ts new file mode 100644 index 0000000000..266e0cb146 --- /dev/null +++ b/services/session-ingest/src/ingest/direct-ingest-rollout.ts @@ -0,0 +1,90 @@ +export type DirectIngestConfigInput = { + DIRECT_INGEST_PERCENT: string | undefined; + DIRECT_INGEST_MAX_BYTES: string | undefined; + DIRECT_INGEST_USER_IDS: string | undefined; +}; + +export type DirectIngestConfig = { + percent: number; + maxBytes: number; + userIds: ReadonlySet; +}; + +export type DirectIngestConfigErrorReason = + | 'invalid_percent' + | 'invalid_max_bytes' + | 'invalid_user_ids'; + +export type DirectIngestConfigResult = + | { ok: true; config: DirectIngestConfig } + | { ok: false; reason: DirectIngestConfigErrorReason }; + +export type DirectIngestSelectionResult = + | { selected: true; reason: 'allowlist' | 'percentage'; bucket: number | null } + | { selected: false; reason: 'not_selected'; bucket: number | null }; + +const unsignedIntegerPattern = /^(0|[1-9]\d*)$/; + +export function parseDirectIngestConfig(input: DirectIngestConfigInput): DirectIngestConfigResult { + const percent = parseUnsignedInteger(input.DIRECT_INGEST_PERCENT); + if (percent === null || percent > 100) { + return { ok: false, reason: 'invalid_percent' }; + } + + const maxBytes = parseUnsignedInteger(input.DIRECT_INGEST_MAX_BYTES); + if (maxBytes === null || maxBytes === 0 || maxBytes > INGEST_CHUNK_MAX_BYTES) { + return { ok: false, reason: 'invalid_max_bytes' }; + } + + if (input.DIRECT_INGEST_USER_IDS === undefined) { + return { ok: false, reason: 'invalid_user_ids' }; + } + + const userIds = new Set( + input.DIRECT_INGEST_USER_IDS.split(',') + .map(userId => userId.trim()) + .filter(userId => userId.length > 0) + ); + + return { ok: true, config: { percent, maxBytes, userIds } }; +} + +export async function selectDirectIngestUser( + config: DirectIngestConfig, + kiloUserId: string +): Promise { + if (config.userIds.has(kiloUserId)) { + return { selected: true, reason: 'allowlist', bucket: null }; + } + + if (config.percent === 0) { + return { selected: false, reason: 'not_selected', bucket: null }; + } + + if (config.percent === 100) { + return { selected: true, reason: 'percentage', bucket: null }; + } + + const bucket = await getDirectIngestUserBucket(kiloUserId); + return bucket < config.percent + ? { selected: true, reason: 'percentage', bucket } + : { selected: false, reason: 'not_selected', bucket }; +} + +export async function getDirectIngestUserBucket(kiloUserId: string): Promise { + const digest = new Uint8Array( + await crypto.subtle.digest('SHA-256', new TextEncoder().encode(kiloUserId)) + ); + const firstFourBytes = new DataView(digest.buffer, digest.byteOffset, 4).getUint32(0); + return firstFourBytes % 100; +} + +function parseUnsignedInteger(value: string | undefined): number | null { + if (value === undefined || !unsignedIntegerPattern.test(value)) { + return null; + } + + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; +} +import { INGEST_CHUNK_MAX_BYTES } from '../util/ingest-limits'; diff --git a/services/session-ingest/src/ingest/direct-ingest.ts b/services/session-ingest/src/ingest/direct-ingest.ts new file mode 100644 index 0000000000..01aa3a1427 --- /dev/null +++ b/services/session-ingest/src/ingest/direct-ingest.ts @@ -0,0 +1,409 @@ +import { withDORetry } from '@kilocode/worker-utils'; + +import { getSessionIngestDO, type IngestResult } from '../dos/SessionIngestDO'; +import type { Env } from '../env'; +import { + INGEST_CHUNK_MAX_BYTES, + INGEST_CHUNK_MAX_ITEMS, + MAX_INGEST_ITEM_BYTES, +} from '../util/ingest-limits'; +import { readBoundedStream } from './bounded-stream-reader'; +import { parseDirectIngestConfig, selectDirectIngestUser } from './direct-ingest-rollout'; +import { applyMetadataChanges } from './metadata'; +import { + StageAndEnqueueError, + stageAndEnqueue, + type StageAndEnqueueFailureStage, +} from './stage-and-enqueue'; +import { validateAndParseIngestPayload } from './validate'; + +type DirectIngestContext = { waitUntil(promise: Promise): void }; + +export type DirectIngestRequest = { + env: Env; + body: ReadableStream; + contentLength: string | undefined; + kiloUserId: string; + sessionId: string; + ingestVersion: number; + ingestedAt: number; + ingestRequestId: string; + executionContext?: DirectIngestContext; +}; + +export type DirectIngestResponse = + | { status: 200; body: { success: true } } + | { status: 400; body: { success: false; error: 'malformed_json' } } + | { status: 404; body: { success: false; error: 'session_not_found' } } + | { status: 413; body: { success: false; error: 'payload_too_large' } }; + +type LegacyReason = + | 'gate_config' + | 'gate_percent' + | 'no_content_length' + | 'invalid_content_length' + | 'oversized_body' + | 'oversized_item' + | 'multi_chunk'; + +type CommonEvent = { + ingestRequestId: string; + sessionId: string; + ingestVersion: number; + declaredBytes: number | null; + actualBytes: number | null; + durationMs: number; + items: number | null; +}; + +const contentLengthPattern = /^(0|[1-9]\d*)$/; + +export async function handleDirectIngestRequest( + request: DirectIngestRequest +): Promise { + const startedAt = performance.now(); + const baseEvent = { + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + ingestVersion: request.ingestVersion, + }; + const r2Key = `ingest/${request.kiloUserId}/${request.sessionId}/${request.ingestRequestId}`; + const configResult = parseDirectIngestConfig(request.env); + + if (!configResult.ok) { + console.error({ event: 'direct_ingest_config_error', reason: configResult.reason }); + return legacy(request, r2Key, request.body, 'gate_config', null, null, null, startedAt); + } + + let selection; + try { + selection = await selectDirectIngestUser(configResult.config, request.kiloUserId); + } catch (error) { + console.error({ + event: 'direct_ingest_config_error', + reason: 'bucket_failure', + error: errorMessage(error), + }); + return legacy(request, r2Key, request.body, 'gate_config', null, null, null, startedAt); + } + if (!selection.selected) { + return legacy(request, r2Key, request.body, 'gate_percent', null, null, null, startedAt); + } + + const contentLength = parseContentLength(request.contentLength); + if (contentLength === 'missing') { + return legacy(request, r2Key, request.body, 'no_content_length', null, null, null, startedAt); + } + if (contentLength === 'invalid') { + return legacy( + request, + r2Key, + request.body, + 'invalid_content_length', + null, + null, + null, + startedAt + ); + } + if (contentLength > configResult.config.maxBytes) { + return legacy( + request, + r2Key, + request.body, + 'oversized_body', + contentLength, + null, + null, + startedAt + ); + } + + const buffered = await readBoundedStream( + request.body, + contentLength, + configResult.config.maxBytes + ); + if (!buffered.ok) { + logEvent('warn', { + event: 'direct_ingest_legacy', + ...baseEvent, + reason: 'oversized_body', + declaredBytes: contentLength, + actualBytes: null, + durationMs: elapsed(startedAt), + items: null, + }); + return { status: 413, body: { success: false, error: 'payload_too_large' } }; + } + + const actualBytes = buffered.bytes.byteLength; + const validation = validateAndParseIngestPayload(buffered.bytes); + if (!validation.ok) { + logEvent('warn', { + event: 'direct_ingest_parse_reject', + ...baseEvent, + declaredBytes: contentLength, + actualBytes, + durationMs: elapsed(startedAt), + items: null, + }); + return { status: 400, body: { success: false, error: 'malformed_json' } }; + } + + if (validation.skippedItemCount > 0) { + console.warn({ + event: 'direct_ingest_items_skipped', + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + skippedItems: validation.skippedItemCount, + }); + } + + if (validation.dataArray !== 'present' || validation.validItemCount === 0) { + logEvent('info', { + event: 'direct_ingest_ok', + ...baseEvent, + declaredBytes: contentLength, + actualBytes, + durationMs: elapsed(startedAt), + items: 0, + metadataChanges: 0, + }); + return { status: 200, body: { success: true } }; + } + + if (validation.maxValidItemBytes > MAX_INGEST_ITEM_BYTES) { + return legacy( + request, + r2Key, + buffered.bytes, + 'oversized_item', + contentLength, + actualBytes, + validation.validItemCount, + startedAt + ); + } + if ( + validation.validItemCount > INGEST_CHUNK_MAX_ITEMS || + validation.totalValidItemBytes > INGEST_CHUNK_MAX_BYTES + ) { + return legacy( + request, + r2Key, + buffered.bytes, + 'multi_chunk', + contentLength, + actualBytes, + validation.validItemCount, + startedAt + ); + } + + let ingestResult: IngestResult; + try { + ingestResult = await withDORetry, IngestResult>( + () => getSessionIngestDO(request.env, request), + async stub => + stub.ingest( + validation.items, + request.kiloUserId, + request.sessionId, + request.ingestVersion, + request.ingestedAt + ), + 'SessionIngestDO.ingest.direct', + { maxAttempts: 1, baseBackoffMs: 0, maxBackoffMs: 0 } + ); + } catch (error) { + return fallbackAfterDirectFailure( + request, + r2Key, + buffered.bytes, + contentLength, + actualBytes, + validation.validItemCount, + startedAt, + error + ); + } + + if (ingestResult.accepted === false) { + logEvent('info', { + event: 'direct_ingest_tombstone', + ...baseEvent, + declaredBytes: contentLength, + actualBytes, + durationMs: elapsed(startedAt), + items: validation.validItemCount, + }); + return { status: 404, body: { success: false, error: 'session_not_found' } }; + } + + await runMetadataProjection(request, ingestResult.changes); + logEvent('info', { + event: 'direct_ingest_ok', + ...baseEvent, + declaredBytes: contentLength, + actualBytes, + durationMs: elapsed(startedAt), + items: validation.validItemCount, + metadataChanges: ingestResult.changes.length, + }); + return { status: 200, body: { success: true } }; +} + +async function legacy( + request: DirectIngestRequest, + r2Key: string, + body: ReadableStream | Uint8Array, + reason: LegacyReason, + declaredBytes: number | null, + actualBytes: number | null, + items: number | null, + startedAt: number +): Promise { + try { + await stageAndEnqueue(request.env, queueParams(request, r2Key), body); + } catch (error) { + logEvent('warn', { + event: 'direct_ingest_legacy', + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + ingestVersion: request.ingestVersion, + reason, + declaredBytes, + actualBytes, + durationMs: elapsed(startedAt), + items, + failureStage: error instanceof StageAndEnqueueError ? error.stage : 'staging_upload', + error: errorMessage(error), + }); + throw error; + } + logEvent('info', { + event: 'direct_ingest_legacy', + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + ingestVersion: request.ingestVersion, + reason, + declaredBytes, + actualBytes, + durationMs: elapsed(startedAt), + items, + }); + return { status: 200, body: { success: true } }; +} + +async function fallbackAfterDirectFailure( + request: DirectIngestRequest, + r2Key: string, + bytes: Uint8Array, + declaredBytes: number, + actualBytes: number, + items: number, + startedAt: number, + directError: unknown +): Promise { + try { + await stageAndEnqueue(request.env, queueParams(request, r2Key), bytes); + } catch (error) { + logFallback( + request, + declaredBytes, + actualBytes, + items, + startedAt, + error, + undefined, + directError + ); + throw error; + } + logFallback(request, declaredBytes, actualBytes, items, startedAt, directError, 'do_rpc'); + return { status: 200, body: { success: true } }; +} + +async function runMetadataProjection( + request: DirectIngestRequest, + changes: Array<{ name: string; value: string | null }> +): Promise { + if (changes.length === 0) return; + const metadataPromise = applyMetadataChanges( + request.env, + request.kiloUserId, + request.sessionId, + new Map(changes.map(change => [change.name, change.value])), + request.executionContext + ).catch(error => { + console.error({ + event: 'direct_ingest_metadata_error', + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + error: errorMessage(error), + }); + }); + + if (request.executionContext) { + request.executionContext.waitUntil(metadataPromise); + } else { + await metadataPromise; + } +} + +function parseContentLength(value: string | undefined): number | 'missing' | 'invalid' { + if (value === undefined) return 'missing'; + if (!contentLengthPattern.test(value)) return 'invalid'; + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 'invalid'; +} + +function queueParams(request: DirectIngestRequest, r2Key: string) { + return { + r2Key, + kiloUserId: request.kiloUserId, + sessionId: request.sessionId, + ingestVersion: request.ingestVersion, + ingestedAt: request.ingestedAt, + }; +} + +function logFallback( + request: DirectIngestRequest, + declaredBytes: number | null, + actualBytes: number | null, + items: number | null, + startedAt: number, + error: unknown, + stage?: 'do_rpc', + directError?: unknown +) { + const failureStage: 'do_rpc' | StageAndEnqueueFailureStage = + stage ?? (error instanceof StageAndEnqueueError ? error.stage : 'staging_upload'); + logEvent('warn', { + event: 'direct_ingest_fallback', + ingestRequestId: request.ingestRequestId, + sessionId: request.sessionId, + ingestVersion: request.ingestVersion, + declaredBytes, + actualBytes, + durationMs: elapsed(startedAt), + items, + stage: failureStage, + error: errorMessage(error), + ...(directError === undefined ? {} : { directError: errorMessage(directError) }), + }); +} + +function logEvent(level: 'info' | 'warn', event: CommonEvent & Record) { + console[level](event); +} + +function elapsed(startedAt: number): number { + return Math.round(performance.now() - startedAt); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/services/session-ingest/src/ingest/metadata.ts b/services/session-ingest/src/ingest/metadata.ts index 044e0ab1c0..378219b268 100644 --- a/services/session-ingest/src/ingest/metadata.ts +++ b/services/session-ingest/src/ingest/metadata.ts @@ -50,7 +50,7 @@ export async function applyMetadataChanges( kiloUserId: string, sessionId: string, mergedChanges: Map, - ctx: ExecutionContext + ctx?: { waitUntil(promise: Promise): void } ): Promise { if (mergedChanges.size === 0) return; @@ -214,7 +214,7 @@ export async function flushPartialMetadataChanges( env: Env, params: { r2Key: string; kiloUserId: string; sessionId: string }, mergedChanges: Map, - ctx: ExecutionContext + ctx: { waitUntil(promise: Promise): void } ): Promise { if (mergedChanges.size === 0) return; try { diff --git a/services/session-ingest/src/ingest/stage-and-enqueue.ts b/services/session-ingest/src/ingest/stage-and-enqueue.ts index c12ab48268..72c71ac16d 100644 --- a/services/session-ingest/src/ingest/stage-and-enqueue.ts +++ b/services/session-ingest/src/ingest/stage-and-enqueue.ts @@ -6,12 +6,28 @@ type StageAndEnqueueParams = Omit & ingestedAt?: number; }; +export type StageAndEnqueueFailureStage = 'staging_upload' | 'queue_send'; + +export class StageAndEnqueueError extends Error { + constructor( + readonly stage: StageAndEnqueueFailureStage, + readonly cause: unknown + ) { + super(cause instanceof Error ? cause.message : String(cause)); + this.name = 'StageAndEnqueueError'; + } +} + export async function stageAndEnqueue( env: Env, params: StageAndEnqueueParams, body: ReadableStream | Uint8Array ): Promise { - await env.SESSION_INGEST_R2.put(params.r2Key, body); + try { + await env.SESSION_INGEST_R2.put(params.r2Key, body); + } catch (error) { + throw new StageAndEnqueueError('staging_upload', error); + } const message: IngestQueueMessage = { ...params, @@ -22,6 +38,6 @@ export async function stageAndEnqueue( await env.INGEST_QUEUE.send(message); } catch (error) { await env.SESSION_INGEST_R2.delete(params.r2Key).catch(() => {}); - throw error; + throw new StageAndEnqueueError('queue_send', error); } } diff --git a/services/session-ingest/src/routes/api.test.ts b/services/session-ingest/src/routes/api.test.ts index 9d3556d7d7..ef99388196 100644 --- a/services/session-ingest/src/routes/api.test.ts +++ b/services/session-ingest/src/routes/api.test.ts @@ -21,10 +21,15 @@ vi.mock('../dos/UserConnectionDO', () => ({ getUserConnectionDO: vi.fn(), })); +vi.mock('../ingest/metadata', () => ({ + applyMetadataChanges: vi.fn(async () => undefined), +})); + import { getWorkerDb } from '@kilocode/db/client'; import { getSessionIngestDO } from '../dos/SessionIngestDO'; import { getSessionAccessCacheDO } from '../dos/SessionAccessCacheDO'; import { getUserConnectionDO } from '../dos/UserConnectionDO'; +import { applyMetadataChanges } from '../ingest/metadata'; import { notifyUserSessionEvent } from '../session-events'; import type * as SessionEvents from '../session-events'; @@ -43,9 +48,12 @@ type TestBindings = { SESSION_INGEST_R2: { put: ReturnType; delete: ReturnType }; INGEST_QUEUE: { send: ReturnType }; NOTIFICATIONS: { sendSessionReadyNotification: ReturnType }; + DIRECT_INGEST_PERCENT: string; + DIRECT_INGEST_USER_IDS: string; + DIRECT_INGEST_MAX_BYTES: string; }; -function makeTestEnv(): TestBindings { +function makeTestEnv(overrides: Partial = {}): TestBindings { return { HYPERDRIVE: { connectionString: 'postgres://test' }, SESSION_INGEST_R2: { @@ -54,6 +62,10 @@ function makeTestEnv(): TestBindings { }, INGEST_QUEUE: { send: vi.fn(async () => undefined) }, NOTIFICATIONS: { sendSessionReadyNotification: vi.fn(async () => ({ dispatched: true })) }, + DIRECT_INGEST_PERCENT: '0', + DIRECT_INGEST_USER_IDS: '', + DIRECT_INGEST_MAX_BYTES: '4194304', + ...overrides, }; } @@ -67,6 +79,29 @@ function makeApiApp() { return app; } +function directIngestEnv(overrides: Partial = {}) { + return makeTestEnv({ DIRECT_INGEST_USER_IDS: 'usr_test', ...overrides }); +} + +function ingestRequest(body: string, contentLength = new TextEncoder().encode(body).byteLength) { + return new Request('http://local/session/ses_12345678901234567890123456/ingest?v=1', { + method: 'POST', + headers: { 'content-length': String(contentLength) }, + body, + }); +} + +function prepareIngestRoute( + ingest: ReturnType = vi.fn(async () => ({ accepted: true, changes: [] })) +) { + const { db } = makeDbFakes(); + vi.mocked(getWorkerDb).mockReturnValue(db); + vi.mocked(getSessionAccessCacheDO).mockReturnValue({ has: vi.fn(async () => true) } as never); + vi.mocked(getSessionIngestDO).mockReturnValue({ ingest } as never); + vi.mocked(applyMetadataChanges).mockResolvedValue(undefined); + return { app: makeApiApp(), ingest }; +} + function makeDbFakes() { type Db = ReturnType; @@ -378,21 +413,14 @@ describe('api routes', () => { expect(typeof queueMsg['ingestedAt']).toBe('number'); }); - it('captures ingestedAt after staging completes', async () => { + it('preserves request-entry ingestedAt in legacy fallback', async () => { const { db } = makeDbFakes(); vi.mocked(getWorkerDb).mockReturnValue(db); vi.mocked(getSessionAccessCacheDO).mockReturnValue({ has: vi.fn(async () => true) } as never); const app = makeApiApp(); const env = makeTestEnv(); - const operations: string[] = []; - env.SESSION_INGEST_R2.put.mockImplementationOnce(async () => { - operations.push('put'); - }); - const now = vi.spyOn(Date, 'now').mockImplementation(() => { - operations.push('now'); - return 123; - }); + const now = vi.spyOn(Date, 'now').mockReturnValue(123); const response = await app.fetch( new Request('http://local/session/ses_12345678901234567890123456/ingest', { @@ -404,7 +432,6 @@ describe('api routes', () => { now.mockRestore(); expect(response.status).toBe(200); - expect(operations).toEqual(['put', 'now']); expect(env.INGEST_QUEUE.send).toHaveBeenCalledWith( expect.objectContaining({ ingestedAt: 123 }) ); @@ -496,6 +523,395 @@ describe('api routes', () => { expect(env.INGEST_QUEUE.send).toHaveBeenCalledTimes(1); }); + describe('direct ingest', () => { + it('persists an eligible payload with one DO RPC and no R2 or queue writes', async () => { + const ingest = vi.fn(async () => ({ + accepted: true as const, + changes: [{ name: 'title' as const, value: 'Direct' }], + })); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + const body = JSON.stringify({ data: [{ type: 'session', data: { title: 'Direct' } }] }); + const now = vi.spyOn(Date, 'now').mockReturnValue(1234); + + const response = await app.fetch(ingestRequest(body), env); + now.mockRestore(); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ success: true }); + expect(ingest).toHaveBeenCalledTimes(1); + expect(ingest).toHaveBeenCalledWith( + [{ type: 'session', data: { title: 'Direct' } }], + 'usr_test', + 'ses_12345678901234567890123456', + 1, + 1234 + ); + expect(applyMetadataChanges).toHaveBeenCalledTimes(1); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + }); + + it('accepts the old DO response shape during gradual deployment', async () => { + const ingest = vi.fn(async () => ({ + changes: [{ name: 'title' as const, value: 'Legacy DO' }], + })); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + const body = JSON.stringify({ data: [{ type: 'session', data: { title: 'Legacy DO' } }] }); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(200); + expect(applyMetadataChanges).toHaveBeenCalledTimes(1); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + }); + + it('completes a bodyless gate miss with an empty staged stream', async () => { + const { app, ingest } = prepareIngestRoute(); + const env = makeTestEnv(); + const request = new Request( + 'http://local/session/ses_12345678901234567890123456/ingest?v=1', + { method: 'POST' } + ); + + const response = await app.fetch(request, env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + const stagedBody = env.SESSION_INGEST_R2.put.mock.calls[0][1] as ReadableStream; + await expect(new Response(stagedBody).arrayBuffer()).resolves.toHaveProperty('byteLength', 0); + expect(env.INGEST_QUEUE.send).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['invalid config', { DIRECT_INGEST_PERCENT: 'invalid' }, 'gate_config'], + ['percent miss', { DIRECT_INGEST_USER_IDS: '' }, 'gate_percent'], + ] as const)('uses the streaming legacy path for %s', async (_name, overrides, reason) => { + const { app, ingest } = prepareIngestRoute(); + const env = makeTestEnv(overrides); + const body = JSON.stringify({ data: [{ type: 'message', data: { id: 'msg_1' } }] }); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).toHaveBeenCalledTimes(1); + expect(env.INGEST_QUEUE.send).toHaveBeenCalledTimes(1); + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_legacy', reason }) + ); + info.mockRestore(); + error.mockRestore(); + }); + + it.each([ + ['missing', undefined, 'no_content_length'], + ['negative', '-1', 'invalid_content_length'], + ['fractional', '1.5', 'invalid_content_length'], + ['non-numeric', 'abc', 'invalid_content_length'], + ['zero', '0', 'invalid_content_length'], + ['over cap', '4194305', 'oversized_body'], + ])('uses the legacy path for %s Content-Length', async (_name, contentLength, reason) => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv(); + const headers = contentLength === undefined ? undefined : { 'content-length': contentLength }; + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const request = new Request( + 'http://local/session/ses_12345678901234567890123456/ingest?v=1', + { method: 'POST', headers, body: '{}' } + ); + + const response = await app.fetch(request, env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).toHaveBeenCalledTimes(1); + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_legacy', reason }) + ); + info.mockRestore(); + }); + + it('keeps gate-miss staging failures out of the direct fallback denominator', async () => { + const { app } = prepareIngestRoute(); + const env = makeTestEnv(); + env.SESSION_INGEST_R2.put.mockRejectedValueOnce(new Error('r2 failed')); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest('{}'), env); + + expect(response.status).toBe(500); + expect(warn).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'direct_ingest_legacy', + reason: 'gate_percent', + failureStage: 'staging_upload', + }) + ); + expect(warn).not.toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_fallback' }) + ); + warn.mockRestore(); + }); + + it('returns 413 when actual bytes exceed the declaration', async () => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv(); + const body = JSON.stringify({ data: [] }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(body, body.length - 1), env); + + expect(response.status).toBe(413); + expect(await response.json()).toEqual({ success: false, error: 'payload_too_large' }); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('accepts a body exactly at the configured cap', async () => { + const { app, ingest } = prepareIngestRoute(); + const body = JSON.stringify({ data: [{ type: 'message', data: { id: 'msg_cap' } }] }); + const env = directIngestEnv({ DIRECT_INGEST_MAX_BYTES: String(body.length) }); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(200); + expect(ingest).toHaveBeenCalledTimes(1); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + }); + + it.each([ + ['malformed JSON', '{"data":[', 400, 'malformed_json'], + [ + 'valid prefix with malformed tail', + '{"data":[{"type":"message","data":{"id":"msg_1"}},broken', + 400, + 'malformed_json', + ], + ] as const)('rejects %s without persistence', async (_name, body, status, responseError) => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv(); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(status); + expect(await response.json()).toEqual({ success: false, error: responseError }); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it.each([ + ['empty', { data: [] }], + ['missing', {}], + ['wrong-shaped', { data: {} }], + ['all-invalid', { data: [{ type: 'message', data: {} }] }], + ])('returns a no-op for %s data', async (_name, payload) => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv(); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(JSON.stringify(payload)), env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + info.mockRestore(); + warn.mockRestore(); + }); + + it('routes an item requiring R2 offload through the queue with exact bytes', async () => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv({ DIRECT_INGEST_MAX_BYTES: '3000000' }); + const body = JSON.stringify({ + data: [{ type: 'message', data: { id: 'msg_large', content: 'x'.repeat(2_100_000) } }], + }); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).toHaveBeenCalledWith( + expect.stringMatching(/\/ses_12345678901234567890123456\//), + new TextEncoder().encode(body) + ); + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_legacy', reason: 'oversized_item' }) + ); + info.mockRestore(); + }); + + it('routes more than 128 valid items through the queue', async () => { + const { app, ingest } = prepareIngestRoute(); + const env = directIngestEnv(); + const items = Array.from({ length: 129 }, (_, index) => ({ + type: 'message', + data: { id: `msg_${index}` }, + })); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + + const response = await app.fetch(ingestRequest(JSON.stringify({ data: items })), env); + + expect(response.status).toBe(200); + expect(ingest).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).toHaveBeenCalledTimes(1); + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'direct_ingest_legacy', + reason: 'multi_chunk', + items: 129, + }) + ); + info.mockRestore(); + }); + + it('falls back with the original bytes and timestamp when the direct RPC fails', async () => { + const retryableError = Object.assign(new Error('rpc failed'), { retryable: true }); + const ingest = vi.fn(async () => { + throw retryableError; + }); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + const body = JSON.stringify({ data: [{ type: 'message', data: { id: 'msg_1' } }] }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const now = vi.spyOn(Date, 'now').mockReturnValue(4567); + const requestId = vi + .spyOn(crypto, 'randomUUID') + .mockReturnValue('11111111-1111-4111-8111-111111111111'); + + const response = await app.fetch(ingestRequest(body), env); + now.mockRestore(); + requestId.mockRestore(); + + expect(response.status).toBe(200); + expect(ingest).toHaveBeenCalledTimes(1); + expect(env.SESSION_INGEST_R2.put).toHaveBeenCalledWith( + 'ingest/usr_test/ses_12345678901234567890123456/11111111-1111-4111-8111-111111111111', + new TextEncoder().encode(body) + ); + expect(env.INGEST_QUEUE.send).toHaveBeenCalledWith( + expect.objectContaining({ ingestedAt: 4567 }) + ); + expect(warn).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_fallback', stage: 'do_rpc' }) + ); + warn.mockRestore(); + }); + + it.each([ + ['staging upload', 'staging_upload'], + ['queue send', 'queue_send'], + ] as const)('logs the %s stage when durable fallback fails', async (failure, stage) => { + const ingest = vi.fn(async () => { + throw new Error('rpc failed'); + }); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + if (stage === 'staging_upload') { + env.SESSION_INGEST_R2.put.mockRejectedValueOnce(new Error('r2 failed')); + } else { + env.INGEST_QUEUE.send.mockRejectedValueOnce(new Error('queue failed')); + } + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const body = JSON.stringify({ data: [{ type: 'message', data: { id: 'msg_1' } }] }); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(500); + expect(warn).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'direct_ingest_fallback', + stage, + directError: 'rpc failed', + }) + ); + warn.mockRestore(); + }); + + it('returns 404 for a tombstoned direct ingest without metadata or fallback', async () => { + const ingest = vi.fn( + async () => ({ accepted: false, reason: 'deleted', changes: [] }) as const + ); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const body = JSON.stringify({ data: [{ type: 'message', data: { id: 'msg_1' } }] }); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(404); + expect(applyMetadataChanges).not.toHaveBeenCalled(); + expect(env.SESSION_INGEST_R2.put).not.toHaveBeenCalled(); + expect(env.INGEST_QUEUE.send).not.toHaveBeenCalled(); + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_tombstone' }) + ); + info.mockRestore(); + }); + + it('keeps direct success when synchronous metadata projection fails', async () => { + const ingest = vi.fn(async () => ({ + accepted: true as const, + changes: [{ name: 'title' as const, value: 'Direct' }], + })); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + vi.mocked(applyMetadataChanges).mockRejectedValueOnce(new Error('metadata failed')); + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const body = JSON.stringify({ data: [{ type: 'session', data: { title: 'Direct' } }] }); + + const response = await app.fetch(ingestRequest(body), env); + + expect(response.status).toBe(200); + expect(error).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_metadata_error' }) + ); + error.mockRestore(); + info.mockRestore(); + }); + + it('schedules caught metadata projection with ExecutionContext', async () => { + const ingest = vi.fn(async () => ({ + accepted: true as const, + changes: [{ name: 'title' as const, value: 'Direct' }], + })); + const { app } = prepareIngestRoute(ingest); + const env = directIngestEnv(); + vi.mocked(applyMetadataChanges).mockRejectedValueOnce(new Error('metadata failed')); + const waitUntil = vi.fn(); + const executionContext = { + waitUntil, + passThroughOnException: vi.fn(), + } as unknown as ExecutionContext; + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + const info = vi.spyOn(console, 'info').mockImplementation(() => {}); + const body = JSON.stringify({ data: [{ type: 'session', data: { title: 'Direct' } }] }); + + const response = await app.fetch(ingestRequest(body), env, executionContext); + await Promise.all(waitUntil.mock.calls.map(([promise]) => promise)); + + expect(response.status).toBe(200); + expect(waitUntil).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalledWith( + expect.objectContaining({ event: 'direct_ingest_metadata_error' }) + ); + error.mockRestore(); + info.mockRestore(); + }); + }); + it('GET /session/:sessionId/export returns 400 for invalid sessionId', async () => { const { db } = makeDbFakes(); vi.mocked(getWorkerDb).mockReturnValue(db); diff --git a/services/session-ingest/src/routes/api.ts b/services/session-ingest/src/routes/api.ts index 079c473a4c..1e5b6a672f 100644 --- a/services/session-ingest/src/routes/api.ts +++ b/services/session-ingest/src/routes/api.ts @@ -11,7 +11,7 @@ import { getSessionAccessCacheDO } from '../dos/SessionAccessCacheDO'; import { getUserConnectionDO } from '../dos/UserConnectionDO'; import { getSessionExport } from '../services/session-export'; import { mapSessionEventRow, notifyUserSessionEvent } from '../session-events'; -import { stageAndEnqueue } from '../ingest/stage-and-enqueue'; +import { handleDirectIngestRequest } from '../ingest/direct-ingest'; export type ApiContext = { Bindings: Env; @@ -216,6 +216,8 @@ api.post('/session/:sessionId/ingest', async c => { } const sessionId = sessionIdParseResult.data; + const ingestedAt = Date.now(); + const ingestRequestId = crypto.randomUUID(); const kiloUserId = c.get('user_id'); const db = getWorkerDb(c.env.HYPERDRIVE.connectionString); @@ -250,20 +252,21 @@ api.post('/session/:sessionId/ingest', async c => { const ingestVersion = ingestVersionSchema.parse(c.req.query('v') ?? 0); - // Stream request body directly to R2 (zero memory) - const r2Key = `ingest/${kiloUserId}/${sessionId}/${crypto.randomUUID()}`; - await stageAndEnqueue( - c.env, - { - r2Key, - kiloUserId, - sessionId, - ingestVersion, - }, - c.req.raw.body ?? new Uint8Array() - ); + const result = await handleDirectIngestRequest({ + env: c.env, + body: + (c.req.raw.body as ReadableStream | null) ?? + (new Blob([]).stream() as ReadableStream), + contentLength: c.req.header('content-length'), + kiloUserId, + sessionId, + ingestVersion, + ingestedAt, + ingestRequestId, + executionContext: getOptionalExecutionContext(c), + }); - return c.json({ success: true }, 200); + return c.json(result.body, result.status); }); api.get('/session/:sessionId/export', async c => { diff --git a/services/session-ingest/worker-configuration.d.ts b/services/session-ingest/worker-configuration.d.ts index fac45700de..ded5a21a6f 100644 --- a/services/session-ingest/worker-configuration.d.ts +++ b/services/session-ingest/worker-configuration.d.ts @@ -1,25 +1,36 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: f343f25001d5eaea82dbaa0d33929d62) -// Runtime types generated with workerd@1.20260305.0 2026-01-27 nodejs_compat +// Generated by Wrangler by running `wrangler types --env-interface CloudflareBindings` (hash: 0f37de76f6c360a7474c52a720a959c5) +// Runtime types generated with workerd@1.20260603.1 2026-01-27 nodejs_compat +interface __BaseEnv_CloudflareBindings { + USER_EXISTS_CACHE: KVNamespace; + SESSION_INGEST_R2: R2Bucket; + HYPERDRIVE: Hyperdrive; + INGEST_QUEUE: Queue; + NEXTAUTH_SECRET_PROD: SecretsStoreSecret; + INTERNAL_API_SECRET_PROD: SecretsStoreSecret; + DIRECT_INGEST_PERCENT: "0"; + DIRECT_INGEST_USER_IDS: ""; + DIRECT_INGEST_MAX_BYTES: "4194304"; + SESSION_INGEST_DO: DurableObjectNamespace; + SESSION_ACCESS_CACHE_DO: DurableObjectNamespace; + USER_CONNECTION_DO: DurableObjectNamespace; + O11Y: Fetcher /* o11y */; + NOTIFICATIONS: Service /* entrypoint NotificationsService from notifications */; +} declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); durableNamespaces: "SessionIngestDO" | "SessionAccessCacheDO" | "UserConnectionDO"; } - interface Env { - USER_EXISTS_CACHE: KVNamespace; - SESSION_INGEST_R2: R2Bucket; - HYPERDRIVE: Hyperdrive; - INGEST_QUEUE: Queue; - NEXTAUTH_SECRET_PROD: SecretsStoreSecret; - INTERNAL_API_SECRET_PROD: SecretsStoreSecret; - SESSION_INGEST_DO: DurableObjectNamespace; - SESSION_ACCESS_CACHE_DO: DurableObjectNamespace; - USER_CONNECTION_DO: DurableObjectNamespace; - O11Y: Fetcher /* o11y */; - } + interface Env extends __BaseEnv_CloudflareBindings {} +} +interface CloudflareBindings extends __BaseEnv_CloudflareBindings {} +type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; +}; +declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} } -interface Env extends Cloudflare.Env {} declare module "*.sql" { const value: string; export default value; @@ -446,23 +457,27 @@ interface ExecutionContext { passThroughOnException(): void; readonly exports: Cloudflare.Exports; readonly props: Props; -} -type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; -type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; -type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; -interface ExportedHandler { - fetch?: ExportedHandlerFetchHandler; - tail?: ExportedHandlerTailHandler; - trace?: ExportedHandlerTraceHandler; - tailStream?: ExportedHandlerTailStreamHandler; - scheduled?: ExportedHandlerScheduledHandler; - test?: ExportedHandlerTestHandler; - email?: EmailExportedHandler; - queue?: ExportedHandlerQueueHandler; + cache?: CacheContext; + tracing?: Tracing; +} +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + connect?: ExportedHandlerConnectHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; } interface StructuredSerializeOptions { transfer?: any[]; @@ -471,24 +486,46 @@ declare abstract class Navigator { sendBeacon(url: string, body?: BodyInit): boolean; readonly userAgent: string; readonly hardwareConcurrency: number; + readonly platform: string; readonly language: string; readonly languages: string[]; } interface AlarmInvocationInfo { readonly isRetry: boolean; readonly retryCount: number; + readonly scheduledTime: number; } interface Cloudflare { readonly compatibilityFlags: Record; } +interface CachePurgeError { + code: number; + message: string; +} +interface CachePurgeResult { + success: boolean; + errors: CachePurgeError[]; +} +interface CachePurgeOptions { + tags?: string[]; + pathPrefixes?: string[]; + purgeEverything?: boolean; +} +interface CacheContext { + purge(options: CachePurgeOptions): Promise; +} +declare abstract class ColoLocalActorNamespace { + get(actorId: string): Fetcher; +} interface DurableObject { fetch(request: Request): Response | Promise; + connect?(socket: Socket): void | Promise; alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise; webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise; webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise; webSocketError?(ws: WebSocket, error: unknown): void | Promise; } -type DurableObjectStub = Fetcher & { +type DurableObjectStub = Fetcher & { readonly id: DurableObjectId; readonly name?: string; }; @@ -496,6 +533,7 @@ interface DurableObjectId { toString(): string; equals(other: DurableObjectId): boolean; readonly name?: string; + readonly jurisdiction?: string; } declare abstract class DurableObjectNamespace { newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId; @@ -524,6 +562,7 @@ interface DurableObjectState { readonly id: DurableObjectId; readonly storage: DurableObjectStorage; container?: Container; + facets: DurableObjectFacets; blockConcurrencyWhile(callback: () => Promise): Promise; acceptWebSocket(ws: WebSocket, tags?: string[]): void; getWebSockets(tag?: string): WebSocket[]; @@ -600,6 +639,15 @@ declare class WebSocketRequestResponsePair { get request(): string; get response(): string; } +interface DurableObjectFacets { + get(name: string, getStartupOptions: () => FacetStartupOptions | Promise>): Fetcher; + abort(name: string, reason: any): void; + delete(name: string): void; +} +interface FacetStartupOptions { + id?: DurableObjectId | string; + class: DurableObjectClass; +} interface AnalyticsEngineDataset { writeDataPoint(event?: AnalyticsEngineDataPoint): void; } @@ -892,7 +940,7 @@ interface CustomEventCustomEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) */ declare class Blob { - constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); + constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); /** * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes. * @@ -1888,8 +1936,31 @@ interface KVNamespaceGetWithMetadataResult { } type QueueContentType = "text" | "bytes" | "json" | "v8"; interface Queue { - send(message: Body, options?: QueueSendOptions): Promise; - sendBatch(messages: Iterable>, options?: QueueSendBatchOptions): Promise; + metrics(): Promise; + send(message: Body, options?: QueueSendOptions): Promise; + sendBatch(messages: Iterable>, options?: QueueSendBatchOptions): Promise; +} +interface QueueSendMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendMetadata { + metrics: QueueSendMetrics; +} +interface QueueSendResponse { + metadata: QueueSendMetadata; +} +interface QueueSendBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendBatchMetadata { + metrics: QueueSendBatchMetrics; +} +interface QueueSendBatchResponse { + metadata: QueueSendBatchMetadata; } interface QueueSendOptions { contentType?: QueueContentType; @@ -1903,6 +1974,19 @@ interface MessageSendRequest { contentType?: QueueContentType; delaySeconds?: number; } +interface QueueMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetadata { + metrics: MessageBatchMetrics; +} interface QueueRetryOptions { delaySeconds?: number; } @@ -1917,12 +2001,14 @@ interface Message { interface QueueEvent extends ExtendableEvent { readonly messages: readonly Message[]; readonly queue: string; + readonly metadata: MessageBatchMetadata; retryAll(options?: QueueRetryOptions): void; ackAll(): void; } interface MessageBatch { readonly messages: readonly Message[]; readonly queue: string; + readonly metadata: MessageBatchMetadata; retryAll(options?: QueueRetryOptions): void; ackAll(): void; } @@ -1941,7 +2027,7 @@ interface R2ListOptions { startAfter?: string; include?: ("httpMetadata" | "customMetadata")[]; } -declare abstract class R2Bucket { +interface R2Bucket { head(key: string): Promise; get(key: string, options: R2GetOptions & { onlyIf: R2Conditional | Headers; @@ -2606,6 +2692,11 @@ interface QueuingStrategyInit { */ highWaterMark: number; } +interface TracePreviewInfo { + id: string; + slug: string; + name: string; +} interface ScriptVersion { id?: string; tag?: string; @@ -2616,7 +2707,7 @@ declare abstract class TailEvent extends ExtendableEvent { readonly traces: TraceItem[]; } interface TraceItem { - readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; + readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; readonly eventTimestamp: number | null; readonly logs: TraceLog[]; readonly exceptions: TraceException[]; @@ -2626,6 +2717,8 @@ interface TraceItem { readonly scriptVersion?: ScriptVersion; readonly dispatchNamespace?: string; readonly scriptTags?: string[]; + readonly tailAttributes?: Record; + readonly preview?: TracePreviewInfo; readonly durableObjectId?: string; readonly outcome: string; readonly executionModel: string; @@ -2636,6 +2729,8 @@ interface TraceItem { interface TraceItemAlarmEventInfo { readonly scheduledTime: Date; } +interface TraceItemConnectEventInfo { +} interface TraceItemCustomEventInfo { } interface TraceItemScheduledEventInfo { @@ -3058,7 +3153,7 @@ declare var WebSocket: { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) */ interface WebSocket extends EventTarget { - accept(): void; + accept(options?: WebSocketAcceptOptions): void; /** * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data. * @@ -3097,6 +3192,22 @@ interface WebSocket extends EventTarget { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) */ extensions: string | null; + /** + * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) + */ + binaryType: "blob" | "arraybuffer"; +} +interface WebSocketAcceptOptions { + /** + * When set to `true`, receiving a server-initiated WebSocket Close frame will not + * automatically send a reciprocal Close frame, leaving the connection in a half-open + * state. This is useful for proxying scenarios where you need to coordinate closing + * both sides independently. Defaults to `false` when the + * `no_web_socket_half_open_by_default` compatibility flag is enabled. + */ + allowHalfOpen?: boolean; } declare const WebSocketPair: { new (): { @@ -3215,12 +3326,41 @@ interface Container { signal(signo: number): void; getTcpPort(port: number): Fetcher; setInactivityTimeout(durationMs: number | bigint): Promise; + interceptOutboundHttp(addr: string, binding: Fetcher): Promise; + interceptAllOutboundHttp(binding: Fetcher): Promise; + snapshotDirectory(options: ContainerDirectorySnapshotOptions): Promise; + snapshotContainer(options: ContainerSnapshotOptions): Promise; + interceptOutboundHttps(addr: string, binding: Fetcher): Promise; +} +interface ContainerDirectorySnapshot { + id: string; + size: number; + dir: string; + name?: string; +} +interface ContainerDirectorySnapshotOptions { + dir: string; + name?: string; +} +interface ContainerDirectorySnapshotRestoreParams { + snapshot: ContainerDirectorySnapshot; + mountPoint?: string; +} +interface ContainerSnapshot { + id: string; + size: number; + name?: string; +} +interface ContainerSnapshotOptions { + name?: string; } interface ContainerStartupOptions { entrypoint?: string[]; enableInternet: boolean; env?: Record; - hardTimeout?: (number | bigint); + labels?: Record; + directorySnapshots?: ContainerDirectorySnapshotRestoreParams[]; + containerSnapshot?: ContainerSnapshot; } /** * The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other. @@ -3283,6 +3423,10 @@ type LoopbackDurableObjectClass DurableObjectClass : (opts: { props?: any; }) => DurableObjectClass); +interface LoopbackDurableObjectNamespace extends DurableObjectNamespace { +} +interface LoopbackColoLocalActorNamespace extends ColoLocalActorNamespace { +} interface SyncKvStorage { get(key: string): T | undefined; list(options?: SyncKvListOptions): Iterable<[ @@ -3302,12 +3446,15 @@ interface SyncKvListOptions { } interface WorkerStub { getEntrypoint(name?: string, options?: WorkerStubEntrypointOptions): Fetcher; + getDurableObjectClass(name?: string, options?: WorkerStubEntrypointOptions): DurableObjectClass; } interface WorkerStubEntrypointOptions { props?: any; + limits?: workerdResourceLimits; } interface WorkerLoader { get(name: string | null, getCode: () => WorkerLoaderWorkerCode | Promise): WorkerStub; + load(code: WorkerLoaderWorkerCode): WorkerStub; } interface WorkerLoaderModule { js?: string; @@ -3322,6 +3469,7 @@ interface WorkerLoaderWorkerCode { compatibilityDate: string; compatibilityFlags?: string[]; allowExperimental?: boolean; + limits?: workerdResourceLimits; mainModule: string; modules: Record; env?: any; @@ -3329,6 +3477,10 @@ interface WorkerLoaderWorkerCode { tails?: Fetcher[]; streamingTails?: Fetcher[]; } +interface workerdResourceLimits { + cpuMs?: number; + subRequests?: number; +} /** * The Workers runtime supports a subset of the Performance API, used to measure timing and performance, * as well as timing of subrequests and other operations. @@ -3340,92 +3492,375 @@ declare abstract class Performance { get timeOrigin(): number; /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ now(): number; + /** + * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) + */ + toJSON(): object; +} +interface Tracing { + enterSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + Span: typeof Span; +} +declare abstract class Span { + get isTraced(): boolean; + setAttribute(key: string, value?: (boolean | number | string)): void; +} +// ============================================================================ +// Agent Memory +// +// Public type surface for user Workers binding to an Agent Memory namespace. +// ============================================================================ +/** Memory type — every memory is classified into exactly one. */ +type AgentMemoryMemoryType = "fact" | "event" | "instruction" | "task"; +/** Search intensity for recall. */ +type AgentMemoryThinkingLevel = "low" | "medium" | "high"; +/** Response verbosity for recall. */ +type AgentMemoryResponseLength = "short" | "medium" | "long"; +/** A conversation message passed to ingest(). */ +interface AgentMemoryMessage { + role: "system" | "user" | "assistant"; + content: string; + /** Optional message timestamp. */ + timestamp?: Date; +} +/** Raw memory content passed to remember(). */ +interface AgentMemoryIncomingMemory { + /** Raw memory content. The service classifies and summarizes automatically. */ + content: string; + /** Optional session identifier to associate with this memory. */ + sessionId?: string | null | undefined; +} +/** A stored memory returned from remember(), get(), and delete(). */ +interface AgentMemoryMemory { + /** Memory ID. */ + id: string; + /** Memory type. */ + type: AgentMemoryMemoryType; + /** Text summary. */ + summary: string; + /** Memory text. */ + content: string; + /** Session that created this memory. */ + sessionId: string | null; + /** Memory creation time. */ + createdAt: Date; + /** Memory last-update time. */ + updatedAt: Date; +} +/** Single entry in a list() response. Same shape as Memory minus full content. */ +type AgentMemoryMemoryListEntry = Omit; +/** A scored memory candidate in a recall result. */ +interface AgentMemoryScoredCandidate { + /** Candidate ID. */ + id: string; + /** Text summary. */ + summary: string; + /** Session that created this candidate, when known. */ + sessionId: string | null; + /** Relevance score (higher is better). Comparable only within a single query. */ + score: number; +} +/** Options for the ingest() method. */ +interface AgentMemoryIngestOptions { + /** Session identifier to associate with memories created during ingestion. */ + sessionId?: string | null | undefined; } -// AI Search V2 API Error Interfaces +/** Options for the getSummary() method. */ +interface AgentMemoryGetSummaryOptions { + /** Session identifier to retrieve session summary for. */ + sessionId?: string | null | undefined; +} +/** Response from the getSummary() method. */ +interface AgentMemoryGetSummaryResponse { + /** Markdown summary. */ + summary: string; +} +/** + * Options for the recall() method. + * + * `referenceDate` accepts a Date object, an ISO-8601 date string + * (YYYY-MM-DD), or a full ISO-8601 datetime string. When provided, this + * date is used as "today" for resolving relative time references + * ("how many days ago", "last week") instead of the server's wall-clock time. + */ +interface AgentMemoryRecallOptions { + /** Recall intensity: "low" (default), "medium", or "high". */ + thinkingLevel?: AgentMemoryThinkingLevel; + /** Response verbosity: "short", "medium" (default), or "long". */ + responseLength?: AgentMemoryResponseLength; + /** Temporal anchor for date arithmetic. */ + referenceDate?: Date | string; +} +/** Response from the recall() method. */ +interface AgentMemoryRecallResult { + /** Number of memories retrieved. */ + count: number; + /** LLM-generated answer synthesizing the matching memories. */ + answer: string; + /** Matching memories ranked by relevance. */ + candidates: AgentMemoryScoredCandidate[]; +} +/** + * Options for the list() method. + * + * `cursor` is the opaque continuation token returned by the previous page; + * pass it back unchanged to fetch the next page. `sessionId` and `type` + * are exact-match filters; combining them is allowed. + */ +interface AgentMemoryListMemoriesOptions { + /** Maximum number of memories to return. Default 20, max 500. */ + limit?: number; + /** Opaque cursor from a previous page. */ + cursor?: string; + /** Exact-match session filter. */ + sessionId?: string; + /** Exact-match memory-type filter. */ + type?: AgentMemoryMemoryType; +} +/** Response from the list() method. */ +interface AgentMemoryListMemoriesResult { + memories: AgentMemoryMemoryListEntry[]; + /** Continuation cursor; absent when this page exhausted the result set. */ + cursor?: string; +} +/** + * A single Agent Memory profile, scoped to a profile name. + * + * Returned by {@link AgentMemoryNamespace.getProfile}. + */ +declare abstract class AgentMemoryProfile { + /** + * Retrieve a memory by ID. + * + * @param memoryId - ULID of the memory to retrieve. + * @throws if the memory does not exist. + */ + get(memoryId: string): Promise; + /** + * Delete a memory by ID. + * + * Removes the memory and any source messages linked by the memory's + * source message IDs. + * + * @param memoryId - ULID of the memory to delete. + * @throws if the memory does not exist. + */ + delete(memoryId: string): Promise; + /** + * Store a memory in this profile. The content is automatically classified, + * summarized, and indexed. + * + * @param memory - Raw memory content to persist. + */ + remember(memory: AgentMemoryIncomingMemory): Promise; + /** + * Extract memories from a conversation. + * + * @param messages - Conversation messages to extract memories from. + * @param options - Optional ingest options. + */ + ingest(messages: Iterable, options?: AgentMemoryIngestOptions): Promise; + /** + * Get a profile summary. + * + * @param options - Optional getSummary options. + */ + getSummary(options?: AgentMemoryGetSummaryOptions): Promise; + /** + * Recall memories in this profile. + * + * @param query - Recall query matched against memory content and keywords. + * @param options - Optional recall parameters. + * @returns Matching memories with relevance scores and a synthesized answer. + */ + recall(query: string, options?: AgentMemoryRecallOptions): Promise; + /** + * List active memories in this profile. + * + * Returns a paginated, filterable view of stored memories. Superseded + * versions are excluded. Use the returned `cursor` (when present) to + * fetch the next page. + * + * @param options - Optional pagination and filter options. + */ + list(options?: AgentMemoryListMemoriesOptions): Promise; + /** + * Soft-delete every memory and message in this profile that is tagged + * with `sessionId`. + * + * Idempotent: deleting a sessionId that has no rows is a no-op. + * + * @param sessionId - Session to delete. + */ + deleteSession(sessionId: string): Promise; +} +/** + * Namespace-level Agent Memory binding. + * + * Used as the type of an `env.MEMORY`-style binding backed by the Agent + * Memory product. + * + * @example + * ```ts + * export default { + * async fetch(_request: Request, env: Env): Promise { + * const profile = await env.MEMORY.getProfile("wrangler-e2e"); + * const summary = await profile.getSummary(); + * return Response.json(summary); + * }, + * }; + * ``` + */ +declare abstract class AgentMemoryNamespace { + /** + * Get a memory profile by name. Profiles are isolated by namespace and + * addressed by a compound key (namespaceId:profileName). + * + * @param profileName - Profile name (validated against naming rules). + * @returns RPC target for interacting with the profile. + */ + getProfile(profileName: string): Promise; + /** + * Soft-delete a profile and schedule deferred purge. Marks all + * memories and messages as deleted. + * + * @param profileName - Name of the profile to delete. + */ + deleteProfile(profileName: string): Promise; +} +// ============ AI Search Error Interfaces ============ interface AiSearchInternalError extends Error { } interface AiSearchNotFoundError extends Error { } -interface AiSearchNameNotSetError extends Error { -} -// Filter types (shared with AutoRAG for compatibility) -type ComparisonFilter = { - key: string; - type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; - value: string | number | boolean; -}; -type CompoundFilter = { - type: 'and' | 'or'; - filters: ComparisonFilter[]; +// ============ AI Search Common Types ============ +/** A single message in a conversation-style search or chat request. */ +type AiSearchMessage = { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; }; -// AI Search V2 Request Types -type AiSearchSearchRequest = { - messages: Array<{ - role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; - content: string | null; - }>; - ai_search_options?: { - retrieval?: { - retrieval_type?: 'vector' | 'keyword' | 'hybrid'; - /** Match threshold (0-1, default 0.4) */ - match_threshold?: number; - /** Maximum number of results (1-50, default 10) */ - max_num_results?: number; - filters?: CompoundFilter | ComparisonFilter; - /** Context expansion (0-3, default 0) */ - context_expansion?: number; - [key: string]: unknown; - }; - query_rewrite?: { - enabled?: boolean; - model?: string; - rewrite_prompt?: string; - [key: string]: unknown; - }; - reranking?: { - /** Enable reranking (default false) */ - enabled?: boolean; - model?: '@cf/baai/bge-reranker-base' | ''; - /** Match threshold (0-1, default 0.4) */ - match_threshold?: number; - [key: string]: unknown; - }; +/** + * Common shape for `ai_search_options` used by both single-instance and multi-instance requests. + * Contains retrieval, query rewrite, reranking, and cache sub-options. + */ +type AiSearchOptions = { + retrieval?: { + /** Which retrieval backend to use. Defaults to the instance's configured index_method. */ + retrieval_type?: 'vector' | 'keyword' | 'hybrid'; + /** Fusion method for combining vector + keyword results. */ + fusion_method?: 'max' | 'rrf'; + /** How keyword terms are combined: "and" = all terms must match, "or" = any term matches. */ + keyword_match_mode?: 'and' | 'or'; + /** Minimum similarity score (0-1) for a result to be included. Default 0.4. */ + match_threshold?: number; + /** Maximum number of results to return (1-50). Default 10. */ + max_num_results?: number; + /** Vectorize metadata filters applied to the search. */ + filters?: VectorizeVectorMetadataFilter; + /** Number of surrounding chunks to include for context (0-3). Default 0. */ + context_expansion?: number; + /** If true, return only item metadata without chunk text. */ + metadata_only?: boolean; + /** If true (default), return empty results on retrieval failure instead of throwing. */ + return_on_failure?: boolean; + /** Boost results by metadata field values. Max 3 entries. */ + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + [key: string]: unknown; + }; + query_rewrite?: { + enabled?: boolean; + model?: string; + rewrite_prompt?: string; [key: string]: unknown; }; + reranking?: { + enabled?: boolean; + model?: string; + /** Match threshold (0-1, default 0.4) */ + match_threshold?: number; + [key: string]: unknown; + }; + cache?: { + enabled?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + }; + [key: string]: unknown; +}; +// ============ AI Search Request Types ============ +/** + * Request body for single-instance search. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options?: AiSearchOptions; +} | { + query?: never; + /** Conversation-style input. At least one user message with non-empty content is required. */ + messages: AiSearchMessage[]; + ai_search_options?: AiSearchOptions; }; type AiSearchChatCompletionsRequest = { - messages: Array<{ - role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; - content: string | null; - }>; + messages: AiSearchMessage[]; model?: string; stream?: boolean; - ai_search_options?: { - retrieval?: { - retrieval_type?: 'vector' | 'keyword' | 'hybrid'; - match_threshold?: number; - max_num_results?: number; - filters?: CompoundFilter | ComparisonFilter; - context_expansion?: number; - [key: string]: unknown; - }; - query_rewrite?: { - enabled?: boolean; - model?: string; - rewrite_prompt?: string; - [key: string]: unknown; - }; - reranking?: { - enabled?: boolean; - model?: '@cf/baai/bge-reranker-base' | ''; - match_threshold?: number; - [key: string]: unknown; - }; - [key: string]: unknown; - }; + ai_search_options?: AiSearchOptions; [key: string]: unknown; }; -// AI Search V2 Response Types +// ============ AI Search Multi-Instance Types (Namespace-Scoped) ============ +/** `ai_search_options` shape for multi-instance requests — requires `instance_ids`. */ +type AiSearchMultiSearchOptions = AiSearchOptions & { + /** Instance IDs to search across (1-10). */ + instance_ids: string[]; +}; +/** + * Request for searching across multiple instances within a namespace. + * `ai_search_options` is required and must include `instance_ids`. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchMultiSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options: AiSearchMultiSearchOptions; +} | { + query?: never; + /** Conversation-style input. */ + messages: AiSearchMessage[]; + ai_search_options: AiSearchMultiSearchOptions; +}; +/** A search result chunk tagged with the instance it originated from. */ +type AiSearchMultiSearchChunk = AiSearchSearchResponse['chunks'][number] & { + instance_id: string; +}; +/** Describes a per-instance error during a multi-instance operation. */ +type AiSearchMultiSearchError = { + instance_id: string; + message: string; +}; +/** Response from a multi-instance search, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiSearchResponse = { + search_query: string; + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +/** Request for chat completions across multiple instances within a namespace. `ai_search_options` is required and must include `instance_ids`. */ +type AiSearchMultiChatCompletionsRequest = Omit & { + ai_search_options: AiSearchMultiSearchOptions; +}; +/** Response from multi-instance chat completions, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiChatCompletionsResponse = Omit & { + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +// ============ AI Search Response Types ============ type AiSearchSearchResponse = { search_query: string; chunks: Array<{ @@ -3444,26 +3879,138 @@ type AiSearchSearchResponse = { keyword_score?: number; /** Vector similarity score (0-1) */ vector_score?: number; + /** Keyword rank position */ + keyword_rank?: number; + /** Vector rank position */ + vector_rank?: number; + /** Reranking model score */ + reranking_score?: number; + /** Fusion method used to combine results */ + fusion_method?: 'rrf' | 'max'; + [key: string]: unknown; + }; + }>; +}; +type AiSearchChatCompletionsResponse = { + id?: string; + object?: string; + model?: string; + choices: Array<{ + index?: number; + message: { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; + [key: string]: unknown; }; + [key: string]: unknown; }>; + chunks: AiSearchSearchResponse['chunks']; + [key: string]: unknown; +}; +type AiSearchStatsResponse = { + queued?: number; + running?: number; + completed?: number; + error?: number; + skipped?: number; + outdated?: number; + last_activity?: string; + /** Storage engine statistics. */ + engine?: { + vectorize?: { + vectorsCount: number; + dimensions: number; + }; + r2?: { + payloadSizeBytes: number; + metadataSizeBytes: number; + objectCount: number; + }; + }; }; -type AiSearchListResponse = Array<{ +// ============ AI Search Instance Info Types ============ +type AiSearchInstanceInfo = { id: string; - internal_id?: string; - account_id?: string; - account_tag?: string; - /** Whether the instance is enabled (default true) */ - enable?: boolean; - type?: 'r2' | 'web-crawler'; + type?: 'r2' | 'web-crawler' | string; source?: string; + source_params?: unknown; + paused?: boolean; + status?: string; + namespace?: string; + created_at?: string; + modified_at?: string; + token_id?: string; + ai_gateway_id?: string; + rewrite_query?: boolean; + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are active. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + /** Sync interval in seconds. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; [key: string]: unknown; -}>; +}; +/** Pagination, search, and ordering parameters for listing instances within a namespace. */ +type AiSearchListInstancesParams = { + page?: number; + per_page?: number; + /** Search instances by ID. */ + search?: string; + /** Field to sort by. */ + order_by?: 'created_at'; + /** Sort direction. */ + order_by_direction?: 'asc' | 'desc'; +}; +type AiSearchListResponse = { + result: AiSearchInstanceInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Config Types ============ type AiSearchConfig = { /** Instance ID (1-32 chars, pattern: ^[a-z0-9_]+(?:-[a-z0-9_]+)*$) */ id: string; - type: 'r2' | 'web-crawler'; - source: string; - source_params?: object; + /** Instance type. Omit to create with built-in storage. */ + type?: 'r2' | 'web-crawler' | string; + /** Source URL (required for web-crawler type). */ + source?: string; + source_params?: unknown; /** Token ID (UUID format) */ token_id?: string; ai_gateway_id?: string; @@ -3473,66 +4020,474 @@ type AiSearchConfig = { reranking?: boolean; embedding_model?: string; ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are used during indexing. Defaults to vector-only. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. "rrf" = reciprocal rank fusion (default), "max" = maximum score. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + /** Minimum similarity score (0-1) for a result to be included. */ + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + /** Similarity threshold for cache hits. Stricter = fewer cache hits but higher relevance. */ + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + namespace?: string; + /** Sync interval in seconds. 3600=1h, 7200=2h, 14400=4h, 21600=6h, 43200=12h, 86400=24h. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; + [key: string]: unknown; }; -type AiSearchInstance = { +// ============ AI Search Item Types ============ +type AiSearchItemInfo = { id: string; - enable?: boolean; - type?: 'r2' | 'web-crawler'; - source?: string; + key: string; + status: 'completed' | 'error' | 'skipped' | 'queued' | 'running' | 'outdated'; + next_action?: 'INDEX' | 'DELETE' | null; + error?: string; + checksum?: string; + namespace?: string; + chunks_count?: number | null; + file_size?: number | null; + source_id?: string | null; + last_seen_at?: string; + created_at?: string; + metadata?: Record; [key: string]: unknown; }; -// AI Search Instance Service - Instance-level operations -declare abstract class AiSearchInstanceService { - /** - * Search the AI Search instance for relevant chunks. - * @param params Search request with messages and AI search options - * @returns Search response with matching chunks - */ - search(params: AiSearchSearchRequest): Promise; - /** - * Generate chat completions with AI Search context. - * @param params Chat completions request with optional streaming - * @returns Response object (if streaming) or chat completion result - */ - chatCompletions(params: AiSearchChatCompletionsRequest): Promise; - /** - * Delete this AI Search instance. - */ - delete(): Promise; -} -// AI Search Account Service - Account-level operations -declare abstract class AiSearchAccountService { - /** - * List all AI Search instances in the account. - * @returns Array of AI Search instances - */ - list(): Promise; - /** - * Get an AI Search instance by ID. - * @param name Instance ID - * @returns Instance service for performing operations - */ - get(name: string): AiSearchInstanceService; - /** - * Create a new AI Search instance. - * @param config Instance configuration - * @returns Instance service for performing operations - */ - create(config: AiSearchConfig): Promise; -} -type AiImageClassificationInput = { - image: number[]; +type AiSearchItemContentResult = { + body: ReadableStream; + contentType: string; + filename: string; + size: number; }; -type AiImageClassificationOutput = { - score?: number; - label?: string; -}[]; -declare abstract class BaseAiImageClassification { - inputs: AiImageClassificationInput; - postProcessedOutputs: AiImageClassificationOutput; -} -type AiImageToTextInput = { - image: number[]; +type AiSearchUploadItemOptions = { + metadata?: Record; +}; +type AiSearchListItemsParams = { + page?: number; + per_page?: number; + /** Search items by key name. */ + search?: string; + /** Sort order for results. */ + sort_by?: 'status' | 'modified_at'; + /** Filter items by processing status. */ + status?: 'queued' | 'running' | 'completed' | 'error' | 'skipped' | 'outdated'; + /** Filter items by source (e.g. "builtin" or "web-crawler:https://example.com"). */ + source?: string; + /** JSON-encoded Vectorize filter for metadata filtering. */ + metadata_filter?: string; +}; +type AiSearchListItemsResponse = { + result: AiSearchItemInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Item Logs Types ============ +type AiSearchItemLogsParams = { + /** Maximum number of log entries to return (1-100, default 50). */ + limit?: number; + /** Opaque cursor for pagination. Pass the `cursor` value from a previous response. */ + cursor?: string; +}; +type AiSearchItemLog = { + timestamp: string; + action: string; + message: string; + fileKey?: string; + chunkCount?: number; + processingTimeMs?: number; + errorType?: string; +}; +/** Paginated response for item processing logs (cursor-based). */ +type AiSearchItemLogsResponse = { + result: AiSearchItemLog[]; + result_info: { + count: number; + per_page: number; + cursor: string | null; + truncated: boolean; + }; +}; +// ============ AI Search Item Chunks Types ============ +type AiSearchItemChunksParams = { + /** Maximum number of chunks to return (1-100, default 20). */ + limit?: number; + /** Offset into the chunks list (default 0). */ + offset?: number; +}; +/** A single indexed chunk belonging to an item, including its text content and byte range. */ +type AiSearchItemChunk = { + id: string; + text: string; + start_byte: number; + end_byte: number; + item?: { + timestamp?: number; + key: string; + metadata?: Record; + }; +}; +/** Paginated response for item chunks (offset-based). */ +type AiSearchItemChunksResponse = { + result: AiSearchItemChunk[]; + result_info: { + count: number; + total: number; + limit: number; + offset: number; + }; +}; +// ============ AI Search Job Types ============ +type AiSearchJobInfo = { + id: string; + source: 'user' | 'schedule'; + description?: string; + last_seen_at?: string; + started_at?: string; + ended_at?: string; + end_reason?: string; +}; +type AiSearchJobLog = { + id: number; + message: string; + message_type: number; + created_at: number; +}; +type AiSearchCreateJobParams = { + description?: string; +}; +type AiSearchListJobsParams = { + page?: number; + per_page?: number; +}; +type AiSearchListJobsResponse = { + result: AiSearchJobInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +type AiSearchJobLogsParams = { + page?: number; + per_page?: number; +}; +type AiSearchJobLogsResponse = { + result: AiSearchJobLog[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Sub-Service Classes ============ +/** + * Single item service for an AI Search instance. + * Provides info, download, sync, logs, and chunks operations on a specific item. + */ +declare abstract class AiSearchItem { + /** Get metadata about this item. */ + info(): Promise; + /** + * Download the item's content. + * @returns Object with body stream, content type, filename, and size. + */ + download(): Promise; + /** + * Trigger re-indexing of this item. + * @returns The updated item info. + */ + sync(): Promise; + /** + * Retrieve processing logs for this item (cursor-based pagination). + * @param params Optional pagination parameters (limit, cursor). + * @returns Paginated log entries for this item. + */ + logs(params?: AiSearchItemLogsParams): Promise; + /** + * List indexed chunks for this item (offset-based pagination). + * @param params Optional pagination parameters (limit, offset). + * @returns Paginated chunk entries for this item. + */ + chunks(params?: AiSearchItemChunksParams): Promise; +} +/** + * Items collection service for an AI Search instance. + * Provides list, upload, and access to individual items. + */ +declare abstract class AiSearchItems { + /** List items in this instance. */ + list(params?: AiSearchListItemsParams): Promise; + /** + * Upload a file as an item. Behaves as an upsert: if an item with the same + * filename already exists, it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata to attach to the item. + * @returns The created item info. + */ + upload(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions): Promise; + /** + * Upload a file and poll until processing completes. + * Behaves as an upsert: if an item with the same filename already exists, + * it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata and polling configuration. + * @returns The item info after processing completes (or timeout). + */ + uploadAndPoll(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions & { + /** Polling interval in milliseconds (default 1000). */ + pollIntervalMs?: number; + /** Maximum time to wait in milliseconds (default 30000). */ + timeoutMs?: number; + }): Promise; + /** + * Get an item by ID. + * @param itemId The item identifier. + * @returns Item service for info, download, sync, logs, and chunks operations. + */ + get(itemId: string): AiSearchItem; + /** + * Delete an item from the instance. + * @param itemId The item identifier. + */ + delete(itemId: string): Promise; +} +/** + * Single job service for an AI Search instance. + * Provides info, logs, and cancel operations for a specific job. + */ +declare abstract class AiSearchJob { + /** Get metadata about this job. */ + info(): Promise; + /** Get logs for this job. */ + logs(params?: AiSearchJobLogsParams): Promise; + /** + * Cancel a running job. + * @returns The updated job info. + * @throws AiSearchNotFoundError if the job does not exist. + */ + cancel(): Promise; +} +/** + * Jobs collection service for an AI Search instance. + * Provides list, create, and access to individual jobs. + */ +declare abstract class AiSearchJobs { + /** List jobs for this instance. */ + list(params?: AiSearchListJobsParams): Promise; + /** + * Create a new indexing job. + * @param params Optional job parameters. + * @returns The created job info. + */ + create(params?: AiSearchCreateJobParams): Promise; + /** + * Get a job by ID. + * @param jobId The job identifier. + * @returns Job service for info, logs, and cancel operations. + */ + get(jobId: string): AiSearchJob; +} +// ============ AI Search Binding Classes ============ +/** + * Instance-level AI Search service. + * + * Used as: + * - The return type of `AiSearchNamespace.get(name)` (namespace binding) + * - The type of `env.BLOG_SEARCH` (single instance binding via `ai_search`) + * + * Provides search, chat, update, stats, items, and jobs operations. + * + * @example + * ```ts + * // Via namespace binding + * const instance = env.AI_SEARCH.get("blog"); + * const results = await instance.search({ + * query: "How does caching work?", + * }); + * + * // Via single instance binding + * const results = await env.BLOG_SEARCH.search({ + * messages: [{ role: "user", content: "How does caching work?" }], + * }); + * ``` + */ +declare abstract class AiSearchInstance { + /** + * Search the AI Search instance for relevant chunks. + * @param params Search request with query or messages and optional AI search options. + * @returns Search response with matching chunks and search query. + */ + search(params: AiSearchSearchRequest): Promise; + /** + * Generate chat completions with AI Search context (streaming). + * @param params Chat completions request with stream: true. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions with AI Search context. + * @param params Chat completions request. + * @returns Chat completion response with choices and RAG chunks. + */ + chatCompletions(params: AiSearchChatCompletionsRequest): Promise; + /** + * Update the instance configuration. + * @param config Partial configuration to update. + * @returns Updated instance info. + */ + update(config: Partial): Promise; + /** Get metadata about this instance. */ + info(): Promise; + /** + * Get instance statistics (item count, indexing status, etc.). + * @returns Statistics with counts per status, last activity time, and engine details. + */ + stats(): Promise; + /** Items collection — list, upload, and manage items in this instance. */ + get items(): AiSearchItems; + /** Jobs collection — list, create, and inspect indexing jobs. */ + get jobs(): AiSearchJobs; +} +/** + * Namespace-level AI Search service. + * + * Used as the type of `env.AI_SEARCH` (namespace binding via `ai_search_namespaces`). + * Scoped to a single namespace. Provides dynamic instance access, creation, deletion, + * and multi-instance search/chat operations. + * + * @example + * ```ts + * // Access an instance within the namespace + * const blog = env.AI_SEARCH.get("blog"); + * const results = await blog.search({ query: "How does caching work?" }); + * + * // List all instances in the namespace + * const instances = await env.AI_SEARCH.list(); + * + * // Create a new instance with built-in storage + * const tenant = await env.AI_SEARCH.create({ id: "tenant-123" }); + * + * // Upload items into the instance + * await tenant.items.upload("doc.pdf", fileContent); + * + * // Search across multiple instances + * const multi = await env.AI_SEARCH.search({ + * query: "caching", + * ai_search_options: { instance_ids: ["blog", "docs"] }, + * }); + * + * // Delete an instance + * await env.AI_SEARCH.delete("tenant-123"); + * ``` + */ +declare abstract class AiSearchNamespace { + /** + * Get an instance by name within the bound namespace. + * @param name Instance name. + * @returns Instance service for search, chat, update, stats, items, and jobs. + */ + get(name: string): AiSearchInstance; + /** + * List instances in the bound namespace. + * @param params Optional pagination, search, and ordering parameters. + * @returns Array of instance metadata with pagination info. + */ + list(params?: AiSearchListInstancesParams): Promise; + /** + * Create a new instance within the bound namespace. + * @param config Instance configuration. Only `id` is required — omit `type` and `source` to create with built-in storage. + * @returns Instance service for the newly created instance. + * + * @example + * ```ts + * // Create with built-in storage (upload items manually) + * const instance = await env.AI_SEARCH.create({ id: "my-search" }); + * + * // Create with web crawler source + * const instance = await env.AI_SEARCH.create({ + * id: "docs-search", + * type: "web-crawler", + * source: "https://developers.cloudflare.com", + * }); + * ``` + */ + create(config: AiSearchConfig): Promise; + /** + * Delete an instance from the bound namespace. + * @param name Instance name to delete. + */ + delete(name: string): Promise; + /** + * Search across multiple instances within the bound namespace. + * Fans out to the specified instance_ids and merges results. + * @param params Search request with required `ai_search_options.instance_ids`. + * @returns Search response with chunks tagged by instance_id and optional partial-failure errors. + */ + search(params: AiSearchMultiSearchRequest): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace (streaming). + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with stream: true and required `ai_search_options.instance_ids`. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace. + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with required `ai_search_options.instance_ids`. + * @returns Chat completion response with choices, chunks tagged by instance_id, and optional partial-failure errors. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest): Promise; +} +type AiImageClassificationInput = { + image: number[]; +}; +type AiImageClassificationOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiImageClassification { + inputs: AiImageClassificationInput; + postProcessedOutputs: AiImageClassificationOutput; +} +type AiImageToTextInput = { + image: number[]; prompt?: string; max_tokens?: number; temperature?: number; @@ -3784,109 +4739,459 @@ declare abstract class BaseAiTranslation { postProcessedOutputs: AiTranslationOutput; } /** - * Workers AI support for OpenAI's Responses API - * Reference: https://github.com/openai/openai-node/blob/master/src/resources/responses/responses.ts - * - * It's a stripped down version from its source. - * It currently supports basic function calling, json mode and accepts images as input. - * - * It does not include types for WebSearch, CodeInterpreter, FileInputs, MCP, CustomTools. - * We plan to add those incrementally as model + platform capabilities evolve. + * Workers AI support for OpenAI's Chat Completions API */ -type ResponsesInput = { - background?: boolean | null; - conversation?: string | ResponseConversationParam | null; - include?: Array | null; - input?: string | ResponseInput; - instructions?: string | null; - max_output_tokens?: number | null; - parallel_tool_calls?: boolean | null; - previous_response_id?: string | null; - prompt_cache_key?: string; - reasoning?: Reasoning | null; - safety_identifier?: string; - service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; - stream?: boolean | null; - stream_options?: StreamOptions | null; - temperature?: number | null; - text?: ResponseTextConfig; - tool_choice?: ToolChoiceOptions | ToolChoiceFunction; - tools?: Array; - top_p?: number | null; - truncation?: "auto" | "disabled" | null; +type ChatCompletionContentPartText = { + type: "text"; + text: string; }; -type ResponsesOutput = { - id?: string; - created_at?: number; - output_text?: string; - error?: ResponseError | null; - incomplete_details?: ResponseIncompleteDetails | null; - instructions?: string | Array | null; - object?: "response"; - output?: Array; - parallel_tool_calls?: boolean; - temperature?: number | null; - tool_choice?: ToolChoiceOptions | ToolChoiceFunction; - tools?: Array; - top_p?: number | null; - max_output_tokens?: number | null; - previous_response_id?: string | null; - prompt?: ResponsePrompt | null; - reasoning?: Reasoning | null; - safety_identifier?: string; - service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; - status?: ResponseStatus; - text?: ResponseTextConfig; - truncation?: "auto" | "disabled" | null; - usage?: ResponseUsage; +type ChatCompletionContentPartImage = { + type: "image_url"; + image_url: { + url: string; + detail?: "auto" | "low" | "high"; + }; }; -type EasyInputMessage = { - content: string | ResponseInputMessageContentList; - role: "user" | "assistant" | "system" | "developer"; - type?: "message"; +type ChatCompletionContentPartInputAudio = { + type: "input_audio"; + input_audio: { + /** Base64 encoded audio data. */ + data: string; + format: "wav" | "mp3"; + }; }; -type ResponsesFunctionTool = { +type ChatCompletionContentPartFile = { + type: "file"; + file: { + /** Base64 encoded file data. */ + file_data?: string; + /** The ID of an uploaded file. */ + file_id?: string; + filename?: string; + }; +}; +type ChatCompletionContentPartRefusal = { + type: "refusal"; + refusal: string; +}; +type ChatCompletionContentPart = ChatCompletionContentPartText | ChatCompletionContentPartImage | ChatCompletionContentPartInputAudio | ChatCompletionContentPartFile; +type FunctionDefinition = { name: string; - parameters: { - [key: string]: unknown; - } | null; - strict: boolean | null; - type: "function"; - description?: string | null; + description?: string; + parameters?: Record; + strict?: boolean | null; }; -type ResponseIncompleteDetails = { - reason?: "max_output_tokens" | "content_filter"; +type ChatCompletionFunctionTool = { + type: "function"; + function: FunctionDefinition; }; -type ResponsePrompt = { - id: string; - variables?: { - [key: string]: string | ResponseInputText | ResponseInputImage; - } | null; - version?: string | null; +type ChatCompletionCustomToolGrammarFormat = { + type: "grammar"; + grammar: { + definition: string; + syntax: "lark" | "regex"; + }; }; -type Reasoning = { - effort?: ReasoningEffort | null; - generate_summary?: "auto" | "concise" | "detailed" | null; - summary?: "auto" | "concise" | "detailed" | null; +type ChatCompletionCustomToolTextFormat = { + type: "text"; }; -type ResponseContent = ResponseInputText | ResponseInputImage | ResponseOutputText | ResponseOutputRefusal | ResponseContentReasoningText; -type ResponseContentReasoningText = { - text: string; - type: "reasoning_text"; +type ChatCompletionCustomToolFormat = ChatCompletionCustomToolTextFormat | ChatCompletionCustomToolGrammarFormat; +type ChatCompletionCustomTool = { + type: "custom"; + custom: { + name: string; + description?: string; + format?: ChatCompletionCustomToolFormat; + }; }; -type ResponseConversationParam = { +type ChatCompletionTool = ChatCompletionFunctionTool | ChatCompletionCustomTool; +type ChatCompletionMessageFunctionToolCall = { id: string; + type: "function"; + function: { + name: string; + /** JSON-encoded arguments string. */ + arguments: string; + }; }; -type ResponseCreatedEvent = { - response: Response; - sequence_number: number; - type: "response.created"; +type ChatCompletionMessageCustomToolCall = { + id: string; + type: "custom"; + custom: { + name: string; + input: string; + }; }; -type ResponseCustomToolCallOutput = { - call_id: string; - output: string | Array; - type: "custom_tool_call_output"; - id?: string; +type ChatCompletionMessageToolCall = ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall; +type ChatCompletionToolChoiceFunction = { + type: "function"; + function: { + name: string; + }; +}; +type ChatCompletionToolChoiceCustom = { + type: "custom"; + custom: { + name: string; + }; +}; +type ChatCompletionToolChoiceAllowedTools = { + type: "allowed_tools"; + allowed_tools: { + mode: "auto" | "required"; + tools: Array>; + }; +}; +type ChatCompletionToolChoiceOption = "none" | "auto" | "required" | ChatCompletionToolChoiceFunction | ChatCompletionToolChoiceCustom | ChatCompletionToolChoiceAllowedTools; +type DeveloperMessage = { + role: "developer"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +type SystemMessage = { + role: "system"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +/** + * Permissive merged content part used inside UserMessage arrays. + * + * Cabidela has a limitation where anyOf/oneOf with enum-based discrimination + * inside nested array items does not correctly match different branches for + * different array elements, so the schema uses a single merged object. + */ +type UserMessageContentPart = { + type: "text" | "image_url" | "input_audio" | "file"; + text?: string; + image_url?: { + url?: string; + detail?: "auto" | "low" | "high"; + }; + input_audio?: { + data?: string; + format?: "wav" | "mp3"; + }; + file?: { + file_data?: string; + file_id?: string; + filename?: string; + }; +}; +type UserMessage = { + role: "user"; + content: string | Array; + name?: string; +}; +type AssistantMessageContentPart = { + type: "text" | "refusal"; + text?: string; + refusal?: string; +}; +type AssistantMessage = { + role: "assistant"; + content?: string | null | Array; + refusal?: string | null; + name?: string; + audio?: { + id: string; + }; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + }; +}; +type ToolMessage = { + role: "tool"; + content: string | Array<{ + type: "text"; + text: string; + }>; + tool_call_id: string; +}; +type FunctionMessage = { + role: "function"; + content: string; + name: string; +}; +type ChatCompletionMessageParam = DeveloperMessage | SystemMessage | UserMessage | AssistantMessage | ToolMessage | FunctionMessage; +type ChatCompletionsResponseFormatText = { + type: "text"; +}; +type ChatCompletionsResponseFormatJSONObject = { + type: "json_object"; +}; +type ResponseFormatJSONSchema = { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; +}; +type ResponseFormat = ChatCompletionsResponseFormatText | ChatCompletionsResponseFormatJSONObject | ResponseFormatJSONSchema; +type ChatCompletionsStreamOptions = { + include_usage?: boolean; + include_obfuscation?: boolean; +}; +type PredictionContent = { + type: "content"; + content: string | Array<{ + type: "text"; + text: string; + }>; +}; +type AudioParams = { + voice: string | { + id: string; + }; + format: "wav" | "aac" | "mp3" | "flac" | "opus" | "pcm16"; +}; +type WebSearchUserLocation = { + type: "approximate"; + approximate: { + city?: string; + country?: string; + region?: string; + timezone?: string; + }; +}; +type WebSearchOptions = { + search_context_size?: "low" | "medium" | "high"; + user_location?: WebSearchUserLocation; +}; +type ChatTemplateKwargs = { + /** Whether to enable reasoning, enabled by default. */ + enable_thinking?: boolean; + /** If false, preserves reasoning context between turns. */ + clear_thinking?: boolean; +}; +/** Shared optional properties used by both Prompt and Messages input branches. */ +type ChatCompletionsCommonOptions = { + model?: string; + audio?: AudioParams; + frequency_penalty?: number | null; + logit_bias?: Record | null; + logprobs?: boolean | null; + top_logprobs?: number | null; + max_tokens?: number | null; + max_completion_tokens?: number | null; + metadata?: Record | null; + modalities?: Array<"text" | "audio"> | null; + n?: number | null; + parallel_tool_calls?: boolean; + prediction?: PredictionContent; + presence_penalty?: number | null; + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: ChatTemplateKwargs; + response_format?: ResponseFormat; + seed?: number | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stop?: string | Array | null; + store?: boolean | null; + stream?: boolean | null; + stream_options?: ChatCompletionsStreamOptions; + temperature?: number | null; + tool_choice?: ChatCompletionToolChoiceOption; + tools?: Array; + top_p?: number | null; + user?: string; + web_search_options?: WebSearchOptions; + function_call?: "none" | "auto" | { + name: string; + }; + functions?: Array; +}; +type PromptTokensDetails = { + cached_tokens?: number; + audio_tokens?: number; +}; +type CompletionTokensDetails = { + reasoning_tokens?: number; + audio_tokens?: number; + accepted_prediction_tokens?: number; + rejected_prediction_tokens?: number; +}; +type CompletionUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: PromptTokensDetails; + completion_tokens_details?: CompletionTokensDetails; +}; +type ChatCompletionTopLogprob = { + token: string; + logprob: number; + bytes: Array | null; +}; +type ChatCompletionTokenLogprob = { + token: string; + logprob: number; + bytes: Array | null; + top_logprobs: Array; +}; +type ChatCompletionAudio = { + id: string; + /** Base64 encoded audio bytes. */ + data: string; + expires_at: number; + transcript: string; +}; +type ChatCompletionUrlCitation = { + type: "url_citation"; + url_citation: { + url: string; + title: string; + start_index: number; + end_index: number; + }; +}; +type ChatCompletionResponseMessage = { + role: "assistant"; + content: string | null; + refusal: string | null; + annotations?: Array; + audio?: ChatCompletionAudio; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + } | null; +}; +type ChatCompletionLogprobs = { + content: Array | null; + refusal?: Array | null; +}; +type ChatCompletionChoice = { + index: number; + message: ChatCompletionResponseMessage; + finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; + logprobs: ChatCompletionLogprobs | null; +}; +type ChatCompletionsMessagesInput = { + messages: Array; +} & ChatCompletionsCommonOptions; +type ChatCompletionsOutput = { + id: string; + object: string; + created: number; + model: string; + choices: Array; + usage?: CompletionUsage; + system_fingerprint?: string | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; +}; +/** + * Workers AI support for OpenAI's Responses API + * Reference: https://github.com/openai/openai-node/blob/master/src/resources/responses/responses.ts + * + * It's a stripped down version from its source. + * It currently supports basic function calling, json mode and accepts images as input. + * + * It does not include types for WebSearch, CodeInterpreter, FileInputs, MCP, CustomTools. + * We plan to add those incrementally as model + platform capabilities evolve. + */ +type ResponsesInput = { + background?: boolean | null; + conversation?: string | ResponseConversationParam | null; + include?: Array | null; + input?: string | ResponseInput; + instructions?: string | null; + max_output_tokens?: number | null; + parallel_tool_calls?: boolean | null; + previous_response_id?: string | null; + prompt_cache_key?: string; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stream?: boolean | null; + stream_options?: StreamOptions | null; + temperature?: number | null; + text?: ResponseTextConfig; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + truncation?: "auto" | "disabled" | null; +}; +type ResponsesOutput = { + id?: string; + created_at?: number; + output_text?: string; + error?: ResponseError | null; + incomplete_details?: ResponseIncompleteDetails | null; + instructions?: string | Array | null; + object?: "response"; + output?: Array; + parallel_tool_calls?: boolean; + temperature?: number | null; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + max_output_tokens?: number | null; + previous_response_id?: string | null; + prompt?: ResponsePrompt | null; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + status?: ResponseStatus; + text?: ResponseTextConfig; + truncation?: "auto" | "disabled" | null; + usage?: ResponseUsage; +}; +type EasyInputMessage = { + content: string | ResponseInputMessageContentList; + role: "user" | "assistant" | "system" | "developer"; + type?: "message"; +}; +type ResponsesFunctionTool = { + name: string; + parameters: { + [key: string]: unknown; + } | null; + strict: boolean | null; + type: "function"; + description?: string | null; +}; +type ResponseIncompleteDetails = { + reason?: "max_output_tokens" | "content_filter"; +}; +type ResponsePrompt = { + id: string; + variables?: { + [key: string]: string | ResponseInputText | ResponseInputImage; + } | null; + version?: string | null; +}; +type Reasoning = { + effort?: ReasoningEffort | null; + generate_summary?: "auto" | "concise" | "detailed" | null; + summary?: "auto" | "concise" | "detailed" | null; +}; +type ResponseContent = ResponseInputText | ResponseInputImage | ResponseOutputText | ResponseOutputRefusal | ResponseContentReasoningText; +type ResponseContentReasoningText = { + text: string; + type: "reasoning_text"; +}; +type ResponseConversationParam = { + id: string; +}; +type ResponseCreatedEvent = { + response: Response; + sequence_number: number; + type: "response.created"; +}; +type ResponseCustomToolCallOutput = { + call_id: string; + output: string | Array; + type: "custom_tool_call_output"; + id?: string; }; type ResponseError = { code: "server_error" | "rate_limit_exceeded" | "invalid_prompt" | "vector_store_timeout" | "invalid_image" | "invalid_image_format" | "invalid_base64_image" | "invalid_image_url" | "image_too_large" | "image_too_small" | "image_parse_error" | "image_content_policy_violation" | "invalid_image_mode" | "image_file_too_large" | "unsupported_image_media_type" | "empty_image_file" | "failed_to_download_image" | "image_file_not_found"; @@ -4140,6 +5445,12 @@ type ReasoningEffort = "minimal" | "low" | "medium" | "high" | null; type StreamOptions = { include_obfuscation?: boolean; }; +/** Marks keys from T that aren't in U as optional never */ +type Without = { + [P in Exclude]?: never; +}; +/** Either T or U, but not both (mutually exclusive) */ +type XOR = (T & Without) | (U & Without); type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = { text: string | string[]; /** @@ -4410,10 +5721,10 @@ declare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En { postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output; } interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { - /** - * Base64 encoded value of the audio data. - */ - audio: string; + audio: string | { + body?: object; + contentType?: string; + }; /** * Supported tasks are 'translate' or 'transcribe'. */ @@ -4431,9 +5742,33 @@ interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { */ initial_prompt?: string; /** - * The prefix it appended the the beginning of the output of the transcription and can guide the transcription result. + * The prefix appended to the beginning of the output of the transcription and can guide the transcription result. */ prefix?: string; + /** + * The number of beams to use in beam search decoding. Higher values may improve accuracy at the cost of speed. + */ + beam_size?: number; + /** + * Whether to condition on previous text during transcription. Setting to false may help prevent hallucination loops. + */ + condition_on_previous_text?: boolean; + /** + * Threshold for detecting no-speech segments. Segments with no-speech probability above this value are skipped. + */ + no_speech_threshold?: number; + /** + * Threshold for filtering out segments with high compression ratio, which often indicate repetitive or hallucinated text. + */ + compression_ratio_threshold?: number; + /** + * Threshold for filtering out segments with low average log probability, indicating low confidence. + */ + log_prob_threshold?: number; + /** + * Optional threshold (in seconds) to skip silent periods that may cause hallucinations. + */ + hallucination_silence_threshold?: number; } interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output { transcription_info?: { @@ -4573,8 +5908,8 @@ interface Ai_Cf_Baai_Bge_M3_Input_Embedding_1 { */ truncate_inputs?: boolean; } -type Ai_Cf_Baai_Bge_M3_Output = Ai_Cf_Baai_Bge_M3_Ouput_Query | Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts | Ai_Cf_Baai_Bge_M3_Ouput_Embedding | Ai_Cf_Baai_Bge_M3_AsyncResponse; -interface Ai_Cf_Baai_Bge_M3_Ouput_Query { +type Ai_Cf_Baai_Bge_M3_Output = Ai_Cf_Baai_Bge_M3_Output_Query | Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts | Ai_Cf_Baai_Bge_M3_Output_Embedding | Ai_Cf_Baai_Bge_M3_AsyncResponse; +interface Ai_Cf_Baai_Bge_M3_Output_Query { response?: { /** * Index of the context in the request @@ -4594,7 +5929,7 @@ interface Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts { */ pooling?: "mean" | "cls"; } -interface Ai_Cf_Baai_Bge_M3_Ouput_Embedding { +interface Ai_Cf_Baai_Bge_M3_Output_Embedding { shape?: number[]; /** * Embeddings of the requested text values @@ -4697,7 +6032,7 @@ interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages { */ role?: string; /** - * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + * The tool call id. If you don't know what to put here you can fall back to 000000001 */ tool_call_id?: string; content?: string | { @@ -4943,10 +6278,16 @@ interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages { * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ role: string; - /** - * The content of the message as a string. - */ - content: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; }[]; functions?: { name: string; @@ -5587,7 +6928,7 @@ interface Ai_Cf_Qwen_Qwq_32B_Messages { */ role?: string; /** - * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + * The tool call id. If you don't know what to put here you can fall back to 000000001 */ tool_call_id?: string; content?: string | { @@ -5708,7 +7049,7 @@ interface Ai_Cf_Qwen_Qwq_32B_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -5974,7 +7315,7 @@ interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6065,7 +7406,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Prompt { */ prompt: string; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6224,7 +7565,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6496,7 +7837,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6726,7 +8067,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6888,10 +8229,16 @@ interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages { * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ role: string; - /** - * The content of the message as a string. - */ - content: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; }[]; functions?: { name: string; @@ -7097,14 +8444,20 @@ interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 { * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ role: string; - /** - * The content of the message as a string. - */ - content: string; - }[]; - functions?: { - name: string; - code: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; }[]; /** * A list of tools available for the assistant to use. @@ -7651,12 +9004,12 @@ declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { - inputs: ResponsesInput; - postProcessedOutputs: ResponsesOutput; + inputs: XOR; + postProcessedOutputs: XOR; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { - inputs: ResponsesInput; - postProcessedOutputs: ResponsesOutput; + inputs: XOR; + postProcessedOutputs: XOR; } interface Ai_Cf_Leonardo_Phoenix_1_0_Input { /** @@ -7776,7 +9129,7 @@ interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input { */ text: string | string[]; /** - * Target language to translate to + * Target langauge to translate to */ target_language: "asm_Beng" | "awa_Deva" | "ben_Beng" | "bho_Deva" | "brx_Deva" | "doi_Deva" | "eng_Latn" | "gom_Deva" | "gon_Deva" | "guj_Gujr" | "hin_Deva" | "hne_Deva" | "kan_Knda" | "kas_Arab" | "kas_Deva" | "kha_Latn" | "lus_Latn" | "mag_Deva" | "mai_Deva" | "mal_Mlym" | "mar_Deva" | "mni_Beng" | "mni_Mtei" | "npi_Deva" | "ory_Orya" | "pan_Guru" | "san_Deva" | "sat_Olck" | "snd_Arab" | "snd_Deva" | "tam_Taml" | "tel_Telu" | "urd_Arab" | "unr_Deva"; } @@ -7855,10 +9208,16 @@ interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages { * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ role: string; - /** - * The content of the message as a string. - */ - content: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; }[]; functions?: { name: string; @@ -8064,10 +9423,16 @@ interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 { * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ role: string; - /** - * The content of the message as a string. - */ - content: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; }[]; functions?: { name: string; @@ -8563,6 +9928,74 @@ declare abstract class Base_Ai_Cf_Deepgram_Aura_2_Es { inputs: Ai_Cf_Deepgram_Aura_2_Es_Input; postProcessedOutputs: Ai_Cf_Deepgram_Aura_2_Es_Output; } +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_4_7_Flash { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_5 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_6 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} interface AiModels { "@cf/huggingface/distilbert-sst-2-int8": BaseAiTextClassification; "@cf/stabilityai/stable-diffusion-xl-base-1.0": BaseAiTextToImage; @@ -8581,7 +10014,6 @@ interface AiModels { "@hf/thebloke/zephyr-7b-beta-awq": BaseAiTextGeneration; "@hf/thebloke/openhermes-2.5-mistral-7b-awq": BaseAiTextGeneration; "@hf/thebloke/neural-chat-7b-v3-1-awq": BaseAiTextGeneration; - "@hf/thebloke/llamaguard-7b-awq": BaseAiTextGeneration; "@hf/thebloke/deepseek-coder-6.7b-base-awq": BaseAiTextGeneration; "@hf/thebloke/deepseek-coder-6.7b-instruct-awq": BaseAiTextGeneration; "@cf/deepseek-ai/deepseek-math-7b-instruct": BaseAiTextGeneration; @@ -8648,6 +10080,14 @@ interface AiModels { "@cf/deepgram/flux": Base_Ai_Cf_Deepgram_Flux; "@cf/deepgram/aura-2-en": Base_Ai_Cf_Deepgram_Aura_2_En; "@cf/deepgram/aura-2-es": Base_Ai_Cf_Deepgram_Aura_2_Es; + "@cf/black-forest-labs/flux-2-dev": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev; + "@cf/black-forest-labs/flux-2-klein-4b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B; + "@cf/black-forest-labs/flux-2-klein-9b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B; + "@cf/zai-org/glm-4.7-flash": Base_Ai_Cf_Zai_Org_Glm_4_7_Flash; + "@cf/moonshotai/kimi-k2.5": Base_Ai_Cf_Moonshotai_Kimi_K2_5; + "@cf/moonshotai/kimi-k2.6": Base_Ai_Cf_Moonshotai_Kimi_K2_6; + "@cf/nvidia/nemotron-3-120b-a12b": Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B; + "@cf/google/gemma-4-26b-a4b-it": Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT; } type AiOptions = { /** @@ -8673,6 +10113,7 @@ type AiOptions = { returnRawResponse?: boolean; prefix?: string; extraHeaders?: object; + signal?: AbortSignal; }; type AiModelsSearchParams = { author?: string; @@ -8699,64 +10140,61 @@ type AiModelsSearchObject = { value: string; }[]; }; +type ChatCompletionsBase = ChatCompletionsMessagesInput; +type ChatCompletionsInput = ChatCompletionsMessagesInput; interface InferenceUpstreamError extends Error { } interface AiInternalError extends Error { } type AiModelListType = Record; +type AiAsyncBatchResponse = { + request_id: string; +}; declare abstract class Ai { aiGatewayLogId: string | null; gateway(gatewayId: string): AiGateway; /** - * Access the AI Search API for managing AI-powered search instances. - * - * This is the new API that replaces AutoRAG with better namespace separation: - * - Account-level operations: `list()`, `create()` - * - Instance-level operations: `get(id).search()`, `get(id).chatCompletions()`, `get(id).delete()` - * - * @example - * ```typescript - * // List all AI Search instances - * const instances = await env.AI.aiSearch.list(); - * - * // Search an instance - * const results = await env.AI.aiSearch.get('my-search').search({ - * messages: [{ role: 'user', content: 'What is the policy?' }], - * ai_search_options: { - * retrieval: { max_num_results: 10 } - * } - * }); - * - * // Generate chat completions with AI Search context - * const response = await env.AI.aiSearch.get('my-search').chatCompletions({ - * messages: [{ role: 'user', content: 'What is the policy?' }], - * model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast' - * }); - * ``` + * @deprecated Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ - aiSearch(): AiSearchAccountService; + aiSearch(): AiSearchNamespace; /** * @deprecated AutoRAG has been replaced by AI Search. - * Use `env.AI.aiSearch` instead for better API design and new features. + * Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ * - * Migration guide: - * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` - * - `env.AI.autorag('id').search({ query: '...' })` → `env.AI.aiSearch.get('id').search({ messages: [{ role: 'user', content: '...' }] })` - * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` - * - * Note: The old API continues to work for backwards compatibility, but new projects should use AI Search. - * - * @see AiSearchAccountService - * @param autoragId Optional instance ID (omit for account-level operations) + * @param autoragId Instance ID */ autorag(autoragId: string): AutoRAG; - run(model: Name, inputs: InputOptions, options?: Options): Promise(model: Name, inputs: { + requests: AiModelList[Name]['inputs'][]; + }, options: AiOptions & { + queueRequest: true; + }): Promise; + // Raw response + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { returnRawResponse: true; - } | { + }): Promise; + // WebSocket + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { websocket: true; - } ? Response : InputOptions extends { + }): Promise; + // Streaming + run(model: Name, inputs: AiModelList[Name]['inputs'] & { stream: true; - } ? ReadableStream : AiModelList[Name]["postProcessedOutputs"]>; + }, options?: AiOptions): Promise; + // Normal (default) - known model + run(model: Name, inputs: AiModelList[Name]['inputs'], options?: AiOptions): Promise; + // Unknown model (fallback). + // + // The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to + // route any model name that is a literal key of `AiModelList` to one of + // the known-model overloads above (so input/output mismatches surface as + // type errors rather than silently falling back to `Record`). + // Names that aren't in `AiModelList` — e.g. third-party gateway models + // like `"google/nano-banana"` — still hit this overload. + run(model: Model extends keyof AiModelList ? never : Model, inputs: Record, options?: AiOptions): Promise>; models(params?: AiModelsSearchParams): Promise; toMarkdown(): ToMarkdownService; toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise; @@ -8854,36 +10292,265 @@ declare abstract class AiGateway { run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?: { gateway?: UniversalGatewayOptions; extraHeaders?: object; + signal?: AbortSignal; }): Promise; getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line } +// Copyright (c) 2022-2025 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +/** + * Artifacts — Git-compatible file storage on Cloudflare Workers. + * + * Provides programmatic access to create, manage, and fork repositories, + * and to issue and revoke scoped access tokens. + */ +/** Information about a repository. */ +interface ArtifactsRepoInfo { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name (e.g. "main"). */ + defaultBranch: string; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 last-updated timestamp. */ + updatedAt: string; + /** ISO 8601 timestamp of the last push, or null if never pushed. */ + lastPushAt: string | null; + /** Fork source (e.g. "github:owner/repo", "artifacts:namespace/repo"), or null if not a fork. */ + source: string | null; + /** Whether the repository is read-only. */ + readOnly: boolean; + /** HTTPS git remote URL. */ + remote: string; +} +/** Result of creating a repository — includes the initial access token. */ +interface ArtifactsCreateRepoResult { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name. */ + defaultBranch: string; + /** HTTPS git remote URL. */ + remote: string; + /** Plaintext access token (only returned at creation time). */ + token: string; + /** ISO 8601 token expiry timestamp. */ + tokenExpiresAt: string; +} +/** Paginated list of repositories. */ +interface ArtifactsRepoListResult { + /** Repositories in this page (without the `remote` field). */ + repos: Omit[]; + /** Total number of repositories in the namespace. */ + total: number; + /** Cursor for the next page, if there are more results. */ + cursor?: string; +} +/** Result of creating an access token. */ +interface ArtifactsCreateTokenResult { + /** Unique token ID. */ + id: string; + /** Plaintext token (only returned at creation time). */ + plaintext: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** ISO 8601 token expiry timestamp. */ + expiresAt: string; +} +/** Token metadata (no plaintext). */ +interface ArtifactsTokenInfo { + /** Unique token ID. */ + id: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** Token state: "active", "expired", or "revoked". */ + state: 'active' | 'expired' | 'revoked'; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 expiry timestamp. */ + expiresAt: string; +} +/** Paginated list of tokens for a repository. */ +interface ArtifactsTokenListResult { + /** Tokens in this page. */ + tokens: ArtifactsTokenInfo[]; + /** Total number of tokens for the repository. */ + total: number; +} +/** + * Handle for a single repository. Returned by Artifacts.get(). + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ +interface ArtifactsRepo extends ArtifactsRepoInfo { + /** + * Create an access token for this repo. + * @param scope Token scope: "write" (default) or "read". + * @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000). + * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range. + */ + createToken(scope?: 'write' | 'read', ttl?: number): Promise; + /** List tokens for this repo (metadata only, no plaintext). */ + listTokens(): Promise; + /** + * Revoke a token by plaintext or ID. + * @param tokenOrId Plaintext token or token ID. + * @returns true if revoked, false if not found. + * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty. + */ + revokeToken(tokenOrId: string): Promise; + // ── Fork ── + /** + * Fork this repo to a new repo. + * @param name Target repository name. + * @param opts Optional: description, readOnly flag, defaultBranchOnly (default true). + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running. + */ + fork(name: string, opts?: { + description?: string; + readOnly?: boolean; + defaultBranchOnly?: boolean; + }): Promise; +} +// ── Error types ────────────────────────────────────────────────────────────── +/** + * Error codes returned by Artifacts binding operations. + * + * Each code maps to a numeric code available on `ArtifactsError.numericCode`. + */ +type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR'; +/** + * Error thrown by Artifacts binding operations. + * + * Uses a string `.code` discriminator following the Cloudflare platform + * convention (StreamError, ImagesError, etc.). The `.numericCode` matches + * the REST API `errors[].code` values. + */ +interface ArtifactsError extends Error { + readonly name: 'ArtifactsError'; + /** String error code for programmatic matching. */ + readonly code: ArtifactsErrorCode; + /** Numeric error code matching the REST API. */ + readonly numericCode: number; +} +// ── Binding ────────────────────────────────────────────────────────────────── +/** + * Artifacts binding — namespace-level operations. + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ +interface Artifacts { + /** + * Create a new repository with an initial access token. + * @param name Repository name (alphanumeric, dots, hyphens, underscores). + * @param opts Optional: readOnly flag, description, default branch name. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists. + */ + create(name: string, opts?: { + readOnly?: boolean; + description?: string; + setDefaultBranch?: string; + }): Promise; + /** + * Get a handle to an existing repository. + * @param name Repository name. + * @returns Repo handle. + * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist. + * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking. + */ + get(name: string): Promise; + /** + * Import a repository from an external git remote. + * @param params Source URL and optional branch/depth, plus target name and options. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid. + * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS. + * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository. + * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication. + * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist. + * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached. + * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + */ + import(params: { + source: { + url: string; + branch?: string; + depth?: number; + }; + target: { + name: string; + opts?: { + description?: string; + readOnly?: boolean; + }; + }; + }): Promise; + /** + * List repositories with cursor-based pagination. + * @param opts Optional: limit (1–200, default 50), cursor for next page. + */ + list(opts?: { + limit?: number; + cursor?: string; + }): Promise; + /** + * Delete a repository and all associated tokens. + * @param name Repository name. + * @returns true if deleted, false if not found. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + */ + delete(name: string): Promise; +} /** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchInternalError instead. - * @see AiSearchInternalError + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ interface AutoRAGInternalError extends Error { } /** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNotFoundError instead. - * @see AiSearchNotFoundError + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ interface AutoRAGNotFoundError extends Error { } /** - * @deprecated This error type is no longer used in the AI Search API. + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ interface AutoRAGUnauthorizedError extends Error { } /** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNameNotSetError instead. - * @see AiSearchNameNotSetError + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ interface AutoRAGNameNotSetError extends Error { } +type ComparisonFilter = { + key: string; + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; + value: string | number | boolean; +}; +type CompoundFilter = { + type: 'and' | 'or'; + filters: ComparisonFilter[]; +}; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchSearchRequest with the new API instead. - * @see AiSearchSearchRequest + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagSearchRequest = { query: string; @@ -8900,26 +10567,23 @@ type AutoRagSearchRequest = { rewrite_query?: boolean; }; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchChatCompletionsRequest with the new API instead. - * @see AiSearchChatCompletionsRequest + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagAiSearchRequest = AutoRagSearchRequest & { stream?: boolean; system_prompt?: string; }; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchChatCompletionsRequest with stream: true instead. - * @see AiSearchChatCompletionsRequest + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagAiSearchRequestStreaming = Omit & { stream: true; }; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchSearchResponse with the new API instead. - * @see AiSearchSearchResponse + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagSearchResponse = { object: 'vector_store.search_results.page'; @@ -8938,9 +10602,8 @@ type AutoRagSearchResponse = { next_page: string | null; }; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchListResponse with the new API instead. - * @see AiSearchListResponse + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagListResponse = { id: string; @@ -8952,52 +10615,489 @@ type AutoRagListResponse = { status: string; }[]; /** - * @deprecated AutoRAG has been replaced by AI Search. - * The new API returns different response formats for chat completions. + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ type AutoRagAiSearchResponse = AutoRagSearchResponse & { response: string; }; /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use the new AI Search API instead: `env.AI.aiSearch` - * - * Migration guide: - * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` - * - `env.AI.autorag('id').search(...)` → `env.AI.aiSearch.get('id').search(...)` - * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` - * - * @see AiSearchAccountService - * @see AiSearchInstanceService + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ declare abstract class AutoRAG { /** - * @deprecated Use `env.AI.aiSearch.list()` instead. - * @see AiSearchAccountService.list + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ list(): Promise; /** - * @deprecated Use `env.AI.aiSearch.get(id).search(...)` instead. - * Note: The new API uses a messages array instead of a query string. - * @see AiSearchInstanceService.search + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ search(params: AutoRagSearchRequest): Promise; /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ aiSearch(params: AutoRagAiSearchRequestStreaming): Promise; /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ aiSearch(params: AutoRagAiSearchRequest): Promise; /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ */ aiSearch(params: AutoRagAiSearchRequest): Promise; } +type BrowserRunLifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; +type BrowserRunResourceType = 'document' | 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'texttrack' | 'xhr' | 'fetch' | 'prefetch' | 'eventsource' | 'websocket' | 'manifest' | 'signedexchange' | 'ping' | 'cspviolationreport' | 'preflight' | 'other'; +/** Options fields shared by all quick actions. */ +interface BrowserRunBaseOptions { + /** Adds `