DOCUMENTATION

Tool Calling

Webhook tools (recommended — Hypervize calls your external tools for you) or standard OpenAI tool schemas (client-driven passthrough). Includes platform tools.

Tool Calling

Hypervize has first-class support for tool / function calling on both Elastic and Dedicated inference.

Recommended for external tool calls: Use webhook tools. Hypervize handles calling your HTTPS endpoint and drives the full multi-turn loop for you — your client only receives the final answer.

We also support two other options:

  1. Standard OpenAI tool schemas (plain JSON Schema, no webhook extension) — client-side execution. We surface tool_calls exactly like OpenAI so frameworks can drive the loop.
  2. Platform tools — built-in tools (Athena, Vesper, Herald, and others) that we execute server-side when you opt in.

You can use plain schemas, webhook tools, and platform tools together when you need them (platform names always run server-side). See Using plain tools with platform tools below.

See the dedicated Platform Tools page for the current built-in tools (Athena, Vesper, Herald, Pandora, Iris, Ledger, Chronos) and how to enable them.

Dual mode (plain vs managed)

ModeToolsWho executesStreaming
A — Client plain toolsOpenAI schemas only (no webhook, not a platform name)Your clientstream: true or false — both work. Non-stream returns a full chat.completion with message.tool_calls.
B — Server-managedPlatform tools (Athena, Vesper, …) and/or tools with webhook.urlHypervizeStream or non-stream: managed loop runs on the server; non-stream returns one aggregated chat.completion. Whole loop must finish within the ~13 minute request wall clock — multi-hour work needs a future batch/async product, not one hold.

Default when you send your own plain tools: platform tools are not auto-attached on that request, even if they are enabled on your account. You get Mode A for those tools. To use platform tools and your plain tools on the same request, enable hybrid mode (below).

Silent empty success is a bug. If tools were in the request, you should get either tool_calls / function_call, final content, or a clear HTTP error — never status: completed with empty body and 0 tokens.

All patterns are documented below so you can choose what fits your architecture.


Standard OpenAI Tool Schemas (Client-Side / Passthrough) — Alternative for full control

Send normal OpenAI-compatible tool definitions without any webhook extension.

JSON
{
  "type": "function",
  "function": {
    "name": "get_platform_data",
    "description": "Query internal platform metrics",
    "parameters": { ... }
  }
}

Hypervize will (Mode A):

  • Forward your plain tools to the model as sent.
  • When the model emits tool_calls:
    • stream: true: emit OpenAI-compatible SSE deltas, then finish_reason: "tool_calls", then usage, then [DONE].
    • stream: false / omitted: return one JSON chat.completion with choices[0].message.tool_calls and usage (not an SSE/[DONE] envelope).
  • We do not re-invoke the model for plain tools. You execute the tool and send role: "tool" on the next request.

This matches calling OpenAI, Anthropic, or Grok directly (your client runs the tool loop).

Requests with only platform tools enabled on the account (and no plain tools in the body) use the managed path (Mode B). Webhook tools also use Mode B.

Full round-trip example (Vercel AI SDK + plain schemas)

TS
// app/api/chat/route.ts
import { streamText, tool } from 'ai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { z } from 'zod';

const hypervize = createOpenAICompatible({
  baseURL: 'https://hypervize.tech/api',
  apiKey: process.env.HVZ_KEY,
});

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

  const result = await streamText({
    model: hypervize('claude-sonnet-5'),
    messages,
    tools: {
      get_platform_data: tool({
        description: 'Fetch current platform metrics for a customer',
        parameters: z.object({
          customerId: z.string(),
        }),
        execute: async ({ customerId }) => {
          // Your real implementation here (DB, internal API, etc.)
          const res = await fetch(`https://your-api.com/metrics/${customerId}`);
          return res.json();
        },
      }),
      search_schema_tool: tool({ ... }),
      execute_custom_sql: tool({ ... }),
    },
  });

  return result.toDataStreamResponse();
}

The Vercel SDK sends plain tool schemas. Hypervize surfaces the tool_calls, the SDK executes your execute functions on the server (or edge), and sends the tool results back in the next completion request. Everything just works.

cURL example (plain tools)

BASH
curl -N https://hypervize.tech/api/chat/completions \
  -H "Authorization: Bearer $HVZ_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "messages": [{"role": "user", "content": "Get platform data for cust_123"}],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_platform_data",
          "description": "Returns usage and billing info for a customer",
          "parameters": {
            "type": "object",
            "properties": {
              "customerId": { "type": "string" }
            },
            "required": ["customerId"]
          }
        }
      }
    ],
    "stream": true
  }'

