# VibeFlare API — Integration Reference

REST API integration, API keys, direct HTTP calls, embedding vf via fetch/SDK. Loaded from the `vibeflare` skill.

OpenAI-compatible REST API on Cloudflare Worker. Drop-in OpenAI SDK replacement — swap base URL + key format.

## Base URL

```
$VIBEGEN_BASE_URL - required to pre-set            # production deploy (use this)
```

`VIBEFLARE_BASE_URL` / `VIBEFLARE_URL` env var → "[insert here]". Tests + apps must set; absent = vibeflare-image steps skip with `VIBEFLARE_BASE_URL not set`.

## Authentication

All `/v1/*` routes require:

```
Authorization: Bearer vf-<40hex>
```

Key format: `vf-` prefix + 40 hex chars. Server stores sha256 hash only — raw key shown once at creation, never again.

**Exception**: same-origin browser requests may use `vg_sess` cookie + `x-vf-browser: 1` header instead of bearer token.

---

## Creating API Keys

### Via vf CLI (recommended)

```bash
vf login https://your-deploy.workers.dev   # first-time setup
vf keys new "my-project"                    # creates key, prints full value once
vf keys new "ci-key" --admin               # admin key (owner only)
vf keys ls                                 # list all keys
vf keys rm <prefix>                        # revoke key
```

### Via Admin API (session auth required)

```http
POST /admin/keys
Content-Type: application/json
Cookie: vg_sess=<session-token>

{"label": "my-project", "is_admin": false}
```

Response (201):
```json
{
  "id": "...",
  "label": "my-project",
  "full": "vf-<40hex>",
  "prefix": "vf-a1b2c3..."
}
```

**Save `full` immediately** — not retrievable after this response.

```http
GET /admin/keys          → list keys (no key_hash returned)
DELETE /admin/keys/:id   → revoke key
```

---

## Endpoints

### POST /v1/chat/completions

```json
{
  "model": "@cf/meta/llama-3.1-8b-instruct",
  "messages": [
    {"role": "system", "content": "You are helpful."},
    {"role": "user", "content": "Hello"}
  ],
  "max_tokens": 256,
  "temperature": 0.7,
  "stream": false,
  "cache": true,
  "system_id": "optional-server-side-prompt-id"
}
```

VibeFlare extensions (not in OpenAI spec):
- `cache: true` — server-side response cache (D1, 7-day TTL, keyed by sha256 of request)
- `system_id` — server-side cached system prompt id (from `/admin/prompts`); replaces `system` message

Optional query param:
- `?chat_id=<id>` — persist messages to chat history in D1

Response: standard OpenAI `chat.completion` shape.  
Streaming: `"stream": true` → SSE `text/event-stream`, chunks as `chat.completion.chunk`, ends `data: [DONE]`.

---

### POST /v1/embeddings

```json
{
  "model": "@cf/baai/bge-small-en-v1.5",
  "input": "text to embed"
}
```

`input` accepts string or string array. Batches chunked at 100 internally.

Response:
```json
{
  "object": "list",
  "model": "@cf/baai/bge-small-en-v1.5",
  "data": [{"object": "embedding", "index": 0, "embedding": [0.123, ...]}],
  "usage": {"prompt_tokens": 12, "total_tokens": 12}
}
```

---

### POST /v1/images/generations

```json
{
  "model": "@cf/stabilityai/stable-diffusion-xl-base-1.0",
  "prompt": "a cat on a keyboard",
  "n": 1,
  "response_format": "b64_json"
}
```

`response_format`: `"b64_json"` (default for small) or `"url"`.

**Size behavior**: images >100KB auto-stored in R2, response returns URL regardless of `response_format`. Images ≤100KB returned as `b64_json` inline unless `response_format: "url"` requested.

Response:
```json
{
  "created": 1234567890,
  "data": [
    {"b64_json": "..."},
    {"url": "https://.../v1/files/<id>"}
  ]
}
```

File URLs expire after 14 days.

---

### POST /v1/audio/transcriptions

Multipart form data:

```
POST /v1/audio/transcriptions
Content-Type: multipart/form-data

file=<audio-blob>
model=@cf/openai/whisper
response_format=json
```

Max file size: 25MB.  
`response_format`: `"json"` (default) → `{"text": "..."}` | `"text"` → plain text body.

---

### POST /v1/audio/speech

```json
{
  "model": "@cf/myshell-ai/melotts",
  "input": "Hello world",
  "voice": "alloy",
  "response_format": "mp3"
}
```

`response_format`: `"mp3"` (default) or `"wav"`.

**Size behavior**: audio ≤100KB → binary response body. Audio >100KB → stored in R2, returns JSON `{"url": "..."}`. Deviates from OpenAI spec (always binary); account for in client code.

---

### GET /v1/models

```http
GET /v1/models
GET /v1/models?task=text-generation
GET /v1/models/@cf/meta/llama-3.1-8b-instruct
```

Returns models from D1 `models` table (synced from CF API daily).  
Task filter values: `text-generation`, `text-to-image`, `speech-recognition`, `text-to-speech`, `feature-extraction`.

---

### GET /v1/files/:id

Fetch R2-stored file (image or audio) by file id. Requires API key or session auth.

```http
GET /v1/files/<id>
Authorization: Bearer vf-...
```

