AI models
One OpenAI-shaped endpoint in front of every provider. Bring your own keys and the spend stays on your account — we never mark them up.
What it is
AI models is one HTTP endpoint in front of every AI provider your app uses — OpenAI, Anthropic, Google and Ollama — speaking the OpenAI chat-completions shape, so one client library reaches all of them. It is bring-your-own-key: you store your provider keys against your app, we call the provider with your key, on your provider account, and add nothing to what they charge you.
Before you start
You need your app API key. It is shown once, when the app is created. If you no longer have it, open the Smart Services console → your app → Settings and use Rotate Key — the old key stops working immediately.
Send it on every request, in either header:
Authorization: Bearer <key>
x-api-key: <key>
Both are accepted. Authorization: Bearer wins if you send both.
Base URL: https://smart-services.io
The key is server-side only. It never belongs in browser code.
Two more things before the first call:
- The AI models service must be enabled on your app, or every endpoint here returns
403. - You must have added a provider key in the console (your app → AI models → Provider Keys). We hold no key for you by default, and a model whose provider you have not configured returns
403.
Your first call
A chat completion against OpenAI. The model field is provider:model — see Gotchas, this is the single most common mistake.
curl -X POST https://smart-services.io/api/v1/maas/chat/completions \
-H "Authorization: Bearer $SMART_SERVICES_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "openai:gpt-4o-mini",
"messages": [
{ "role": "user", "content": "Name the capital of Morocco. One word." }
]
}'
The same call with fetch:
const res = await fetch(
"https://smart-services.io/api/v1/maas/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMART_SERVICES_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai:gpt-4o-mini",
messages: [
{ role: "user", content: "Name the capital of Morocco. One word." },
],
}),
}
);
const data = await res.json();
console.log(data.choices[0].message.content);
The response:
{
"id": "chatcmpl_k2f9d1xq",
"object": "chat.completion",
"created": 1758182400,
"model": "openai:gpt-4o-mini",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Rabat" },
"finish_reason": "stop"
}
],
"meta": {
"provider": "openai",
"usage": { "prompt_tokens": 18, "completion_tokens": 2, "total_tokens": 20 },
"cost": 0.0000039,
"timing": {
"totalLatencyMs": 612,
"phases": {
"authMs": 11,
"bodyParseMs": 1,
"modelResolutionMs": 0,
"providerConfigMs": 8,
"ragQueryMs": 0,
"contextCompressionMs": 0,
"providerCallMs": 585,
"responseProcessingMs": 7
}
}
}
}
choices is OpenAI-shaped, so an existing OpenAI client reads it unchanged. meta is ours: which provider actually answered, the token counts we recorded, the computed cost of that call against your provider's published prices, and where the latency went.
The rest of the endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/maas/chat/completions | Chat completion. Set "stream": true for SSE. |
| GET | /api/v1/maas/models | Live model list, pulled from each provider you have configured. Add ?provider=openai for one. |
| GET | /api/v1/maas/providers | Which providers your app has configured, whether a key is present, and your preferred-model list. Makes no upstream calls. |
| POST | /api/v1/maas/embeddings | Embeddings from the platform's own model. No provider key needed. |
POST /api/v1/chat/completions is the same handler as /api/v1/maas/chat/completions; both paths work.
Limits and errors
Errors are JSON with an error string, and sometimes a details field carrying the specifics:
{ "error": "No provider key configured for 'anthropic'. Configure it in App > AI models > Provider Keys." }
| Status | Means |
|---|---|
400 | Bad JSON, no model and no app default, or a model string that does not name its provider. |
401 | Missing or invalid API key. |
403 | AI models is not enabled on your app, or you have no key for that model's provider. |
500 | The provider call threw. error carries the provider's message. |
502 | Every candidate model failed. details lists each attempt and its error. |
There is no per-request rate limit on this endpoint and no token metering. AI models is a flat, untiered service: we do not resell tokens and take no cut. Your spend is with your provider, on your provider account, under your provider's own rate limits — when you get throttled, it is them, not us, and the provider's message comes back in error. What we record is a usage log per call so the console can show you token counts and computed cost. Current service state and tier for your app show in the console.
Gotchas
A model must name its provider. "gpt-4o-mini" is a 400. "openai:gpt-4o-mini" works. We used to guess the provider from the model name and it was wrong in both directions — gpt-oss is an open-weight family served by Ollama, not by OpenAI's API, and guessing sent it to an endpoint that had never heard of it. Everything after the first colon is the model id, so ollama:llama3.1:8b is correct and parses as you would expect. Known providers: openai, google, anthropic, ollama, ninjacat.
Don't hardcode model ids — ask /models. We keep no catalogue of model names. GET /api/v1/maas/models returns each provider's own live list, already prefixed as provider:model, ready to paste into the model field. When a provider is down, that provider's entry appears in an errors array alongside the data rather than silently shortening the list, so you can tell "down" from "has no models".
Your app's preferred models are silent fallbacks. If the model you ask for fails, we walk the rest of your app's preferred-models list in order before giving up. A 502 means every one of them failed — read details. And if your app has forceDefaultModel set, the model you send in the body is overridden by the app default; the model field in the response tells you what actually ran.
Fallback does not survive streaming. Once bytes are on the wire we are committed. Fallback only covers establishing the stream; a mid-stream provider failure surfaces as a stream error, not a retry on the next model.
meta.usage can be missing. Not every provider reports tokens on every path, and where usage is absent, cost is absent too. Treat both as optional. For streaming calls, usage only arrives at end-of-stream.
Long prompts get truncated by default. Past the context window we drop the middle of the conversation rather than fail. If you would rather have the content compressed with a map-reduce pass — slower, and it makes extra provider calls billed to your account — send "contextCompression": true.
Embeddings are one model for everyone, on purpose. /api/v1/maas/embeddings serves the platform's own model and rejects any other model value, and rejects dimensions outright, rather than quietly returning off-model vectors. Mixed vectors in one corpus are unsearchable and the damage is invisible until you try. Store the returned model alongside every vector so a future model change is detectable.
Rotating your key is instant and total. There is no overlap window. The old key dies the moment the new one appears, so deploy the new one before you rotate, not after.