You will see normal tool_calls deltas in the stream (exactly like OpenAI) followed by a finish_reason: "tool_calls" chunk, usage, and [DONE].

OpenAI SDK (Node / TypeScript)

TS
import OpenAI from "openai";

const openai = new OpenAI({
  baseURL: "https://hypervize.tech/api",
  apiKey: process.env.HVZ_KEY,
});

const stream = await openai.chat.completions.create({
  model: "llama-3-3-70b-instruct",
  messages: [{ role: "user", content: "What is the weather in Nashville?" }],
  tools: [
    {
      type: "function",
      function: {
        name: "get_weather",
        description: "Get current weather",
        parameters: {
          type: "object",
          properties: { location: { type: "string" } },
          required: ["location"],
        },
      },
    },
  ],
  stream: true,
});

for await (const chunk of stream) {
  // Handle tool_calls deltas the same way you do for OpenAI
  console.log(chunk.choices?.[0]?.delta?.tool_calls);
}

After receiving the tool call, execute it yourself and send a follow-up request containing the tool result message.

Handling the tool result (follow-up request)

JSON
{
  "model": "claude-sonnet-5",
  "messages": [
    { "role": "user", "content": "Get data for cust_123" },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_abc",
          "type": "function",
          "function": {
            "name": "get_platform_data",
            "arguments": "{\"customerId\":\"cust_123\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc",
      "content": "{\"activeUsers\": 1240, \"mrr\": 48200}"
    }
  ]
}

The model will now see the result and continue.


Using plain tools with platform tools

By default, if your request includes your own plain function tools, Hypervize does not attach enabled platform tools on that request. Your plain tools run in Mode A (you execute them).

To use both on one request (platform tools execute on our side; your plain tools still return as tool_calls for you to run):

  1. Enable the platform tools you need in the dashboard (Alexandria), and
  2. Turn on hybrid tools for the request with either:

Body field

JSON
"hypervize_hybrid_tools": true

Or header

HTTP
x-hypervize-hybrid-tools: 1

Example:

BASH
curl https://hypervize.tech/api/chat/completions \
  -H "Authorization: Bearer $HVZ_KEY" \
  -H "Content-Type: application/json" \
  -H "x-hypervize-hybrid-tools: 1" \
  -d '{
    "model": "claude-sonnet-5",
    "stream": false,
    "hypervize_hybrid_tools": true,
    "messages": [{"role": "user", "content": "What time is it UTC, then look up customer cust_123 with my tool."}],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_platform_data",
          "description": "Returns usage and billing info for a customer",
          "parameters": {
            "type": "object",
            "properties": { "customerId": { "type": "string" } },
            "required": ["customerId"]
          }
        }
      }
    ]
  }'

When hybrid is on and the model calls both kinds of tools in one turn:

  • Platform / webhook tools run on Hypervize.
  • Your plain tools appear in choices[0].message.tool_calls for you to execute.
  • Non-stream responses may include managed_tool_results: an array of { tool_call_id, name, content } for tools we already ran, so you can put those results into your next messages if you need them.

Omit the flag (and header) to keep the default: plain tools only on that request.


Hypervize Platform Tools (Built-in)

When you have opted in to tools in the dashboard (Alexandria), Hypervize automatically makes their schemas available. Current platform tools include:

  • athena — time/date and basic calculation
  • vesper — web search and page fetch/extract
  • herald — send email (only with explicit user permission)
  • pandora_* — Google Workspace (Drive, Gmail, Docs, Sheets, Slides, Calendar create+invites)
  • iris — image generation
  • ledger — create, read, and convert Library files (CSV, JSON, PDF, text)

Chronos is a separate scheduling product (not a model-callable tool); enable it to use Chat Schedule and the Chronos API. See Platform Tools and Chronos.

These platform tools are always executed on our side when called. They are billed per call and go through the same enablement checks as everything else.

If you (or the model) use one of these names in your own tools array, we force server-side execution for security and billing reasons (name collision protection). Platform tool names always run as platform tools.

We are expanding our tool library. If you have a tool you want to see added, please reach out to support.


Webhook Tools — How They Work

