Home Playground Docs hub

Guide G-20 · Our AI Models

Chatting with Bella

Verified against local · 2026-09-23 · bella 0.1.0 (workspace HEAD)
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)

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.

the template carries it. A system/developer message you do send is treated as per-call steering layered on top of the persona.

chat-completions contract — messages, choices[0].message.content, stream — so existing OpenAI client code points at Bella with a URL swap.

the reasoning endpoint (guide G-21). Bella tunes for a consistent, product-ready voice instead.

The one call

A text question, answered:

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

json
{
  "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:

bash
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.json

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

bash
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]:

bash
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 6

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

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

bash
#!/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

Agent prompt — copy and hand to your agent
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

Next journeys