Docs

Documentation

One gateway in front of every major model provider. Send the request shape your SDK already speaks and reach all of them — create a key, change the base URL, keep everything else.

Quickstart

Create a key, change the base URL, keep everything else. NicoSoft speaks three wire formats, so most SDKs work unmodified.

1. Call it from your SDK

from openai import OpenAI

client = OpenAI(
    base_url="https://nicosoft.ai/v1",
    api_key="sk-ns-...",
)

response = client.chat.completions.create(
    model="deepseek/deepseek-v4-pro",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)

2. Authentication

Every header your SDK might send is accepted, so you never have to fight it over which one it picks. Bearer tokens must carry the sk-ns- prefix. The key never travels in the query string: URLs are written down in access logs, traces and shell history, and none of those places are ours to purge.

Authorization: Bearer sk-ns-... X-Api-Key: sk-ns-... x-goog-api-key: sk-ns-...

3. Endpoints

Path Surface Notes
/v1/chat/completions openai Chat Completions, streaming and non-streaming
/v1/responses openai Responses
/v1/messages anthropic Messages; count_tokens lives beside it
/v1beta/models/{model}:generateContent gemini :streamGenerateContent for streaming; image models answer here too
/v1/images/generations openai Synchronous image generation, image models only
/v1/videos openai Async video jobs: submit, poll /{id}, download /{id}/content
/v1/models all Model listing, in each surface's own shape

4. Errors

Every response carries the request id in the x-request-id header (request-id for Anthropic SDKs); error bodies repeat it as request_id. Quote it when you contact support.

HTTP/1.1 402 Payment Required
x-request-id: 8f3d92c41b7a4e02a6d51c9f0b82e743

{
  "error": {
    "type": "invalid_request_error",
    "message": "Insufficient balance, please deposit to your account.",
    "code": "insufficient_balance",
    "param": null,
    "request_id": "8f3d92c41b7a4e02a6d51c9f0b82e743"
  }
}

The error.type field follows each wire's own vocabulary; error.code is the stable machine-readable category, identical across surfaces. The ones you will actually see:

invalid_parameter insufficient_balance rate_limit_exceeded upstream_error internal_error

Chat Completions

The OpenAI-compatible surface. Any model whose Accepted column on the models page lists chat-completions can be called here.

Request

POST /v1/chat/completions — the standard Chat Completions body. Unknown parameters are forwarded to the model provider, so provider-specific options keep working.

curl
curl https://nicosoft.ai/v1/chat/completions \
  -H "Authorization: Bearer sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek/deepseek-v4-pro",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

Response

200
{
  "id": "...",
  "object": "chat.completion",
  "model": "...",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello! How can I help?" },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21 }
}

The usage block is what billing reads; the same numbers appear on your Requests screen per call.

Streaming

Set "stream": true and the response arrives as server-sent events — one data: chunk per delta, closed by data: [DONE].

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"Hel"}}]}

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"lo!"}}]}

data: [DONE]

Errors that occur after the stream has opened arrive as a final data: frame carrying the standard error envelope.

Responses

The newer OpenAI surface. It serves the same openai-endpoint models as Chat Completions — pick whichever shape your tooling speaks.

Request

POST /v1/responses — the standard Responses body: input as a string or a message list.

python
from openai import OpenAI

client = OpenAI(
    base_url="https://nicosoft.ai/v1",
    api_key="sk-ns-...",
)

response = client.responses.create(
    model="deepseek/deepseek-v4-pro",
    input="Write a haiku about gateways.",
)
print(response.output_text)

Streaming

Set "stream": true and deltas arrive as named events from the official Responses stream vocabulary (response.output_text.delta and friends), ending with response.completed.

event: response.output_text.delta
data: {"type":"response.output_text.delta","delta":"Gate"}

event: response.completed
data: {"type":"response.completed","response":{"usage":{...}}}

An error after the stream opens arrives as a named event: error frame.

Messages

The Anthropic-compatible surface, for models whose Accepted column lists messages. max_tokens is required, and the system prompt is the top-level system field — both exactly as the Anthropic API defines them.

Request

POST /v1/messages

curl
curl https://nicosoft.ai/v1/messages \
  -H "X-Api-Key: sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-opus-4-8",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

Response

200
{
  "type": "message",
  "role": "assistant",
  "model": "...",
  "content": [ { "type": "text", "text": "Hello! How can I help?" } ],
  "stop_reason": "end_turn",
  "usage": { "input_tokens": 10, "output_tokens": 12 }
}

Streaming

Set "stream": true and events follow the Anthropic vocabulary — message_start, content_block_delta, message_delta, message_stop. An error after the stream opens arrives as a named event: error.