Prefer "stream": true for webhook and platform (server-managed) tools. We run tools and continue the model turn over SSE. Non-stream Mode B is supported via server-side aggregation to one JSON completion.

  1. You send a tools array containing one or more definitions with a webhook object (or rely on pre-attached tools).
  2. We merge in any platform tools you have opted into.
  3. We collect the full tool call, POST a signed payload to your webhook.url, and wait for the result.
  4. We feed the result back into the model as a role: "tool" message and continue until the model produces a final answer with no tool calls.
  5. Your client sees only the final content + aggregated usage + [DONE].

If no tools (yours or platform) are enabled for the account/request, the path is a pure passthrough — identical to a normal OpenAI-compatible endpoint.

Webhook Tools — Adding the webhook Extension

Use the exact same schema as a normal tool, plus a webhook object:

JSON
{
  "model": "claude-sonnet-5",
  "messages": [
    { "role": "user", "content": "What's the weather in Nashville?" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_current_weather",
        "description": "Get the current weather for a given location.",
        "parameters": {
          "type": "object",
          "properties": {
            "location": { "type": "string" },
            "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
          },
          "required": ["location"]
        }
      },
      "webhook": {
        "url": "https://your-app.com/api/webhook-tools/weather",
        "key": "sk_live_your_webhook_secret",
        "timeout_seconds": 30
      }
    }
  ]
}

Comparison of the two approaches

AspectPlain OpenAI Schema (Client-Driven)Webhook Tool (Recommended for external calls)
Who executesYou / your SDK / browserHypervize (calls your HTTPS endpoint)
Who drives the loopYour client codeHypervize (multi-turn; stream preferred)
What client seestool_calls + finish_reasonOnly final content + usage (managed)
Streamingtrue or false — non-stream returns full chat.completion with tool_callsPrefer stream: true. Non-stream returns one aggregated chat.completion after the loop.
Best forFull control, browser/edge execution, or when using client SDKs that manage the loop themselves"Set and forget" — Hypervize calls the tool for you
Billing for executionNone (you pay your infra)Per-call tool metering (see billing)
Name collision protectionCannot shadow platform tool namesN/A (we always execute our tools)

Defining Tools in a Request

You can include tools directly on every request, or (recommended for production) attach them once in the dashboard so they are auto-injected on your requests.

The webhook Object

  • url (required): Publicly reachable HTTPS endpoint.
  • key (required): Secret used for Authorization: Bearer and HMAC signing.
  • timeout_seconds (optional, default 30).

Only HTTPS is allowed.

Webhook Request Payload (for webhook tools)

We send a POST request with Content-Type: application/json.

JSON
{
  "tool_call_id": "call_abc123def456",
  "name": "get_current_weather",
  "arguments": {
    "location": "Nashville, TN",
    "unit": "fahrenheit"
  },
  "context": {
    "user_id": "usr_1234567890",
    "api_key_id": "key_9876543210",
    "request_id": "req_abcdef123456",
    "model": "grok-4.3"
  }
}

Field Descriptions

  • tool_call_id: The unique ID the model assigned to this particular tool call. You should return this (or we will match it) when providing the result.
  • name: The name of the tool as defined in your tool schema.
  • arguments: The arguments the model chose, already parsed as a JSON object (matching your parameters schema).
  • context.user_id: The Hypervize user who initiated the request.
  • context.api_key_id: The specific API key used for this conversation.
  • context.request_id: A unique identifier for this top-level inference request (useful for logging and tracing).
  • context.model: The model the user is talking to.

Authentication note: We authenticate the original inference request using your Hypervize API key (the Authorization: Bearer hvz_live_... header). We only invoke a webhook for a tool that was supplied in an authenticated request from that same key. Your endpoint can therefore trust that the call represents legitimate activity initiated by you.

To let you cryptographically verify that the request really came from Hypervize and has not been tampered with or replayed, we use your key in two ways:

  • We include it in the Authorization: Bearer <your-key> header (quick rejection of unauthorized callers).
  • We compute an HMAC-SHA256 signature of the request and send it in X-Hypervize-Signature: t=<unix-timestamp>,v1=<hex-signature>.

The signature is calculated as:

TEXT
HMAC-SHA256(key, "t=" + timestamp + "." + rawRequestBody)