---

## Error Shape

All errors use OpenAI error format:

```json
{"error": {"type": "quota_exceeded", "message": "daily quota exceeded"}}
```

| Status | Type | Meaning |
|--------|------|---------|
| 400 | `invalid_request` | missing/bad fields |
| 401 | `auth` | missing/invalid bearer token |
| 403 | `forbidden` | insufficient role |
| 404 | `not_found` | unknown model, file, or route |
| 413 | `payload_too_large` | audio >25MB |
| 429 | `quota_exceeded` | 10k neurons/day limit hit |
| 500 | `server_error` | AI inference failed |

---

## Quota

10,000 neurons/day. Shared across all keys on instance. Resets 00:00 UTC.  
Check: `GET /admin/quota` (session auth) or `vf usage`.  
Warn threshold: 90%. Hard cutoff: 100% → 429 on all inference endpoints.

---

## Integration Patterns

### Drop-in OpenAI SDK replacement (Node/Bun/Deno)

```typescript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: process.env.VIBEFLARE_URL + '/v1',
  apiKey: process.env.VIBEFLARE_KEY,
});

const res = await client.chat.completions.create({
  model: '@cf/meta/llama-3.1-8b-instruct',
  messages: [{ role: 'user', content: 'Hello' }],
  max_tokens: 256,
});
```

Works with any OpenAI-compatible SDK. Pass CF model ids as `model`.

### Raw fetch (any language)

```typescript
const res = await fetch(`${VIBEFLARE_URL}/v1/chat/completions`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${VIBEFLARE_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: '@cf/meta/llama-3.1-8b-instruct',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 256,
  }),
});
const data = await res.json();
const text = data.choices[0].message.content;
```

### Python (openai SDK)

```python
import openai, os

client = openai.OpenAI(
    base_url=os.environ["VIBEFLARE_URL"] + "/v1",
    api_key=os.environ["VIBEFLARE_KEY"],
)

resp = client.chat.completions.create(
    model="@cf/meta/llama-3.1-8b-instruct",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=256,
)
print(resp.choices[0].message.content)
```

### Embeddings + vector search

```typescript
const res = await fetch(`${VIBEFLARE_URL}/v1/embeddings`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${VIBEFLARE_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    model: '@cf/baai/bge-small-en-v1.5',
    input: texts,  // string or string[]
  }),
});
const { data } = await res.json();
const vectors = data.map(d => d.embedding);
```

### Response caching (saves neurons on repeated calls)

```typescript
body: JSON.stringify({
  model: '...',
  messages: [...],
  cache: true,   // ← add this field
})
```

Cache hit: neurons charged = 0, response identical to original.

### Server-side system prompts

Create prompt once via admin API or `vf` UI, get its `id`, reference by id in API calls:

```typescript
body: JSON.stringify({
  model: '...',
  messages: [{ role: 'user', content: '...' }],
  system_id: 'prompt-id-from-admin',   // replaces system message
})
```

Avoids resending large system prompts every request.

### Streaming chat

```typescript
const res = await fetch(`${VIBEFLARE_URL}/v1/chat/completions`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${VIBEFLARE_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ model: '...', messages: [...], stream: true }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const lines = decoder.decode(value).split('\n');
  for (const line of lines) {
    if (!line.startsWith('data: ')) continue;
    const payload = line.slice(6);
    if (payload === '[DONE]') break;
    const chunk = JSON.parse(payload);
    process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
  }
}
```

---

## Environment Variables for Projects

| Var | Value |
|-----|-------|
| `VIBEFLARE_URL` / `VIBEFLARE_BASE_URL` | `insert here` |
| `VIBEFLARE_KEY` | `vf-<40hex>` (from `vf keys new`) |
| `VIBEFLARE_MODEL` | default model id (optional) |

---

## Key Management Best Practices

- One key per project/service — rotate independently
- Label keys descriptively (`"ci-pipeline"`, `"my-app-prod"`)
- Admin keys (`is_admin: true`) only for tooling needing `/admin/*` — avoid in app code
- Revoke via `vf keys rm <prefix>` or `DELETE /admin/keys/:id`
- Keys stored as sha256 hash only — if lost, create new + revoke old

---

## Admin API (session auth — not for app code)

Require browser session (`vg_sess` cookie), not API key. Use for tooling/dashboards only.

```
GET  /admin/quota           — current neurons used/limit
GET  /admin/keys            — list keys (no hashes)
POST /admin/keys            — create key
DELETE /admin/keys/:id      — revoke key
GET  /admin/audit           — recent request log (?limit=N, max 200)
GET  /admin/usage           — Analytics Engine timeseries (?range=24h|7d|30d)
GET  /admin/models          — model list (?task=filter)
GET  /admin/prompts         — list cached system prompts
POST /admin/prompts         — create cached system prompt {label, content}
DELETE /admin/prompts/:id   — delete prompt
GET  /admin/cache           — response cache entries
DELETE /admin/cache         — clear all cache entries
GET  /admin/files           — list R2 files
POST /admin/files           — upload file (multipart)
DELETE /admin/files/:id     — delete file
```

---

## Health Check

```http
GET /health
→ {"ok": true, "version": "0.1.0"}
```

No auth required. Use for liveness probes.