Crux
API ReferenceAdapters

Adapter Interface

Provider adapter abstraction for building AI provider integrations.

import { defineProviderRuntime } from '@crux/core/adapter'
import type {
  CruxAdapter,
  AdapterGenerateOptions,
  AdapterStreamOptions,
  AdapterGenerateResult,
  AdapterResponse,
  CallArgs,
  StreamHandle,
  ToolResultEntry,
  SingleTurnRuntimeContract,
  LoopOwnedRuntimeContract,
  NativeTranscriptCodec,
} from '@crux/core/adapter'
import { transcriptCodecConformance } from '@crux/core/adapter/testing'

Overview

The adapter package provides the public authoring surface for AI provider adapters. defineProviderRuntime() binds a stable runtime id to either turn, where Crux owns the provider step loop, or loop, where the SDK owns the loop and Crux steers policy around it. Both branches compile into the same execution engine and share the same policy modules (validation retry, tool instrumentation, approvals) plus the same per-call Safety session, so behavior does not diverge between runtimes.

Built-in adapters are built on this abstraction: @crux/openai, @crux/anthropic, and @crux/google export single-turn provider runtimes, while @crux/ai exports a loop-owned provider runtime for the Vercel AI SDK. Native provider packages own a NativeTranscriptCodec for provider message conversion, assistant text/tool-call extraction, and provider-specific tool-round appends. Core injects transcript-produced providerMessages into request builders and composes transcript.readAssistant(raw) with response-level metadata. Core owns the canonical Message[], CallArgs, AdapterResponse, ToolResultEntry, and small tool-output helpers those codecs share.

Which contract do I implement?

Your SDK…Runtime branchExample
Exposes text/structured/stream chat calls and leaves tool execution to Cruxturn@crux/openai, @crux/anthropic, @crux/google
Runs its own multi-step tool loop (generateText with stopWhen, native tool execution)loop@crux/ai
Needs a bespoke non-chat boundarycore IR directlycustom provider

Never implement more than one generation runtime for one SDK. In either branch, core owns the policy layer — prompt resolution, fallback()/router()/cascade() routing, validation retry, constraints, guardrails, the tool-approval protocol, instrumentation, timeouts, and observability. A provider runtime implements mechanics only, and should delegate to SDK-native capabilities wherever one exists.

