Crux
API ReferenceAdapters

@crux/ai

Vercel AI SDK adapter — generate, stream, dense embedding helpers, reranking helpers, and the SdkGateway test seam.

import { generate, stream, embedding, reranker, aiSdkProviderRuntime } from '@crux/ai'
import { generateObjectFn, generateTextFn, createCruxAi, CruxAIError } from '@crux/ai'
import { resolve } from '@crux/ai/agent'

For usage examples and detailed walkthrough, see the Execution guide.

Architecture

@crux/ai exports aiSdkProviderRuntime, a loop-owned provider runtime for the Vercel AI SDK. Core owns all policy (prompt resolution, fallback()/router()/cascade() routing, validation retry, the Safety session for constraints/guardrails, the tool-approval protocol, instrumentation, timeouts); the adapter delegates mechanics to AI SDK natives (stopWhen, prepareStep, experimental_repairText, native needsApproval, abortSignal, experimental_transform for streaming guardrails). The bound runtime also exposes AI SDK embedding and reranking extensions through the same SdkGateway seam.

The only module that calls AI SDK functions is the SdkGateway — inject a scripted one via createCruxAi to test without module mocks.

Internally, @crux/ai keeps AI SDK request planning and result projection in a private call-plan codec. The executor asks the codec for a generateText, generateObject, streamText, streamObject, or replay plan, invokes the selected SdkGateway method, then decodes or attaches the raw SDK result. This is not a public extension API; it exists to keep the gateway seam stable and the adapter behavior testable.

Cross-adapter parity: switching a prompt between @crux/ai and a native adapter (@crux/openai, @crux/anthropic, @crux/google) changes nothing observable except model behavior itself — same default tool-loop budget (maxSteps: 10), same corrective-retry messages, same approval suspend/resume protocol, same canonical tool.call / tool.args / raw and model-facing tool.result telemetry, same routing and audit metadata. This is enforced by a cross-dialect parity suite in @crux/core. Note this differs from the raw AI SDK's own generateText default (stopWhen: stepCountIs(1)): Crux adapters loop tools by default; pass maxSteps: 1 for single-step behavior.

Hallucinated tool calls are part of that parity: where the raw AI SDK throws NoSuchToolError/InvalidToolInputError and aborts the loop, @crux/ai uses the SDK's native experimental_repairToolCall to feed the model the same error tool result core-driven adapters produce ({"error":"Tool \"x\" not found"}), so the model self-corrects and the loop continues. The SDK's tool-error taxonomy never reaches your code.

@crux/ai/agent

Use @crux/ai/agent for AI SDK-compatible agent frameworks that own their own model loop, such as Convex Agent or Mastra. It composes instructions through the normal core prompt pipeline, then wraps the model with AI SDK middleware when Crux execution hooks are installed.

import { resolve } from '@crux/ai/agent'

const { instructions, model } = await resolve(chatPrompt, {
  model: languageModel,
  input: { mode: 'support' },
  tools: Object.keys(tools),
})

The returned model reports generate/stream traces, stream progress, tool timing estimates, provider metadata cost, and parent-child links back to the prompt-resolution trace.

generate(prompt, options)

Execute a prompt using the Vercel AI SDK.

FieldTypeDescription
promptPromptThe prompt to execute
options.modelLanguageModel | FallbackModel<LanguageModel> | AnyRouterModel<LanguageModel> | CascadeModel<LanguageModel>AI SDK model or a fallback(), router(), or cascade() wrapper
options.inputMergedInput<TOwnInput, TContexts>Input values — merged across the prompt's own schema and every context's input schema
options.toolsToolSet?Additional tools to merge at call time
options.toolMiddlewareToolMiddleware | readonly ToolMiddleware[]?Call-site tool wrappers. See Tool Middleware.
options.toolChoiceToolChoice<ToolSet>?Tool choice strategy
options.stopWhenStopCondition<ToolSet> | StopCondition<ToolSet>[]?Custom stop condition(s), replacing the maxSteps budget (e.g. hasToolCall(name))
options.maxStepsnumber?Maximum tool-loop steps, identical across all Crux adapters. Enforced natively via AI SDK stopWhen. Default: 10
options.activeToolsstring[]?Restrict available tools
options.messagesModelMessage[]?Explicit AI SDK message history. Used for approval resume and advanced chat-loop control.
options.tokenBudgetnumber?Token budget for context dropping
options.timeoutMsnumber?Hard timeout for the provider call. Crux passes an AbortSignal to the AI SDK and closes the generation span with AbortError if the call does not settle.
options.validationRetryValidationRetryOptions?Retry structured output on validation failure
options.constraintsConstraint[]?Per-call semantic constraints (highest precedence in the safety merge)
options.constraintMaxRetriesnumber?Shared cap on total constraint retries across all constraints
options.guardrailsGuardrail[]?Per-call guardrails (highest precedence in the safety merge)

