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)
import anthropic
client = anthropic.Anthropic(
base_url="https://nicosoft.ai",
api_key="sk-ns-...",
)
message = client.messages.create(
model="anthropic/claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
print(message.content[0].text)
from google import genai
client = genai.Client(
api_key="sk-ns-...",
http_options={"base_url": "https://nicosoft.ai"},
)
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Hello!",
)
print(response.text)
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.
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:
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 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
{
"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.
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 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
{
"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 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
{
"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 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
{
"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 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"}]}]
}'
{
"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:
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 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"
}'
{
"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 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:
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
{
"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.
{
"data": [
{ "type": "model", "id": "anthropic/claude-opus-4-8", "display_name": "Claude Opus 4.8" }
],
"has_more": false
}
Gemini shape
GET /v1beta/models
{
"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.