Counting tokens

POST /v1/messages/count_tokens takes the same body (without max_tokens) and returns the input size without running the model:

{ "input_tokens": 10 }

Gemini

The Google-compatible surface, for models whose Accepted column lists generate-content. The model name lives in the path, and the key rides the x-goog-api-key header — which is what the Google SDKs send. Google's own REST samples put it in ?key= instead; that form is not accepted here.

Request

POST /v1beta/models/{model}:generateContent

curl
curl https://nicosoft.ai/v1beta/models/gemini-2.5-flash:generateContent \
  -H "x-goog-api-key: sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [{"parts": [{"text": "Hello!"}]}]
  }'

Response

200
{
  "candidates": [
    {
      "content": { "parts": [ { "text": "Hello! How can I help?" } ], "role": "model" },
      "finishReason": "STOP"
    }
  ],
  "usageMetadata": { "promptTokenCount": 4, "candidatesTokenCount": 11, "totalTokenCount": 15 }
}

Streaming

Use the :streamGenerateContent action. With ?alt=sse the chunks arrive as server-sent data: frames — this is what the official SDK sends. Without it, the response is one JSON array of chunks, exactly as the Google API behaves.

POST /v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse

data: {"candidates":[{"content":{"parts":[{"text":"Hel"}]}}]}

data: {"candidates":[{"content":{"parts":[{"text":"lo!"}]}}],"usageMetadata":{...}}

Image generation

Two surfaces draw: OpenAI's images endpoint and Gemini's generateContent, each in its own native shape. Both are billed per picture returned rather than per token — a model that draws is priced by the image.

OpenAI · request

POST /v1/images/generations — the standard Images body, forwarded as you send it, so provider options (size, quality, background, …) keep working. This endpoint has no streaming form.

curl
curl https://nicosoft.ai/v1/images/generations \
  -H "Authorization: Bearer sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-image-1-mini",
    "prompt": "a tiny red cube on a white background, product photo",
    "size": "1024x1024",
    "n": 1
  }'

OpenAI · response

200
{
  "created": 1786445645,
  "background": "opaque",
  "data": [
    { "b64_json": "iVBORw0KGgoAAAANSUhEUgAA..." }
  ],
  "usage": { "input_tokens": 17, "output_tokens": 272, "total_tokens": 289 }
}

Ask for n images and data carries n entries. The usage block is the provider's own, passed through as it arrived — for a per-image model it is informational, not the basis of the charge.

Gemini · inline images

An image model on :generateContent answers on the same endpoint as text, with the picture inline as an inlineData part.

curl
curl https://nicosoft.ai/v1beta/models/gemini-2.5-flash-image:generateContent \
  -H "x-goog-api-key: sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [{"parts": [{"text": "a tiny red cube on a white background"}]}]
  }'
