Skip to content

@helix-agents/llm-vercel ​

Vercel AI SDK adapter for the Helix Agents framework. Provides LLM integration using the Vercel AI SDK's streamText function.

Installation ​

bash
npm install @helix-agents/llm-vercel ai @ai-sdk/openai

ai is a peer dependency, imported the first time an adapter calls the model (not when you import @helix-agents/llm-vercel), so importing the package builds none of ai's schemas at startup. If ai cannot be loaded, that first call fails through the adapter's error path, and the next call tries again. See Cloudflare Workers: Startup Cost.

VercelAIAdapter ​

Main adapter class implementing the LLMAdapter interface.

typescript
import { VercelAIAdapter } from '@helix-agents/llm-vercel';

const llmAdapter = new VercelAIAdapter();

Options ​

typescript
interface VercelAIAdapterOptions {
  logger?: Logger;
  telemetry?: TelemetrySettings; // passed to the AI SDK as experimental_telemetry
  /**
   * Override or extend the built-in model capability table (proxy aliases,
   * models newer than the table). Return a resolution to replace the built-in
   * one (recorded as source 'override'), or undefined to keep it.
   */
  capabilities?: CapabilitiesResolver;
  /**
   * Opt-in deadline (ms) for the provider to answer a model call (DI-76): the
   * model's `doStream` must resolve within it, else the attempt ends
   * `provider_unreachable` (retried once). No default. Model objects only.
   */
  connectTimeoutMs?: number;
}

type CapabilitiesResolver = (
  config: LLMConfig,
  builtIn: CapabilityResolution
) => CapabilityResolution | undefined;

resolveCapabilities / checkOutputFormat ​

typescript
llmAdapter.resolveCapabilities(config); // CapabilityResolution for config.model
llmAdapter.checkOutputFormat(format, config); // { ok: true } | { ok: false, reason }

resolveCapabilities applies the capabilities override over the built-in table (Anthropic models; every other provider is legacy). checkOutputFormat runs the same conversion as a native-output call: it converts an LLMOutputFormat schema with z.toJSONSchema for every model (refusing what cannot convert, such as z.date()), and for Anthropic models also normalises it to the native structured-output subset, reporting a construct the subset cannot represent. See Vercel adapter → Model capabilities.

Capability exports ​

typescript
import {
  PROFILES, // Record<ProfileName, ModelCapabilities>
  type ProfileName, // 'legacy' | 'anthropic-structured' | 'anthropic-5.5'
  ANTHROPIC_MODEL_TABLE, // ReadonlyArray<[idPrefix, ProfileName]>, first match wins
  resolveAnthropicCapabilities, // (normalisedModelId: string) => CapabilityResolution
  resolveVercelCapabilities, // (config, override?) => CapabilityResolution
  normalizeModelIdentity, // (model) => { provider, modelId, family: 'anthropic' | 'other' }
  normalizeAnthropicOutputSchema, // (jsonSchema) => { ok: true, schema } | { ok: false, reason }
  type CapabilitiesResolver,
  type AnthropicSchemaResult,
  type JSONSchema,
} from '@helix-agents/llm-vercel';
ProfileforcedToolChoicenativeStructuredOutputhistoryBoundThinking
legacytruefalsefalse
anthropic-structuredtruetruefalse
anthropic-5.5falsetruetrue

@ai-sdk/anthropic is an optional peer dependency (>=3.0.38), needed only for Anthropic models.

generateStep ​

Generate a single agent step with the LLM.

typescript
const result = await llmAdapter.generateStep({
  messages,      // Conversation history
  tools,         // Available tools
  llmConfig,     // Model configuration
  outputSchema,  // Optional: for structured output
}, {
  // Optional callbacks for streaming
  onTextDelta: async (delta) => { ... },
  onThinking: async (content, isComplete) => { ... },
  onToolStart: async (id, name, args) => { ... },
  onToolEnd: async (id, result) => { ... },
});

Parameters:

typescript
interface LLMGenerateInput<TOutput> {
  messages: Message[];
  tools: LLMTool[];
  llmConfig: LLMConfig;
  outputSchema?: ZodType<TOutput>;
}

interface LLMStreamCallbacks {
  onTextDelta?: (delta: string) => Promise<void>;
  onThinking?: (content: string, isComplete: boolean) => Promise<void>;
  onToolStart?: (id: string, name: string, args: unknown) => Promise<void>;
  onToolEnd?: (id: string, result: unknown) => Promise<void>;
}

Returns: StepResult

formatAssistantMessage ​

