How to Integrate Tool Calling with DS4's OpenAI-Compatible API Endpoints

Use DS4's self-hosted OpenAI-compatible server to send tool definitions, receive streaming function calls, and return results by including tool_call_id in follow-up requests.

DS4 (DeepSeek 4) implements a fully self-hosted OpenAI-compatible server that supports tool calling—the same function-calling feature available in OpenAI's Chat Completion API. This integration allows you to define custom tools, receive structured function calls from the model, and feed results back into the conversation. Understanding how to integrate tool calling with DS4's OpenAI-compatible API endpoints unlocks agentic workflows without relying on external APIs.

Parsing Tool Definitions from Client Requests

When your HTTP request includes a tools array, DS4 extracts and validates each function schema before passing them to the model. The parsing logic resides in ds4_server.c, specifically within parse_tools_value().

The function iterates over the raw JSON array, unwraps each OpenAI-style "function" wrapper via openai_function_schema_from_tool(), and stores the result in an internal tool_schema_orders structure:

// ds4_server.c lines 1608-1625
// parse_tools_value() processes the "tools" JSON array
// Each entry becomes a tool_schema_orders entry with name, description, and parameters

Your tool definitions must follow the standard OpenAI format:

{
  "type": "function",
  "function": {
    "name": "weather_lookup",
    "description": "Get current weather for a location",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "description": "City name"},
        "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["city"]
    }
  }
}

DS4 stores these schemas in the request struct field tool_orders and sets has_tools = true, signaling the inference engine to include tool instructions in the system prompt.

Receiving Streaming Tool Calls from DS4

DS4 streams responses using the openai_stream structure. When the model emits a tool call, the server captures it through openai_tool_stream objects at lines 1196-1210 of ds4_server.c.

// ds4_server.c lines 1190-1212
// openai_tool_stream_init() prepares incremental JSON output
// Emits call_id, function name, and arguments as partial SSE chunks

The streaming SSE payload includes a tool_calls array matching OpenAI's format:

{
  "choices": [{
    "delta": {
      "tool_calls": [{
        "index": 0,
        "id": "call_abc123xyz",
        "type": "function",
        "function": {
          "name": "weather_lookup",
          "arguments": "{\"city\":\"Berlin\",\"units\":\"celsius\"}"
        }
      }]
    }
  }]
}

DS4 handles fragmented arguments across multiple SSE chunks, buffering until the complete JSON is available.

Returning Tool Results to Continue Generation

After executing the requested function, your client must send a follow-up request containing:

  • role: "assistant" with the original tool_calls array (replayed exactly)
  • A second assistant message with tool_call_id and tool_result

DS4 matches the ID via chat_msg_add_tool_call_id() and appends the result text through append_tool_result_text() at lines 889-903:

// ds4_server.c lines 889-903
// parse_messages() locates matching tool_call_id
// chat_msg structure stores result for prompt replay

The chat_msg struct maintains the full conversation state, enabling DS4 to resume generation with tool outputs available as context.

Complete Tool Calling Example

Step 1: Request with Tool Definition

curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [{"role":"user","content":"What is the weather in Tokyo?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "weather_lookup",
        "description": "Get current weather for a location",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {"type": "string"},
            "units": {"type": "string", "enum": ["celsius","fahrenheit"]}
          },
          "required": ["city"]
        }
      }
    }],
    "tool_choice": "auto",
    "stream": true
  }'

Step 2: Receive and Parse Tool Call

Extract from the SSE stream:

  • tool_calls[0].id → "call_weather_001"
  • tool_calls[0].function.name → "weather_lookup"
  • tool_calls[0].function.arguments → parse as JSON

Step 3: Return Tool Result

curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [
      {"role":"user","content":"What is the weather in Tokyo?"},
      {
        "role":"assistant",
        "tool_calls": [{
          "id":"call_weather_001",
          "type":"function",
          "function":{"name":"weather_lookup","arguments":"{\"city\":\"Tokyo\",\"units\":\"celsius\"}"}
        }]
      },
      {
        "role":"assistant",
        "tool_call_id":"call_weather_001",
        "tool_result":"{\"temperature\":22,\"condition\":\"partly cloudy\",\"humidity\":65}"
      }
    ],
    "stream": false
  }'

DS4 continues generation, now able to formulate a natural language response using the weather data.

Key Architecture Components

Component Location Purpose
request struct ds4_server.c:1064-1086 Holds tool_orders, has_tools flag, and replay state
tool_schema_orders ds4_server.c:1069-1079 Dynamic collection of parsed tool schemas
openai_stream / openai_tool_stream ds4_server.c:1190-1212 Incremental JSON output for text and tool fragments
tool_calls / tool_call ds4_server.c:3347-3372 Representation of single invocation; freed post-request
parse_tools_value() ds4_server.c:1608-1625 Main entry point for tool JSON parsing
chat_msg_add_tool_call_id() ds4_server.c:889-903 Re-attaches results to conversation

Summary

  • Tool definition: Send OpenAI-standard tools array; DS4 parses via parse_tools_value() into tool_schema_orders
  • Tool emission: DS4 streams tool_calls through openai_tool_stream with full OpenAI-compatible SSE formatting
  • Result replay: Include tool_call_id in assistant messages; DS4 matches via chat_msg_add_tool_call_id() and continues generation
  • State management: The request struct maintains has_tools and tool_orders across the full exchange

Frequently Asked Questions

Does DS4 validate tool call arguments against the JSON schema?

DS4 extracts tool schemas for prompt construction but does not enforce JSON Schema validation on returned arguments. The model generates arguments as JSON strings; your client should validate before execution. Schema validation occurs implicitly during parsing—malformed tool definitions are rejected at openai_function_schema_from_tool().

Can I force a specific tool with tool_choice?

Yes. Set "tool_choice": {"type": "function", "function": {"name": "specific_tool"}} to force that tool, or "tool_choice": "auto" for model discretion, or "none" to disable. DS4 passes this through the request struct to influence sampling.

What happens if I send a tool_call_id that doesn't exist?

DS4's chat_msg_add_tool_call_id() attempts to locate the matching ID. If not found, the message is treated as a standard assistant message without tool context. The conversation continues but may produce inconsistent results since the model lacks expected grounding.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →