Skip to main content

Integrate Morpheus API Gateway with Vercel AI SDK

Learn how to integrate the Morpheus API Gateway with Vercel’s AI SDK v5 to build AI-powered applications with free, decentralized AI inference. This guide covers streaming responses, tool calling, and handling Morpheus-specific implementation details.

Overview

The Morpheus API Gateway provides free AI inference through a decentralized compute marketplace. By integrating with Vercel’s AI SDK, you get access to powerful models like Qwen, Llama, and more while maintaining a familiar OpenAI-compatible API structure.
The Morpheus API Gateway is currently in Open Beta, providing free access to AI inference without requiring wallet connections or staking MOR tokens.

Prerequisites

Before you begin, ensure you have:
  • Node.js 18+ installed on your system
  • A Morpheus API key from openbeta.mor.org
  • Basic knowledge of Next.js and React
  • Familiarity with TypeScript
1

Create a Morpheus API Key

Visit openbeta.mor.org and sign in to create your API key.
  1. Navigate to the API Keys section
  2. Click “Create API Key” and provide a name
  3. Copy your API key immediately (it won’t be shown again)
Store your API key securely. Never commit it to version control or expose it in client-side code.
2

Install Required Dependencies

Install the Vercel AI SDK and OpenAI-compatible provider:
npm install ai @ai-sdk/openai-compatible zod
Verify installation by running npm list ai to see the installed version.
3

Configure Environment Variables

Create a .env.local file in your project root:
.env.local
MORPHEUS_API_KEY=your_api_key_here
For production, you can optionally use type-safe environment validation with @t3-oss/env-nextjs:
src/lib/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    MORPHEUS_API_KEY: z.string(),
  },
  runtimeEnv: {
    MORPHEUS_API_KEY: process.env.MORPHEUS_API_KEY,
  },
  emptyStringAsUndefined: true,
});
Never commit your API key to version control. Add .env.local to your .gitignore file.

Basic Integration

Setting Up the Morpheus Provider

The Morpheus API Gateway is OpenAI-compatible, allowing you to use the @ai-sdk/openai-compatible provider:
lib/morpheus.ts
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';

export const morpheus = createOpenAICompatible({
  name: 'morpheus',
  apiKey: process.env.MORPHEUS_API_KEY!,
  baseURL: 'https://api.mor.org/api/v1',
});
The createOpenAICompatible provider allows any OpenAI-compatible API to work seamlessly with the AI SDK, including Morpheus.

Available Models

Query the available models using the Morpheus API:
curl -X GET 'https://api.mor.org/api/v1/chat/models' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json'
Popular models available through Morpheus:
  • llama-3.3-70b:web - Meta’s Llama 3.3 with web search tool calling
  • llama-3.3-70b - Meta’s Llama 3.3 base model
  • qwen3-235b:web - Qwen 3 with web search tool calling
  • qwen3-235b - Qwen 3 base model
Model availability may vary based on provider availability in the Morpheus marketplace. The API automatically routes to the highest-rated provider for your selected model. The :web suffix indicates models optimized for web content and browsing tasks.

Text Generation

Basic Text Generation

Use the generateText function for simple, non-streaming text generation:
server-action.ts
'use server';

import { generateText } from 'ai';
import { morpheus } from '@/lib/morpheus';

export async function generateRecipe(prompt: string) {
  const { text } = await generateText({
    model: morpheus('llama-3.3-70b'),
    prompt,
    maxTokens: 1024,
  });

  return text;
}

Streaming Text Generation

For interactive applications, use streamText to stream responses in real-time:
app/api/chat/route.ts
import { streamText } from 'ai';
import { morpheus } from '@/lib/morpheus';

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const result = streamText({
    model: morpheus('llama-3.3-70b:web'),
    prompt,
    temperature: 0.7,
  });

  return result.toDataStreamResponse();
}
Use toDataStreamResponse() for easy integration with AI SDK UI components. For more control, use toTextStreamResponse() or iterate over result.textStream directly.

Complete API Route Implementation

Here’s a complete example of a chat API route using Morpheus:
app/api/chat/route.ts
import { streamText, convertToModelMessages } from 'ai';
import { morpheus } from '@/lib/morpheus';