Format assistant message plan to internal Message format.

typescript
const message = llmAdapter.formatAssistantMessage({
  content: 'Hello!',
  toolCalls: [],
  subAgentCalls: [],
  thinking: { type: 'text', text: 'thinking...' },
});

Strict (constrained decoding) tools ​

When a tool sets strict: true (including the auto-injected __finish__ tool when llmConfig.strictOutput is enabled), VercelAIAdapter forwards that flag onto the AI SDK tool spec. From there, ai-core copies it to the provider, which applies constrained decoding subject to model capability — the Anthropic provider gates on its per-model supportsStructuredOutput table, while OpenAI maps it to strict function calling. On a model or provider that doesn't support constrained decoding the flag is simply ignored and the request is sent without strict, so enabling it is a safe no-op rather than an error.

LLMConfig ​

Model configuration passed to the adapter.

typescript
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';

const config: LLMConfig = {
  // Required: Model from AI SDK
  model: openai('gpt-6-luna'),

  // Optional: Temperature (0-2). Non-reasoning models only: reasoning models such as
  // gpt-6-luna ignore it, and Claude 5.5 models reject non-default values.
  temperature: 0.7,

  // Optional: Max output tokens
  maxOutputTokens: 4096,

  // Optional: Prompt caching (opt-in; use a helper matching your provider)
  // import { anthropicCache } from '@helix-agents/core';
  cache: anthropicCache({ ttl: '1h' }),

  // Optional: Provider-specific options
  providerOptions: {
    // For OpenAI reasoning models
    openai: {
      reasoningEffort: 'medium',
    },
    // For Anthropic with extended thinking
    anthropic: {
      thinking: {
        type: 'adaptive', // Claude 5.5 models: depth is set by `effort`, not a token budget
      },
    },
  },
};

Supported Providers ​

Any provider supported by Vercel AI SDK:

typescript
// OpenAI
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-6-luna');

// Anthropic
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-sonnet-5-5');

// Google
import { google } from '@ai-sdk/google';
const model = google('gemini-1.5-pro');

// Azure OpenAI
import { azure } from '@ai-sdk/azure';
const model = azure('gpt-6-luna');

// Cohere
import { cohere } from '@ai-sdk/cohere';
const model = cohere('command-r-plus');

Thinking/Reasoning Support ​

Anthropic Extended Thinking ​

typescript
const agent = defineAgent({
  llmConfig: {
    model: anthropic('claude-sonnet-5-5'),
    providerOptions: {
      anthropic: {
        thinking: {
          type: 'adaptive', // Claude 5.5 models: depth is set by `effort`, not a token budget
        },
      },
    },
  },
});

Thinking content is:

  • Streamed via onThinking callback
  • Included in StepResult.thinking
  • Stored in AssistantMessage.thinking
  • Recorded block by block, with each block's signature, in the ordered StepResult.parts / AssistantMessage.parts, which the adapter replays verbatim (see Preserved thinking)

OpenAI o-series Reasoning ​

typescript
const agent = defineAgent({
  llmConfig: {
    model: openai('gpt-6-luna'),
    providerOptions: {
      openai: {
        reasoningEffort: 'high', // 'low' | 'medium' | 'high'
      },
    },
  },
});

Chunk Mapping Utilities ​

Convert Vercel AI SDK chunks to Helix stream chunks.

typescript
import {
  mapVercelChunkToStreamChunk,
  isTextContentChunk,
  isToolChunk,
  isCompletionChunk,
} from '@helix-agents/llm-vercel';

// Check chunk type
if (isTextContentChunk(vercelChunk)) {
  const helixChunk = mapVercelChunkToStreamChunk(vercelChunk, {
    agentId: 'run-123',
    timestamp: Date.now(),
  });
}

Usage Example ​

typescript
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { defineAgent, defineTool } from '@helix-agents/core';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// Define agent
const MyAgent = defineAgent({
  name: 'my-agent',
  systemPrompt: 'You are a helpful assistant.',
  outputSchema: z.object({ response: z.string() }),
  tools: [
    defineTool({
      name: 'search',
      description: 'Search for information',
      inputSchema: z.object({ query: z.string() }),
      outputSchema: z.object({ results: z.array(z.string()) }),
      execute: async ({ query }) => ({ results: [`Result for: ${query}`] }),
    }),
  ],
  llmConfig: {
    model: openai('gpt-6-luna'),
    maxOutputTokens: 4096,
  },
});

// Create executor with Vercel adapter
const executor = new JSAgentExecutor(
  new InMemoryStateStore(),
  new InMemoryStreamManager(),
  new VercelAIAdapter()
);