Recommended validation on your endpoint (see the complete code example below for a working implementation):

  1. Check that the Authorization header equals Bearer <your-key>.
  2. Parse the X-Hypervize-Signature header to extract the timestamp (t=) and the hex signature (v1=).
  3. Reject the request if the timestamp is more than ~5 minutes old (replay protection).
  4. Recompute HMAC-SHA256(your-key, "t=" + timestamp + "." + rawBody) exactly.
  5. Use a constant-time comparison (e.g. Node's crypto.timingSafeEqual) to compare your computed hex signature against the one in the header.
  6. Only if both checks pass, process the request.

We always use HTTPS. This scheme is modeled on common webhook verification patterns (e.g. Stripe, GitHub). A full working implementation is provided later in this document under "Complete Webhook Endpoint Example".

Headers we send:

  • Content-Type: application/json

  • User-Agent: Hypervize-Tool-Executor/1.0

  • X-Hypervize-Request-ID: Same as context.request_id

  • Authorization: Bearer <your-key>

  • X-Hypervize-Signature: t=<unix-timestamp>,v1=<hex-encoded-hmac-sha256>

    The value is computed as HMAC-SHA256(your-key, "t=" + timestamp + "." + rawRequestBody). See the validation steps above for the full recommended checks (including replay protection).

Webhook Response Format

Your endpoint must respond with a JSON object (HTTP 200). We support two equivalent shapes:

JSON
{
  "content": "The current weather in Nashville, TN is 72°F and sunny with clear skies."
}

The value of content becomes the tool result message sent back to the model.

Structured result

JSON
{
  "result": {
    "temperature": 72,
    "unit": "fahrenheit",
    "condition": "sunny",
    "humidity": 45,
    "wind_speed": 5
  }
}

We will serialize the result object (usually via JSON.stringify) and use it as the tool result content.

Error response

JSON
{
  "error": "Unable to fetch weather data: external API rate limit exceeded. Please try again later."
}

If your response contains an error field (or returns a non-2xx status), we treat the value as the tool result. The model will see the error and can decide how to respond to the user.

Timeouts: If your webhook does not respond within the configured timeout_seconds (default 30), we treat it as an error and pass an appropriate message back to the model.

Complete Webhook Endpoint Example (Next.js / Vercel)

Here is a complete, copy-pasteable example of a webhook endpoint that performs the full recommended security checks.

TS
// app/api/aifunction/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string | null,
  key: string,
): boolean {
  if (!signatureHeader) return false;

  // Parse "t=<timestamp>,v1=<hex-signature>"
  const match = signatureHeader.match(/t=(\d+),v1=([a-f0-9]+)/);
  if (!match) return false;

  const [, timestamp, receivedSignature] = match;

  // Replay protection: reject requests older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  // Reconstruct the signed payload exactly as we did on our side
  const signedPayload = `t=${timestamp}.${rawBody}`;

  // Compute HMAC-SHA256 using the key you registered
  const expectedSignature = createHmac("sha256", key)
    .update(signedPayload, "utf8")
    .digest("hex");

  // Constant-time comparison to prevent timing attacks
  try {
    return timingSafeEqual(
      Buffer.from(receivedSignature, "hex"),
      Buffer.from(expectedSignature, "hex"),
    );
  } catch {
    return false;
  }
}

