Journey. As a developer, I want to open a conversation with an AI model I
choose by capability, send messages, and get answers — without hosting or
managing any model infrastructure.
Evergreen AI is the platform's model-front-door service: you describe what you need (criteria), it selects and loads a model, and you exchange messages over a session. This guide walks the full lifecycle — create, chat, stream, review history, end — against the local stack (Evergreen AI :40071). Verified with a locally-running Ollama backend.
Prerequisites
- The Evergreen AI Service is reachable (local stack
:40071, or the shared
dev subdomain).
- Your tenant key pair in
INAYA_API_KEY/INAYA_APP_KEY, and a user JWT
in TOKEN (obtain one via guide G-01; the full example below fetches one via the QA sign-in shortcut).
- An inference backend with at least one model available. On the local stack
this is Ollama (http://127.0.0.1:11434) with a model pulled, e.g. qwen3-vl:4b.
curl, or any HTTP client. Snippets also show Python and Node.- Patience for the first call. The first message in a session pays the
model-load cost (tens of seconds on a laptop); later messages in the same session are fast. On a memory-constrained dev machine the first call can stretch to several minutes — size your client timeouts accordingly.
Concepts (only what this journey needs)
- A session is a configured conversation. Creating one picks the model,
records generation parameters (temperature, system prompt, …), and allocates server-side history. Sessions expire on their own (history_ttl_in_seconds, default 24 h) — treat the id as ephemeral.
- You select models by criteria, not by URL.
selection_criteriais a
filter list (specific model, architecture, parameter count, minimum context window, capability, …) each marked MustHave or NiceToHave. The service picks the best locally-available match. This guide pins one model (MustHave + Model) for determinism.
- Two ways to receive a reply.
POST …/chat-messagesreturns the complete
AiChatMessage JSON; POST …/streamed-chat-messages returns SSE message events carrying text deltas you concatenate, terminated by done.
- History is server-side. Every exchange is recorded against the session;
GET …/chat-history replays it. Ending the session (DELETE) discards it.
Step 1 — Create a session
curl -s -X POST "${BASE_URL}/v3/sessions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-d '{
"required_modalities": ["Text"],
"selection_criteria": [
{ "Model": { "optionality": "MustHave", "name": "Qwen3_VL_4B_Q4" } }
],
"system_prompt": "You are a helpful assistant. Answer in one short sentence.",
"temperature": 0.2
}'import json, os, urllib.request
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/v3/sessions",
data=json.dumps({
"required_modalities": ["Text"],
"selection_criteria": [
{"Model": {"optionality": "MustHave", "name": "Qwen3_VL_4B_Q4"}},
],
"system_prompt": "You are a helpful assistant. Answer in one short sentence.",
"temperature": 0.2,
}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['TOKEN']}",
"x-api-key": os.environ["INAYA_API_KEY"],
"x-client-app-key": os.environ["INAYA_APP_KEY"],
},
)
print(json.dumps(json.load(urllib.request.urlopen(req, timeout=60)), indent=2))const res = await fetch(`${process.env.BASE_URL}/v3/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TOKEN}`,
"x-api-key": process.env.INAYA_API_KEY,
"x-client-app-key": process.env.INAYA_APP_KEY,
},
body: JSON.stringify({
required_modalities: ["Text"],
selection_criteria: [
{ Model: { optionality: "MustHave", name: "Qwen3_VL_4B_Q4" } },
],
system_prompt: "You are a helpful assistant. Answer in one short sentence.",
temperature: 0.2,
}),
});
console.log(await res.json());Expected response — 200 with the session id and the selected model's default image constraints (relevant when you later send images):
{
"ai_session_id": "01M38BVKKPGTB0D4YRRQD4BV7M",
"image_size_constraints": { "max_supported": { "width": 1344, "height": 1344 }, "standard_size": { "width": 448, "height": 448 } }
}TIP
Criteria compose. A realistic production pick looks like [{ "MinContextWindowLength": { "optionality": "MustHave", "length": 32768 } }, { "ParameterCount": { "optionality": "NiceToHave", "count": "7B" } }] — hard requirements plus preferences, letting the service choose the best available model. Pinning Model directly, as above, is best for tests and docs.
PITFALL
404 on session creation almost always means no suitable model: either the pinned model isn't pulled in the backend, or no model satisfies your MustHave criteria. Check the backend's model list before retrying.
Step 2 — Send a message and get the full reply
Messages are multipart/form-data — the same call accepts a binary image_path field later, so text rides along as a form field:
curl -s -X POST "${BASE_URL}/v3/sessions/${SESSION_ID}/chat-messages" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-F "user_message=Say hello in exactly three words."import json, os, urllib.request
boundary = "----g10boundary"
body = (
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="user_message"\r\n\r\n'
"Say hello in exactly three words.\r\n"
f"--{boundary}--\r\n"
).encode()
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/v3/sessions/{os.environ['SESSION_ID']}/chat-messages",
data=body,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Authorization": f"Bearer {os.environ['TOKEN']}",
"x-api-key": os.environ["INAYA_API_KEY"],
"x-client-app-key": os.environ["INAYA_APP_KEY"],
},
)
# Generous timeout: the first call of a session includes model-load time
print(json.dumps(json.load(urllib.request.urlopen(req, timeout=300)), indent=2))const form = new FormData();
form.set("user_message", "Say hello in exactly three words.");
const res = await fetch(
`${process.env.BASE_URL}/v3/sessions/${process.env.SESSION_ID}/chat-messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"x-api-key": process.env.INAYA_API_KEY,
"x-client-app-key": process.env.INAYA_APP_KEY,
},
body: form,
},
);
console.log(await res.json());Expected response — 200 with the complete assistant message (shape of response_time_ms grows to the full generation time; the first call of a session also includes model load):
{
"author": "Assistant",
"content": [{ "Text": "Hello, how are you?" }],
"creation_time": "2026-09-24T00:08:36Z",
"id": "60OKQCPO89BJGC1IAGQL6C9JB90LGDQ1AT534L2E8K",
"model_name": "Qwen3_VL_4B_Q4",
"response_time_ms": 20695
}PITFALL
This endpoint has a generous internal timeout, but the route-level timeout
does not cover streaming (see Step 3). If a non-streamed call returns 500 with an engine error after a long wait, the model likely failed to load — check the service logs and the backend's health before retrying.
Step 3 — Stream the reply (SSE)
For interactive UIs, stream tokens as they generate:
curl -s -N --max-time 180 -X POST \
"${BASE_URL}/v3/sessions/${SESSION_ID}/streamed-chat-messages" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-F "user_message=Count from one to three."Expected response — SSE events; concatenate the Text of every message event until done:
event: message
data: {"author":"Assistant","content":[{"Text":"One"}],"creation_time":"…","id":"…","model_name":"Qwen3_VL_4B_Q4","response_time_ms":0}
event: message
data: {"author":"Assistant","content":[{"Text":", two"}],"creation_time":"…","id":"…","model_name":"Qwen3_VL_4B_Q4","response_time_ms":0}
event: done
data: trueNOTE
Chunks are deltas, not cumulative — each message event carries only the
newly generated fragment. Some fragments may be empty (e.g. reasoning markers with thinking models); skip them. An event: error terminates the stream on failure. The --max-time on the snippet is a safety cap for scripts, not a requirement.
Step 4 — Review the conversation
History is stored server-side per session:
curl -s "${BASE_URL}/v3/sessions/${SESSION_ID}/chat-history" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}"import json, os, urllib.request
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/v3/sessions/{os.environ['SESSION_ID']}/chat-history",
headers={
"Authorization": f"Bearer {os.environ['TOKEN']}",
"x-api-key": os.environ["INAYA_API_KEY"],
"x-client-app-key": os.environ["INAYA_APP_KEY"],
},
)
print(json.dumps(json.load(urllib.request.urlopen(req, timeout=60)), indent=2))const res = await fetch(
`${process.env.BASE_URL}/v3/sessions/${process.env.SESSION_ID}/chat-history`,
{
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"x-api-key": process.env.INAYA_API_KEY,
"x-client-app-key": process.env.INAYA_APP_KEY,
},
},
);
console.log(await res.json());Expected response — 200 with every recorded message in order (user and assistant):
{ "messages": [ { "author": "User", "content": [{ "Text": "Say hello in exactly three words." }], "creation_time": "…", "id": "…" }, { "author": "Assistant", "content": [{ "Text": "Hello, how are you?" }], "creation_time": "…", "id": "…", "model_name": "Qwen3_VL_4B_Q4", "response_time_ms": 20695 } ] }Step 5 — End the session
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X DELETE \
"${BASE_URL}/v3/sessions/${SESSION_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}"Expected response — 200; the session id and its history are discarded.
Full working example
A complete create → chat → history → end cycle (first_chat.sh) — it fetches a user JWT via the QA shortcut first (local/QA environments only; production uses the G-01 flows):
#!/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\": \"evg-demo-device\", \"locale\": \"en-US\", \"tenant_id\": \"${TENANT_ID}\", \"username\": \"evg-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. Create a session
SESSION_ID=$(curl -sf -X POST "${BASE_URL}/v3/sessions" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{
"required_modalities": ["Text"],
"selection_criteria": [ { "Model": { "optionality": "MustHave", "name": "Qwen3_VL_4B_Q4" } } ],
"temperature": 0.2
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["ai_session_id"])')
echo "session: ${SESSION_ID}"
# 2. Chat (first call loads the model — allow time)
curl -sf -X POST "${BASE_URL}/v3/sessions/${SESSION_ID}/chat-messages" \
"${AUTH[@]}" -F "user_message=Say hello in exactly three words."
echo
# 3. History
curl -sf "${BASE_URL}/v3/sessions/${SESSION_ID}/chat-history" "${AUTH[@]}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("messages:", len(d["messages"]))'
# 4. End the session
curl -sf -X DELETE "${BASE_URL}/v3/sessions/${SESSION_ID}" "${AUTH[@]}" \
&& echo "session ended"(AUTH_URL is the Auth Service base URL — http://127.0.0.1:40004 on the local stack; TENANT_ID is your tenant's id.)
Try it live
Open the Playground → Applied Intelligence → Evergreen AI · Session Console — the same create/chat flow with model-criteria controls, running against a live service.
Copy agent prompt
You are working with the Inaya Labs Evergreen AI Service at ${BASE_URL}. The
tenant key pair is in INAYA_API_KEY / INAYA_APP_KEY env vars; obtain a user JWT
via the QA login from guide G-01 if TOKEN is not already set. Steps: (1) POST
/v3/sessions with required_modalities ["Text"] and selection_criteria pinning
model "Qwen3_VL_4B_Q4" as MustHave; report the ai_session_id. (2) POST
/v3/sessions/<id>/chat-messages as multipart with field user_message "Say hello
in exactly three words."; report the reply text and model_name. Note the first
call may take tens of seconds while the model loads. (3) GET
/v3/sessions/<id>/chat-history and report the message count. (4) DELETE
/v3/sessions/<id> to end the session. Never log the JWT.API reference
Next journeys
- Grounding answers in your documents (G-11) — add
use_ragto the session and ground replies in your own collections. - Choosing Evergreen vs Bella vs Chloe (G-13) — which of the platform's three AI surfaces fits your use case.