// Execute
const handle = await executor.execute(MyAgent, 'Search for TypeScript tutorials');
const result = await handle.result();

Error Handling ​

The adapter classifies Vercel AI SDK errors into typed HelixError instances.

mapVercelError ​

Convert Vercel AI SDK errors to typed HelixError:

typescript
import { mapVercelError, mapStatusCodeToErrorCode } from '@helix-agents/llm-vercel';

const helixError = mapVercelError(vercelError);
// Returns HelixError with code, category, retryable, statusCode

Handles ApiCallError (status code mapping), RetryError (extracts last error), connection drops (below), and generic errors.

mapStatusCodeToErrorCode ​

Map HTTP status codes to error codes:

typescript
import { mapStatusCodeToErrorCode } from '@helix-agents/llm-vercel';

mapStatusCodeToErrorCode(429); // 'provider_rate_limited'
mapStatusCodeToErrorCode(503); // 'provider_overloaded'
mapStatusCodeToErrorCode(401); // 'provider_auth_error'
Status CodeErrorCode
401, 403provider_auth_error
429provider_rate_limited
408provider_timeout
400, 422provider_invalid_request
503, 529provider_overloaded
Other 5xxprovider_error

isConnectionDropError ​

Whether an error is a dropped connection to the provider, recognised by the runtime's structured error, never its message: workerd's plain Error with own retryable: true and not overloaded (Network connection lost.), or undici's TypeError whose cause code is UND_ERR_SOCKET (a close) or ECONNRESET (a reset) (fetch failed, terminated). A HelixError or another Error subclass is never one. mapVercelError maps one to provider_error, retryable: true.

typescript
import { isConnectionDropError } from '@helix-agents/llm-vercel';

isConnectionDropError(error); // boolean

connectionDropRetryDelayMs ​

generateStep retries a connection drop that happens before any model output, up to llmConfig.maxRetries (default 3, 0 disables it), waiting connectionDropRetryDelayMs(retry) before retry retry + 1: 2000 ms times 2 to the power of retry, uncapped — the AI SDK's own policy. See Error handling: connection drops.

typescript
import { connectionDropRetryDelayMs } from '@helix-agents/llm-vercel';

connectionDropRetryDelayMs(0); // 2000
connectionDropRetryDelayMs(2); // 8000

Provider unreachable (DI-76) and connectTimeoutMs ​

On Cloudflare Workers, set connectTimeoutMs. Without it, an unreachable provider holds a model call for workerd's own 75 s connect timeout before it fails (workerd masks it as internal error; reference = … with overloaded: true, and that attempt is not retried). new VercelAIAdapter({ connectTimeoutMs: 15_000 }) aborts a call whose provider has not started answering by then. There is no default because a proxy or gateway that buffers the upstream answer before it sends its own headers would fail every long call: pick a value above your provider's (or proxy's) time to first byte.

A provider call that fails before it produced anything maps to provider_unreachable (category provider, retryable: false), with the person-safe message The model provider could not be reached. and the raw error as its cause (its workerd overloaded / remote flags reach ErrorDetail.flags). generateStep retries it once (min(maxRetries, 1), 2 s), never when the cause is overloaded. Recognition: on a model object, a plain Error / TypeError that the model's own doStream rejected with (the adapter wraps the model per call); on a gateway string id, which it cannot wrap, a plain Error / TypeError before any model output (residual: a framework TypeError inside streamText reads as one there). See Error handling: provider unreachable.

Error Flow ​

When the LLM adapter encounters an error, the runtime's onError callback receives the classified HelixError and writes an ErrorChunk to the stream with code and recoverable fields. See Error Handling Guide for the complete flow.

Stop Reason Mapping ​

The adapter normalizes finish reasons to StopReason:

Provider ReasonHelix StopReason
stopend_turn
end_turnend_turn
tool-callstool_use
tool_usetool_use
lengthmax_tokens
max_tokensmax_tokens
content-filtercontent_filter
Otherunknown

A filtered or truncated reply completes the run on Temporal and DBOS (DI-26)

max_tokens, content_filter, error and unknown are error stop reasons, and this adapter returns them on a final text step (shouldStop: true), including when the reply has no text. It never returns refusal. For an agent without outputSchema, JS and both Cloudflare runtimes then fail the run; Temporal and DBOS end it completed with no error. Observed live for content_filter on both and for max_tokens on Temporal. See DI-26 in the conformance findings.

See Also ​

Released under the MIT License.