Journey. As a developer, I want a friendly assistant in my product —
already persona-tuned, already safety-configured — without writing a system
prompt or picking a model. This guide shows the one call that defines the
Bella surface, plus the variations (images, streaming, roles) you will
actually use.
Bella is the platform's curated-persona chat surface. Everything that makes the assistant the assistant — the persona prompt, the model selection, the generation defaults — lives server-side in a predefined session template (igentic.bella.v1). Your integration is a single OpenAI-compatible endpoint that is stateless: every call is a fresh, one-shot conversation. There is no session id to create, keep, or delete, and nothing to clean up after a call.
Concepts (only what this journey needs)
- One call per conversation. The service begins the predefined session,
sends your message, collects the reply, and ends the session — all inside your one HTTP call. That makes Bella trivially scalable, but it also means Bella has no memory: each request is answered on its own.
- Persona is server-side. You don't send a system prompt in production;
the template carries it. A system/developer message you do send is treated as per-call steering layered on top of the persona.
- OpenAI-compatible shape. The request and response mirror the OpenAI
chat-completions contract — messages, choices[0].message.content, stream — so existing OpenAI client code points at Bella with a URL swap.
- The effort knob is Chloe's, not Bella's. Budget/depth control lives on
the reasoning endpoint (guide G-21). Bella tunes for a consistent, product-ready voice instead.
The one call
A text question, answered:
curl -s --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "inaya-bella",
"messages": [ { "role": "user", "content": "Give me one kind habit to start this week." } ]
}'Expected response — 200 with an OpenAI-shaped body:
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1790223878,
"model": "GptOss_120B_Cloud",
"choices": [
{ "index": 0,
"message": { "role": "assistant", "content": "**Hello, dear friend**" },
"finish_reason": "stop" }
],
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }
}The answer is choices[0].message.content. Two fields to read correctly: model names the fleet model that actually answered (the template's pinned pick — useful for support tickets), and usage is a placeholder (all zeros) reserved for a future metering release. model in the request is optional and purely a client label; Bella always answers with the persona's model.
Images: multipart content
Attach an image with an image_url content part. The url may be a data:image/…;base64,… URI or an http(s) URL the service fetches for you:
python3 - <<'PY'
import base64, io, json
from PIL import Image, ImageDraw
img = Image.new("RGB", (256, 256), (240, 240, 240))
draw = ImageDraw.Draw(img)
draw.ellipse((60, 60, 196, 196), fill=(200, 30, 30))
buf = io.BytesIO()
img.save(buf, format="PNG")
data_uri = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
body = {
"model": "inaya-bella",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "Describe this image in one short sentence." },
{ "type": "image_url", "image_url": { "url": data_uri } },
],
}],
}
open("bella_image_request.json", "w").write(json.dumps(body))
print("request written")
PY
curl -s --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d @bella_image_request.jsonExpected response — 200, same body shape as text-only. The image is accepted and forwarded to the model; how well the reply uses it depends on the model pinned by the persona's template — a text-only fleet model answers without seeing it. The text part can also be a plain string when there is no image, exactly as in the first example.
Multi-turn appearance, one-shot reality
Clients that keep chat history (including the platform Playground) send the whole transcript — user and assistant turns — on every call. Bella accepts that shape: prior assistant messages are allowed and ignored, and a system/developer message is applied as per-call steering. Only the current user text is actually answered:
curl -s --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "inaya-bella",
"messages": [
{ "role": "system", "content": "Answer in one word." },
{ "role": "user", "content": "Hi" },
{ "role": "assistant", "content": "Hello! How can I help?" },
{ "role": "user", "content": "Pick a color." }
]
}'Expected response — 200. The service does not reject transcript-shaped bodies, but it does not remember them either. If your product needs real server-side memory, use an Evergreen session (guide G-10) or start a predefined session yourself (guide G-13, Path B) instead of this facade.
Streaming
Set stream: true and the endpoint returns an SSE stream of chat.completion.chunk events — a role delta first, content deltas next, a final finish_reason: "stop" chunk, then data: [DONE]:
curl -s -N --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "inaya-bella",
"stream": true,
"messages": [ { "role": "user", "content": "Count from one to three, separated by spaces." } ]
}' | grep '^data:' | head -n 6Expected output — SSE data: lines, e.g. a {"delta":{"role":"assistant"}} chunk, one or more {"delta":{"content":"…"}} chunks, a {"delta":{},"finish_reason":"stop"} chunk, and finally data: [DONE]. In streaming mode the model field on chunks echoes your request label; the non-streaming response's model field names the fleet model that answered.
Error contract
Three failures cover almost everything that goes wrong:
KEYS=(-H "x-api-key: ${INAYA_API_KEY}" -H "x-client-app-key: ${INAYA_APP_KEY}")
echo "== missing JWT =="
curl -s -o /tmp/bella_err1.json -w "%{http_code}\n" -X POST "${BELLA_URL}/v3/bella/chat/completions" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hi"}]}'
cat /tmp/bella_err1.json; echo
echo "== unsupported role =="
curl -s -w "\n%{http_code}\n" -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" "${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"tool","content":"hi"}]}'
echo "== empty messages =="
curl -s -w "\n%{http_code}\n" -X POST "${BELLA_URL}/v3/bella/chat/completions" \
-H "Authorization: Bearer ${TOKEN}" "${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"messages":[]}'Expected — 401 with {"error":"Missing Authorization token"} for a missing/invalid JWT; 400 with Invalid request: Unsupported role 'tool'. Only 'user', 'assistant', 'system', and 'developer' are supported. for a bad role; 400 with Invalid request: messages array must not be empty for an empty array. Evergreen AI unreachable at answer time → 500 with an AI chat failed: … message — treat it as retryable.
NOTE
Accepted-but-inert fields: temperature and max_tokens parse but are ignored (the persona template owns generation defaults), and an effort field is silently ignored — effort budgeting is Chloe's surface (G-21).
Full working example
One script (bella_chat.sh) — QA-login, a text turn, an image turn, and a streamed turn, each asserted (AUTH_URL is the Auth Service — http://127.0.0.1:40004; BELLA_URL is Bella — http://127.0.0.1:40005; TENANT_ID is your sandbox tenant):
#!/usr/bin/env bash
set -euo pipefail
# 0. Obtain a user JWT (QA shortcut — see G-01 for production flows)
TOKEN=$(curl -sf -X POST "${AUTH_URL}/v3/auth/qa/login" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-d "{\"device_id\": \"g20-demo-device\", \"locale\": \"en-US\", \"tenant_id\": \"${TENANT_ID}\", \"username\": \"g20-demo\"}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
AUTH=(-H "Authorization: Bearer ${TOKEN}"
-H "x-api-key: ${INAYA_API_KEY}"
-H "x-client-app-key: ${INAYA_APP_KEY}")
# 1. Text turn — assert the OpenAI response shape
curl -sf --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{"model":"inaya-bella","messages":[{"role":"user","content":"Say hello in exactly three words."}]}' \
| python3 -c '
import json, sys
d = json.load(sys.stdin)
assert d["object"] == "chat.completion", d
assert d["choices"][0]["finish_reason"] == "stop", d
assert d["choices"][0]["message"]["role"] == "assistant"
assert d["choices"][0]["message"]["content"].strip(), "empty reply"
print("text turn: ok | model:", d["model"])'
# 2. Transcript-shaped body (assistant history + system steering) is accepted
curl -sf --max-time 280 -o /dev/null -w "transcript turn: %{http_code}\n" \
-X POST "${BELLA_URL}/v3/bella/chat/completions" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{"model":"inaya-bella","messages":[{"role":"user","content":"Hi"},{"role":"assistant","content":"Hello!"},{"role":"user","content":"Bye"}]}'
# 3. Streamed turn — collect deltas until [DONE]
CHUNKS=$(curl -s -N --max-time 280 -X POST "${BELLA_URL}/v3/bella/chat/completions" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{"model":"inaya-bella","stream":true,"messages":[{"role":"user","content":"Count from one to three, separated by spaces."}]}')
echo "${CHUNKS}" | grep -q 'data: \[DONE\]' && echo "stream turn: ok (terminated with [DONE])"Try it live
Open the Playground → Our AI Models: Bella is a fully functional chat UI on this exact endpoint — suggestions, image attachments, and streaming included.
Copy agent prompt
You are helping a developer call the Bella chat endpoint (${BELLA_URL}).
Bella is a stateless, OpenAI-compatible chat surface: POST
/v3/bella/chat/completions with header Authorization: Bearer ${TOKEN} plus the
x-api-key / x-client-app-key pair. Body: optional model label ("inaya-bella"),
messages array (roles user, assistant, system, developer; content either a
plain string or parts of type text / image_url where image_url.url is a
data:image/...;base64 URI or an http(s) URL), optional stream:true for SSE
chunks ending in "data: [DONE]"). Each call is a fresh one-shot session — the
assistant role in history is accepted but ignored; Bella has no memory.
Steps: (1) POST one user message and report choices[0].message.content and the
model field. (2) Repeat with stream:true and count the content deltas before
[DONE]. Errors: 401 {"error":"Missing Authorization token"}; 400 for
unsupported roles or empty messages. Never log the JWT.API reference
- Bella — Swagger UI · Scalar — the curated-persona chat facade
Next journeys
- Choosing Evergreen vs Bella vs Chloe (G-13) — where this surface fits among the platform's three AI surfaces.
- Chatting with Chloe (G-21) — the reasoning endpoint: one-shot calls with a budget knob instead of a persona.
- Your first AI chat session (G-10) — full server-side conversation memory via Evergreen sessions, when Bella's one-shot model is not enough.