Returns:

  • With output schema: GenerateObjectResult<T>.object (typed)
  • Without output: GenerateTextResult.text

stream(prompt, options)

Stream a prompt execution. Same options as generate(), except cascade() is not supported (cascade needs full results for tier evaluation). Streaming guardrails (stream: { buffer } / onChunk) execute automatically on text streams — mounted through the AI SDK's experimental_transform, so holds, mid-stream transforms, and blocks reach textStream consumers; constraints run report-only at end-of-stream and audits land on the completion meta.

FieldTypeDescription
promptPromptThe prompt to execute
options.modelLanguageModel | FallbackModel<LanguageModel> | AnyRouterModel<LanguageModel>AI SDK model or fallback() / router() wrapper
options.inputMergedInput<TOwnInput, TContexts>Input values — merged across the prompt's own schema and every context's input schema
options.toolsToolSet?Additional tools to merge at call time
options.toolMiddlewareToolMiddleware | readonly ToolMiddleware[]?Call-site tool wrappers. See Tool Middleware.
options.toolChoiceToolChoice<ToolSet>?Tool choice strategy
options.stopWhenStopCondition<ToolSet> | StopCondition<ToolSet>[]?Custom stop condition(s), replacing the maxSteps budget
options.maxStepsnumber?Maximum tool-loop steps (default 10), identical across all Crux adapters
options.activeToolsstring[]?Restrict available tools
options.messagesModelMessage[]?Explicit AI SDK message history. Used for approval resume and advanced chat-loop control.
options.tokenBudgetnumber?Token budget for context dropping

Returns:

  • With output: ObjectStreamResult<T>.partialObjectStream
  • Without output: TextStreamResult.textStream
const result = await stream(editDraft, {
  model: openai('gpt-4o'),
  input: { instruction: 'Fix the intro' },
})
for await (const partial of result.partialObjectStream) {
  console.log(partial)
}

Stream results additionally carry a typed completion promise:

const result = await stream(chatPrompt, { model, input: { message } })
for await (const delta of result.textStream) process.stdout.write(delta)
const meta = await result.completion
// { usage, cost, finishReason, text, streaming: { ttftMs, tokensPerSecond, totalChunks } }

createCruxAi(options?)

Build a @crux/ai instance bound to a specific SdkGateway. The package-level exports are an instance created with the live gateway; reach for this when you need the test seam.

FieldTypeDescription
options.gatewaySdkGateway?The AI SDK gateway to execute against. Defaults to liveSdkGateway()

Returns: CruxAi{ generate, stream, generateTextFn, generateObjectFn, embedding, reranker }

import { createCruxAi } from '@crux/ai'

const ai = createCruxAi({ gateway: myScriptedGateway })
const result = await ai.generate(myPrompt, { model, input })

CruxAIError

Coded error wrapper. generate()/stream() propagate underlying errors unchanged (existing ValidationExhaustedError/AggregateError handling keeps working); use CruxAIError.classify(error) at your boundary for a stable, machine-readable code.

MemberTypeDescription
code'timeout' | 'validation_exhausted' | 'provider' | 'aborted'Stable failure category
classify(error: unknown) => CruxAIError (static)Classify any error, preserving cause

generateObjectFn

Pre-bound GenerateObjectFn for @crux/core APIs (judges, extraction). Pass model per-call.

