# sparks-gateway: GLM-5.3 Flash

OpenAI-compatible access to one local model, for agents and harnesses.
This page is machine readable: request it with `Accept: application/json`
or `?format=json` for a structured version. It contains no secrets; replace
`$SPARKS_API_KEY` with your own per-person key.

- Base URL: `https://sparks.davis7.space/v1`
- Model status right now: `glm-5.3-flash` is loaded and serving
- Model id (the only one): `glm-5.3-flash`
- Under the hood: GLM-5.3 Flash (EXL3 4 bpw) on vLLM, tensor-split across
  four DGX Sparks. Context 850,000 tokens, 8 requests run at once (shared
  between everyone), roughly 50 tok/s solo. Full context is allowed; one
  huge request can slow everyone down, so be a good neighbour.
- Privacy: prompts and completions are never stored. Per-key request
  counts, status, latency, and token counts are.

## Connecting

Endpoints (OpenAI compatible):

| Endpoint | Purpose |
|---|---|
| `GET https://sparks.davis7.space/v1/models` | Lists `glm-5.3-flash`, or an empty list while the model is off |
| `POST https://sparks.davis7.space/v1/chat/completions` | Chat, streaming, tools, images |
| `GET /help`, `/llms.txt` | This document (no key needed) |
| `GET /healthz` | Liveness only: `{"ok": true}` |

Not supported: `/embeddings`, `/responses`, Anthropic `/messages`, audio.
Unknown paths and unknown models return OpenAI-shaped `404` errors.

## Auth

Send your key on every `/v1` request, as a bearer token (preferred) or an
`x-api-key` header:

```bash
export SPARKS_API_KEY="sk-sparks-..."        # your personal key, keep it secret
export SPARKS_BASE_URL="https://sparks.davis7.space/v1"
curl -s $SPARKS_BASE_URL/models -H "Authorization: Bearer $SPARKS_API_KEY"
```

Keys look like `sk-sparks-<random>`, are per person, and can be revoked.
Never commit one. If you do not have a key, ask Davis; there is no signup.
A missing or revoked key gives `401` with code `invalid_api_key`.

## Reasoning

Use the normal OpenAI control; the gateway translates it for GLM:

| `reasoning_effort` | Effect |
|---|---|
| `none`, `minimal` | Thinking off (fastest) |
| `low` | Short thinking |
| `medium`, `high` | Long thinking |
| `xhigh`, `max` | Longest thinking |

`reasoning: {"effort": "high"}` works too. Sending a native
`chat_template_kwargs` with `enable_thinking` / `reasoning_effort`
(`low` / `high` / `max`) overrides the translation. Omitting everything
keeps the model default, which is thinking on. Thinking arrives in the
`reasoning` field (`message.reasoning`, or `delta.reasoning` while streaming),
separate from `content`.

## Examples

Non-streaming chat:

```bash
curl -s $SPARKS_BASE_URL/chat/completions \
  -H "Authorization: Bearer $SPARKS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "glm-5.3-flash",
    "messages": [{"role": "user", "content": "What is 19 + 23?"}],
    "reasoning_effort": "none",
    "max_tokens": 256
  }'
```

Example response (trimmed):

```json
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "GLM-5.3-Flash-EXL3",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "42"},
    "finish_reason": "stop"
  }],
  "usage": {"prompt_tokens": 18, "completion_tokens": 3, "total_tokens": 21}
}
```

Streaming (`"stream": true`) is standard SSE: `data:` lines with
`choices[].delta` carrying `reasoning` first (when thinking),
then `content`, ending with `data: [DONE]`. Add
`"stream_options": {"include_usage": true}` and the final chunk carries
`usage`. Example stream (trimmed):

```text
data: {"choices":[{"delta":{"reasoning":"19+23 is..."}}]}
data: {"choices":[{"delta":{"content":"42"}}]}
data: {"choices":[],"usage":{"prompt_tokens":18,"completion_tokens":9,"total_tokens":27}}
data: [DONE]
```

