@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
303 lines (272 loc) • 9.09 kB
text/typescript
/**
* Rerank Activity
*
* Reorders a set of documents by semantic relevance to a query.
* This is a self-contained module with implementation, types, and JSDoc.
*/
import { aiEventClient } from '@tanstack/ai-event-client'
import { resolveDebugOption } from '../../logger/resolve'
import { isAbortShapedError } from '../error-payload'
import {
createGenerationContext,
runGenerationAbort,
runGenerationError,
runGenerationFinish,
runGenerationStart,
runGenerationUsage,
} from '../middleware/run'
import type { InternalLogger } from '../../logger/internal-logger'
import type { DebugOption } from '../../logger/types'
import type { GenerationMiddleware } from '../middleware/types'
import type { RerankAdapter } from './adapter'
import type { RerankResult } from '../../types'
// ===========================
// Activity Kind
// ===========================
/** The adapter kind this activity handles */
export const kind = 'rerank' as const
// ===========================
// Type Extraction Helpers
// ===========================
/** Extract provider options from a RerankAdapter via ~types */
export type RerankProviderOptions<TAdapter> = TAdapter extends {
'~types': { providerOptions: infer P extends object }
}
? P
: object
// ===========================
// Activity Options Type
// ===========================
/**
* Options for the rerank activity. The model is extracted from the adapter's
* model property.
*
* @template TAdapter - The rerank adapter type
* @template TDocument - The document element type (string or object)
*/
export interface RerankActivityOptions<
TAdapter extends RerankAdapter<string, RerankProviderOptions<TAdapter>>,
TDocument extends string | object = string,
> {
/** The rerank adapter to use (must be created with a model) */
adapter: TAdapter & { kind: typeof kind }
/** The query documents are scored against. */
query: string
/**
* Documents to rerank. Either strings or JSON-serializable objects — object
* documents are serialized with `JSON.stringify` before being sent to the
* provider, and the original element (string or object) is returned in the
* result, preserving its type.
*/
documents: Array<TDocument>
/** Return only the top N results. */
topN?: number
/** Provider-specific options */
modelOptions?: RerankProviderOptions<TAdapter>
/** Forwarded to the provider request for cancellation. */
abortSignal?: AbortSignal
/**
* Observe-only middleware notified on start, usage, success, abort, and
* error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement
* the `GenerationMiddleware` contract for a custom backend.
*/
middleware?: Array<GenerationMiddleware>
/**
* Enable debug logging. Pass `true` to enable all categories, `false` to
* silence everything including errors, or a `DebugConfig` object for granular
* control and/or a custom `Logger`.
*/
debug?: DebugOption
}
// ===========================
// Helper Functions
// ===========================
function createId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
}
/** Serialize a document for the provider. Strings pass through untouched. */
function serializeDocument(document: string | object): string {
return typeof document === 'string' ? document : JSON.stringify(document)
}
function isAbortError(error: unknown, signal?: AbortSignal): boolean {
// Prefer the error's own identity over the signal state. A genuine
// cancellation throws an abort-shaped error (DOM `AbortError`, the OpenRouter
// SDK's `RequestAbortedError`, …). Classifying on `signal.aborted` alone would
// misroute a real failure — e.g. the out-of-range-index throw below — to the
// abort hook whenever a shared/long-lived signal happens to already be
// aborted, hiding it from `onError` observers.
if (isAbortShapedError(error)) return true
// Fall back to signal state only for non-Error throws we can't otherwise
// identify; a real Error with a non-abort name is never an abort.
return error instanceof Error ? false : signal?.aborted === true
}
// ===========================
// Activity Implementation
// ===========================
/**
* Rerank activity - reorders documents by relevance to a query.
*
* @example Basic reranking
* ```ts
* import { rerank } from '@tanstack/ai'
* import { cohereRerank } from '@tanstack/ai-cohere'
*
* const { ranking, rerankedDocuments } = await rerank({
* adapter: cohereRerank('rerank-v3.5'),
* query: 'talk about rain',
* documents: ['sunny day at the beach', 'rainy afternoon in the city'],
* topN: 2,
* })
*
* console.log(rerankedDocuments[0]) // 'rainy afternoon in the city'
* ```
*
* @example Reranking object documents
* ```ts
* const { ranking } = await rerank({
* adapter: cohereRerank('rerank-v3.5'),
* query: 'best laptop for travel',
* documents: [
* { id: 1, text: 'A heavy gaming desktop' },
* { id: 2, text: 'A lightweight ultrabook with all-day battery' },
* ],
* })
*
* // ranking[0].document is the original object, fully typed.
* console.log(ranking[0].document.id)
* ```
*/
export async function rerank<
TAdapter extends RerankAdapter<string, RerankProviderOptions<TAdapter>>,
TDocument extends string | object = string,
>(
options: RerankActivityOptions<TAdapter, TDocument>,
): Promise<RerankResult<TDocument>> {
const {
adapter,
query,
documents,
topN,
modelOptions,
abortSignal,
middleware,
} = options
const model = adapter.model
const requestId = createId('rerank')
const startTime = Date.now()
const logger: InternalLogger = resolveDebugOption(options.debug)
if (documents.length === 0) {
throw new Error('rerank() requires at least one document')
}
const mwCtx = createGenerationContext({
requestId,
// `rerank` joins the GenerationActivity union; otel maps it to its own
// gen_ai.operation.name.
activity: 'rerank',
provider: adapter.name,
model,
modelOptions,
createId,
})
await runGenerationStart(middleware, mwCtx)
aiEventClient.emit('rerank:request:started', {
requestId,
provider: adapter.name,
model,
documentCount: documents.length,
timestamp: startTime,
})
logger.request(`activity=rerank provider=${adapter.name}`, {
provider: adapter.name,
model,
documentCount: documents.length,
})
// Serialize once; reuse for the request only. Original documents are mapped
// back by index below so the caller's element type is preserved.
const serialized = documents.map(serializeDocument)
try {
const result = await adapter.rerank({
model,
query,
documents: serialized,
topN,
modelOptions,
abortSignal,
logger,
})
const ranking = result.ranking.map((r) => {
const document = documents[r.index]
if (document === undefined) {
throw new Error(
`rerank(): provider ${adapter.name} returned out-of-range index ${r.index}`,
)
}
return { index: r.index, score: r.score, document }
})
const rerankedDocuments = ranking.map((r) => r.document)
const duration = Date.now() - startTime
aiEventClient.emit('rerank:request:completed', {
requestId,
provider: adapter.name,
model,
documentCount: documents.length,
resultCount: ranking.length,
duration,
timestamp: Date.now(),
})
aiEventClient.emit('rerank:usage', {
requestId,
model,
usage: result.usage,
timestamp: Date.now(),
})
logger.output(`activity=rerank results=${ranking.length}`, {
resultCount: ranking.length,
})
await runGenerationUsage(middleware, mwCtx, result.usage)
await runGenerationFinish(middleware, mwCtx, {
duration,
usage: result.usage,
})
return {
id: result.id,
model,
ranking,
rerankedDocuments,
usage: result.usage,
}
} catch (error) {
const duration = Date.now() - startTime
if (isAbortError(error, abortSignal)) {
await runGenerationAbort(middleware, mwCtx, {
reason: error instanceof Error ? error.message : undefined,
duration,
})
} else {
await runGenerationError(middleware, mwCtx, { error, duration })
}
logger.errors('rerank activity failed', { error, source: 'rerank' })
throw error
}
}
// ===========================
// Options Factory
// ===========================
/**
* Create typed options for the rerank() function without executing.
*/
export function createRerankOptions<
TAdapter extends RerankAdapter<string, RerankProviderOptions<TAdapter>>,
TDocument extends string | object = string,
>(
options: RerankActivityOptions<TAdapter, TDocument>,
): RerankActivityOptions<TAdapter, TDocument> {
return options
}
// Re-export adapter types
export type {
RerankAdapter,
RerankAdapterConfig,
AnyRerankAdapter,
} from './adapter'
export { BaseRerankAdapter } from './adapter'