Signature: GenerateObjectFn(opts: { model, system?, prompt, schema }) => Promise<{ object: T }>

generateObjectFn shares the same AI SDK structured-attempt mechanics used by generate() for prompts with an output schema: provider-specific schema sanitation, core-backed experimental_repairText, and router/cascade model resolution all happen before the helper returns { object }.

const judge = llmJudge({ generate: generateObjectFn, model, ... })

For a GenerateObjectFn that also runs through full adapter prompt execution semantics, use createGenerateObjectFnFromGenerate(generate) from @crux/core/compaction.

generateTextFn

Pre-bound GenerateTextFn for @crux/core APIs (summarization, compaction).

Signature: GenerateTextFn(opts: { model, system?, prompt }) => Promise<{ text: string }>

reranker(config)

Create a retrieval reranker backed by AI SDK rerank().

const docsReranker = reranker({
  name: 'docs-reranker',
  model: openai.reranking('gpt-4.1-mini'),
  topN: 5,
})

Use it with retriever({ rerank }) when you want model-based reordering after raw retrieval and before prompt injection or tool output.

FieldTypeDescription
namestringStable reranker identifier
modelRerankingModelAI SDK reranking model
topNnumber?Limit reranked output
maxRetriesnumber?Retry budget for the rerank call
document(hit) => string?Optional serializer for the text sent to the reranker

embedding(config)

Create a dense Crux embedding backed by AI SDK embedMany().

const docsEmbedding = embedding({
  name: 'docs-embedding',
  model: openai.textEmbeddingModel('text-embedding-3-small'),
  dimensions: 1536,
  maxInputTokens: 8192,
})

Use it with memory, indexers, or retrievers when you want the AI SDK model registry and provider abstraction, but still want a first-class Crux embedding object with batching and telemetry.

FieldTypeDescription
namestringStable embedding identifier
modelEmbeddingModelAI SDK embedding model
dimensionsnumberOutput vector dimensionality
maxInputTokensnumberPer-input token ceiling
batch.maxSizenumber?Top-level Crux batch size. Defaults to 100.
batch.concurrencynumber?Top-level Crux batch concurrency. Defaults to 1.
maxRetriesnumber?Retry budget for AI SDK embedding calls
maxParallelCallsnumber?Internal AI SDK split-call concurrency. Defaults to 1.
headersRecord<string, string>?Optional request headers
providerOptionsRecord<string, unknown>?Provider-specific AI SDK options

Tool approvals

When a tool with needsApproval (set directly or via approval middleware) fires, generation suspends: the result has _meta.finishReason === 'tool_approval_required', pendingApprovals carries the minted requests (with anti-forgery tokens), and messages ends with the approval-request message. Persist the messages, collect a decision, append a tool-approval-response, and call generate() again.

import { appendToolApprovalResponse } from '@crux/core/tool-middleware'

const first = await generate(assistant, { model, input, tools })

if (first.pendingApprovals?.length) {
  const approval = first.pendingApprovals[0]
  const resumed = await generate(assistant, {
    model,
    input,
    tools,
    messages: appendToolApprovalResponse(first.messages, {
      approvalId: approval.approvalId,
      approved: true,
      approvalToken: approval.approvalToken,
    }),
  })
}

The approved tool executes during resume, its result is replayed into the loop as a normal tool round, and generation continues. Denied tools produce an execution-denied output the model can reason about.

What is not exported

AI SDK helpers (tool, stepCountIs, hasToolCall) and AI SDK types are no longer re-exported — import them from 'ai' directly. Agent compositions come from @crux/core/agent or from executorAdapter(). The legacy toMessages/fromMessages/createAIExecutor exports were removed (RFC use-crux/crux#28).

Types

import type {
  AIGenerateOptions,
  AIEmbeddingConfig,
  AIRerankerConfig,
  CruxAi,
  CruxAiOptions,
  CruxAIErrorCode,
  CruxStreamExtensions,
  SdkGateway,
  TextStreamResult,
  ObjectStreamResult,
  GenerateReturn,
  StreamReturn,
} from '@crux/ai'

On this page