export async function POST(request: NextRequest) {
  const rawBody = await request.text();
  const body = JSON.parse(rawBody);

  const authHeader = request.headers.get("authorization");
  const signatureHeader = request.headers.get("x-hypervize-signature");
  const expectedKey = "my-super-secret-verification-key"; // the one you put in the "key" field

  // 1. Quick bearer check
  if (authHeader !== `Bearer ${expectedKey}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 2. Signature verification (recommended)
  if (!verifyWebhookSignature(rawBody, signatureHeader, expectedKey)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const { tool_call_id, name, arguments: args, context } = body;

  console.log(
    `Tool call ${tool_call_id} for ${name} from user ${context.user_id}`,
  );

  if (name === "get_current_weather") {
    // Your real logic here (call a weather API, query DB, etc.)
    const { location, unit = "fahrenheit" } = args;

    // Fake response for demo
    const temp = unit === "fahrenheit" ? 72 : 22;
    const result = `The current weather in ${location} is ${temp}°${unit[0].toUpperCase()} and sunny.`;

    return NextResponse.json({
      content: result,
    });
  }

  return NextResponse.json(
    {
      error: `Unknown tool: ${name}`,
    },
    { status: 400 },
  );
}

Your endpoint must be reachable over the public internet (or via a tunnel during development).

Full Request Example with Webhook Tool

JSON
{
  "model": "mistral-large-3",
  "messages": [
    {
      "role": "user",
      "content": "Find the latest news about AI regulation and summarize the top 3 stories."
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "summarize_articles",
        "description": "Summarize a list of articles",
        "parameters": {
          "type": "object",
          "properties": {
            "urls": { "type": "array", "items": { "type": "string" } }
          }
        }
      },
      "webhook": {
        "url": "https://your-app.com/api/tools/summarize",
        "key": "your_webhook_key_here"
      }
    }
  ],
  "stream": true
}

For live web research without a custom webhook, enable Vesper as a platform tool instead of defining a search tool yourself.


More Code Samples

Python (OpenAI SDK) with tools + manual loop

PYTHON
from openai import OpenAI
import json

client = OpenAI(base_url="https://hypervize.tech/api", api_key="hvz_...")

tools = [{
    "type": "function",
    "function": {
        "name": "get_user_profile",
        "description": "Look up a user profile",
        "parameters": {"type": "object", "properties": {"user_id": {"type": "string"}}}
    }
}]

messages = [{"role": "user", "content": "Tell me about user 42"}]

response = client.chat.completions.create(
    model="claude-sonnet-5",
    messages=messages,
    tools=tools,
    stream=True,
)

# In a real app you would accumulate the tool call and then call your function
for chunk in response:
    if chunk.choices and chunk.choices[0].delta.tool_calls:
        print("Tool call received:", chunk.choices[0].delta.tool_calls)

Sending a tool result back (any language)

After your code executes the tool, make another completion request:

JSON
{
  "model": "claude-sonnet-5",
  "messages": [
    { "role": "user", "content": "Look up user 42" },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_123",
          "type": "function",
          "function": {
            "name": "get_user_profile",
            "arguments": "{\"user_id\":\"42\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_123",
      "content": "{\"name\": \"Ada Lovelace\", \"plan\": \"enterprise\"}"
    }
  ]
}

Using the Feature (Summary)

Recommended for calling your own external tools: Use webhook tools (see below). Hypervize will call your endpoint and manage the entire agent loop.

  • Webhook / server-managed: Add a webhook object (or use platform tools). Hypervize executes the tool on your behalf and only returns the final answer to the client. Requires "stream": true.
  • Plain OpenAI schemas (client-driven): Send normal tool definitions with no webhook. We emit tool_calls in the stream; your code (or SDK) executes them and sends role: "tool" results in follow-up requests.
  • Mix freely (with name collision protection for platform tools).

Both patterns work on Elastic and Dedicated endpoints. See the comparison table above for trade-offs.

Best Practices & Limitations

  • Webhook vs plain schemas: For external tool calls we recommend our webhook approach — Hypervize calls your tool on your behalf and handles the loop so the client only sees the final result. Use plain OpenAI schemas when you need full client-driven control (e.g. browser execution or specific SDK loop management).
  • Keep webhooks fast: Target < 1-2s responses. For long jobs, return a job ID and have the model call a status tool later.
  • Security (webhooks): Always use HTTPS + the full signature verification (timestamp + HMAC + constant time compare). Never trust the body without verifying X-Hypervize-Signature.
  • Name collisions: Give your tools unique names. If you use a name that collides with a platform tool, we will always execute the platform version server-side.
  • Billing (dual system):
    • Platform tools + webhook tools executed by Hypervize are billed per call (separate from tokens) and deducted from prepaid balance.
    • Plain custom tools (no webhook): you only pay for the inference tokens used to generate the tool_calls. Your own execution cost is on you.
    • On dedicated endpoints: managed tool charges still come from prepaid — they are not on the weekly GPU invoice. See Tool Calls.
  • Debugging: Use context.request_id (and X-Hypervize-Request-ID header) for correlation. All tool activity is visible in request logs.

Mixing Tools + Name Collision Protection

If a tool name collides with a platform tool (for example athena or vesper), the platform version wins and runs server-side. You cannot shadow platform tools with a plain schema of the same name.

To put your plain tools and platform tools on the same request, use hybrid mode. Without hybrid, a request that includes plain tools does not auto-attach platform tools.

Webhook tools in the request body are always server-managed.


Raw / Client-Driven Tool Calling

If you want full control (the standard OpenAI experience), send plain tools with no webhook key and leave hybrid off (see the first section of this document). We emit tool_calls and end the hop. You drive the rest of the conversation.

Feedback

We support both the client-driven (plain schemas) and server-managed (webhooks) patterns. If something feels off with tool surfacing, collisions, webhook signing, or streaming behavior, contact support.

This document (together with the OpenAI Chat Completions spec) is the source of truth for current behavior.

Was this helpful?Send feedback