OpenAI API

Paddock serves the OpenAI wire format on /v1, so the official OpenAI SDKs and existing OpenAI clients work against a local model by changing the base URL.

Base URL and Authentication

The server listens on http://127.0.0.1:11540 by default. OpenAI clients use /v1 as the base path:

http://localhost:11540/v1

On the default loopback bind no API key is required; any placeholder key satisfies SDK clients that insist on one. Authentication is Bearer-based and follows the bind address:

  • An explicit key (api_key in paddock.toml, PADDOCK_API_KEY, or --api-key) is required on every /v1 and /api request as Authorization: Bearer <key>.
  • Binding to a non-loopback address with no key configured auto-generates a key and requires it. An exposed server is never left open.
  • Keys created through POST /api/keys are accepted alongside the configured key.

Endpoints

EndpointDescription
GET /v1/modelsLists installed GGUF models plus the currently served model, in the OpenAI list shape.
POST /v1/chat/completionsChat completions with streaming, tool calling, structured outputs, logprobs, and vision input.
POST /v1/completionsLegacy text completions, non-streaming and SSE streaming.
POST /v1/responsesThe OpenAI Responses API, including the typed streaming event protocol, server-side MCP tools, and web search.
POST /v1/embeddingsText embeddings in the OpenAI shape, with float or base64 encoding.
POST /v1/rerankQuery-document reranking in the de facto Cohere/Jina shape (no OpenAI equivalent).

Paddock also serves the Anthropic Messages API on the same port; see the Anthropic API page.

Request Validation

Unknown request fields are rejected with a 400 that names the field, matching the OpenAI API's own behavior. Parameters the server does not implement are also rejected, including those that would require server-side persistence (store: true and non-empty metadata). Error bodies use the OpenAI format everywhere, including 404s.

Chat Completions

Messages and tool definitions are passed verbatim to the model's own chat template, so any model's chat format is supported rather than one hand-coded case. Supported sampling and control parameters:

ParameterNotes
temperature, top_p, presence_penalty, frequency_penaltyStandard OpenAI semantics.
max_completion_tokensCurrent name for the output cap. The deprecated max_tokens is accepted; sending both is a 400.
n1 to 8 choices, merged into one SSE stream when streaming.
logprobs, top_logprobsPer-token logprobs; up to 20 top alternatives per token.
stop, seed, logit_biaslogit_bias is applied to the logits before sampling, not accepted-and-ignored.
stream, stream_optionsstream_options.include_usage adds usage to a terminal streaming chunk.
response_formatStructured outputs; see below.
reasoning_effortThe full current vocabulary (none, minimal, low, medium, high, xhigh, max), clamped to the model's three effort levels; a 400 on models without an effort knob.
verbosityAccepted and validated (low, medium, high); local models have no verbosity knob, so it does not change generation. The Responses API accepts text.verbosity the same way.
top_k, min_p, repeat_penalty, chat_template_kwargsLocal extensions beyond the OpenAI spec.

Streaming

Streaming responses are server-sent events carrying chat.completion.chunk objects and ending with data: [DONE]. Reasoning models stream reasoning_content deltas alongside content deltas (the DeepSeek/vLLM convention). Each tool call streams atomically as its block completes: fragmenting the arguments of a non-JSON tool dialect is not prefix-stable, so there are no partial-argument deltas.

Tool Calling

Function tools use the standard tools and tool_choice fields ("auto", "none", "required", or a specific function). Tool-call output is enforced by constrained decoding against the declared schemas. parallel_tool_calls: false keeps only the first parsed call; by default a model may emit several calls in one turn.

Structured Outputs

response_format accepts text, json_object, and json_schema. Schemas are enforced by constrained decoding during generation, not by prompting, so the output is guaranteed to parse. The Responses API exposes the same machinery through text.format. The OpenAI SDK's .parse() helpers work on both endpoints.

Reasoning Models

gpt-oss models take reasoning_effort: the full current vocabulary is accepted, and the outer values clamp to the model's three levels (none and minimal become low; xhigh and max become high). Qwen3.5-family models toggle thinking through chat_template_kwargs:

{ "chat_template_kwargs": { "enable_thinking": true } }

Reasoning text is returned as reasoning_content on the message, separate from content.

Vision and PDF Input

When a vision model is served (a model with a vision tower loaded via mmproj), image_url content parts are accepted as data: URIs. The server does not fetch remote image URLs; requests with remote URLs are rejected. PDF attachments (chat file parts and Responses input_file parts carrying an application/pdf data URI) are rendered server-side into page images when a pdfium_lib is configured; pages past the configured cap (20 by default) are dropped, and the response notes the truncation.

Responses API

POST /v1/responses takes input as a string or an array of input items, plus instructions, tools, tool_choice, parallel_tool_calls, text.format for structured outputs, and reasoning.effort. Streaming uses the typed event protocol: response.created, response.in_progress, response.output_item.added, response.output_text.delta, response.reasoning_text.delta, response.function_call_arguments.delta, and the terminal response.completed, response.incomplete, or response.failed.

Two server-executed tool types run inside an agent loop on this endpoint:

  • mcp tools, which call tools on registered or inline MCP servers; see the MCP page.
  • web_search (including the web_search_preview aliases), which runs real searches through a provider you configure in the Studio (Exa or Tavily, your own API key) and emits web_search_call output items.

Server-side conversation state is not persisted: store: true is rejected, and previous_response_id is accepted only to resume a response paused on an MCP tool approval.

Embeddings and Reranking

POST /v1/embeddings takes input as a string, an array of strings, an array of token ids, or an array of token-id arrays, and returns the OpenAI list shape. encoding_format may be "float" (default) or "base64". The dimensions (Matryoshka) parameter is not supported yet and returns a 400.

POST /v1/rerank takes model, query, and documents, with optional top_n, return_documents, and instruction, and returns results sorted by relevance_score. Embedding and reranker models (the Qwen3 embedding and reranker families) are served by the same daemon.

Examples

A chat completion against a served gpt-oss-20b:

curl http://localhost:11540/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-oss-20b",
    "messages": [{"role": "user", "content": "Summarize what a KV cache does."}],
    "max_completion_tokens": 256
  }'

A streaming request with a schema-enforced JSON response:

curl -N http://localhost:11540/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-oss-20b",
    "messages": [{"role": "user", "content": "Extract the city and country from: Malmo, Sweden"}],
    "stream": true,
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "location",
        "schema": {
          "type": "object",
          "properties": {"city": {"type": "string"}, "country": {"type": "string"}},
          "required": ["city", "country"]
        }
      }
    }
  }'

For throughput and latency measurements, see the benchmarks.