Guides
- Quickstart
- Authentication and keys
- Key protection
- Models, aliases and fallbacks
- Chat completions
- Streaming
- Structured outputs and tools
- Logprobs
- Vision
- Responses, Claude and Ollama
- Embeddings, rerank, audio, images
- Files and batches
- LoRA adapters
- Project tags and costs
- Prefix caching
- Headers, rate limits and spend caps
- Errors
- Privacy and retention
- Verifiable receipts
- Pricing
- Billing
- Status
- SDKs
- Integrations
- Run a node
- Architecture
- Changelog
API reference
Documentation
Everything you need to build.
Silicon Network speaks the OpenAI wire format. If you already call an OpenAI-compatible API, you are a base URL away from running on a distributed network of open models.
API reference
38 routes in 10 groups, generated from the gateway's OpenAPI document (snapshot).
- ChatText generation in three wire formats: OpenAI chat completions, the OpenAI Responses API, and Anthropic Messages. Same models, same key.POST /v1/chat/completions POST /v1/responses POST /v1/messages
- Embeddings and rerankingVectors for retrieval and cross-encoder reranking, on the OpenAI embeddings shape and the Cohere and Jina rerank shape.POST /v1/embeddings POST /v1/rerank
- AudioSpeech to text and text to speech.POST /v1/audio/transcriptions POST /v1/audio/speech
- ImagesImage generation and edits, billed per image.POST /v1/images/generations POST /v1/images/edits
- Batches and filesAsynchronous, discounted inference for work that can wait: upload a JSONL file (or send the requests inline), create a batch, collect the results within the completion window. The OpenAI batch flow works unchanged.GET /v1/batches POST /v1/batches GET /v1/batches/{batch_id} POST /v1/batches/{batch_id}/cancel GET /v1/batches/{batch_id}/output GET /v1/batches/{batch_id}/errors GET /v1/files POST /v1/files GET /v1/files/{file_id} DELETE /v1/files/{file_id} GET /v1/files/{file_id}/content
- ModelsWhat the network can serve right now, and the full catalog a node could load.GET /v1/models
- OllamaThe Ollama API for clients that speak it: chat, generate, and the discovery routes a model picker uses.POST /ollama/api/chat POST /ollama/api/generate GET /ollama/ HEAD /ollama/ GET /ollama HEAD /ollama GET /ollama/api/version GET /ollama/api/tags GET /ollama/api/ps
- Receipt keysThe platform's receipt signing keys, active and retired, for verifying signed receipts offline. Public; no key needed.GET /v1/receipts/keys GET /.well-known/silicon-receipt-keys.json
- Key safetyWhere GitHub secret scanning reports Silicon Network keys found in public. A reported key is revoked (or flagged, as the org chooses) and the org is told.POST /v1/keys/leaked
- Health and metricsLiveness, readiness, instance serving state, and Prometheus exposition. No key needed.GET /metrics GET /health GET /livez GET /readyz GET /servingz
Quickstart
Create an API key on the API Keys page, add balance on the Billing page, and point your client at https://api.siliconnetwork.io/v1. Ask GET /v1/models which models are loaded right now; the examples use llama-3.2-3b, or the routing alias fast, which picks whichever small model is serving.
curl
export SILICON_API_KEY=sn_live_...
curl https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "llama-3.2-3b",
"messages": [{"role": "user", "content": "Say hello in one line."}]
}'Python SDK
# pip install siliconnetwork
from siliconnetwork import SiliconNetwork
client = SiliconNetwork() # reads SILICON_API_KEY
out = client.chat.completions.create(
model="llama-3.2-3b",
messages=[{"role": "user", "content": "Say hello in one line."}],
)
print(out.choices[0].message.content)
print(out.meta.request_id, out.meta.model, out.meta.node)TypeScript SDK
// npm install @siliconnetwork/sdk
import { SiliconNetwork } from "@siliconnetwork/sdk";
const client = new SiliconNetwork(); // reads SILICON_API_KEY
const out = await client.chat.completions.create({
model: "llama-3.2-3b",
messages: [{ role: "user", content: "Say hello in one line." }],
});
console.log(out.choices[0].message.content);
console.log(out.meta.requestId, out.meta.model, out.meta.node);OpenAI SDK, unchanged
The official openai packages work with the base URL swapped. Send Silicon Network extensions (models, quality, enable_thinking, metadata.project) through extra_body in Python or as extra fields in Node.
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
resp = client.chat.completions.create(
model="llama-3.2-3b",
messages=[{"role": "user", "content": "Say hello in one line."}],
extra_body={"models": ["fast"]}, # fall back to any fast model if this one is not loaded
)
print(resp.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.siliconnetwork.io/v1", apiKey: process.env.SILICON_API_KEY });
const res = await client.chat.completions.create({
model: "llama-3.2-3b",
messages: [{ role: "user", content: "Say hello in one line." }],
});
console.log(res.choices[0].message.content);There is no legacy /v1/completions route. A client that needs it will not work by swapping the base URL; use a chat path.
Authentication and keys
Every request that reaches a model carries an API key as a bearer token. The gateway reads Authorization: Bearer and nothing else, so a client that authenticates with x-api-key needs the header set explicitly. Keys are created on the API Keys page and shown once, at creation: only a SHA-256 hash is stored, so a lost key cannot be recovered, only replaced.
Authorization: Bearer sn_live_...Health checks, metrics, GET /v1/models, the receipt key list and the Ollama discovery routes need no key. Everything that reaches a model does.
Key types
| Prefix | What it is |
|---|---|
| sn_live_ | An API key. Belongs to one organization, is metered against its credits, and carries the policy you set on it. |
| sn_test_ | The same kind of key with a different prefix, for telling your environments apart. It is billed and served exactly like a live key: there is no sandbox tier. |
| snop_ | An operator node credential, minted for approved operators. It registers nodes; it cannot call the inference API. |
The chat app and the dashboard Playground use short lived sn_live_ keys that the web server mints and holds for you. They never reach the browser and do not appear in your key list.
What you can set on a key
Everything below is optional, can be set at creation (POST /v1/keys or the dashboard form) and changed later in place (PATCH /v1/keys/{id}, where null clears a field). The secret is never regenerated by an edit. Members manage their own keys; owners and admins manage every key in the organization.
| Field | Effect |
|---|---|
| scope | * (every model), or a comma separated list of model ids the key may call, matched case-insensitively. A key scoped to an alias may call every variant of it. |
| rate_limit_per_min | Requests per minute for this key, 1 to 100,000. Default 60. |
| expires_in_days | 1 to 3,650. An expired key answers 401. |
| spend_cap_micros | A monthly cap for this key in micro-USD (1,000,000 is $1). Past it, 402. |
| allowed_cidrs | An IP allowlist (see Key protection). |
| allowed_regions, allowed_operators, require_attestation | Pin the key to nodes in these regions, run by these operators, or with a current attestation. No eligible node answers 503 rather than serving elsewhere. |
| receipts | Attach a signed receipt to every response (see Verifiable receipts). |
Revoke a key with DELETE /v1/keys/{id} or from the dashboard. The gateway caches a verified key for 15 seconds, so revocation, a new allowlist or a credit change takes effect within that window. Work that was already in flight when the key was revoked is still billed.
Key protection
Four layers stand between a leaked key and your balance: a recognisable format that scanners can find, an optional IP allowlist, anomaly alerts with optional automatic suspension, and GitHub secret scanning. The settings live in the dashboard's key protection panel on the API Keys page.
The key format
A key is sn_live_ or sn_test_, 40 random base62 characters, and a 6 character base62 checksum: 54 characters in all. The checksum is CRC32 of everything before it, written in base62 (digits, then upper case, then lower case), most significant digit first and padded to 6. A key that fails its checksum is refused without a database lookup, and anyone can match a key in text with:
\bsn_(?:live|test)_[0-9A-Za-z]{46}\b
# keys issued before the checksum format: 48 lower case hex characters, still valid
\bsn_(?:live|test)_[0-9a-f]{48}\bIP allowlists
Set allowed_cidrs on a key and every keyed request, including GET /v1/models with a key, must come from one of those ranges. Entries are IPv4 or IPv6 CIDRs or bare addresses (a bare address means /32 or /128), at most 64 after duplicates are merged. A range with host bits set is refused with a suggestion of the network you meant. A list that covers the whole internet (a /0, or ranges that add up to all of IPv4 or IPv6) must be confirmed with allow_any_ip: true. [] or null removes the allowlist.
{
"allowed_cidrs": ["203.0.113.0/24", "198.51.100.7", "2001:db8::/48"]
}A request from anywhere else answers 403: client address 198.51.100.7 is not in this API key's IP allowlist. The member who created the key, or an org owner or admin, can change its allowlist (a member edits only the keys they created). The gateway reads the client address from the hop its own load balancer appended to X-Forwarded-For; entries a caller writes further left are never trusted. A batch is checked once, when it is submitted.
Anomaly alerts and suspension
Each key learns its own baseline from the last week of traffic (its first 24 hours are a learning period) and raises an alert for:
| Kind | Fires when |
|---|---|
| spike | This hour's requests or tokens exceed 10 times the key's mean hour and 3 times its busiest hour (and at least 200 requests or 500,000 tokens). |
| distinct_ips | More distinct client addresses in the last hour than 3 times the key's usual peak (and more than 25). |
| new_country | A key with settled history is used from a country it has not been used from, where the deployment has a GeoIP database. |
| model_mix | Half or more of at least 50 requests this hour go to models the key almost never calls. |
Each kind fires at most once per key every 6 hours. Owners and admins get an email, and your webhooks get key.anomaly. Turn on automatic suspension (auto_suspend) and an anomalous key is also suspended on the spot, which sends key.suspended. Batch traffic is not judged. Client addresses are kept only as keyed hashes, for 30 days.
A suspended key answers 403 api key suspended: <reason> and is never served from the gateway's cache. Anyone who may edit a key can suspend it by hand (POST /v1/keys/{id}/suspend, with an optional reason); only an owner or admin can reinstate it (POST /v1/keys/{id}/reinstate), which sends key.reinstated.
Leaked key scanning
Silicon Network keys are registered with GitHub secret scanning. When a key turns up in a public repository, GitHub reports it to POST https://api.siliconnetwork.io/v1/keys/leaked, signed with GitHub's published key; an unsigned or forged report is refused. What happens next is your organization's leaked_action: revoke (the default) revokes the key at once, flag keeps it working and marks it leaked. Either way owners and admins get an email, your webhooks get key.leaked with the URL where it was found, and the dashboard marks the key.
GET /v1/keys/security # any member
PUT /v1/keys/security # owner or admin
{
"auto_suspend": false, # suspend a key when an anomaly fires
"leaked_action": "revoke", # or "flag"
"notify_email": true # email owners and admins
}
GET /v1/keys/security/events?key_id=...&limit=50Key management, including these settings, is part of the account API the dashboard calls with your signed-in session; an API key cannot manage keys. Webhook deliveries are signed (X-Silicon-Signature); set them up on the dashboard's Webhooks page.
Models, aliases and fallbacks
Pass a model in the model field: a catalog alias from the table below, or a full engine id (anything with a slash, passed through untouched, including a model an operator published on the marketplace). Aliases are case-insensitive. Every alias resolves, but only the models an operator has loaded serve: the network is machines, not a warm datacenter. Ask for one that is not loaded and you get a 404 with code: "model_not_available" and an available_models list of what is serving.
GET /v1/models needs no key and is the only live answer to "what can I call right now": an id it returns serves. It lists each loaded model with its aliases, the variants and routing aliases that can serve, approved marketplace models, and the pricing policy in pricing_policy. Add ?all=true (with a key) for the full catalog a node could load, where each card carries its kind (chat, embeddings, rerank, audio_transcription, audio_speech, image). The table holds the chat models first, then the embedding, rerank, audio and image models, each callable only on its own route.
| llama-3.2-1b | Llama | Text | mlx-community/Llama-3.2-1B-Instruct-4bit |
| llama-3.2-3b | Llama | Text | mlx-community/Llama-3.2-3B-Instruct-4bit |
| llama-3.1-8b | Llama | Text | mlx-community/Meta-Llama-3.1-8B-Instruct-4bit |
| llama-3.1-70b | Llama | Text | mlx-community/Meta-Llama-3.1-70B-Instruct-4bit |
| llama-3.3-70b | Llama | Text | mlx-community/Llama-3.3-70B-Instruct-4bit |
| deepseek-v3.1 | DeepSeek | Text | mlx-community/DeepSeek-V3.1-4bit |
| deepseek-v3.2 | DeepSeek | Text | mlx-community/DeepSeek-V3.2-4bit |
| qwen3-235b | Qwen | Text | mlx-community/Qwen3-235B-A22B-Instruct-2507-4bit |
| qwen3-vl-4b | Qwen | Vision | mlx-community/Qwen3-VL-4B-Instruct-4bit |
| kimi-k2-thinking | Kimi | Reasoning | mlx-community/Kimi-K2-Thinking |
| glm-4.7 | GLM | Text | mlx-community/GLM-4.7-4bit |
| gpt-oss | OpenAI OSS | Text | mlx-community/gpt-oss-20b-MXFP4-Q8 |
| gpt-oss-20b | OpenAI OSS | Text | mlx-community/gpt-oss-20b-MXFP4-Q8 |
| gpt-oss-120b | OpenAI OSS | Text | mlx-community/gpt-oss-120b-MXFP4-Q8 |
| bge-m3 | BGE | Embeddings | mlx-community/bge-m3-mlx-fp16 |
| qwen3-embedding-0.6b | Qwen | Embeddings | mlx-community/Qwen3-Embedding-0.6B-8bit |
| all-minilm-l6-v2 | MiniLM | Embeddings | mlx-community/all-MiniLM-L6-v2-bf16 |
| qwen3-reranker-0.6b | Qwen | Rerank | mlx-community/Qwen3-Reranker-0.6B-mxfp8 |
| qwen3-reranker-4b | Qwen | Rerank | mlx-community/Qwen3-Reranker-4B-mxfp8 |
| whisper-large-v3-turbo | Whisper | Transcription | mlx-community/whisper-large-v3-turbo |
| whisper-small | Whisper | Transcription | mlx-community/whisper-small-mlx |
| kokoro-82m | Kokoro | Speech | mlx-community/Kokoro-82M-bf16 |
| flux-schnell | FLUX | Image | exolabs/FLUX.1-schnell-4bit |
| flux-dev | FLUX | Image | exolabs/FLUX.1-dev-4bit |
| flux-krea-dev | FLUX | Image | exolabs/FLUX.1-Krea-dev-4bit |
| flux-kontext-dev | FLUX | Image edit | exolabs/FLUX.1-Kontext-dev-4bit |
| qwen-image | Qwen | Image | exolabs/Qwen-Image-4bit |
| qwen-image-edit | Qwen | Image edit | exolabs/Qwen-Image-Edit-2509-4bit |
Routing aliases: fast and best
Two names stand for an ordered list of models, and the first one that is serving answers. GET /v1/models lists a routing alias only while one of its members is available, with the chain in resolves_to and the member it would use now in serving.
| Alias | Tries, in order |
|---|---|
| fast | llama-3.2-3b, llama-3.2-1b, gpt-oss-20b |
| best | deepseek-v3.2, qwen3-235b, gpt-oss-120b, llama-3.3-70b |
curl https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "best", "messages": [{"role": "user", "content": "Prove there are infinitely many primes."}]}'A key scoped to fast may call its whole chain; otherwise each member must be in the key's scope.
Fallback chains
Name several models and the gateway tries them in order: model may be a list, and an OpenRouter-style models array is tried after it. Routing aliases expand in place, duplicates are dropped, and a request may name at most 5 models after that. Chains work on every JSON surface (chat, Responses, Messages, Ollama, embeddings, rerank, speech, image generation), streaming included, and never on multipart uploads.
from siliconnetwork import SiliconNetwork
client = SiliconNetwork()
out = client.chat.completions.create(
model="llama-3.3-70b",
models=["qwen3-235b", "fast"], # tried in order when the first cannot serve
messages=[{"role": "user", "content": "Summarise RFC 9110 in three lines."}],
)
print(out.meta.model) # the engine id that actually answeredThe next model is tried when the current one cannot serve: it is not loaded (404), no node can take it (503, including a pinned key with no eligible node, an adapter not deployed, or no node with the context), the prompt is too long for its context, the engine is unreachable or times out, or it answers 408, 429 or any 5xx. Any other 4xx is your request's fault and is answered at once, as is anything after the first byte has reached you. Scope and credit are checked for every candidate before the first attempt.
Only the attempt that answers is billed, at the served model's price. Silicon-Model names the engine id that served, and Silicon-Fallback-From (the first candidate's engine id) appears whenever a later candidate answered.
Quantization variants and quality
Some models are offered in several quantizations. The alias alone is the default, listed first below (4-bit for the chat and image models; the small embedding and audio models default to their wider weights); <alias>@<label> asks for one exactly, and only nodes serving that variant take it. Or send quality: economy (the narrowest), standard (the default) or max (the widest). quality applies to chat surfaces and to a model named by alias; an exact @label or full id is served as named. It never reaches the engine, and the image surface keeps its own OpenAI quality.
| Alias | Labels |
|---|---|
| llama-3.2-3b | 4bit, 8bit |
| llama-3.1-8b | 4bit, 8bit, bf16 |
| llama-3.3-70b | 4bit, 8bit, fp16 |
| deepseek-v3.1, deepseek-v3.2 | 4bit, 8bit |
| qwen3-235b | 4bit, 8bit |
| glm-4.7 | 4bit, 6bit |
| qwen3-embedding-0.6b | 8bit, 4bit |
| all-minilm-l6-v2 | bf16, 4bit |
| whisper-large-v3-turbo | fp16, 4bit |
| kokoro-82m | bf16, 8bit |
| flux-schnell, flux-dev, flux-krea-dev, flux-kontext-dev | 4bit, 8bit, bf16 |
| qwen-image, qwen-image-edit | 4bit, 8bit, bf16 |
import { SiliconNetwork } from "@siliconnetwork/sdk";
const client = new SiliconNetwork();
const exact = await client.chat.completions.create({
model: "llama-3.1-8b@8bit",
messages: [{ role: "user", content: "Hello" }],
});
const widest = await client.chat.completions.create({
model: "llama-3.3-70b",
quality: "max", // fp16 for this model
messages: [{ role: "user", content: "Hello" }],
});An unknown label is a 400 that lists the variants that exist, and an unknown quality is a 400 too. A variant without its own price costs its default's price scaled by bit width (8-bit 1.5 times, bf16 and fp16 2 times a 4-bit default; the table is in pricing_policy). A key scoped to the alias may call every variant and the full id the alias resolves to; a key scoped to alias@label only that one. The variant served is named in Silicon-Model, the usage record and the receipt.
Long-context tier and pricing
A text request whose prompt plus output limit is above 32,768 tokens is routed only to nodes that report enough context for that model, and is billed at 1.5 times its whole price. Billing uses the engine's actual prompt count plus the output limit forwarded. The gateway always forwards a limit on text surfaces (8,192 when you set none), so without max_tokens a request crosses the threshold once its prompt is above about 24,576 tokens; set max_tokens to what you need to keep a long prompt below it.
The gateway estimates the prompt before routing from its text (UTF-8 bytes divided by 3, plus allowances for turns, tools and images), which over-counts English and code; billing never uses the estimate. A prompt that cannot fit the model at all is a 400 that names the context and suggests a longer one. If no node can hold the request right now, the answer is a 503; with a fallback chain both move on to the next model. The threshold and multiplier are published in GET /v1/models (pricing_policy.long_context) and on the pricing page.
Context windows and parameter counts come from each model card at load time, so they are not hardcoded here.
Chat completions
POST /v1/chat/completions takes the OpenAI shape. The generated reference lists every route; this section is about what the gateway does with the body.
Parameters that work
messages, stream, stream_options, max_tokens and max_completion_tokens, temperature, top_p, top_k, min_p, stop, seed, frequency_penalty, presence_penalty, repetition_penalty, logprobs, top_logprobs, tools, tool_choice, response_format, reasoning_effort, and enable_thinking (chain of thought on thinking models, returned separately as reasoning_content). Silicon Network adds models and quality (see Models) and metadata.project (see Project tags). Fields with no value are omitted rather than null: no tool_calls key unless the model called a tool, no reasoning_content unless it thought.
Sampling parameter bounds
Sampler settings must be finite numbers in range, or the request is refused with 400 before any engine sees it. The same bounds apply on the Responses, Claude and Ollama surfaces (inside Ollama's options too) and to batch requests on those endpoints. A null is ignored.
| Parameter | Range |
|---|---|
| temperature | 0 to 2 |
| top_p, min_p | 0 to 1 |
| top_k | an integer from -1 to 1000 (0 and -1 mean off) |
| repetition_penalty, repeat_penalty | 0.01 to 10 |
| presence_penalty, frequency_penalty | -2 to 2 |
{"detail": "invalid sampling parameter: 'temperature' must be between 0 and 2"}The other reasons read 'top_k' must be an integer, 'top_p' must be a number and 'min_p' must be a finite number.
Parameters that are refused
logit_bias (no per-token biasing), n above 1 (one completion per request), and parallel_tool_calls: false (calls cannot be forced to run one at a time). Nothing below the gateway reads them, so a request that depends on one is refused rather than silently doing something else:
{"detail": "unsupported parameter: 'n': only one completion is generated per request. Remove and retry."}Sending the value that matches what already happens is fine: n: 1, an empty logit_bias, parallel_tool_calls: true.
Output length
A requested output length above 8,192 tokens is clamped down to 8,192, without an error, so one call cannot run up an unbounded charge. When you set no limit the gateway sends 8,192 for you. The same applies to max_output_tokens on Responses and options.num_predict on Ollama.
Streaming
Set "stream": true and the answer arrives as server-sent events: chat.completion.chunk objects with an incremental delta, then data: [DONE]. Reasoning streams as delta.reasoning_content and tool calls as delta.tool_calls[].function.arguments fragments. Responses streams its own events (response.output_text.delta, with usage on response.completed), Claude Messages streams Anthropic events (message_start, content_block_delta, message_stop), and the Ollama routes stream newline-delimited JSON, by default, as Ollama does.
curl -N https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "llama-3.2-3b", "stream": true,
"messages": [{"role": "user", "content": "Count to five."}]}'
: keep-alive
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"1"}}]}
...
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
"usage":{"prompt_tokens":14,"completion_tokens":9,"total_tokens":23,"prompt_tokens_details":{"cached_tokens":0}}}
: generation_stats {...}
data: [DONE]from siliconnetwork import SiliconNetwork
client = SiliconNetwork()
stream = client.chat.completions.stream(
model="llama-3.2-3b",
messages=[{"role": "user", "content": "Count to five."}],
)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
print()
print(stream.meta.ttft_ms, "ms to first token")const stream = await client.chat.completions.create({
model: "llama-3.2-3b",
messages: [{ role: "user", content: "Count to five." }],
stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");What to expect
Lines that start with a colon are SSE comments: the engine opens every stream with : keep-alive and may send : prefill_progress while it reads a long prompt, and : generation_stats at the end. Every client ignores them. The last chunk of a managed stream, the one with the finish_reason, always carries usage (the gateway turns stream_options.include_usage on, because it bills from it), so read cached tokens from the body there; Silicon-Cached-Tokens is a non-streaming header. Silicon-TTFT-Ms is set on streams. With a receipt requested, one more event follows [DONE] (see Verifiable receipts).
Closing the connection cancels generation on the node; an abandoned stream is billed a conservative floor estimated from what was sent. A fallback chain can only move to the next model before the first byte reaches you. If the serving node fails mid-answer the stream ends without [DONE] or a usage chunk, and that request is not billed: treat a stream without [DONE] as failed and retry it.
Structured outputs and tools
response_format is enforced with grammar-constrained decoding on the serving node, so the output is valid JSON (json_object) or matches your schema (json_schema with strict: true) at any temperature, not just when the model feels like it. Supported schema keywords: object, array, string, number, integer, boolean, null, enum, const, required, additionalProperties: false, nested objects and arrays, and anyOf of scalars. An unsupported keyword is refused with a 400 that names it, before any tokens are generated. The same machinery serves the Responses API (text.format), Claude Messages (tool_choice of type tool or any), and Ollama's format ("json" or a schema).
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
resp = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Extract: Ada Lovelace, born 1815 in London."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"born": {"type": "integer"},
"city": {"type": "string"},
},
"required": ["name", "born", "city"],
"additionalProperties": False,
},
},
},
)
print(resp.choices[0].message.content) # {"name": "Ada Lovelace", "born": 1815, "city": "London"}Tool calls
Pass tools and the model decides whether to call one; calls come back in tool_calls with finish_reason: "tool_calls". tool_choice: "required" makes it call some tool, and {"type": "function", "function": {"name": ...}} forces that one, with the arguments constrained to the tool's parameter schema. Forced calls work on the Llama, Qwen, GLM, Kimi, MiniMax, Gemma, gpt-oss and DeepSeek families; a model whose tool format cannot be constrained answers 400 instead of pretending.
import { SiliconNetwork } from "@siliconnetwork/sdk";
const client = new SiliconNetwork();
const out = await client.chat.completions.create({
model: "llama-3.3-70b",
messages: [{ role: "user", content: "What is the weather in Oslo?" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
},
},
],
tool_choice: { type: "function", function: { name: "get_weather" } },
});
const call = out.choices[0].message.tool_calls?.[0];
console.log(call?.function.name, JSON.parse(call?.function.arguments ?? "{}"));Send the result back as a {"role": "tool", "tool_call_id": ..., "content": ...} message and call again, exactly as with OpenAI.
Logprobs
Set logprobs: true and every generated token comes back with its log-probability and the most likely alternatives at that position, streaming and non-streaming, on every text model. Useful for classification, confidence thresholds, and eval harnesses that score tokens rather than parse text.
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
resp = client.chat.completions.create(
model="llama-3.2-3b",
messages=[{"role": "user", "content": "Answer with one word: is 97 prime?"}],
max_tokens=1,
logprobs=True,
top_logprobs=5,
)
first = resp.choices[0].logprobs.content[0]
print(first.token, first.logprob)
for alt in first.top_logprobs:
print(alt.token, alt.logprob)top_logprobs is passed through as you send it; the only bound is the model vocabulary, and the OpenAI limit of 20 is not enforced here. Omit it while logprobs is true and you get five alternatives per token. Each entry carries token and logprob, and every alternative also carries its UTF-8 bytes. When you stream, each chunk carries the logprobs for the token in that chunk.
Vision
qwen3-vl-4b accepts images in the standard OpenAI content-parts shape. Pass a data URL, or an https:// URL that the serving node can reach; the node fetches the URL itself, so a link only your machine can see will fail. Several images in one message are fine.
import base64
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
with open("chart.png", "rb") as fh:
data_url = "data:image/png;base64," + base64.b64encode(fh.read()).decode()
resp = client.chat.completions.create(
model="qwen3-vl-4b",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "What does this chart show?"},
{"type": "image_url", "image_url": {"url": data_url}},
],
}
],
)
print(resp.choices[0].message.content)Responses, Claude Messages and Ollama
Same models, same key, four request shapes: OpenAI Chat Completions at /v1/chat/completions, OpenAI Responses at /v1/responses, Anthropic Messages at /v1/messages, and Ollama at /ollama/api/chat and /ollama/api/generate. The bodies are the ones those APIs define, and every surface gets the same aliases, fallbacks, bounds, refusals and output clamp.
OpenAI Responses
input, instructions, tools, stream, reasoning, max_output_tokens, and text.format for structured output. Cached tokens are in usage.input_tokens_details.cached_tokens.
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
resp = client.responses.create(
model="gpt-oss-120b",
instructions="Answer in one sentence.",
input="Why is the sky blue?",
)
print(resp.output_text)Claude Messages
model, messages, system, max_tokens, stream, tools, tool_choice and thinking. The body is drop-in for the Anthropic SDK; the credential is not. Those clients send x-api-key, which the gateway does not read, so set the bearer header yourself.
from anthropic import Anthropic
client = Anthropic(
base_url="https://api.siliconnetwork.io",
api_key="unused", # required by the client; Silicon Network reads the header below
default_headers={"Authorization": "Bearer sn_live_..."},
)
msg = client.messages.create(
model="glm-4.7",
max_tokens=512,
messages=[{"role": "user", "content": "One sentence on trustless inference."}],
)
print(msg.content[0].text)curl https://api.siliconnetwork.io/v1/messages \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "llama-3.2-3b", "max_tokens": 256,
"messages": [{"role": "user", "content": "Hello"}]}'Ollama
For Ollama-native clients the base URL is https://api.siliconnetwork.io/ollama. Discovery (the root probe, /api/version, /api/tags, /api/ps) needs no key and lists only the models loaded right now, so a client's model picker never offers one that will not serve. Chat and generate need your key, so use a client that lets you set an Authorization header. They stream newline-delimited JSON unless you send "stream": false.
curl https://api.siliconnetwork.io/ollama/api/chat \
-H "Authorization: Bearer $SILICON_API_KEY" \
-d '{"model": "llama-3.2-3b", "stream": false,
"messages": [{"role": "user", "content": "Hello"}],
"options": {"temperature": 0.2}}'POST /ollama/api/show and the other Ollama management routes are not served by the managed API.
Embeddings, rerank, audio and images
These models have short aliases like the chat models, resolved the same way: case-insensitive, any full id passes through, and a key scoped to an alias may call it by its full id too. Each serves only on its own route. GET /v1/models shows which are loaded now, and GET /v1/models?all=true gives each catalog card's kind. Where a model comes in several quantizations, <alias>@<label> picks one (the variants table under Models, aliases and fallbacks).
| Alias | Kind | Route | Engine id |
|---|---|---|---|
bge-m3 | Embeddings | /v1/embeddings | mlx-community/bge-m3-mlx-fp16 |
qwen3-embedding-0.6b | Embeddings | /v1/embeddings | mlx-community/Qwen3-Embedding-0.6B-8bit |
all-minilm-l6-v2 | Embeddings | /v1/embeddings | mlx-community/all-MiniLM-L6-v2-bf16 |
qwen3-reranker-0.6b | Rerank | /v1/rerank | mlx-community/Qwen3-Reranker-0.6B-mxfp8 |
qwen3-reranker-4b | Rerank | /v1/rerank | mlx-community/Qwen3-Reranker-4B-mxfp8 |
whisper-large-v3-turbo | Transcription | /v1/audio/transcriptions | mlx-community/whisper-large-v3-turbo |
whisper-small | Transcription | /v1/audio/transcriptions | mlx-community/whisper-small-mlx |
kokoro-82m | Speech | /v1/audio/speech | mlx-community/Kokoro-82M-bf16 |
flux-schnell | Image | /v1/images/generations | exolabs/FLUX.1-schnell-4bit |
flux-dev | Image | /v1/images/generations | exolabs/FLUX.1-dev-4bit |
flux-krea-dev | Image | /v1/images/generations | exolabs/FLUX.1-Krea-dev-4bit |
flux-kontext-dev | Image edit | /v1/images/edits | exolabs/FLUX.1-Kontext-dev-4bit |
qwen-image | Image | /v1/images/generations | exolabs/Qwen-Image-4bit |
qwen-image-edit | Image edit | /v1/images/edits | exolabs/Qwen-Image-Edit-2509-4bit |
Embeddings
POST /v1/embeddings, OpenAI shape: input as a string, a list of strings, or token ids; encoding_format float or base64; dimensions to truncate Matryoshka models. Billed per prompt token. Aliases: bge-m3, qwen3-embedding-0.6b, all-minilm-l6-v2.
from siliconnetwork import SiliconNetwork
client = SiliconNetwork()
out = client.embeddings.create(
model="bge-m3",
input=["The quick brown fox", "jumps over the lazy dog"],
)
print(len(out.data), len(out.data[0].embedding))Rerank
POST /v1/rerank in the Cohere and Jina shape: query, documents (strings or {"text": ...}), optional top_n and return_documents. Answers {model, results: [{index, relevance_score, document?}], usage}. Billed per token.
const ranked = await client.rerank.create({
model: "qwen3-reranker-0.6b",
query: "How do I rotate an API key?",
documents: ["Keys are shown once.", "Revoke a key and create a new one.", "Pricing is per token."],
top_n: 2,
});
for (const r of ranked.results) console.log(r.index, r.relevance_score);Speech to text
POST /v1/audio/transcriptions, multipart: file, model, optional language, and response_format json, text, verbose_json, srt or vtt. Whisper on MLX, billed per second of audio. Uploads are capped at 25 MB.
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
with open("meeting.m4a", "rb") as fh:
text = client.audio.transcriptions.create(model="whisper-large-v3-turbo", file=fh)
print(text.text)Text to speech
POST /v1/audio/speech: model, input (up to 4,096 characters), optional voice and speed (0.25 to 4), and response_format wav (the default) or pcm; mp3 only where the node has an encoder, else a 400 naming the formats it has. Kokoro on MLX, billed per input character.
curl https://api.siliconnetwork.io/v1/audio/speech \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "kokoro-82m", "input": "Hello from Silicon Network.", "response_format": "wav"}' \
-o hello.wavImages
POST /v1/images/generations (JSON) and POST /v1/images/edits (multipart), FLUX and Qwen image models, billed per image. Image models serve only where an operator has enabled and loaded one, so check GET /v1/models for one first. Results come back as b64_json by default.
import base64
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
img = client.images.generate(
model="flux-schnell",
prompt="a lighthouse on a dusk coastline, flat illustration",
size="1024x1024",
)
open("lighthouse.png", "wb").write(base64.b64decode(img.data[0].b64_json))Files and batches
Work that can wait runs as a batch at half price. The OpenAI flow works unchanged: upload a JSONL file with POST /v1/files, create the batch, poll it, and download the output file.
{"custom_id": "q1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "llama-3.2-3b", "messages": [{"role": "user", "content": "Capital of France?"}]}}
{"custom_id": "q2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "llama-3.2-3b", "messages": [{"role": "user", "content": "Capital of Japan?"}]}}import time
from openai import OpenAI
client = OpenAI(base_url="https://api.siliconnetwork.io/v1", api_key="sn_live_...")
f = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")
batch = client.batches.create(
input_file_id=f.id,
endpoint="/v1/chat/completions",
completion_window="24h",
metadata={"job": "capitals"},
)
while batch.status not in ("completed", "failed", "cancelled", "expired"):
time.sleep(30)
batch = client.batches.retrieve(batch.id)
if batch.output_file_id:
for line in client.files.content(batch.output_file_id).text.splitlines():
print(line) # {"id", "custom_id", "response": {"status_code", "body"}, "error"}curl https://api.siliconnetwork.io/v1/files -H "Authorization: Bearer $SILICON_API_KEY" \
-F purpose=batch -F file=@requests.jsonl
curl https://api.siliconnetwork.io/v1/batches -H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input_file_id": "file-...", "endpoint": "/v1/chat/completions", "completion_window": "24h"}'
curl https://api.siliconnetwork.io/v1/batches/batch_... -H "Authorization: Bearer $SILICON_API_KEY"Rules
| What | Rule |
|---|---|
| endpoint | /v1/chat/completions or /v1/embeddings, the same url on every line. |
| completion_window | 24h. |
| Size | Up to 10,000 requests and 32 MB per batch or file; an organization's live uploads at most 256 MB together. |
| Each line | Valid JSON, a unique custom_id (at most 200 characters), method POST, and an object body whose model your key may use. A bad line is a 400 naming its line number (403 for a model outside the key's scope). |
| Inline form | Instead of input_file_id, send requests: [{custom_id, method, url, body}] in the create body (never both). |
| Statuses | validating, in_progress, completed, failed, cancelling, cancelled, expired. |
| Results | GET /v1/files/{output_file_id}/content, or GET /v1/batches/{id}/output and /errors. The error file holds only the failed requests. |
| Price | Half the interactive price. Batch requests run when interactive traffic allows and use the key's rate limit with a reserve kept for interactive calls. |
| Ownership | Files belong to the organization: any of its keys can use them, and no other organization can see that they exist (404). |
An upload may set expires_after[anchor]=created_at and expires_after[seconds] (3,600 to 2,592,000); otherwise it is kept 30 days. Send X-Silicon-No-Retention: true with the upload and it is deleted as soon as a batch is created from it, or after an hour if none is. Batch requests never carry receipts. What is stored, and for how long, is under Privacy.
LoRA adapters
Serve your own fine-tune on top of a base model without hosting it: register a LoRA adapter, deploy it to your nodes, and call it by appending its id to the base model.
Register
Adapters are managed on the account API with your signed-in session (owners and admins write, every member reads); there is no dashboard screen for them yet. Register from the Hugging Face Hub, or upload the files:
{
"name": "support-tone",
"base_model": "mlx-community/Llama-3.2-3B-Instruct-4bit",
"hf_repo": "your-org/support-tone-lora",
"revision": "main"
}
# or POST /v1/adapters/upload, multipart:
# name, base_model, config=@adapter_config.json, weights=@adapters.safetensorsThe answer carries the id (adp_ and 32 hex characters), the callable model string, and the rank, scale and target modules read from the files. Register with the base model's full engine id.
| Rule | Limit |
|---|---|
| Format | mlx_lm LoRA: adapter_config.json plus adapters.safetensors. PEFT, DoRA and pickle files (.bin, .pt, .npz) are refused. |
| Rank and scale | Rank 1 to 64; scale (or alpha over rank) above 0 and at most 64. |
| Size | Config at most 64 KB; weights at most 256 MB from the Hub or 64 MB uploaded. |
| Per organization | 50 live adapters and 256 MB of uploads. |
| Base model | If the config names the model it was trained on, it must match base_model. |
| Not yet | Sharded (multi-node) base models, mixture-of-experts layers, embedding targets. |
Deploy
POST /v1/adapters/{id}/deploy installs it on your organization's ready single-node deployments of the base model, in the background (each install goes pending to ready or failed; GET /v1/adapters/{id} shows them). With no such deployment it answers 409, unless you send {"place_base": true} to place the base model first. PATCH renames it or sets visibility to public so other organizations can deploy and call it (never change it); DELETE uninstalls it and deletes the uploaded files.
Call
Put <base>:<adapter id> in model. The base may be its alias; the gateway resolves it and keeps the suffix.
curl https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "llama-3.2-3b:adp_0123456789abcdef0123456789abcdef",
"messages": [{"role": "user", "content": "My order is late."}]}'A key scoped to the base model may call its adapters. Adapter requests serve only on nodes with the adapter installed, never on the platform's own engine, and are text generation only. They cost the base model's price plus the adapter's surcharge (zero unless the platform sets one), and the usage export names the adapter_id.
| Status | Meaning |
|---|---|
| 404 | adapter 'adp_...' not found: unknown, or another organization's private adapter. |
| 400 | The adapter is for a different base model; the message says how to call it. |
| 503 | Not deployed on any online node (a fallback chain moves on), or the adapter registry is unreachable (retry). |
The uploaded config and weights are stored until you delete the adapter and are copied to each node it is deployed on, where that node's operator can read them.
Project tags and the cost explorer
Label a request with a project and the dashboard's cost explorer breaks spend down by it. Send "metadata": {"project": "search-api"} in the body of any JSON surface, or a Silicon-Project: search-api header on any surface (the only way on multipart audio and image uploads; X-Silicon-Project works too).
curl https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-H "Silicon-Project: search-api" \
-d '{"model": "fast", "messages": [{"role": "user", "content": "Hello"}]}'from siliconnetwork import SiliconNetwork
client = SiliconNetwork()
client.chat.completions.create(
model="fast",
messages=[{"role": "user", "content": "Hello"}],
metadata={"project": "search-api"},
)The tag is lowercased and must be 1 to 64 characters of letters, digits, ., _ and -, starting with a letter or digit. Anything else, or a body and header that disagree, is a 400 before the request is admitted. The gateway removes the tag before forwarding (other metadata keys pass through) and records it on the usage event. It is a grouping label: do not put names, emails or user ids in it.
The cost explorer
Owners, admins and billing members see spend, tokens and requests grouped by one or two of day, key, model and project, over 7, 30 or 90 days or a custom range (up to 366 days, UTC days), filtered by key, model or project (- selects untagged traffic), and can export the same aggregate as CSV. The CSV has the dimension columns (a key expands to its id, name and prefix) and then requests, errors, prompt_tokens, completion_tokens, cached_tokens, tokens, units and cost_micros (micro-USD).
Prefix caching
The engine keeps the attention state of recent prompts. When a new request starts with a prefix it has already processed (a long system prompt, a document, the earlier turns of a conversation), those prompt tokens are served from cache: the answer starts sooner and those tokens bill at a quarter of the prompt rate unless a model sets its own cached price.
You do not have to do anything to get it, but you can help it: put the parts that repeat first (system prompt, tools, documents) and the parts that change last. The gateway also steers requests from your organization that share a long prefix (about 1 KB or more) to the node that served it within the last ten minutes, as a preference, when that node is eligible and not busy. The cache is partitioned per organization, so a hit only ever comes from your own earlier requests, and a request sent with X-Silicon-No-Retention: true is never cached.
| Surface | Field |
|---|---|
| Chat completions | usage.prompt_tokens_details.cached_tokens |
| Responses | usage.input_tokens_details.cached_tokens |
| Non-streaming responses | the Silicon-Cached-Tokens header, when above zero |
| SDKs | meta.cached_tokens (Python), meta.cachedTokens (TypeScript) |
Claude Messages and Ollama responses have no cached-token field, so they report none, though the cache still works for them.
Headers, rate limits and spend caps
| Header | Meaning |
|---|---|
| Silicon-Request-Id, x-request-id | The same id under both names, minted by the gateway and forwarded to the serving node. Quote it in support requests and disputes. Not set on a 401, which is answered before an id exists. |
| Silicon-Model | The full engine id that served the request (aliases, variants and casing resolved). Successes only. |
| Silicon-Fallback-From | The first candidate's engine id, when a later model in a fallback chain answered. |
| Silicon-Node | The registered node that served. Absent when the platform's own engine served. |
| Silicon-TTFT-Ms | Time to first token, on streams. |
| Silicon-Cached-Tokens | Prompt tokens served from the prefix cache, on non-streaming responses. |
| Silicon-Attestation | sha256=<model hash>;operator=<org>;egress=locked|open, when the serving node has a current attestation. |
| Silicon-Receipt | The signed receipt (base64url JSON), when you asked for one. On a stream it is a final event instead. |
| x-ratelimit-limit, -remaining, -reset | The key's requests per minute, what is left, and seconds until the budget is whole. Omitted when no limit was enforced. |
| Retry-After | Seconds to wait, on 429 and on a draining 503. |
Request headers you can send
| Header | Effect |
|---|---|
| Authorization: Bearer | Your API key. The only credential the gateway reads. |
| Silicon-Receipt | 1 (or true, yes, on) asks for a signed receipt; 0 (or false, no, off) skips one a key would otherwise get. |
| X-Silicon-No-Retention | true: no verification sampling, and the engine keeps nothing of the request (see Privacy). |
| Silicon-Project | A project tag for the cost explorer (X-Silicon-Project also works). |
The gateway mints its own request id; it does not read one from the caller. Keep your own correlation id on your side, next to the Silicon-Request-Id you get back.
Rate limits
Each key has a token bucket of 60 requests per minute by default, adjustable per key (rate_limit_per_min). The bucket starts full, so a whole minute's budget can be spent in a burst, and refills continuously. Over the budget, the answer is 429 rate limit exceeded with Retry-After. Inference routes, files, batches and a keyed GET /v1/models all draw from it; batch work leaves a quarter of it for interactive calls.
Spend caps
Set a monthly cap for the organization on the Billing page, and optionally a cap per key (spend_cap_micros, over a rolling 30 days). Once reached, requests answer 402 with monthly spend cap reached or API key spend cap reached; an empty balance answers insufficient credits. Webhooks warn you first: spend.threshold at 50, 80 and 100 percent of the organization cap, and credits.low when the balance drops below your threshold ($5 by default). Caps and balance changes reach the gateway within 15 seconds.
Errors
An error the gateway raises is JSON with a detail string. Two have an OpenAI-style error object as well: a model that is not loaded, and a gateway that is draining for a deploy. An error from the engine is passed through as the engine wrote it. Every error carries the request id header except a 401.
{"error": {"message": "No engine is currently serving model 'llama-3.3-70b' (full id 'mlx-community/Llama-3.3-70B-Instruct-4bit'). ...",
"type": "not_found", "code": "model_not_available", "param": "model",
"available_models": ["mlx-community/Llama-3.2-3B-Instruct-4bit"]}}| Status | Meaning |
|---|---|
| 400 | A parameter the gateway cannot honor, a sampler setting out of range, a bad model list, variant, quality or project tag, a prompt too long for the model, an unsupported schema keyword, or a malformed body. The detail names it. |
| 401 | missing bearer API key, or invalid or revoked API key (expired keys included). |
| 402 | insufficient credits, monthly spend cap reached, or API key spend cap reached. |
| 403 | The key's scope does not include the model; the client address is outside the key's IP allowlist; or the key is suspended. |
| 404 | model_not_available (with available_models), an unknown adapter, or an unknown file or batch. |
| 413 | A body over the limit: 64 MB for JSON, 25 MB for uploads, 32 MB for a batch file. |
| 429 | rate limit exceeded. Honor Retry-After. |
| 502 | The engine was unreachable, timed out, dropped the connection mid-response, or answered too large. Not billed; safe to retry. |
| 503 | No eligible node for a pinned key, no node with the context the request needs, an adapter not deployed, the control plane unreachable for a key it has not seen recently, or a draining instance (code draining, Retry-After: 1). |
With a fallback chain, 404, 408, 429, every 5xx and the too-long-prompt 400 move on to the next model instead of being returned (see Models). Retry 429, 502 and 503 with backoff; the SDKs do it for you on non-streaming calls.
Privacy and retention
Requests run on an operator's machine, so that node necessarily processes your prompt, and unless you say otherwise it also retains it. Here is exactly who stores what. The privacy policy says the same in plain terms.
| Where | What is kept |
|---|---|
| Serving node | Prompts and completions, in the engine's event log on the operator's disk, until the log rotates away; the engine also serves that log on its own API (open unless the operator sets a node key). Not with X-Silicon-No-Retention: true. |
| Gateway | Nothing persisted. The body is held in memory to check your key, resolve the model, forward the request and meter the answer. |
| Usage record | Organization, key, model id, token counts, latency, status, region, cost, project tag and timestamp. No field for prompt or completion text. |
| Batches and files | Uploaded batch files and a batch's requests and responses, for a bounded time (below). |
| Chat app | Conversations you save, encrypted, for the period you choose (below). Temporary chats: nothing. |
| LoRA adapters | The config and weights you upload, until you delete the adapter, and a copy on each node it is deployed on. |
| Key protection | Client addresses seen per key, only as keyed hashes, for 30 days, to detect anomalies. |
| Receipts | Not stored. You get the only copy. |
Batches and files
A batch input file is deleted at its expiry (30 days by default, or the expires_after you set) or when you delete it. A batch's requests and responses are deleted 30 days after it finishes, or as soon as you delete its output and error files. An upload sent with X-Silicon-No-Retention: true is deleted as soon as a batch is created from it, or an hour after upload if none is. After that, a batch keeps its status, counts, timestamps, metadata and each request's custom_id, and no bodies.
The chat app
A conversation saved in /chat (title, system prompt, and each message with its attachments) is encrypted at rest and readable only by you, in the organization you started it in. It is deleted 1, 7, 30, 90 or 365 days after its last message, as you choose (30 by default), or when you delete it, delete all your chats, or delete your account. Model ids, token counts, cost and timestamps are stored in the clear next to it. A temporary chat stores nothing on our side and sends X-Silicon-No-Retention: true with every request. Chat requests are ordinary metered requests on a short lived key the web server holds for you; each person's chat spend is capped at $5 per rolling 24 hours by default.
What can widen the gateway's view
Two features are off unless the platform turns them on: content moderation, which sends prompt text to a moderation service before serving, and verification sampling, which re-runs a sampled request on the platform's own engine to check the node that served it.
X-Silicon-No-Retention
Send X-Silicon-No-Retention: true and the gateway skips verification sampling for that request, so your prompt is never re-sent to a second engine, and forwards the header to the serving node. The engine honors it on every surface (chat, completions, responses, embeddings, rerank, speech, transcription and images): it redacts your prompt and the output from its event log, its API logs and its event feed, and never writes the request to its prefix cache. A generated image is held in memory only, for up to an hour.
curl https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Silicon-No-Retention: true" \
-d '{"model": "fast", "messages": [{"role": "user", "content": "Draft a reply to this customer email: ..."}]}'This is enforced by the engine software on the node, so it binds a node that runs that software unmodified. Only operators the platform has approved can register a node into the routable pool. For confidentiality that does not depend on someone else's machine, pin your key to approved operators and attested nodes (the attestation comes back in Silicon-Attestation), or run your own node, where your machine is the whole data path.
Verifiable receipts
Send Silicon-Receipt: 1 (or turn receipts on for a key) and a successful response carries a signed receipt: the serving node signs the hashes of the request it received and the answer it produced, with the key it attests its model with, and the gateway countersigns which node served it, its operator, region and attestation, and the hashes of what you sent and got. Hashes and counts only, never content. It arrives as the Silicon-Receipt header, or on a stream as a final silicon.receipt event after [DONE].
import httpx
from siliconnetwork import fetch_receipt_keys, verify_receipt
body = b'{"model": "fast", "messages": [{"role": "user", "content": "Hi"}]}'
resp = httpx.post(
"https://api.siliconnetwork.io/v1/chat/completions",
content=body,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
"Silicon-Receipt": "1"},
)
result = verify_receipt(
resp.headers["silicon-receipt"], fetch_receipt_keys(),
request_body=body, response_body=resp.content,
)
print(result.valid, result.bound) # bound: the receipt is about these exact bytesvalid means every signature and cross-check passed; bound means, in addition, that the hashes match the bytes you passed in, which is what you want when a receipt is your proof of an exchange. Platform keys are published at GET /v1/receipts/keys; a retired key stays listed with the time it stopped signing (not_after), so old receipts still verify and a receipt it signed later does not. Read the receipts guide for the format, streams, the command line, and exactly what a receipt does and does not prove.
Pricing
You pay per token for the model you actually call, metered from real requests. Prompt tokens served from the prefix cache bill at a quarter of the prompt rate, batch requests at half price, requests in the long-context tier at 1.5 times, and higher-precision variants scale with bit width. Audio bills per second, speech per character, images per image. A model with no published row bills at $0.50 per million prompt tokens and $1.50 per million completion tokens. There are no seats and no idle clusters to rent, so the meter only runs when you do.
Per-model rates are on the pricing page and the public GET /v1/billing/pricing and /v1/billing/pricing/policy routes of the account API.
Billing
Billing is prepaid credit in US dollars, held in micro-USD. Top up with stablecoins or a card on the Billing page (owners, admins and billing members) under Add balance. One USDT or USDC buys one dollar of credit. A top-up fee, when one applies, is shown before you pay; you send the gross amount and are credited the net.
Stablecoins
| Network | Token | Credited after |
|---|---|---|
| Arbitrum One | USDT, USDC | the block is finalized, usually 15 to 20 minutes |
| Base | USDC | the block is finalized, usually 15 to 20 minutes |
| Tron | USDT (TRC-20) | the block is solidified, about a minute |
First bind the wallet you will pay from, then send the token to the receive address the dashboard shows for that network, from that wallet. Send only the listed token on the listed network: anything else cannot be credited. A transfer is matched to your organization by the sending address, so it must come from the bound wallet; a withdrawal straight from an exchange cannot be matched, so withdraw to your own wallet first. The dashboard updates by itself when the transfer is final. Each transfer is credited once, and the control plane checks it on chain before crediting. A single top-up may be up to $100,000.
Binding a wallet
Binding proves the wallet is yours with a signature; it authorizes no transaction and moves no funds. One EVM wallet covers Arbitrum and Base (signed with personal_sign), and a Tron wallet is bound separately (signed with TronLink's signMessageV2). An address can pay for only one organization; binding again replaces the old wallet. The message you sign names the site, your organization and the wallet family, and expires after 15 minutes:
www.siliconnetwork.io wants you to link your wallet to a Silicon Network org.
Only sign this on www.siliconnetwork.io. Payments from this wallet will credit the org named below. Signing authorizes no transaction and moves no funds.
URI: https://www.siliconnetwork.io
Org: <your org name> (<org id>)
Wallet: EVM (Ethereum compatible chains)
Nonce: <32 hex characters>
Issued At: 2026-10-03T12:00:00Z
Expiration Time: 2026-10-03T12:15:00ZThe site in the message is the one you are signing on. Do not sign a binding message that names another site or an organization you do not recognize. Renaming the organization invalidates an unsigned challenge.
Card
Pay with card signs you in to Privy with your email, which creates an embedded wallet linked to your organization. The card provider buys USDC on Base into that wallet, and the dashboard sends it to the receive address with the network fee sponsored, so it is credited like any other USDC top-up, once final. The provider applies its own limits and may add its own fee at checkout; USDC left in the embedded wallet can be sent to your balance later.
Webhooks and the API
credits.topup fires with the gross, fee and credited amounts and the transaction hash. The account API answers your balance, monthly spend, cap and recent transactions at GET /v1/billing/credits, and the accepted networks, tokens, receive addresses and bound wallets at GET /v1/billing/status.
Status
The status page probes the inference API, the control plane and the relay on every visit and refreshes every 30 seconds. Below the live checks it shows 90 days of uptime per component (from checks every minute; stablecoin top-ups are a component too) and the incident history. An incident opens automatically after five minutes of a component down or degraded, and resolves after five minutes of recovery; updates are posted as it progresses.
| URL | Format |
|---|---|
| /status/rss.xml | RSS 2.0, the last 50 incidents. |
| /status/feed.json | JSON Feed 1.1, the same items, tagged with status, impact and components. |
An item's id changes with every update, so a feed reader shows each update as new. The control plane serves the same data as JSON at GET /v1/status and GET /v1/status/incidents (public, cached for 30 seconds). For your own probes, GET https://api.siliconnetwork.io/health answers whether inference can be served right now (200 with the number of warm models, or 503 degraded or unhealthy) and /livez whether the process is up.
SDKs
Two first-party clients, thin and typed, with no runtime dependencies beyond httpx and pydantic (Python) or fetch (TypeScript). The official openai packages also work with the base URL set.
pip install siliconnetwork # Python 3.10+
pip install "siliconnetwork[receipts]" # adds signature checks and silicon-verify-receipt
npm install @siliconnetwork/sdk # Node 20+ (receipt checks use WebCrypto Ed25519), Bun, Deno, browsersIf a package is not on its registry yet, install it from the repository: pip install "git+https://github.com/ekempinski/siliconnetwork#subdirectory=sdks/python", or clone it and npm install ./sdks/typescript.
| Call | Python / TypeScript |
|---|---|
| Chat | chat.completions.create(...), chat.completions.stream(...) |
| Models | models.list() |
| Embeddings | embeddings.create(model, input, ...) |
| Rerank | rerank.create(model, query, documents, ...) |
| Any JSON route | request(method, path, body): Responses, Messages, Ollama, speech, image generation, batches. Multipart uploads (files, transcription, image edits) need the openai package or a plain HTTP client. |
| Receipts | fetch_receipt_keys / fetchReceiptKeys, verify_receipt / verifyReceipt, split_stream_receipt / splitStreamReceipt |
Every result carries the gateway headers as meta: request id, serving node, resolved model, cached tokens, time to first token, the rate-limit budget, and the receipt when there is one. Failures raise SiliconError with status, code, the request id and retry_after, plus is_rate_limited and is_out_of_credits. Non-streaming calls retry 429 and 5xx twice by default, honoring Retry-After. Both read SILICON_API_KEY, and SILICON_BASE_URL as an origin without /v1 (default https://api.siliconnetwork.io).
Python
from siliconnetwork import SiliconNetwork, SiliconError
client = SiliconNetwork(
default_headers={"Silicon-Project": "checkout", "X-Silicon-No-Retention": "true"},
)
try:
out = client.chat.completions.create(
model="fast",
messages=[{"role": "user", "content": "Hello"}],
)
print(out.choices[0].message.content, out.meta.request_id, out.meta.cached_tokens)
except SiliconError as err:
print(err.status, err.code, err.request_id)
# routes without a typed method
data, meta = client.request("POST", "/v1/responses", {"model": "fast", "input": "Hello"})AsyncSiliconNetwork has the same surface with await. Constructor options: api_key, base_url, timeout (seconds, 120), max_retries (2), default_headers.
TypeScript
import { SiliconNetwork, SiliconError } from "@siliconnetwork/sdk";
const client = new SiliconNetwork({ defaultHeaders: { "Silicon-Project": "checkout" } });
try {
const out = await client.chat.completions.create(
{ model: "fast", messages: [{ role: "user", content: "Hello" }] },
{ headers: { "X-Silicon-No-Retention": "true" }, timeout: 30_000 },
);
console.log(out.choices[0].message.content, out.meta.requestId, out.meta.cachedTokens);
} catch (err) {
if (err instanceof SiliconError) console.log(err.status, err.code, err.requestId);
}Options: apiKey, baseURL, timeout (milliseconds, 120,000), maxRetries (2), defaultHeaders, and your own fetch. Every call takes { signal, headers, timeout } as its second argument.
Receipts in the SDKs
The TypeScript client puts the raw Silicon-Receipt header on meta.receipt; the Python client puts the decoded receipt there, and on a non-streaming call with a receipt adds meta.request_sha256 and meta.response_sha256 of the exact bytes, which verify_receipt takes in place of the bodies. The stream iterators skip the receipt event, so to verify a streamed answer read the raw bytes and split it off with split_stream_receipt. Both verifiers return valid, bound, the list of checks and the errors, honor retired key windows, and require the gateway signature unless you pass require_gateway=False (requireGateway: false). See Verifiable receipts.
from siliconnetwork import SiliconNetwork, fetch_receipt_keys, verify_receipt
client = SiliconNetwork(default_headers={"Silicon-Receipt": "1"})
out = client.chat.completions.create(model="fast", messages=[{"role": "user", "content": "Hi"}])
result = verify_receipt(
out.meta.receipt, fetch_receipt_keys(),
request_sha256=out.meta.request_sha256, response_sha256=out.meta.response_sha256,
)
print(result.valid, result.bound, result.errors)Integrations
Every framework that speaks /v1/chat/completions works with a base URL swap. These packages and configs add the catalog, key lookup and Silicon Network extras on top. Note that the framework packages read SILICON_BASE_URL with /v1, unlike the SDKs.
| Framework | Install |
|---|---|
| LangChain | pip install langchain-siliconnetwork |
| LlamaIndex | pip install llama-index-llms-siliconnetwork |
| Vercel AI SDK | npm install @siliconnetwork/ai-provider ai |
| LiteLLM | pip install litellm-siliconnetwork |
| n8n | n8n-nodes-siliconnetwork (community node) |
| Continue, Open WebUI, Zapier | config and app files in the repository |
LangChain
from langchain_siliconnetwork import ChatSiliconNetwork, SiliconNetworkEmbeddings
llm = ChatSiliconNetwork(model="llama-3.2-3b") # reads SILICON_API_KEY
print(llm.invoke("Hello").content)
emb = SiliconNetworkEmbeddings(model="bge-m3")
print(len(emb.embed_query("Hello")))ChatSiliconNetwork is a ChatOpenAI that checks the model against the catalog; pass validate_model_name=False for a routing alias, a variant or an adapter. Plain langchain_openai.ChatOpenAI with base_url="https://api.siliconnetwork.io/v1" works too.
LlamaIndex
from llama_index.core import Settings
from llama_index_siliconnetwork import SiliconNetwork, SiliconNetworkEmbedding
Settings.llm = SiliconNetwork(model="llama-3.2-3b")
Settings.embed_model = SiliconNetworkEmbedding(model="bge-m3")
print(Settings.llm.complete("Hello"))
# or the generic OpenAI-like class; is_chat_model=True is required, or the
# client calls the legacy /v1/completions route, which is not served.
from llama_index.llms.openai_like import OpenAILike
llm = OpenAILike(model="llama-3.2-3b", api_base="https://api.siliconnetwork.io/v1", api_key="sn_live_...", is_chat_model=True)Vercel AI SDK
import { createSiliconNetwork } from "@siliconnetwork/ai-provider";
import { generateText } from "ai";
const silicon = createSiliconNetwork({
apiKey: process.env.SILICON_API_KEY,
headers: { "Silicon-Project": "web" },
});
const { text } = await generateText({ model: silicon("llama-3.2-3b"), prompt: "Hello" });
console.log(text);silicon.textEmbeddingModel(id) gives embeddings. With @ai-sdk/openai-compatible directly, use createOpenAICompatible with the base URL. With @ai-sdk/openai, take the chat model explicitly (openai.chat("llama-3.2-3b")), because that provider's default targets OpenAI's Responses format.
Continue
models:
- name: Silicon Network
provider: openai
model: llama-3.3-70b
apiBase: https://api.siliconnetwork.io/v1
apiKey: ${{ secrets.SILICON_API_KEY }}
roles: [chat, edit, apply]The full config in integrations/continue adds an autocomplete model and embeddings. Cursor: Settings, Models, add your key, override the OpenAI base URL with https://api.siliconnetwork.io/v1, and add a model id by hand. Aider: set OPENAI_API_BASE=https://api.siliconnetwork.io/v1 and OPENAI_API_KEY to your key, then run aider --model openai/llama-3.2-3b.
Open WebUI
Admin Panel, Settings, Connections, add an OpenAI API connection with the URL https://api.siliconnetwork.io/v1, your key, and the prefix ID silicon. With Model IDs left empty, Open WebUI fills its picker from GET /v1/models, which lists every loaded model, embedding and rerank models included; list the chat models under Model IDs to keep the picker to chat. The connection's Headers field can carry a project tag. Or import the whole setup (connection, filtered model list, a fast task model for titles, embeddings for documents) from integrations/openwebui under Admin Panel, Settings, Database, Import Config.
{
"openai.api_base_urls": ["https://api.siliconnetwork.io/v1"],
"openai.api_keys": ["sn_live_..."],
"openai.api_configs": {
"0": {
"enable": true,
"prefix_id": "silicon",
"model_ids": ["llama-3.2-3b", "llama-3.3-70b", "gpt-oss-120b"],
"headers": { "Silicon-Project": "open-webui" }
}
},
"task.model.external": "silicon.llama-3.2-3b",
"rag.embedding_engine": "openai",
"rag.embedding_model": "bge-m3",
"rag.openai.api_base_url": "https://api.siliconnetwork.io/v1",
"rag.openai.api_key": "sn_live_..."
}Importing replaces Open WebUI's whole list of OpenAI connections; merge with an export first if you have others. The optional Silicon Network function (silicon_network_pipe.py, pasted under Admin Panel, Functions) lists only the chat models loaded right now, shows the serving node and a receipt check under each answer, and adds per-user keys, project tags and a no-retention switch. An Ollama connection works too: point it at https://api.siliconnetwork.io/ollama.
LiteLLM
The proxy reaches Silicon Network through its OpenAI-compatible route; each model is exposed as silicon/<alias>.
model_list:
- model_name: silicon/llama-3.2-3b
litellm_params:
model: openai/llama-3.2-3b
api_base: https://api.siliconnetwork.io/v1
api_key: os.environ/SILICON_API_KEY
extra_headers: { Silicon-Project: litellm }
- model_name: silicon/*
litellm_params:
model: openai/*
api_base: https://api.siliconnetwork.io/v1
api_key: os.environ/SILICON_API_KEY
# SILICON_API_KEY=sn_live_... litellm --config config.yamlIn Python, litellm-siliconnetwork (tested with LiteLLM 1.90 to 1.103) registers a silicon/ provider for chat, streaming, async and embeddings, and maps the public price list onto every alias, so litellm.completion_cost and the proxy's spend logs use Silicon Network prices. Batch requests bill at half price, which LiteLLM does not know about. It reads SILICON_API_KEY and SILICON_API_BASE.
import litellm
import litellm_siliconnetwork
litellm_siliconnetwork.register(project="checkout") # key from SILICON_API_KEY
litellm_siliconnetwork.register_costs() # prices from the pricing endpoint
resp = litellm.completion(
model="silicon/llama-3.2-3b",
messages=[{"role": "user", "content": "Hello"}],
)
print(litellm.completion_cost(resp))
# model_info blocks with current prices, for the proxy config:
# python -m litellm_siliconnetwork.pricing --format yamln8n
Install the community node n8n-nodes-siliconnetwork (Settings, Community Nodes, Install), then create a Silicon Network API credential with your key; the base URL defaults to https://api.siliconnetwork.io/v1. The Silicon Network node does chat completions (prompt or message list, JSON output, a receipt, no-retention, a project tag), embeddings, the live model list filtered by kind, and batches: create one from the incoming items or from JSON, and fetch its status and parsed output. A requested receipt is decoded into the output, not verified.
mkdir -p ~/.n8n/custom && cd ~/.n8n/custom
npm install n8n-nodes-siliconnetwork
# restart n8n; the node appears as "Silicon Network"Zapier
The Silicon Network Zapier app has two actions, Generate Text and Create Embedding, each with an optional project tag, and a polling trigger, Batch Completed, that fires once per finished batch with its counts and output link. It authenticates with your API key (and an optional base URL). Until it is listed in Zapier's app directory, deploy it to your own Zapier account from integrations/zapier and invite your team.
cd integrations/zapier && npm install && npm test
npx zapier-platform login
npx zapier-platform register "Silicon Network" # once
npx zapier-platform pushThe Zapier embedding action defaults to the bge-m3 alias; any other embedding alias (qwen3-embedding-0.6b, all-minilm-l6-v2) or full id works too.
Run a node
A node runs the Silicon Network engine on Apple Silicon, loads models, and serves what the gateway routes to it. Approved operators join from the menu bar app or the engine dashboard with a device sign-in, serve from behind NAT through the relay, choose the models they offer, and accrue credit for verified work (payouts are off today).
SILICON_API_KEY=choose-a-long-secret uv run --extra mlx silicon
# then open http://localhost:52415/network and choose Sign in and joinRead the operator guide for installation, device sign-in, the relay, offered models, attestation, verification and escrow, and updates.
Architecture
Four parts: the engine on each node, the gateway that authenticates, routes and meters requests, the control plane that holds accounts, keys, usage, billing and node state, and the relay that lets nodes anywhere on the internet serve without opening a port. The full design is in the litepaper.
One request, end to end: admission (key, allowlist, rate limit), optional moderation, routing to a node, serving, metering, and a receipt when you ask for one.