200
{
  "candidates": [
    {
      "content": {
        "parts": [
          { "inlineData": { "mimeType": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAA..." } }
        ],
        "role": "model"
      },
      "finishReason": "STOP"
    }
  ],
  "usageMetadata": { "promptTokenCount": 7, "candidatesTokenCount": 1417, "totalTokenCount": 1424 }
}

Streaming works too: with :streamGenerateContent the inlineData parts arrive in the frames as they are produced, and images are counted across the whole stream.

Billing

The charge is the number of images the provider actually returned times the model's per-image price, with the model's per-call minimum as the floor — the same on either surface. Token counts are recorded on the request for reference; they do not price it.

A 2xx that carries no image is not an answer: nothing is billed and the request is retried on another provider before you ever see it.

Picking a model

Image models are listed like every other model — they appear in /v1/models, /v1beta/models and on the models page, where the Type column tells them apart and prices them per image. Names carry the same prefix the chat models do: openai/gpt-image-1-mini on the OpenAI surface, gemini-2.5-flash-image on the Gemini one.

Each endpoint admits only the kind of model it can serve. A chat model sent to the images endpoint — or an image model sent to /v1/chat/completions — is refused before anything is spent:

wrong_endpoint

Video generation

Video is a job, not a request: submit it, poll it, then download the bytes. The three endpoints follow the OpenAI video shape, and every response body is the provider's own, passed through untouched.

1. Submit

POST /v1/videos — answers as soon as the job is queued, long before the video exists.

curl
curl https://nicosoft.ai/v1/videos \
  -H "Authorization: Bearer sk-ns-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/sora-2",
    "prompt": "a red cube slowly rotating on a white studio background",
    "seconds": "4",
    "size": "720x1280"
  }'
200
{
  "id": "video_6a7aff712a688193a7f0eb8a9c123fbc",
  "object": "video",
  "created_at": 1786445681,
  "status": "queued",
  "progress": 0,
  "seconds": "4",
  "size": "720x1280"
}

2. Poll

GET /v1/videos/{id} — the status walks queuedin_progress (with a progress percentage) → completed. Polling is free.

GET /v1/videos/video_6a7aff712a688193a7f0eb8a9c123fbc

{"id":"video_6a7aff...","status":"in_progress","progress":99,"seconds":"4"}

3. Download

GET /v1/videos/{id}/content streams the finished file back with the provider's own content type; a ?variant= parameter is passed through. Downloading is free, and you can download as often as the provider keeps the job.

curl
curl https://nicosoft.ai/v1/videos/video_6a7aff.../content \
  -H "Authorization: Bearer sk-ns-..." \
  -o video.mp4

Billing and ownership

The charge lands once, at submit: the requested duration in seconds times the model's per-second price, with the model's minimum duration as the floor. Polls and downloads are never billed. If the provider ends up reporting the job as failed, the charge is refunded automatically the first time you poll it.

A job belongs to the account that created it. Any other account asking after it gets the same answer as for an id that never existed:

unknown_url

Model listing

Each surface lists the models it can serve, in its own native shape. Custom models you define in the console appear in your own listing alongside the catalog.

OpenAI shape

GET /v1/models

200
{
  "object": "list",
  "data": [
    { "id": "deepseek/deepseek-v4-pro", "object": "model", "owned_by": "deepseek" },
    { "id": "anthropic/claude-opus-4-8", "object": "model", "owned_by": "anthropic" }
  ]
}

Anthropic shape

The Anthropic SDK builds the same absolute URL — its base has no /v1 and it appends /v1/models. The shapes are told apart by the anthropic-version header, which every Anthropic client sends and no OpenAI client does.

200 · with anthropic-version
{
  "data": [
    { "type": "model", "id": "anthropic/claude-opus-4-8", "display_name": "Claude Opus 4.8" }
  ],
  "has_more": false
}

Gemini shape

GET /v1beta/models

200
{
  "models": [
    { "name": "models/gemini-2.5-flash", "displayName": "gemini-2.5-flash" }
  ]
}

Each list is filtered to what that surface can actually serve, so a name you pick off a listing is always callable on the surface that listed it. Chat, image and video models all appear; which endpoint of the surface takes a given name follows from what it produces — see image generation and video generation.

Rate limits

The platform imposes no request quota of its own. Every key starts unlimited; you can give a key its own ceilings — a spend limit, a requests-per-minute cap, an expiry — from the console, to contain a leaked or shared key. Beyond those, what gates a request is your balance, the platform's live capacity, and a few abuse protections — each with its own status code, so your retry logic can tell them apart.

What can stop a request

Status Code Meaning
402 insufficient_balance Checked before any work is done. Deposit and retry.
403 api_key_credit_limit The key reached the spend limit you set on it. Raise the limit or use another key.
429 rate_limit_exceeded The key exceeded the per-minute cap you set on it. Slow down, or raise the cap.
413 request_too_large The request body exceeds the size cap (10 MB by default).
503 capacity_exceeded Every route to the model is saturated right now. Back off and retry.
503 model_temporarily_unavailable The model has no serving route at the moment. Try again, or another model.
403 ip_blocked The caller's network is not permitted.

Handling them

Treat 5xx and 429 as retryable with exponential backoff — the official OpenAI, Anthropic and Google SDKs already do this by default, so pointing them at NicoSoft keeps their retry behavior. 4xx responses are not retryable: the request itself must change.

Prefer streaming for long generations — it holds one connection instead of retrying long polls, and you see failures at the moment they happen. When contacting support about a limit, quote the x-request-id of a rejected call.

Data retention

The short version: your prompts and the models' responses are never stored. What is kept is the metadata that bills and operates the service.

Never stored

Request and response content — prompts, messages, files, generated text — passes through the gateway to complete each call and is not persisted anywhere. It is also never used to train models.

What is kept

Data Window Why
Request metadata (model, endpoint, token counts, latency, status, cost) Life of the account Billing record — it is your Requests screen
Credit ledger and deposit orders As tax rules require Accounting
Operational error events ~90 days Debugging and abuse investigation
Security emails log (codes, notices) ~90 days Delivery troubleshooting

Upstream providers

Requests are completed by third-party model providers, which receive the prompt they serve and apply their own retention and abuse-monitoring policies. Review the relevant provider's terms before routing sensitive data. The full statement of rights and windows lives in the Privacy Policy.