Paper Instruments
Ultramarine API

Chat completions

Request parameters, reasoning, tools, and streaming.

POST https://api.paperinstruments.com/v1/chat/completions
Authorization: Bearer $PAPERINSTRUMENTS_API_KEY
Content-Type: application/json

We reject unsupported request and message fields with 400; error.param identifies the field.

Create a chat completion

Install openai and export PAPERINSTRUMENTS_API_KEY as in the quickstart.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.paperinstruments.com/v1",
    api_key=os.environ["PAPERINSTRUMENTS_API_KEY"],
)

stream = client.chat.completions.create(
    model="ultramarine-1-flash",
    messages=[{"role": "user", "content": "Explain EBITDA in two sentences."}],
    reasoning_effort="max",
    max_completion_tokens=8192,
    stream=True,
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)

Parameters

ParameterNotes
modelRequired. ultramarine-1-flash.
messagesRequired, non-empty array. Roles: system, user, assistant, tool. Content is a string or text and image_url parts; see images. Use system for instructions. We reject developer messages.
reasoning_effortlow, high, or max. Default: max.
max_completion_tokensInteger, 1–32,768, including reasoning. max_tokens is an alias; send only one. Omit to use the model's output ceiling.
streamBoolean. Default: false. Use true for server-sent events.
stream_optionsSet {"include_usage": true} to receive stream usage.
tools, tool_choice, parallel_tool_callsFunction tools and tool selection. Your application executes the calls.
response_formatStructured output: json_object or json_schema, or plain text.
temperature, top_p, top_k, min_pSampling controls. Start with defaults.
presence_penalty, frequency_penalty, repetition_penalty, logit_biasToken selection controls.
logprobs, top_logprobsToken probabilities. top_logprobs must be an integer, 0–20.
nInteger, 1–16. Default: 1. Each completion adds output usage.
stopA string or up to 16 non-empty strings, each at most 1,024 characters. Matched against content only, not reasoning_content. Generation continues after a match: those tokens are not returned but count in completion_tokens.
seedSampling seed.
prompt_cache_key, userOptional cache hint and user identifier. See conversation caching.

Limit request bodies to 20 MiB. We reject audio, legacy functions, store, metadata, and service_tier.

Images

Send image_url parts with a base64 data URL or a public HTTP(S) URL:

{
  "role": "user",
  "content": [
    {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0..."}},
    {"type": "text", "text": "What does this chart show?"}
  ]
}

Formats: PNG, JPEG, WebP, and GIF. Up to 16 images per request; each at most 10 MiB and 32 million pixels. Animated images are rejected. URL images must load within 10 seconds. Images count as input tokens.

Reasoning

We default to max thinking (reasoning effort) and recommend it for best results. Test high or low to use fewer reasoning tokens.

The model requires reasoning. Set the top-level reasoning_effort field. We return 400 for disabled thinking, reasoning.effort, or effort values other than low, high, and max.

Read the answer from message.content and reasoning from message.reasoning_content. During streaming, read delta.content and delta.reasoning_content. Include reasoning and answer tokens in your output budget. We charge both at the output rate.

Streaming and usage

Follow the streaming example. For a single JSON response, set stream=false and read choices[0].message.content.

Set stream_options={"include_usage": true} to receive the final usage chunk. It has choices: []; check for choices before reading a delta.

Usage fieldCounts
prompt_tokensAll input tokens, including cached input.
prompt_tokens_details.cached_tokensCached input, a subset of input.
completion_tokensAll output tokens, including reasoning.
completion_tokens_details.reasoning_tokensReasoning, a subset of output.
total_tokensInput plus output.

A successful stream ends with [DONE]. An interrupted stream emits data: {"error": ...} and ends without [DONE]. See errors for retry handling.

Output limits

  • Output that reaches max_completion_tokens returns finish_reason: "length", including a tool call cut off mid-arguments. Do not run that call.
  • Input that fits but leaves less room than the limit runs until the context window is full and returns finish_reason: "length".
  • Input larger than the context window returns 400.

Tool calls

Define tools with type: "function" and a function containing name, description, and JSON Schema parameters. Handle tool_calls as follows:

  1. Append the assistant message's role, content, tool_calls, and any reasoning_content to your history.
  2. Run each function with its parsed function.arguments.
  3. Append one tool message per call, with tool_call_id set to the call's id and content set to the result text.
  4. Repeat the request with the updated messages and the same tools until the model returns an answer.

Conversation caching

The x-session-id header is optional. You can use it if you want sticky routing. When using it, reuse the same value for every turn, retry, and tool result in a conversation, and use a different value for each conversation.

In Python, pass extra_headers={"x-session-id": "your-conversation-id"}; in Node.js, pass { headers: { "x-session-id": "your-conversation-id" } } as the second argument to create. The header takes precedence over prompt_cache_key.

Keep repeated prompt prefixes stable. Cache hits depend on matching prefixes and cache availability. Send the full message history on each request.

GET /v1/models lists available models; GET /v1/models/{id} retrieves one. Responses include x-request-id. Save it when reporting an error.

On this page