export async function POST(req: Request) {
  const { messages, model = 'llama-3.3-70b:web' } = await req.json();

  const result = streamText({
    model: morpheus(model),
    messages: convertToModelMessages(messages),
    temperature: 0.7,
    maxTokens: 2048,
  });

  return result.toDataStreamResponse();
}
The convertToModelMessages function transforms AI SDK UI messages into the format expected by language models. It handles user messages, assistant messages, and system prompts automatically.

Tool Calling

Enable your AI models to execute functions and interact with external systems through tool calling.

Defining Tools

Define tools using Zod schemas for type-safe parameter validation:
lib/tools.ts
import { tool } from 'ai';
import { z } from 'zod';

export const tools = {
  get_random_number: tool({
    description: 'Generate a random number within a specified range',
    parameters: z.object({
      min: z.number().describe('Minimum value (inclusive)'),
      max: z.number().describe('Maximum value (inclusive)'),
    }),
    execute: async ({ min, max }) => {
      const random = Math.floor(Math.random() * (max - min + 1)) + min;
      return { number: random };
    },
  }),

  get_weather: tool({
    description: 'Get current weather for a location',
    parameters: z.object({
      city: z.string().describe('City name'),
      country: z.string().optional().describe('Country code (e.g., US)'),
    }),
    execute: async ({ city, country }) => {
      // Call weather API
      const response = await fetch(
        `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city},${country || ''}`
      );
      const data = await response.json();
      
      return {
        temperature: data.current.temp_c,
        condition: data.current.condition.text,
      };
    },
  }),
};

Using Tools with Streaming

Define tools using Zod schemas and integrate them with your streaming endpoint:
app/api/chat/route.ts
import { streamText, convertToModelMessages, tool } from 'ai';
import { morpheus } from '@/lib/morpheus';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages, model = 'llama-3.3-70b:web' } = await req.json();

  const result = streamText({
    model: morpheus(model),
    messages: convertToModelMessages(messages),
    tools: {
      get_weather: tool({
        description: 'Get current weather for a location',
        parameters: z.object({
          city: z.string().describe('City name'),
        }),
        execute: async ({ city }) => {
          // Your weather API call here
          return {
            temperature: 72,
            condition: 'sunny',
            city,
          };
        },
      }),
      calculate: tool({
        description: 'Perform a mathematical calculation',
        parameters: z.object({
          expression: z.string().describe('Math expression to evaluate'),
        }),
        execute: async ({ expression }) => {
          // Safely evaluate the expression
          const result = eval(expression);
          return { result };
        },
      }),
    },
    maxSteps: 5, // Limit tool calling iterations
  });

  return result.toDataStreamResponse();
}
Use maxSteps to prevent infinite tool calling loops. The AI SDK will automatically handle multi-step tool execution.

Tool Calling Best Practices

Clear descriptions

Provide detailed descriptions for tools and parameters to help the model understand when and how to use them.

Validate inputs

Use Zod schemas to enforce parameter types and constraints, preventing invalid tool executions.

Handle errors gracefully

Wrap tool execution logic in try-catch blocks and return meaningful error messages to the model.

Limit steps

Use stopWhen: stepCountIs(n) to prevent infinite tool calling loops and control costs.

Client-Side Implementation

Build an interactive chat interface using the AI SDK’s useChat hook:
app/page.tsx
'use client';

import { useChat } from 'ai/react';
import { useState } from 'react';

const models = [
  { name: 'Llama 3.3 70B (Web)', value: 'llama-3.3-70b:web' },
  { name: 'Llama 3.3 70B', value: 'llama-3.3-70b' },
  { name: 'Qwen 3 235B (Web)', value: 'qwen3-235b:web' },
  { name: 'Qwen 3 235B', value: 'qwen3-235b' },
];

