Skip to content

API reference

Base URL is /v1. Set NEEDLE_API_KEY to require Authorization: Bearer <key> on every endpoint except /health and /.

Endpoint Method Purpose
/v1/chat/completions POST Tools, streaming, structured output. The main endpoint.
/v1/completions POST Legacy text completion.
/v1/models GET List the served model.
/v1/models/{id} GET Fetch one model card.
/v1/needle/extract POST Native extraction: text + JSON Schema → object.
/v1/needle/complete POST Passthrough returning the engine's raw response.
/health GET Readiness, queue depth, token-count mode.
/ GET Endpoint index.

POST /v1/chat/completions

Supported parameters

Parameter Support Notes
messages system, developer, user, tool, function. Assistant messages are dropped — see fidelity notes.
tools type: "function" only.
functions Deprecated form, accepted.
tool_choice ⚠️ Honoured by narrowing what is declared. "required" cannot be enforced.
response_format ⚠️ json_schema → extraction mode. json_object is rejected.
stream ⚠️ Synthesized from the finished result, not true token streaming.
stream_options.include_usage Appends the usage chunk.
max_tokens / max_completion_tokens Maps to the engine's max_new_tokens.
model Any name accepted unless NEEDLE_STRICT_MODEL=true.
temperature, top_p, seed, stop, logprobs, presence_penalty, frequency_penalty, logit_bias, n Accepted, ignored, and reported in x_needle.warnings. Decoding is deterministic.
Image / audio content parts Rejected with 400. Text only.

Response

Standard chat.completion, plus an x_needle block. A tool call:

{
  "id": "chatcmpl-751cf77b6deea9564ca661d1",
  "object": "chat.completion",
  "created": 1786719243,
  "model": "needle-2",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": null,
      "refusal": null,
      "tool_calls": [{
        "id": "call_e00c85cfc7c8c9e729c55b7e",
        "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"Lagos\"}"}
      }],
      "reasoning_content": "User asks for current weather in Lagos. get_weather with city 'Lagos' from query."
    },
    "logprobs": null,
    "finish_reason": "tool_calls"
  }],
  "usage": {"prompt_tokens": 34, "completion_tokens": 18, "total_tokens": 52},
  "x_needle": { "...": "see below" }
}

finish_reason is tool_calls when the model called something, stop otherwise. When nothing is called, content carries the model's reasoning trace, because that is its only textual output.

The x_needle block

Needle reports signals the OpenAI schema has nowhere to put. Disable the block with --no-extras / NEEDLE_EXPOSE_EXTRAS=false.

{
  "type": "call",
  "confidence": 0.9568,
  "reasoning": "User asks for current weather in Lagos. get_weather with city 'Lagos' from query.",
  "validation": {"ungrounded": [], "negation": false},
  "prefill_tps": 299.3,
  "decode_tps": 180.0,
  "peak_ram_mb": 103.1,
  "replayed_turns": 1,
  "reinitialized": false,
  "queue_wait_seconds": 0.0004,
  "compute_seconds": 0.2433,
  "warnings": []
}
Field Meaning
type call (tools invoked), respond (done, answering), refuse (declined).
confidence Calibrated 0–1 score. The field to gate on. Context-sensitive — calibrate your own threshold.
reasoning Short natural-language trace. Also mirrored to message.reasoning_content.
validation.ungrounded Arguments the model could not trace back to the input — a hallucination check.
validation.negation Whether a negation was detected in the request.
prefill_tps / decode_tps Throughput, tokens/second.
peak_ram_mb Peak resident memory of the engine.
replayed_turns Engine calls this request needed. 1 means the session prefix was reused.
reinitialized Whether the tool set or system prompt forced a fresh session.
queue_wait_seconds Time spent waiting for the single engine thread.
warnings Parameters that were accepted but had no effect.

Streaming

stream: true returns text/event-stream with chat.completion.chunk events terminated by data: [DONE]. The sequence is: a role delta, then tool-call deltas (name and id first, then argument fragments) or content deltas, then a finish-reason chunk carrying x_needle, then optionally a usage chunk.

The chunks are well-formed and reassemble correctly, but they are cut from an already-finished result — see fidelity notes.

POST /v1/needle/extract

Native extraction without the chat envelope.

curl -s localhost:8000/v1/needle/extract -H 'Content-Type: application/json' -d '{
  "text": "Invoice from Acme Corp, $1,200.00, due 2026-09-01",
  "name": "Invoice",
  "schema": {"type": "object",
             "properties": {"vendor": {"type": "string"},
                            "total": {"type": "number"},
                            "due_date": {"type": "string"}},
             "required": ["vendor", "total", "due_date"]}
}'
{
  "object": "needle.extraction",
  "data": {"vendor": "Acme Corp", "total": 1200.0, "due_date": "2026-09-01"},
  "matched": true,
  "x_needle": {"type": "call", "confidence": 0.3794, "...": "..."}
}

matched is false and data is null when the model declined to produce an object.

POST /v1/needle/complete

Passthrough that returns the engine's response verbatim — useful for debugging or when you want the native shape rather than the OpenAI translation.

curl -s localhost:8000/v1/needle/complete -H 'Content-Type: application/json' -d '{
  "query": "what is it like in Lagos right now?",
  "tools": [{"name": "get_weather",
             "parameters": {"type": "object",
                            "properties": {"city": {"type": "string"}}}}]
}'

Accepts either query + system, or a full messages array. tools may be a JSON array or a JSON string, and uses Needle's flat schema shape ({name, description, parameters}) rather than OpenAI's nesting.

POST /v1/completions

Legacy endpoint. Text prompt becomes a single user turn; the response text is the tool calls as JSON, or the reasoning trace if nothing was called. Token-array prompts are rejected with 400.

GET /health

Open even when an API key is configured, so orchestrators can probe it.

{"status": "ok", "model": "needle-2", "weights": "needle-2 (base)",
 "queue_depth": 0, "max_queue_depth": 32, "exact_token_counts": true}

status is loading until the engine finishes its first load. queue_depth shows requests waiting for the engine thread; requests are rejected with 429 once it reaches max_queue_depth.

Errors

OpenAI-shaped envelopes throughout:

{"error": {"message": "...", "type": "invalid_request_error",
           "param": "response_format", "code": "invalid_request_error"}}
Status When
400 Unmappable request — unsupported tool type, image content, json_object, empty conversation.
401 Missing or wrong bearer token.
404 Unknown model, with NEEDLE_STRICT_MODEL=true.
429 Engine queue full (code: engine_busy). Retry with backoff.
502 The engine itself reported an error.
503 Model still loading (code: not_ready), or shutting down.
504 Request exceeded NEEDLE_REQUEST_TIMEOUT.

Usage / token counts

The engine reports throughput but not token counts, so usage is reconstructed. With sentencepiece installed (the full image target) the model's own tokenizer gives exact counts; otherwise a character heuristic is used and usage.estimated: true is set. Either figure excludes the engine's internal chat-template framing, so treat it as a close lower bound rather than a billing number.