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/media/image nicosoft Our own drawing protocol; nicosoft/image-preview 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

Media · Image

Our own drawing protocol, on /v1/media/image. It is deliberately not the OpenAI Images shape: the model behind it cannot honour size, n or quality, and a schema that accepts fields it silently drops is worse than one that says so. Every response carries an applied block naming what actually happened.

Request

A prompt is the only thing required. One call takes 25–60 seconds (count: 4 can reach 110), so set a client timeout of at least 300 seconds.

curl
curl https://nicosoft.ai/v1/media/image \
  -H "Authorization: Bearer sk-ns-..." \
  -H "Content-Type: application/json" \
  --max-time 300 \
  -d '{
    "model": "nicosoft/image-preview",
    "prompt": "a red maple leaf on a wooden table, soft morning light",
    "aspect_ratio": "16:9"
  }'

Fields

Field Level Notes
prompt required Up to 4000 characters
aspect_ratio best-effort "16:9" or "1920x1080" — shape only, never resolution
transparent best-effort True alpha background. Read applied.transparent — see below
count best-effort 1–4; fewer may come back, applied.count is the truth
format contract png (default), jpeg, webp
compression contract 0–100, jpeg and webp only; png is lossless
images contract 0–16 inline data: URLs, 4 MB each. One edits, many compose
mask contract PNG with an alpha channel; needs exactly one entry in images
stream contract SSE. The terminal frame always arrives
preview best-effort Progressive frames, needs stream. Priced separately — see Billing

There is no size, quality, n or seed. Those are not omissions — the upstream cannot act on them, and accepting a field we would drop is how a caller ends up debugging a request that was never wrong.

Response

200
{
  "id": "media_9f3c1e0a-...",
  "model": "nicosoft/image-preview",
  "created": 1788734849,
  "images": [
    { "b64": "iVBORw0KGgo...", "format": "png",
      "width": 1672, "height": 941, "bytes": 1527808 }
  ],
  "applied": {
    "aspect_ratio": "16:9", "transparent": false,
    "count": 1, "format": "png", "quality": "medium"
  },
  "revised_prompt": "A red maple leaf resting on weathered oak...",
  "usage": {
    "input_tokens": 2321, "output_tokens": 51, "total_tokens": 2372,
    "image_input_tokens": 0, "image_output_tokens": 229, "image_text_tokens": 0
  }
}

applied describes the bytes you were actually sent, not the request you made — judge success by it. b64 is raw base64 with no data: prefix. revised_prompt may be absent entirely; do not depend on it.

Size: you choose the shape, not the resolution

The pixel budget is fixed at roughly 1.57 million pixels. aspect_ratio spends that budget in a different shape — it does not buy more of it. No aspect ratio is sharper than another; scale on the client if you need exact dimensions.

aspect_ratio You get Notes
(omitted) 1254 x 1254 Square is the upstream default
16:9 1672 x 941 Wide shapes are cheaper per image and no less sharp
2:3 1024 x 1536 Portrait

Transparency is best-effort — check applied.transparent

Whether a request comes back with a real alpha channel depends on the upstream account that served it. Measured across the pool: roughly four in five accounts produce true alpha, the rest never do, and an individual account is consistent either way. The contract holds regardless — applied.transparent always tells the truth about the bytes — so a caller that needs alpha should read it and retry, which usually lands on a different account.

python
out = generate(prompt, transparent=True)
if not out["applied"]["transparent"]:
    out = generate(prompt, transparent=True)   # a second try usually draws another account

transparent cannot be combined with format: "jpeg" — JPEG has no alpha channel at all, so the pair is refused rather than silently resolved.

Editing and composition

Send source images inline in images. One entry edits that picture, several compose them. https:// URLs are refused: fetching a caller-supplied address is a request-forgery primitive, and reading the bytes yourself costs nothing you were not already paying.

A mask confines the edit. It must be a PNG that carries an alpha channel, and it uses the standard polarity — transparent marks the region to change, opaque is kept. A mask without an alpha channel is refused rather than accepted as a no-op, because "200 and nothing changed" is the worst answer available.

json
{
  "model": "nicosoft/image-preview",
  "prompt": "put a yellow rubber duck here",
  "images": ["data:image/png;base64,iVBORw0KGgo..."],
  "mask":    "data:image/png;base64,iVBORw0KGgo..."
}

Sending a larger source image does not buy detail: the upstream down-samples on arrival and caps what one image can cost, so an oversized upload only spends bandwidth and latency.

Streaming

With stream: true the response is text/event-stream carrying three event names. Each frame repeats its own name in a type field, so a client that reads only data: lines can still tell them apart without holding the previous line.

event: image.partial
data: {"type":"image.partial","index":0,"b64":"...","format":"png"}

event: image.done
data: {"type":"image.done","id":"media_...","images":[...],"applied":{...},"usage":{...}}

event: image.error
data: {"type":"image.error","error":{"code":"upstream_error","message":"..."}}

stream on its own does not request progressive frames; preview: true does, and it is a separate field because it is separately expensive (see Billing). Streaming guarantees the terminal frame arrives; it does not guarantee how many partial frames precede it, and it does not make the first byte arrive sooner.

Errors

Same envelope as everywhere else on the gateway. Quote request_id when reporting one.

code HTTP When
missing_required_field 400 No model, or no prompt
invalid_parameter 400 count out of range, unknown format, malformed aspect_ratio
unsupported_capability 400 transparent with jpeg; a mask with no image; an https: URL
media_format_unsupported 400 An image will not decode, or a mask carries no alpha channel
request_too_large 413 A single inline image above 4 MB
wrong_endpoint 400 Model and endpoint disagree; the message names the right one

Billing

Priced per million tokens like a text model, against the input_tokens and output_tokens in usage. The three image_* counters beside them are informational and are not charged — a picture's own tokens are on us.

A typical text-to-image call is about 2.3K input tokens, near US$0.03. Turning on preview raises the input count to roughly 4.2K — about 73% more on the bill — because each progressive frame is fed back through the model that drew it. Ask for it when a person is watching, not by default.

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 queued → in_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.