export default function ChatPage() {
  const [selectedModel, setSelectedModel] = useState(models[0].value);
  
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat',
    body: { model: selectedModel },
  });

  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto p-6">
      {/* Model selector */}
      <div className="mb-6">
        <label className="block text-sm font-medium mb-2">Select Model</label>
        <select
          value={selectedModel}
          onChange={(e) => setSelectedModel(e.target.value)}
          className="w-full p-2 border rounded-lg"
          disabled={isLoading}
        >
          {models.map((model) => (
            <option key={model.value} value={model.value}>
              {model.name}
            </option>
          ))}
        </select>
      </div>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`p-4 rounded-lg ${
              message.role === 'user' 
                ? 'bg-blue-100 ml-auto max-w-[80%]' 
                : 'bg-gray-100 max-w-[80%]'
            }`}
          >
            <div className="font-semibold mb-2 text-sm">
              {message.role === 'user' ? 'You' : 'Assistant'}
            </div>
            <div className="whitespace-pre-wrap">{message.content}</div>
            
            {/* Tool invocations */}
            {message.toolInvocations?.map((tool) => (
              <div key={tool.toolCallId} className="mt-3 p-3 bg-white rounded border">
                <div className="text-sm font-mono text-gray-700">
                  🔧 {tool.toolName}
                </div>
                <div className="text-xs text-gray-500 mt-1">
                  {JSON.stringify(tool.args, null, 2)}
                </div>
                {tool.state === 'result' && (
                  <div className="text-sm text-green-600 mt-2">
                    ✓ {JSON.stringify(tool.result)}
                  </div>
                )}
              </div>
            ))}
          </div>
        ))}
        
        {isLoading && (
          <div className="p-4 bg-gray-100 rounded-lg animate-pulse">
            Thinking...
          </div>
        )}
      </div>

      {/* Input form */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask anything..."
          className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading || !input.trim()}
          className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
        >
          Send
        </button>
      </form>
    </div>
  );
}
The useChat hook automatically handles message state, streaming updates, and tool invocations. It provides a simple interface for building chat applications with minimal boilerplate.

Model Selection

Allow users to switch between different Morpheus models:
app/page.tsx
const models = [
  { name: 'Llama 3.3 70B (Web)', value: 'llama-3.3-70b:web' },
  { name: 'Llama 3.3 70B', value: 'llama-3.3-70b' },
  { name: 'Qwen 3 235B (Web)', value: 'qwen3-235b:web' },
  { name: 'Qwen 3 235B', value: 'qwen3-235b' },
];

export default function ChatPage() {
  const [selectedModel, setSelectedModel] = useState(models[0].value);
  
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat',
    body: { model: selectedModel }, // Pass selected model to API
  });

  return (
    <select
      value={selectedModel}
      onChange={(e) => setSelectedModel(e.target.value)}
    >
      {models.map((model) => (
        <option key={model.value} value={model.value}>
          {model.name}
        </option>
      ))}
    </select>
  );
}
Your API route receives the model parameter and passes it to Morpheus:
app/api/chat/route.ts
export async function POST(req: Request) {
  const { messages, model = 'llama-3.3-70b:web' } = await req.json();

  const result = streamText({
    model: morpheus(model), // Use the selected model
    messages: convertToModelMessages(messages),
  });

  return result.toDataStreamResponse();
}
The :web suffix indicates models optimized for web browsing and content generation. These models typically perform better for tasks involving current events or web-based information.

Troubleshooting

Cause: Morpheus may send tool call metadata and arguments in separate chunks during streaming.Solution: This issue has been resolved in recent versions of the Morpheus API. If you still encounter it, implement a custom stream transformer:
const customFetch = async (url: RequestInfo, init?: RequestInit) => {
  const response = await fetch(url, init);
  
  if (!response.body) return response;

  let toolCallBuffer: Record<string, { arguments: string }> = {};

  const transformStream = new TransformStream({
    transform(chunk, controller) {
      const text = new TextDecoder().decode(chunk);
      const lines = text.split('\n');
      let modifiedChunk = '';

      for (const line of lines) {
        if (!line.startsWith('data: ') || line === 'data: [DONE]') {
          modifiedChunk += line + '\n';
          continue;
        }

        try {
          const data = JSON.parse(line.substring(6));
          
          if (data.choices?.[0]?.delta?.tool_calls) {
            for (const toolCall of data.choices[0].delta.tool_calls) {
              const key = `${toolCall.index || 0}`;
              
              if (!toolCallBuffer[key]) {
                toolCallBuffer[key] = { arguments: '' };
              }

              // Remove initial empty "{}" but keep metadata
              if (toolCall.function?.arguments === '{}' && 
                  toolCallBuffer[key].arguments === '') {
                delete toolCall.function.arguments;
              } else if (toolCall.function?.arguments) {
                toolCallBuffer[key].arguments += toolCall.function.arguments;
              }
            }
          }

          modifiedChunk += `data: ${JSON.stringify(data)}\n`;
        } catch {
          modifiedChunk += line + '\n';
        }
      }

      controller.enqueue(new TextEncoder().encode(modifiedChunk));
    },
  });

  return new Response(response.body.pipeThrough(transformStream), {
    headers: response.headers,
  });
};

