Server-side AI proxy for the Stack-IDE Android app, per docs/ai/. The server
holds the base system prompt, the tool definitions, and the LLM-provider keys;
the device sends messages + device_context and receives an SSE stream of
delta / tool_call / done / error events. The wire never carries the
system prompt or the tool schemas.
cd ai-server
pip install -r requirements.txt
export AI_PROXY_HMAC_SECRET="dev-secret-change-me"
# The default provider (OpenCode Zen) works out of the box with the built-in
# defaults — no API key required (the endpoint is open).
export ZEN_BASE_URL="https://opencode.ai/zen/v1"
export ZEN_MODEL="mimo-v2.5-free"
# GLM is still available as an alternative provider:
export GLM_API_KEY="sk-..." # default is hardcoded below; override in prod
export GLM_BASE_URL="https://rx37rjd.abc-tunnel.us/v1"
export GLM_MODEL="Nx/zai-org/GLM-5.2"
python -m uvicorn app.main:app --port 8081Health check:
curl http://127.0.0.1:8081/v1/health
# {"status":"ok","version":"v1","server_time":...}| Variable | Default | Purpose |
|---|---|---|
AI_PROXY_PORT |
8081 |
Listen port |
AI_PROXY_DB_PATH |
ai_proxy.db |
SQLite database file |
AI_PROXY_HMAC_SECRET |
change-me-in-production |
Server secret for key hashing (NOT the LLM key) |
OPENAI_API_KEY |
— | OpenAI key (alternative provider; unset = unavailable) |
ANTHROPIC_API_KEY |
— | Anthropic key (alternative provider; unset = unavailable) |
ZEN_API_KEY |
"" |
Default provider key. Empty = no auth (endpoint is open). |
ZEN_BASE_URL |
https://opencode.ai/zen/v1 |
Default provider base URL (OpenAI-compatible, 200k context / 16k output) |
ZEN_MODEL |
mimo-v2.5-free |
Default provider model ID |
GLM_API_KEY |
sk-8156528465a3c504-222nvw-24f7adf8 |
Alternative provider key. Hardcoded default works out of the box; OVERRIDE in production. |
GLM_BASE_URL |
https://rx37rjd.abc-tunnel.us/v1 |
Alternative provider base URL (OpenAI-compatible) |
GLM_MODEL |
Nx/zai-org/GLM-5.2 |
Alternative provider model ID |
AI_PROXY_DEFAULT_MODEL |
mimo-v2.5-free |
Default model (used when the request omits options.model) |
AI_PROXY_DEFAULT_PROVIDER |
zen |
zen (default), glm, openai, or anthropic |
AI_PROXY_RATE_LIMIT_MAX |
60 |
Max requests per key per window |
AI_PROXY_RATE_LIMIT_WINDOW_SECS |
60 |
Rate-limit window |
AI_PROXY_TIMESTAMP_WINDOW_SECS |
300 |
Replay timestamp skew |
AI_PROXY_NONCE_TTL_SECS |
600 |
Nonce replay cache TTL |
AI_PROXY_MAX_BODY_BYTES |
524288 |
Max request body size |
AI_PROXY_DEFAULT_TOKEN_QUOTA |
100000 |
Default per-key token quota |
AI_PROXY_DEFAULT_REQUEST_QUOTA |
1000 |
Default per-key request quota |
Returns { status, version, server_time }.
Request body:
{
"conversation_id": "string (opaque to server)",
"messages": [ { "role": "user", "content": "..." } ],
"device_context": {
"active_file": { "name": "main.py", "content": "..." },
"open_tabs": ["path/a.py"],
"selection": { "text": "..." },
"memories": ["The user prefers tabs."]
},
"options": { "model": "gpt-4o-mini", "temperature": 0.2 }
}Response: SSE stream of data: <json>\n\n events:
{"type":"delta","text":"..."}— text fragment{"type":"tool_call","tool_call_id":"call_1","tool_name":"file-edit","arguments":"{...}"}— tool-call envelope{"type":"done","finish_reason":"stop"}— stream complete{"type":"error","code":"...","message":"..."}— error (terminates stream)
Returns the authenticated key's usage and quota.
Returns 501 Not Implemented. v1 keys are admin-issued via SQL (see below).
| Header | Purpose |
|---|---|
Authorization: Bearer sk-stack-<...> |
API key |
X-Timestamp |
Epoch ms, ±300s window |
X-Nonce |
Random UUID, single-use within TTL |
X-Signature |
HMAC-SHA256(timestamp:nonce:body, <api-key>) hex |
User-Agent: Stack-IDE/<version> |
Client identification |
KEY="sk-stack-$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
HASH=$(python3 -c "import hmac, hashlib, os; \
print(hmac.new(os.environ['AI_PROXY_HMAC_SECRET'].encode(), '$KEY'.encode(), hashlib.sha256).hexdigest())")
sqlite3 ai_proxy.db "INSERT INTO api_keys \
(user_id, key_prefix, key_hash, active, plan, token_quota, request_quota, created_at, revoked_at) \
VALUES (1, '${KEY:0:12}', '$HASH', 1, 'free', 100000, 1000, datetime('now'), NULL);"
echo "Give this key to the user (it is never re-derivable): $KEY"cd ai-server
pip install -r requirements.txt
pip install pytest pytest-asyncio
pytestdocker build -t stack-ai-proxy .
docker run -p 8081:8081 \
-e AI_PROXY_HMAC_SECRET=... \
stack-ai-proxyThe image defaults to the OpenCode Zen provider (mimo-v2.5-free,
https://opencode.ai/zen/v1) — no API key required. To opt back into GLM,
override AI_PROXY_DEFAULT_PROVIDER=glm and set the GLM env vars:
docker run -p 8081:8081 \
-e AI_PROXY_HMAC_SECRET=... \
-e AI_PROXY_DEFAULT_PROVIDER=glm \
-e GLM_API_KEY=sk-... \
-e GLM_BASE_URL=https://rx37rjd.abc-tunnel.us/v1 \
-e GLM_MODEL=Nx/zai-org/GLM-5.2 \
stack-ai-proxyTLS is terminated by a reverse proxy (nginx/Caddy) in front of the FastAPI app.