@use-crux/ai
v0.5.0
Published
Vercel AI SDK adapter for Crux — run Crux prompts, agents, and flows against any AI SDK model (generate & stream).
Maintainers
Readme
@use-crux/ai
Vercel AI SDK adapter for Crux. Runs Crux prompts against any AI SDK LanguageModel via generate() and stream().
Orchestration — prompt composition, context engineering, memory, tools, routing, safety — lives in @use-crux/core. This package exports aiSdkProviderRuntime, a normal Crux adapter with ownership: 'loop-owned' for SDKs that own their own tool loop, and delegates mechanics to AI SDK natives: stopWhen for loop budgets, prepareStep for mid-loop steering, experimental_repairText for cheap JSON repair, SDK-owned tool approval hooks, abortSignal for timeouts. It owns no policy of its own.
Internally, AI SDK request planning and result projection live in a private call-plan codec. The codec prepares generateText, generateObject, streamText, streamObject, and cached replay plans; the executor only invokes the selected SdkGateway method and decodes or attaches the raw SDK result. SdkGateway remains the only runtime seam that calls the ai package.
The package exports aiSdkProviderRuntime for advanced adapter composition. createCruxAi({ gateway }) binds aiSdkProviderRuntime.create(gateway), including its embedding and reranking extensions; public integrations should depend on the provider runtime rather than the package's internal executor.
Install
pnpm add @use-crux/ai @use-crux/core ai@^6ai is a peer dependency (^6.0.0). Add a provider package (e.g. @ai-sdk/openai) for the models you call. react is an optional peer, required only for the @use-crux/ai/stream transport.
Usage
import { prompt } from "@use-crux/core";
import { generate } from "@use-crux/ai";
import { openai } from "@ai-sdk/openai";
const fixTypos = prompt({
id: "fix-typos",
template: ({ instruction }: { instruction: string }) => instruction,
});
const result = await generate(fixTypos, {
model: openai("gpt-4o"),
input: { instruction: "Fix typos" },
});
result.text; // stringA prompt with an output schema routes through generateObject and returns a typed result.object, with tiered repair/retry when validationRetry is set. Models may be plain or wrapped in core's fallback() / router() / cascade(). Tool loops run up to maxSteps: 10 by default — identical to every Crux adapter (and unlike the raw AI SDK's single-step default), so prompts behave the same when moved between adapters. Use Crux's portable maxTokens, topK, stopSequences, seed, toolChoice, stopWhen, maxSteps(), hasToolCall(), reasoning, and toolApproval settings for adapter-neutral control; AI SDK-native stop conditions, tool-choice variants, providerOptions, headers, retries, and fine-grained provider reasoning controls belong under the typed extra option. Tools that declare contextSchema require toolsContext.<toolName> at the call site, and runtimeContext is threaded through tool execution, middleware, and function-form approval policies. timeout accepts structured budgets (totalMs, stepMs, chunkMs, toolMs, and tools[name]) and expired budgets reject with TimeoutError. generate() returns the canonical Crux envelope: accumulated text, optional complete usage, optional cost, steps, finalStep, provider-neutral messages, retained _meta, and typed .raw for the AI SDK result. stream() returns { textStream, raw, completion }; raw is the AI SDK stream result, and completion resolves to the canonical completion envelope. Use createUIMessageStreamResponse(result) or pipeUIMessageStreamToResponse(result, options) for AI SDK useChat integration. embedding() binds AI SDK embedding models as Crux primitives, and generateObjectFn / generateTextFn satisfy @use-crux/core APIs that expect a generate function (e.g. llmJudge, summarizeMessages, and retrieval recipe model steps). generateObjectFn uses the same AI SDK structured-attempt mechanics as prompt structured generation: provider schema sanitation, core-backed experimental_repairText, and router/cascade model resolution before returning { object }. If a GenerateObjectFn call needs to go through full adapter prompt execution, use createGenerateObjectFnFromGenerate(generate) from @use-crux/core/compaction.
For useChat route handlers, keep the AI SDK message edge native:
import { convertToModelMessages } from "ai";
import { createUIMessageStreamResponse, stream } from "@use-crux/ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await stream(chatPrompt, {
model,
input: { tenantId: "acme" },
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse(result);
}toParams(resolved, { model }) exposes the AI SDK request-planning codec for
headless power users, and fromResponse(response) normalizes an AI SDK
generate result into Crux response facts. These helpers translate only; use
managed generate()/stream() for Crux-owned tools, approvals, memory,
validation retry, safety, and observability.
prepare(prompt, opts) returns a sans-I/O handle over the package's
SdkGateway seam: inspect params, call the AI SDK yourself, then pass the
raw SDK result to finish(response). Use generate(prompt, { ...opts,
transport }) when Crux should keep owning the loop and your callback should
make each SdkGateway call. Streaming with transport is not supported and
rejects with CruxTransportStreamUnsupportedError.
Streaming structured output uses AI SDK streamObject(). In AI SDK v6 that API does not expose the text-loop tool event surface, so Crux omits tools for structured streams and emits a one-time warning when a structured stream declares tools. Use generate() for structured tool loops, or stream without an output schema when live tool observability is required.
Media
AI SDK ModelMessage and useChat content stay model-owned. Crux preserves
normal image/audio/video/file parts and ordered mixed assistant output without
adding a second message or tool loop. The package exports native AI SDK
generateImage(), transcribe(), and generateSpeech() wrappers with the
shared Crux result tail, cancellation, timeout, and descriptor-only reporting.
Provider-native controls remain in typed extra; persistence is a separate
application assetStore.put() call.
For Anthropic AI SDK models, Crux converts the stable provider-cache prefix into system messages and places providerOptions.anthropic.cacheControl on the single cacheBoundary block.
Agent frameworks
Use @use-crux/ai/agent when an AI SDK-compatible framework, such as Convex Agent or Mastra, owns the model loop but you still want Crux prompt resolution and tracing:
import { resolve } from "@use-crux/ai/agent";
const { instructions, model } = await resolve(chatPrompt, {
model: languageModel,
input: { mode: "support" },
tools: Object.keys(tools),
});@use-crux/ai/agent composes instructions through the normal core prompt pipeline and adds the AI SDK runtime binding: when Crux execution hooks are installed, the returned model is wrapped with wrapLanguageModel() middleware for generate/stream traces, tool timing estimates, stream progress, and provider metadata cost extraction.
Testing without module mocks
The only module that calls AI SDK functions is the SdkGateway. Bind your own with createCruxAi({ gateway }) to script results in tests — no vi.mock('ai'):
import { createCruxAi } from "@use-crux/ai";
const ai = createCruxAi({ gateway: myScriptedGateway });
const result = await ai.generate(myPrompt, { model, input });For loop-mechanics coverage, pass MockLanguageModelV3 (from ai/test) models through the default live gateway.
AI SDK helpers such as tool() are not re-exported — import them from 'ai' or this package's helper exports as documented. Crux's portable loop helpers (maxSteps, hasToolCall) come from @use-crux/core. Agent compositions come from @use-crux/core/agent.
See the @use-crux/core reference and the Crux docs for the full API.