// Use in createModel:
const morpheus = createOpenAICompatible({
  name: 'morpheus',
  apiKey: env.MORPHEUS_AI_API_KEY,
  baseURL: 'https://api.mor.org/api/v1',
  fetch: customFetch,
});
Cause: The system prompt doesn’t provide clear guidance on when to use tools vs. direct answers.Solution: Add explicit instructions in your system prompt:
const result = streamText({
  model: morpheus('llama-3.3-70b:web'),
  system: `You are a helpful AI assistant. Answer simple questions directly without using tools. Only use tools when you need to:
  - Access external data or APIs
  - Perform complex calculations
  - Execute actions that require tool capabilities`,
  messages,
  tools,
});
Cause: Morpheus may be sending error JSON mixed into the SSE stream.Solution: Ensure your transformer filters out non-SSE error messages:
// Add this check in your transformer:
if (line.startsWith('{') && line.includes('"error"')) {
  console.log('[MORPHEUS FIX] Filtering error JSON:', line);
  continue; // Skip this line
}
Cause: Some Morpheus models struggle with complex tool calls requiring multiple parameters.Solution: Try a different model (llama-3.3-70b often performs better) or simplify your tools. Provide explicit examples in tool descriptions:
description: 'Generate a random number. Example: for "1 to 10", use min=1, max=10'
Cause: Morpheus occasionally sends malformed error responses that break SSE parsing.Solution: The stream transformer should filter these out. Add comprehensive logging to identify problematic chunks:
if (text.includes('tool_calls') || text.includes('finish_reason') || text.includes('error')) {
  console.log('[MORPHEUS FULL CHUNK]:', text);
}

Advanced Configuration

Custom Headers and Options

Pass additional configuration to the Morpheus provider:
lib/morpheus.ts
export const morpheus = createOpenAICompatible({
  name: 'morpheus',
  apiKey: process.env.MORPHEUS_API_KEY!,
  baseURL: 'https://api.mor.org/api/v1',
  headers: {
    'X-Custom-Header': 'value',
  },
});

Token Usage Tracking

Track token consumption using the onFinish callback:
const result = streamText({
  model: morpheus('llama-3.3-70b'),
  messages,
  onFinish: async ({ text, usage, finishReason }) => {
    console.log('Generation complete:', {
      textLength: text.length,
      promptTokens: usage.promptTokens,
      completionTokens: usage.completionTokens,
      totalTokens: usage.totalTokens,
      finishReason,
    });
    
    // Log to database or analytics
    await logUsage({
      model: 'llama-3.3-70b',
      tokens: usage.totalTokens,
      timestamp: new Date(),
    });
  },
});
While Morpheus currently provides free inference during the Open Beta, tracking usage is good practice for understanding your application’s resource consumption.

Error Handling

Implement robust error handling for production applications:
app/api/chat/route.ts
export async function POST(req: Request) {
  try {
    const { messages, model = 'llama-3.3-70b:web' } = await req.json();

    const result = streamText({
      model: morpheus(model),
      messages: convertToModelMessages(messages),
      maxRetries: 2, // Retry failed requests
      temperature: 0.7,
    });

    return result.toDataStreamResponse();
  } catch (error) {
    console.error('Chat error:', error);

    // Return user-friendly error
    return new Response(
      JSON.stringify({
        error: 'Failed to generate response. Please try again.',
        details: error instanceof Error ? error.message : 'Unknown error',
      }),
      {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      }
    );
  }
}
Use maxRetries to automatically retry failed requests. This is especially useful for handling temporary network issues or provider timeouts.

Next Steps

Summary

You’ve successfully integrated the Morpheus API Gateway with Vercel’s AI SDK! Key takeaways:
OpenAI Compatibility: Morpheus works seamlessly with the AI SDK’s OpenAI-compatible provider
Streaming Support: Real-time streaming responses work out of the box with streamText
Tool Calling: Define tools with Zod schemas for type-safe, multi-step interactions
Model Selection: Choose between different Morpheus models (Llama, Qwen) based on your needs
Free Inference: Build AI applications with free, decentralized inference during the Open Beta
The combination of Morpheus’s free, decentralized AI inference and the AI SDK’s powerful abstractions enables you to build sophisticated AI applications without infrastructure costs or vendor lock-in.