Under the hood, turn compiles to AdapterSpec: core calls call() once per loop turn and appendToolRound() formats results. loop.bind() compiles to ExecutorSpec: the SDK owns the loop and core steers each step through a StepObserver:

  • run() runs the SDK loop, awaiting observer.onStepFinish(step) after every step and applying the returned StepDirective before the next one — continue, stop, or amend (swap system prompt/active tools mid-loop, optionally refundStep so bookkeeping steps like LoadSkill don't consume budget).
  • attemptStructured() makes exactly ONE structured-output attempt and returns invalid as a value instead of throwing — core owns the corrective-retry loop.
  • stream() returns the SDK's stream result untouched plus a typed completion(). When request.safety is set (a SafetyStream — text streams with streaming guardrails or constraints), the spec must drive it: feed every outgoing text delta, forward emit content, swallow holds, surface a thrown GuardrailBlockedError as a stream error, and call finish() at end-of-stream, emitting the seal's pending tail. With the AI SDK this is one experimental_transform.
  • Tool-approval needs surface as the suspended outcome; core mints approval ids/tokens and owns resume.

Test provider runtimes through providerRuntime.create(scriptedClient) or package-level createX() factories. The scripted client should own only the remote SDK seam, while assertions inspect public adapter behavior and provider request bodies:

const adapter = myProviderRuntime.create(scriptedClient)
const result = await adapter.generate(myPrompt, { model: 'model-id', input })

expect(result.text).toBe('ok')
expect(scriptedClient.calls[0].messages).toEqual(expectedProviderMessages)

Test executors with fakeExecutor() (a scripted in-memory reference implementation) and prove contract fidelity with executorSpecConformance():

import { executorAdapter, fakeExecutor, executorSpecConformance } from '@crux/core/adapter'

// Policy tests, zero SDK:
const fake = fakeExecutor({ structured: ['not json', '{"ok":true}'] })
const executor = executorAdapter(fake.spec)(fake.client)

// Contract suite for custom executor authors:
const violations = await executorSpecConformance(mySpec, myHarness)
expect(violations).toEqual([])

defineProviderRuntime({ turn })

Compile a raw chat SDK provider spec into a Crux adapter runtime.

import { defineProviderRuntime } from '@crux/core/adapter'

export const myProviderRuntime = defineProviderRuntime({
  id: 'my-provider',
  turn: {
    bind: (client) => ({
      call: (request) => client.chat.create(request),
      stream: (request) => client.chat.stream(request),
    }),
    request: buildRequest,
    transcript: myTranscript,
    response: {
      meta: responseMeta,
      text: structuredTextOverride,
    },
    stream: {
      request: (request) => ({ ...request, stream: true }),
      textDelta: (chunk) => chunk.delta?.text,
    },
    settings: mapSettings,
    outputSchema: wrapOutputSchema,
  },
})

export const createMyProvider = myProviderRuntime.create

The provider runtime owns provider wire facts: request bodies, SDK method binding, transcript conversion, assistant-turn extraction, response metadata extraction, stream delta extraction, settings/schema mapping, and provider-specific dependencies. Single-turn specs should use transcript: NativeTranscriptCodec plus response.meta(raw); response.text(raw, assistant) is only needed when the SDK exposes parsed structured output separately from assistant message text. Crux owns prompt resolution, tool execution, default canonical tool-round appends, validation retry, safety, memory capture, and observability.

request(args, ctx) receives the usual canonical CallArgs plus args.providerMessages, already produced by transcript.fromMessages(args.messages). Request builders should use providerMessages rather than re-running public fromMessages() wrappers.

Test transcript codecs directly with transcriptCodecConformance():

import { transcriptCodecConformance } from '@crux/core/adapter/testing'

const violations = transcriptCodecConformance({
  name: 'my-provider transcript',
  transcript: myTranscript,
  canonicalMessages,
  providerMessages,
  decodedMessages,
  rawAssistant,
  assistant: { text: 'Checking.', toolCalls: [{ id: 'call_1', name: 'lookup', args: {} }] },
})

expect(violations).toEqual([])

Provider packages may expose GenerateTextFn and GenerateObjectFn helpers when useful, but those helpers should stay explicitly smaller than adapter generate(): no prompt resolution, tool loop, safety, cassettes, memory capture, or instrumentation.

Internal IR: adapter(spec)

Create a core-driven adapter from a compiled AdapterSpec. Provider packages should implement defineProviderRuntime({ turn }); use this only when testing core adapter internals or building a bespoke runtime compiler.

ParameterTypeDescription
specAdapterSpec<TClient, TRawResponse, TRawStream, TExtra>Provider-specific adapter specification

Returns: (client: TClient) => CruxAdapter<TClient, TRawResponse, TRawStream, TExtra>

import { adapter } from '@crux/core/adapter'

const createMyAdapter = adapter({
  providerId: 'my-provider',

  async call(client, args) {
    const response = await client.chat({
      model: args.model,
      system: args.system,
      messages: args.messages,
      ...args.settings,
    })
    return {
      raw: response,
      extracted: {
        text: response.content,
        toolCalls: undefined,
        usage: {
          inputTokens: response.usage.input,
          outputTokens: response.usage.output,
          totalTokens: response.usage.input + response.usage.output,
        },
        finishReason: response.stop_reason,
        responseId: response.id,
        actualModelId: response.model,
      },
    }
  },

  async stream(client, args) {
    const stream = await client.chatStream({ model: args.model, messages: args.messages })
    return {
      rawStream: stream,
      extractTextDelta: (chunk) => chunk.delta?.text,
      completion: async () => undefined,
    }
  },

  appendToolRound(messages, response, results) {
    return [
      ...messages,
      { role: 'assistant', content: response.text },
      ...results.map((r) => ({ role: 'tool' as const, content: r.content })),
    ]
  },

  mapSettings(settings) {
    return {
      temperature: settings.temperature,
      max_tokens: settings.maxTokens,
      top_p: settings.topP,
    }
  },
})

// Create an adapter instance
const adapter = createMyAdapter(myClient)
const result = await adapter.generate(myPrompt, { model: 'my-model', input: { topic: 'AI' } })

AdapterSpec<TClient, TRawResponse, TRawStream, TExtra>

The core-step execution IR produced by the turn provider-runtime branch.

Type ParameterDescription
TClientThe provider's SDK client type
TRawResponseThe provider's raw API response type
TRawStreamThe provider's raw stream type
TExtraProvider-specific options (e.g., tool_choice for Anthropic)
MethodSignatureDescription
providerIdstringProvider identifier for adaptation matching (e.g., 'anthropic', 'openai')
call(client, args: CallArgs<TExtra>) => Promise<{ raw, extracted }>Execute a non-streaming API call
stream(client, args: CallArgs<TExtra>) => Promise<StreamHandle<TRawStream>>Execute a streaming API call
appendToolRound(messages, response, results) => Message[]Format tool results for the next tool loop turn
mapSettings(settings: GenerationSettings) => Record<string, unknown>Map canonical settings to provider-native field names
sanitizeToolSchema?(schema) => Record<string, unknown>Optional. Post-process JSON Schema output for this provider.
wrapOutputSchema?(schema: ZodType) => Record<string, unknown>Optional. Convert structured output schema to provider-native params.

CruxAdapter<TClient, TRawResponse, TRawStream, TExtra>

The adapter interface returned by the factory.

Property / MethodTypeDescription
providerIdstringProvider identifier from the spec
generate(prompt, opts)Promise<AdapterGenerateResult<TRawResponse>>Execute a prompt (non-streaming) with automatic tool loop
stream(prompt, opts)Promise<StreamHandle<TRawStream>>Execute a prompt (streaming)
parallelfunctionRun multiple agents concurrently
pipelinefunctionChain agents sequentially
consensusfunctionRun agents and pick a winner via voting
swarmfunctionRun a swarm of agents with peer-to-peer routing

CallArgs<TExtra>

Canonical args assembled by the adapter from prompt resolution. Passed to spec.call() and spec.stream().

FieldTypeDescription
modelstringModel identifier
systemstring | undefinedSystem message
systemBlocksreadonly SystemBlock[] | undefinedSystem message blocks with provider-level caching hints. Joining block.text with \n\n produces system.
messagesMessage[]Conversation messages
settingsRecord<string, unknown>Mapped generation settings
schemaZodType | undefinedOutput schema (for structured output)
schemaParamsRecord<string, unknown> | undefinedProvider-native schema params
toolsArray<{ name, description, parameters, execute }> | undefinedTool definitions
extraTExtraProvider-specific options

AdapterResponse

Canonical response extracted by the adapter from the provider's raw response. Used by the tool loop to determine if another iteration is needed.

FieldTypeDescription
textstringExtracted text
toolCallsArray<{ id, name, args }> | undefinedTool call requests
usage{ inputTokens, outputTokens, totalTokens }Token usage
finishReasonstring | undefinedWhy generation stopped
responseIdstring | undefinedProvider response ID
actualModelIdstring | undefinedActual model used (may differ from requested)

StreamHandle<TRawStream>

Stream handle returned by the adapter's stream method.

FieldTypeDescription
rawStreamTRawStream & AsyncIterable<unknown>The raw SDK stream
extractTextDelta(chunk: unknown) => string | undefinedExtract text delta from a stream chunk
completion() => Promise<TraceMeta | undefined>Get completion metadata after stream ends

AdapterGenerateOptions<TExtra>

Options for adapter generate() calls.

FieldTypeDescription
modelstringModel identifier
inputRecord<string, unknown>?Input for the prompt
providerstring?Provider identifier for adaptation matching
tokenBudgetnumber?Token budget for system message
maxStepsnumber?Maximum tool loop iterations. Default: 10.
settingsGenerationSettings?Additional generation settings (highest precedence)
extraTExtra?Provider-specific options
messagesMessage[]?Additional messages to prepend (e.g., conversation history)
validationRetryValidationRetryOptions?Retry structured output on Zod validation failure

AdapterGenerateResult<TRawResponse>

FieldTypeDescription
rawTRawResponseThe raw SDK response
textstringExtracted text
_metaTraceMetaNormalized metadata (usage, finish reason, tool calls, etc.)
stepsnumberNumber of tool loop iterations performed

ToolResultEntry

Tool result to feed back into the next tool loop turn.

FieldTypeDescription
toolCallIdstringThe tool call ID this result corresponds to
namestringTool name
contentstringSerialized result content
isErrorboolean?Whether this result represents an error

Internal IR: executorAdapter(spec)

Create an SDK-loop adapter from a compiled ExecutorSpec. Provider packages should implement defineProviderRuntime({ loop }); use this only when testing executor internals or building a bespoke runtime compiler.

ParameterTypeDescription
specExecutorSpec<TClient, TModel, TRawResponse, TRawStream>Executor specification

generate() accepts ExecutorGenerateOptions: model (plain or routing wrapper), input, tools, toolMiddleware, messages, maxSteps, settings, tokenBudget, timeoutMs, validationRetry, constraints, guardrails, observer, activeTools, and a spec-specific extra passthrough. The result carries raw (the SDK's own result object), text, object, _meta, steps, messages, and pendingApprovals when suspended on tool approval.

ExecutorSpec

The SDK-loop execution IR produced by the loop provider-runtime branch.

MethodDescription
executorIdExecutor identifier for observability (e.g. 'ai-sdk')
describeModel(model)Extract ModelInfo from an SDK model reference
mapSettings(settings, model)Map canonical settings to SDK option names
runLoop(client, request)Multi-step text + tools; SDK owns the loop, core steers via request.observer
attemptStructured(client, request)ONE structured attempt; returns invalid as a value, never throws on schema failure
runStream(client, request)Streaming; returns the SDK stream result untouched + typed completion(). Must drive request.safety when present (streaming guardrails)
replayStream?(cached)Optional semantic-cache stream replay

Types

import type {
  // Factory and result types (core-driven loop)
  CruxAdapter,
  AdapterGenerateOptions,
  AdapterStreamOptions,
  AdapterGenerateResult,
  // Spec interfaces
  AdapterSpec,
  ExecutorSpec,
  // Executor contract types (SDK-driven loop)
  ExecutorRequest,
  StructuredRequest,
  ExecutorStep,
  StepDirective,
  StepObserver,
  ExecutorOutcome,
  StructuredAttempt,
  ExecutorStreamHandle,
  ExecutorStreamMeta,
  CruxExecutor,
  ExecutorGenerateOptions,
  ExecutorGenerateResult,
  PendingToolApproval,
  ApprovalRequestInfo,
  // Canonical types
  AdapterResponse,
  CallArgs,
  StreamHandle,
  ToolResultEntry,
  StatusDelta,
} from '@crux/core/adapter'

On this page