One API key for every AI model, over the standard chat API. You store your own provider keys (encrypted); tinyai routes each request to the right provider, signed with your key — you pay providers directly, tinyai meters and controls access.
base_url: https://ai.tinyappmart.com/v1
auth: Authorization: Bearer sk-… # a tinyai key from the dashboard
endpoint: POST /chat/completions # standard chat-completions format
models: claude-opus-4-8 · claude-sonnet-4-6 · claude-haiku-4-5 · claude-fable-5
aliases: opus · sonnet · haiku · fable
any other: "<provider>/<model-id>" # e.g. "openai/gpt-6-astra" — needs that provider's key
works: streaming, tools/function-calling, images, response_format, usage accounting
errors: {"error": {"message", "type"}} + HTTP status; mid-stream errors arrive as an SSE error event
Point any chat-completions client at the base URL and it works.
sk-… key is shown once; copy it.curl https://ai.tinyappmart.com/v1/chat/completions \
-H "Authorization: Bearer sk-YOUR-TINYAI-KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "haiku",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Or with any OpenAI-compatible library — just set the base URL:
# Python (pip install openai)
from openai import OpenAI
client = OpenAI(base_url="https://ai.tinyappmart.com/v1", api_key="sk-YOUR-TINYAI-KEY")
# plain HTTP works too — no AI library needed:
import requests
r = requests.post("https://ai.tinyappmart.com/v1/chat/completions",
headers={"Authorization": "Bearer sk-YOUR-TINYAI-KEY"},
json={"model": "haiku", "messages": [{"role": "user", "content": "Hello!"}]})
print(r.json()["choices"][0]["message"]["content"])
tinyai key (sk-…) | provider key | |
|---|---|---|
| what it is | The key you put in your apps. Maps to your account; can be scoped and capped; revocable any time. | Your existing key from a provider (Anthropic, OpenAI, …). Stored encrypted; decrypted only in memory to sign the outbound call. |
| who sees it | your apps / bots / scripts | only tinyai → the provider |
| billing | nothing — it's an access token | you pay the provider directly (BYOK) |
Requests flow: app calls with a tinyai key → tinyai picks the right provider key for the requested model (your newest one for that provider, unless the tinyai key is scoped) → forwards the call signed with it → returns the standard response and records usage.
No provider key for that model? When tinyai has a built-in key for
the provider, the call runs on it — metered at the tinyai price (provider
list price × a multiplier) against a small monthly allowance, then
402. Add your own key any time to switch back to raw provider
prices with no allowance.
Base URL: https://ai.tinyappmart.com/v1 · auth on every
endpoint: Authorization: Bearer sk-…
The standard chat-completions contract. Request fields:
| field | type | notes |
|---|---|---|
model | string, required | see Models |
messages | array, required | roles system/developer, user, assistant, tool; user content may be a string or parts (text, image_url) |
max_tokens | int | default 4096 (max_completion_tokens also accepted) |
stream | bool | SSE streaming — see Streaming |
temperature, top_p | number | silently dropped for models that reject them (Opus 4.8/4.7, Fable 5) |
stop | string | array | stop sequences |
tools, tool_choice | function calling — see §5 | |
response_format | object | json_object / json_schema — see §5 |
Response (non-streaming):
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1788700000,
"model": "claude-haiku-4-5",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "Hi! How can I help?"},
"finish_reason": "stop" // stop | length | tool_calls | content_filter
}],
"usage": {"prompt_tokens": 9, "completion_tokens": 8, "total_tokens": 17}
}
For OpenAI-compatible providers the body is passed through verbatim (only the model id is rewritten), so every parameter that provider supports works. For Anthropic models tinyai translates natively — the fields above are the supported surface.
Lists the curated models: {"object": "list", "data": [{"id": …,
"owned_by": …}]}. Dynamic provider/model names (§4)
are valid even though they're not in this list.
Your own custom request shape — see Endpoints.
(Names are scoped to your key — no login in the URL needed.
/v1/x/… still works as a legacy alias.)
| model id | alias | provider |
|---|---|---|
claude-opus-4-8 | opus | anthropic |
claude-sonnet-4-6 | sonnet | anthropic |
claude-haiku-4-5 | haiku | anthropic |
claude-fable-5 | fable | anthropic |
Any other model uses the dynamic form
"<provider>/<model-id>":
"model": "openai/gpt-6-astra" → your OpenAI key, model id "gpt-6-astra"
"model": "google/gemini-3-pro" → your Google key
"model": "myhost/llama-x" → your custom provider named "myhost" (its own endpoint)
The provider part must match a provider key in your dashboard; the model part is passed to the provider verbatim. Routing picks your newest key for that provider (unless the tinyai key is scoped to specific keys).
Standard tools / tool_choice contract, on every
provider including Anthropic models (translated natively, streaming
included):
"tools": [{"type": "function", "function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}}}],
"tool_choice": "auto" // auto | none | required | {"type":"function","function":{"name":…}}
The model answers with finish_reason: "tool_calls" and
message.tool_calls[]; you execute the function and send its
output back as a {"role": "tool", "tool_call_id": …,
"content": …} message.
"messages": [{"role": "user", "content": [
{"type": "text", "text": "What is in this picture?"},
{"type": "image_url", "image_url": {"url": "https://…/photo.jpg"}},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR…"}}
]}]
"response_format": {"type": "json_object"} — the model is
instructed to answer with a single JSON object."response_format": {"type": "json_schema", "json_schema":
{"schema": {…}}} — schema-enforced output (Anthropic structured
outputs; support varies by model).Set "stream": true. The response is standard server-sent
events — chunks with choices[0].delta, terminated by
data: [DONE]:
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"…":"…","choices":[{"index":0,"delta":{"content":"Hel"},"finish_reason":null}]}
data: {"…":"…","choices":[{"index":0,"delta":{"content":"lo!"},"finish_reason":null}]}
data: {"…":"…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Tool calls stream too (index-tracked delta.tool_calls with
incremental arguments). If the upstream fails mid-stream you
get an error event before [DONE] instead of a silent cut:
data: {"error": {"message": "upstream error: …", "type": "api_error"}}
data: [DONE]
Each tinyai key can carry optional guardrails (dashboard → key row → ⋯). Models and provider keys default to unrestricted; new keys start with a $100/month spend cap (raise, lower or clear it per key). The guardrails exist so a key that lives in code you don't fully control can't burn your provider account:
| limit | effect when violated |
|---|---|
allowed models — e.g. haiku, sonnet |
403 permission_error — key can't call other models |
| provider keys allowlist | 400/403 — key can only spend from the ticked provider keys |
| monthly cap, USD | 402 insufficient_quota once the key's estimated monthly
spend reaches the cap; resets next calendar month |
Every call is recorded — tokens in/out, model, cost, and a status — and shown in the dashboard (day/week/month/year, filterable by project and by key). Since tinyai is BYOK, the cost shown is what your own provider charges you — raw provider prices, no markup, not a bill from tinyai. OpenRouter keys are exact (OpenRouter reports each call's cost); other providers are priced from tables and feeds:
Usage is grouped by project. Each key has a default project (set at
creation, editable under ⋯), and you can override it per call by appending
the project to the key itself — Authorization: Bearer
sk-yourkey.myproject — so one key can serve several apps and
the dashboard still splits their spend. Letters, digits, .
_ - only (max 64 chars).
| status | meaning |
|---|---|
ok | completed; cost is exact — reported by the provider (OpenRouter) or from the curated list-price table |
estimated | completed, but the cost is an estimate — the model is priced from a public price feed, or the stream disconnected before final token counts (output estimated from length) |
unpriced | completed, but the price is unknown — the cost is not shown and missing from totals (the dashboard warns when this happens) |
error | the upstream call failed |
Errors use the standard shape
{"error": {"message": "…", "type": "…"}}
(auth failures return {"detail": "invalid api key"}):
| HTTP | type | when |
|---|---|---|
| 400 | invalid_request_error | bad JSON; no provider key for the requested provider; endpoint/adapter misconfiguration |
| 401 | — | missing or revoked tinyai key |
| 402 | insufficient_quota | the key's monthly cap is reached |
| 403 | permission_error | model or provider key outside the key's scope |
| 404 | invalid_request_error | unknown model name |
| 429 | rate_limit_error | more than 8 concurrent requests per account |
| 502 | api_error | the provider errored — the message includes the upstream detail |
A custom endpoint is your AI's remote control. Your app sends a dumb,
stable request to POST /v1/my/<name>; which model
runs and what system prompt wraps it live in the endpoint — edit
them in the dashboard and every app changes on its next request, no redeploy.
It also frees your app from the chat-completions format: your endpoint
accepts your JSON shape and answers in your shape.
Your app (this code never changes again):
r = requests.post(
"https://ai.tinyappmart.com/v1/my/telegrambot", # ① your endpoint
headers={"Authorization": f"Bearer {TINYAI_KEY}"},
json={"text": user_message}, # ② any JSON you invent
)
reply = r.json()["reply"] # ⑥ your shape comes back
The endpoint (defined in the editor):
function request(input) { // ③ input = the body from ②
return { // this return IS the AI request:
model: "claude-haiku-4-5", // ← swap model live, no redeploy
messages: [
{ role: "system", content: "You are a friendly bot." }, // ← live prompt
{ role: "user", content: input.text },
],
}; // ④ tinyai sends it — model picks the
} // provider, your stored key signs
function response(result) { // ⑤ result = the provider's answer
return { reply: result.choices[0].message.content };
}
Both functions are edited in the endpoint editor (dashboard → Endpoints → edit):
Declarative field-picking. Example — accept {"q": "…"} and
return {"answer": "…"}:
request spec: response spec:
{ {
"model": {"const": "haiku"}, "answer": {"from": "choices.0.message.content"},
"messages": {"from_text": "q"}, "spent": {"from": "usage.total_tokens"}
"max_tokens": {"from": "limit", }
"default": 1024}
}
| rule | meaning |
|---|---|
{"from": "path.to.field"} | copy a field (dots descend into objects and array indices) |
{"from_text": "q"} | take a plain string and wrap it as a one-message chat |
{"const": …} | always use this value |
"default": … | fallback when the source field is absent |
function request(input) { /* your shape → standard request */
return { model: "haiku",
messages: [{ role: "user", content: input.q }] };
}
function response(result){ /* standard response → your shape */
return { answer: result.choices[0].message.content };
}
Runs in the same sandbox as adapters (see §11 rules). The model field your request() returns is the only router — if it names a provider you have no key for, the call fails with a clear error. Call it with your normal tinyai key:
curl https://ai.tinyappmart.com/v1/my/myshape \
-H "Authorization: Bearer sk-…" \
-d '{"q": "Hello!"}'
Streaming responses pass through untransformed (the response transform applies to non-streaming calls).
Mirror image of a custom endpoint: it changes how tinyai talks out to a provider whose API is non-standard. You write two small JavaScript functions on the provider key (dashboard → key row → ⋯ → adapter). Assumes no JS knowledge — there's a survival kit below.
ctx has three fields: ctx.request (the standard
request as your app sent it), ctx.model (model id with the
provider prefix stripped), ctx.api_key (your decrypted key).
Return what tinyai should send:
function toProvider(ctx) {
return {
path: "/generate", // appended to the key's base URL (default /chat/completions)
headers: { "X-Api-Key": ctx.api_key }, // merged over defaults (Authorization: Bearer + JSON content type)
body: { prompt: "..." }, // the JSON payload your provider expects
};
}
// { name: value } is a dictionary literal; dot = field access
ctx.request.max_tokens // 256, or undefined if absent
// arrays: [i] to index, .length for size; last element (no negative indexing):
var msgs = ctx.request.messages;
msgs[msgs.length - 1].content // text of the newest message
// x || y = "x, or y if x is missing":
ctx.request.max_tokens || 512
// flatten chat history into one string:
msgs.map(function(m){ return m.role + ": " + m.content; }).join("\n")
res.status is the HTTP status, res.body the
parsed JSON reply. Return a standard chat answer — the only line you
really adapt is content:. Optional: skip this function
entirely if the provider already answers in the standard format.
function fromProvider(res) {
return {
object: "chat.completion",
choices: [{ index: 0, finish_reason: "stop",
message: { role: "assistant", content: res.body.text } }],
usage: res.body.usage || {}, // token counts if reported; {} = tinyai estimates
};
}
A provider documented as: POST https://api.acme-llm.dev/v2/complete,
header Acme-Token, body {"input", "max_len"},
returns {"output", "used_tokens"}. Add a provider key
(kind other…, name acme, base URL
https://api.acme-llm.dev), enable the adapter, write:
function toProvider(ctx) {
var msgs = ctx.request.messages;
return {
path: "/v2/complete",
headers: { "Acme-Token": ctx.api_key },
body: { input: msgs[msgs.length - 1].content,
max_len: ctx.request.max_tokens || 500 },
};
}
function fromProvider(res) {
return {
object: "chat.completion",
choices: [{ index: 0, finish_reason: "stop",
message: { role: "assistant", content: res.body.output } }],
usage: { completion_tokens: res.body.used_tokens },
};
}
Now "model": "acme/anything" works from any app.
arr[arr.length-1], not
arr.at(-1).429 beyond that).