Tools use the normal OpenAI `tools` / `tool_choice` fields, streaming or
not. Images use OpenAI `image_url` content parts (URL or base64 data URL;
GLM accepts many per prompt, request bodies up to 100 MB):

```json
{"role": "user", "content": [
  {"type": "text", "text": "Describe this image."},
  {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
]}
```

`GET /v1/models` response while the model is loaded:

```json
{"object": "list", "data": [
  {"id": "glm-5.3-flash", "object": "model", "created": 0, "owned_by": "sparks-gateway"}
]}
```

## Errors

All errors are `{"error": {"message", "type", "code"}}` in OpenAI shape.

| HTTP | Code | Meaning / what to do |
|---|---|---|
| 400 | `invalid_request` | Bad JSON, missing `messages`, or bad `reasoning_effort`; fix the request |
| 401 | `invalid_api_key` | Key missing, wrong, or revoked; check the key |
| 403 | `forbidden` | Request did not come through the public edge; use the base URL above |
| 404 | `model_not_found` / `not_found` | Only `glm-5.3-flash` and the endpoints above exist |
| 413 | `request_too_large` | Body over 100 MB; shrink embedded images |
| 503 | `model_unavailable` | GLM is not loaded right now; retry later, do not hammer |

One non-gateway error: Cloudflare in front of this API rejects the default
`Python-urllib` user agent with a plain-text `error code: 1010`. Set any
normal `User-Agent` header (the OpenAI SDKs, httpx, curl, and Node clients
already send one) and the request goes through.

## Harnesses

Any client that accepts an OpenAI base URL, API key, and model id works
with `base URL = https://sparks.davis7.space/v1`, `api key = $SPARKS_API_KEY`,
`model = glm-5.3-flash`. Set the key in an environment variable or secret
store, never inline in committed config.

Python (OpenAI SDK):

```python
import os
from openai import OpenAI

client = OpenAI(base_url=os.environ["SPARKS_BASE_URL"],
                api_key=os.environ["SPARKS_API_KEY"])
reply = client.chat.completions.create(
    model="glm-5.3-flash",
    messages=[{"role": "user", "content": "Hello"}],
    reasoning_effort="high",
)
print(reply.choices[0].message.content)
```

JavaScript / TypeScript (OpenAI SDK):

```ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: process.env.SPARKS_BASE_URL,
  apiKey: process.env.SPARKS_API_KEY,
});
const reply = await client.chat.completions.create({
  model: "glm-5.3-flash",
  messages: [{ role: "user", content: "Hello" }],
  reasoning_effort: "high",
});
```

Pi (`models.json` provider block; keep the real key out of git):

```json
{
  "providers": {
    "sparks": {
      "baseUrl": "https://sparks.davis7.space/v1",
      "api": "openai-completions",
      "apiKey": "sk-sparks-...",
      "models": [{ "id": "glm-5.3-flash", "contextWindow": 850000 }]
    }
  }
}
```

Then run `pi --provider sparks --model glm-5.3-flash` and use Pi's normal
thinking controls; they map onto `reasoning_effort`.

OpenCode (`opencode.json`, key read from the environment):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "sparks": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Sparks GLM",
      "options": { "baseURL": "https://sparks.davis7.space/v1", "apiKey": "{env:SPARKS_API_KEY}" },
      "models": { "glm-5.3-flash": { "name": "GLM-5.3 Flash (Sparks)" } }
    }
  },
  "model": "sparks/glm-5.3-flash"
}
```

Other UIs and agents (chat apps, IDE extensions, LangChain, and similar):
choose the "OpenAI compatible" / "custom OpenAI endpoint" option and fill
in the base URL, key, and model id above. Harnesses that only speak the
Anthropic Messages or OpenAI Responses APIs cannot use this gateway
directly; they need an OpenAI-chat-compatible mode or a translating router.

Agent notes: check `GET /v1/models` before a long run (an empty list means
the model is off), prefer `reasoning_effort: "none"` for quick tool turns,
expect queueing when more than 8 streams are active, and back off on
`503` instead of retrying in a tight loop.
