tinyai

tinyai documentation

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.

TL;DR — for AI agents and impatient humans. Everything you need:
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.
1 · Quick start 2 · The two kinds of keys 3 · API reference 4 · Models & routing 5 · Tools, images, JSON output 6 · Streaming 7 · Key limits & scoping 8 · Usage & cost accounting 9 · Errors 10 · Endpoints (custom request shapes) 11 · Adapters (custom providers) 12 · Limits & guarantees

1 · Quick start

  1. Add a provider key — dashboard → Add provider key → paste the API key you already have (Anthropic, OpenAI, Google, or any other provider via its endpoint). Stored encrypted; used only to sign your own calls.
  2. Create a tinyai key — Create API key. The full sk-… key is shown once; copy it.
  3. Call the API:
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"])

2 · The two kinds of keys

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 ityour apps / bots / scriptsonly tinyai → the provider
billingnothing — it's an access tokenyou 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.

3 · API reference

Base URL: https://ai.tinyappmart.com/v1 · auth on every endpoint: Authorization: Bearer sk-…

POST /chat/completions

The standard chat-completions contract. Request fields:

fieldtypenotes
modelstring, requiredsee Models
messagesarray, requiredroles system/developer, user, assistant, tool; user content may be a string or parts (text, image_url)
max_tokensintdefault 4096 (max_completion_tokens also accepted)
streamboolSSE streaming — see Streaming
temperature, top_pnumbersilently dropped for models that reject them (Opus 4.8/4.7, Fable 5)
stopstring | arraystop sequences
tools, tool_choicefunction calling — see §5
response_formatobjectjson_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.

GET /models

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.

POST /my/{endpoint-name}

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.)

4 · Models & routing

model idaliasprovider
claude-opus-4-8opusanthropic
claude-sonnet-4-6sonnetanthropic
claude-haiku-4-5haikuanthropic
claude-fable-5fableanthropic

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).

5 · Tools, images, JSON output

Function calling

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.

Images

"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…"}}
]}]

JSON output

6 · Streaming

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]

7 · Key limits & scoping

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:

limiteffect 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

8 · Usage & cost accounting

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).

statusmeaning
okcompleted; cost is exact — reported by the provider (OpenRouter) or from the curated list-price table
estimatedcompleted, 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)
unpricedcompleted, but the price is unknown — the cost is not shown and missing from totals (the dashboard warns when this happens)
errorthe upstream call failed

9 · Errors

Errors use the standard shape {"error": {"message": "…", "type": "…"}} (auth failures return {"detail": "invalid api key"}):

HTTPtypewhen
400invalid_request_errorbad JSON; no provider key for the requested provider; endpoint/adapter misconfiguration
401—missing or revoked tinyai key
402insufficient_quotathe key's monthly cap is reached
403permission_errormodel or provider key outside the key's scope
404invalid_request_errorunknown model name
429rate_limit_errormore than 8 concurrent requests per account
502api_errorthe provider errored — the message includes the upstream detail

10 · Endpoints — your own custom API surface

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 custom request → request transform → standard request → provider → standard response → response transform → your custom response

One message, end to end

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):

Mapping mode (no code)

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}
}
rulemeaning
{"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

Code mode (JavaScript)

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).

11 · Adapters — connect a custom provider

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.

your app ⇄ tinyai ⇄ toProvider() → custom provider → fromProvider() ⇄ tinyai ⇄ your app

toProvider(ctx) — build the outgoing request

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
  };
}

JavaScript survival kit (60 seconds)

// { 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")

fromProvider(res) — translate the answer back

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
  };
}

Worked example

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.

12 · Limits & guarantees

tinyai · part of TinyAppMart one key · every model · mission control