# VIABLE Lab API Agent Guide

Use this guide when connecting an app, script, or coding agent to the VIABLE Lab API.

## Base URL

`https://api.viablelab.org`

Use OpenAI-compatible request shapes where possible. Prefer full model IDs for new integrations. VIABLE aliases such as `viable-2` remain stable for existing projects.

The current public chat and Agent contract is OpenAI-compatible. Choosing a
Gemini model makes VIABLE Lab translate that request to Gemini internally; the
API does not currently expose Gemini-native `generateContent`, native Google
SDK authentication, or Gemini Files API routes. A Google Gemini SDK client
therefore cannot yet switch only its key and base URL without changing its
request shape.

For production integrations, create a VIABLE Lab API key from the developer dashboard and send it from server-side code:

```bash
-H "Authorization: Bearer $VIABLELAB_API_KEY"
```

Existing integrations may still work during the migration window when API key enforcement is optional, but new builds should use keys so usage, limits, and request logs are attributed correctly.

## Core Endpoints

| Task | Endpoint | Request shape | Common model |
| --- | --- | --- | --- |
| Chat, reasoning, images, files, PDFs | `POST /v1/chat/completions` | JSON with `model` and `messages` | `gpt-4.1-nano` or `viable-2` |
| Vetted educational chat | `POST /v1/chat/vetting` | Chat JSON plus optional `vettingConfig` | `viable-2` |
| Full answer-key verification | `POST /v1/vetting/full` | JSON with `mode`, `config`, `context`, and `messages` | configured per request |
| Embeddings | `POST /v1/embeddings` | JSON with `model` and `input` | `nomic-embed-text-v1.5` |
| Image generation | `POST /v1/images/generations` | JSON with `model`, `prompt`, optional `size` and `n` | `flux.2-klein` |
| Text to speech | `POST /v1/audio/speech` | JSON with `model`, `input`, `voice`, optional `response_format` | `gpt-4o-mini-tts` |
| Audio transcription | `POST /v1/audio/transcriptions` | multipart form with `model` and `file` | `whisper-large-v3` or `gpt-4o-transcribe` |
| Published agent orchestration | `POST /v1/agents/{slug}/runs` | JSON with `input` and optional `conversation_id`; `input` may be text or content parts | configured by the agent graph |

## Model Catalog

- Human-readable model picker: `https://developers.viablelab.org/models/`
- OpenAPI JSON: `https://developers.viablelab.org/openapi/openapi.json`
- Live API catalog: `GET https://api.viablelab.org/v1/models`
- Machine-readable docs metadata: `GET https://api.viablelab.org/v1/docs/metadata`

Provider labels in public docs are `OpenAI`, `Google`, and `VIABLE Lab`.

## Chat

```bash
curl https://api.viablelab.org/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{
    "model": "gpt-4.1-nano",
    "messages": [
      { "role": "user", "content": "Write one sentence." }
    ]
  }'
```

For multimodal chat, use OpenAI-style content arrays with `text`, `image_url`, `file`, or `input_file` parts. When a Gemini model is selected, supported base64 data URI files and images are converted to Gemini inline data internally; do not rely on remote file fetching or Gemini Files API URIs.

## Agent Orchestration

Agents are visual/code workflows created in the developer dashboard. They can chain model calls, branch, loop within configured budgets, generate images, call allow-listed HTTP tools, and expose the whole workflow behind one API-key-authenticated endpoint.

Use agents when one product interaction needs multiple coordinated steps behind a single endpoint. For example, a writing tutor agent can inspect the latest student message, decide whether an image would help, generate a clean image prompt, call the image model, and then return a student-facing explanation with the generated asset.

Do not force every integration into an agent. If the product only needs one chat, image, embedding, speech, transcription, or VETTING request, use the direct endpoint. Recommend the dashboard agent builder when the user needs branching, repeated model/tool calls, a reusable workflow, or one stable endpoint that hides internal orchestration.

Choose the simplest surface that fits:

| Need | Recommended surface |
| --- | --- |
| One model response, multimodal analysis, coding, classification, or tutoring reply | `POST /v1/chat/completions` |
| Educational guardrails or answer-key verification | `POST /v1/chat/vetting` or `POST /v1/vetting/full` |
| One image, embedding, speech, transcription, encryption, or decryption operation | The direct endpoint for that operation |
| Branching, loops, multiple model/tool calls, image generation plus discussion, or one reusable endpoint that hides internal steps | Dashboard Agents, then `POST /v1/agents/{slug}/runs` |

### Dashboard workflow

1. Open `https://developers.viablelab.org/dashboard/agents`.
2. Create a new agent from a template or open an existing agent.
3. Build visually on the canvas, or open the right-side **Code / AI** tab.
4. If using an external coding agent, paste valid workflow JSON into **Code / AI**, then click **Sync to canvas**.
5. Inspect the canvas and node settings, then click **Validate**.
6. Click **Save** after the graph validates.
7. Use the **Test** tab before publishing. Test runs are metered and use a selected API key.
8. Click **Publish** when ready. The run endpoint is `POST /v1/agents/{slug}/runs`.
9. Make sure the runtime API key has the `agents:execute` scope and access to the agent's project or an explicit grant.

### Helping a user build an agent graph

If you are a coding agent helping a human use VIABLE Lab's agent builder, you can produce the workflow JSON for them. Keep the interaction practical:

- First decide whether they actually need an agent. If a direct endpoint is simpler, say so.
- If they need orchestration, ask what the workflow should do, what models it may use, what it should return, and whether it should preserve conversation history.
- If they already have an agent open, ask them to use **Code / AI → Copy AI prompt** and paste that prompt to you. It includes the current graph, available models, node types, and schema details.
- If the guide is enough, generate a single valid graph JSON object. Do not wrap it in Markdown fences unless the user explicitly asks.
- Tell the user to paste the JSON into **Code / AI**, click **Sync to canvas**, review the canvas, click **Validate**, then **Save**, test in the **Test** tab, and **Publish** when ready.

### Graph JSON contract

The workflow JSON is the same artifact used by the canvas. External AI tools can generate or edit it, and the user can paste it into **Code / AI** to sync the visual canvas.

Top-level shape:

```json
{
  "version": 1,
  "name": "Workflow name",
  "entry": "input",
  "variables": {},
  "nodes": [
    { "id": "input", "type": "input", "next": "tutor", "x": 80, "y": 120 },
    {
      "id": "tutor",
      "type": "llm",
      "config": {
        "model": "gpt-4.1-nano",
        "system": "You are a helpful tutor.",
        "includeHistory": true
      },
      "next": "out",
      "x": 340,
      "y": 120
    },
    {
      "id": "out",
      "type": "output",
      "config": { "text": "{{tutor.content}}" },
      "x": 600,
      "y": 120
    }
  ],
  "edges": []
}
```

Top-level fields:

- `version`: currently `1`.
- `name`: human-readable workflow name.
- `entry`: id of the first node, usually `input`.
- `variables`: optional initial values, referenced as `{{vars.name}}`.
- `nodes`: execution steps.
- `edges`: optional visual edge metadata; runtime execution uses `next` and `branches`.

Each node has this general form:

```json
{
  "id": "unique-id",
  "type": "llm",
  "config": {},
  "next": "another-node-id",
  "branches": {},
  "x": 340,
  "y": 120
}
```

Node id guidance:

- Use short, readable ids such as `input`, `router`, `image`, `discuss`, and `out`.
- Keep ids stable when editing an existing graph. Node ids appear in traces and logs, and stable ids make debugging easier.
- Do not reuse the same id for two nodes in one graph.
- Use `run_id` for one execution, `request_id` for API log/support correlation, and `conversation_id` for the ongoing end-user chat thread. These are runtime identifiers, not graph node ids.

Flow rules:

- `next` points to the next node for single-output nodes.
- `branches` maps branch handles to node ids. A condition usually uses `{ "true": "image", "false": "replyOut" }`.
- At least one `output` node is required. The output node is what the API returns to the caller.
- `llm` and `image` nodes require `config.model`.
- `x` and `y` are canvas coordinates. They are optional, but preserving them keeps the visual layout stable.

Common node types:

| Type | Runtime role | Key config |
| --- | --- | --- |
| `input` | Entry point for the latest user message and conversation history. | none |
| `llm` | Chat/model reasoning, structured decisions, and final text. | `model`, `system`, optional `user`, optional `includeHistory`, optional `outputs` |
| `condition` | Branching based on an earlier field or expression. | `field`, `op`, optional `value`, or advanced `expr` |
| `image` | Image generation from a clean standalone prompt. | `model`, `prompt`, optional `size`, optional `n` |
| `http` | Allow-listed HTTPS tool call. Disabled unless the API owner has configured the allowlist. | `url`, `method`, optional `body` |
| `loop` | Controlled repetition by count or expression. | `count` or `while`, optional `var`; branches are `continue` and `done` |
| `transform` | Lightweight value reshaping with templates. | `output` JSON |
| `output` | Final response text and optional image attachment URL. | `text`, optional `attachments` |
| `subagent` | Reserved for nested workflows. | not enabled in v1 runtime validation |

### LLM nodes and structured outputs

LLM nodes are chat-first. `config.system` should describe the role and task clearly, not just a routing label. By default, `config.includeHistory` is true, so the model sees prior turns for the current `conversation_id`.

Use `config.outputs` when later steps need reliable fields:

```json
{
  "id": "router",
  "type": "llm",
  "config": {
    "model": "gpt-4.1-nano",
    "includeHistory": true,
    "system": "You are a writing tutor. Reply warmly and decide whether an image would help.",
    "outputs": [
      { "name": "reply", "type": "string", "description": "Student-facing reply when no image is needed." },
      { "name": "use_image", "type": "boolean", "description": "True when the student asks for an image or one would genuinely help." },
      { "name": "image_prompt", "type": "string", "description": "Clean standalone image description based on the student request or writing need; otherwise empty." }
    ]
  },
  "next": "branch"
}
```

Structured fields are exposed as `{{router.json.reply}}`, `{{router.json.use_image}}`, and `{{router.json.image_prompt}}`. If an LLM has no structured outputs, its plain text is exposed as `{{router.content}}`.

### Conditions, templates, and expressions

Condition nodes usually test a structured output from an earlier LLM:

```json
{
  "id": "branch",
  "type": "condition",
  "config": { "field": "router.json.use_image", "op": "truthy" },
  "branches": { "true": "image", "false": "replyOut" }
}
```

Supported condition operators include `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `truthy`, and `falsy`.

Any string config value can interpolate earlier values:

- `{{input}}`: the latest user message.
- `{{router.content}}`: plain text from an LLM node.
- `{{router.json.image_prompt}}`: a structured output field.
- `{{image.url}}`: generated image URL from an image node.
- `{{vars.count}}`: a workflow variable.

Advanced condition expressions and loop `while` expressions support dot paths and basic operators: `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`, `+`, `-`, `*`, and `/`. Do not rely on function calls or arbitrary JavaScript evaluation.

### Image-generation pattern

When an LLM decides whether to generate an image, keep the image prompt separate from the chat reply:

- The router LLM returns `use_image`, `reply`, and `image_prompt`.
- The `condition` node sends image requests only when `use_image` is true.
- The `image` node receives only `{{router.json.image_prompt}}`, not the whole chat transcript.
- A follow-up LLM can discuss the generated image using the prompt and the student message, even if the chat model does not inspect image pixels.
- The `output` node returns text and the generated asset URL with `attachments`.

Example graph:

```json
{
  "version": 1,
  "name": "Writing tutor with optional image",
  "entry": "input",
  "variables": {},
  "nodes": [
    { "id": "input", "type": "input", "next": "router", "x": 40, "y": 220 },
    {
      "id": "router",
      "type": "llm",
      "x": 250,
      "y": 220,
      "config": {
        "model": "gpt-4.1-nano",
        "includeHistory": true,
        "system": "You are a warm, patient writing tutor. Chat naturally, help the student write, and only use an image when it would genuinely help.",
        "outputs": [
          { "name": "reply", "type": "string", "description": "Student-facing reply when no image is needed." },
          { "name": "use_image", "type": "boolean", "description": "True if the student asks for an image or one would help this turn." },
          { "name": "image_prompt", "type": "string", "description": "Clean standalone image prompt based on the student request or writing need; otherwise empty." }
        ]
      },
      "next": "branch"
    },
    {
      "id": "branch",
      "type": "condition",
      "x": 470,
      "y": 220,
      "config": { "field": "router.json.use_image", "op": "truthy" },
      "branches": { "true": "image", "false": "replyOut" }
    },
    {
      "id": "image",
      "type": "image",
      "x": 690,
      "y": 110,
      "config": { "model": "flux.2-klein", "prompt": "{{router.json.image_prompt}}" },
      "next": "discuss"
    },
    {
      "id": "discuss",
      "type": "llm",
      "x": 910,
      "y": 110,
      "config": {
        "model": "gpt-4.1-nano",
        "includeHistory": true,
        "system": "Continue as the same writing tutor. An image matching the student's request or writing need has just been generated and shown to the student. Present it as an image created for the student; if the student requested the image, say it is what they asked for. Never say you asked for the image. Then connect it to their writing and keep helping.",
        "user": "The student just said: \"{{input}}\". The generated image is described as: \"{{router.json.image_prompt}}\". Present it as an image created for the student request or writing goal, and never say \"I asked for\"."
      },
      "next": "outImage"
    },
    {
      "id": "outImage",
      "type": "output",
      "x": 1140,
      "y": 110,
      "config": { "text": "{{discuss.content}}", "attachments": "{{image.url}}" }
    },
    {
      "id": "replyOut",
      "type": "output",
      "x": 690,
      "y": 320,
      "config": { "text": "{{router.json.reply}}" }
    }
  ],
  "edges": []
}
```

### Running a published agent

```bash
curl https://api.viablelab.org/v1/agents/my-writing-tutor/runs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $VIABLELAB_API_KEY" \
  -d '{
    "input": "Can you show me a cat and help me write about it?"
  }'
```

The response includes `conversation_id`, `run_id`, `output`, `usage`, `cost`,
and `input_assets` for exact media archived during that turn.

For image input, send supported content-part aliases and make sure the LLM node that receives the user turn uses an image-capable model. For example, `gemma-4-31b-it` accepts text plus image input, while `gpt-oss-120b` is text-only and returns `unsupported_input_modality` before a provider call. Inline images are limited to 8 MiB each (10 MiB total per turn); the full Agent JSON body is limited to 12 MiB:

```bash
curl https://api.viablelab.org/v1/agents/my-writing-tutor/runs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $VIABLELAB_API_KEY" \
  -d '{
    "input": [
      { "type": "input_text", "text": "What is this image?" },
      { "type": "input_image", "image_url": "https://example.com/image.png", "filename": "classroom.png" }
    ]
  }'
```

The dashboard also accepts a local PNG, JPEG, WebP, GIF, HEIC, or HEIF file.
The API validates its declared type and signature, archives the exact bytes in
private research storage, and stores an authenticated asset reference in the
request log, run trace, and conversation history. An HTTPS image URL is kept
exactly as restricted provenance, including query parameters, but VIABLE Lab
does not fetch arbitrary remote URLs automatically. Source-controlled signed
URLs may expire; URLs with embedded username/password credentials are rejected.

When an LLM node defines a custom `config.user`, current-turn media is appended
to that rendered prompt by default. Set `includeInputMedia: false` only when a
node intentionally needs text alone. Historical media is retained for
compatible models within an 8 MiB history-media budget and represented by a
clear label when the model cannot accept it, the budget is exhausted, or the
stored object is unavailable. The private archive remains available to
authorized researchers even when media is omitted from a model call.

Request-log and Agent-run JSONL exports include both `download_url` and
`view_url` for each archived research asset. CSV exports keep the complete
`research_assets` JSON and also include `research_asset_count`,
`research_asset_ids`, `research_asset_view_urls`, and
`research_asset_download_urls` columns for scripts and spreadsheets.
Researchers can open `view_url` to inspect an asset or `download_url` to sign in
and start an authorized download of a private archived object. External URL
records remain view-only because VIABLE Lab stores their exact provenance but
does not fetch or redistribute the remote bytes. Users do not need to know or
manage the portal's underlying authentication provider. These links do not
make private R2 objects public and are not long-lived signed URLs.

Preserve the returned `conversation_id` in your app and send it on later turns:

```bash
curl https://api.viablelab.org/v1/agents/my-writing-tutor/runs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $VIABLELAB_API_KEY" \
  -d '{
    "input": "Can we make the cat green instead?",
    "conversation_id": "agtc_..."
  }'
```

### Conversation IDs

`conversation_id` is currently specific to agent runs. It identifies one continuing chat thread between an end user and one agent. The first agent run creates it; later runs should pass it back to preserve server-side history and turn ordering.

This is different from normal chat endpoints:

- `/v1/chat/completions`, `/v1/chat/vetting`, and `/v1/vetting/full` do not use this agent `conversation_id`. Include the context required by the request, and treat any app- or provider-level session state as separate from this field.
- `/v1/agents/{slug}/runs` stores text plus private research asset references server-side and uses `conversation_id` to continue it. Compatible vision models can receive a bounded materialized copy of prior image inputs on later turns.
- A `conversation_id` is bound to the agent and the API key or user that created it. Passing another key's conversation returns an agent conversation error instead of opening someone else's thread.
- If a client does not save `conversation_id`, every request starts a new agent conversation. Manually putting prior turns into `input` can help the model see context, but it is not reliable for platform-level traceability, ordering, run grouping, or later audit.

Recommended client storage pattern:

```text
your_end_user_session_id + agent_slug -> conversation_id
```

Use `run_id` for one specific agent execution, `request_id` for API support/log correlation, and `conversation_id` for the ongoing chat thread.

## VETTING

VETTING is not a model. It is a verification architecture layered on top of chat models. A chat model drafts a response, then a verification pass checks whether the response follows the requested educational or policy constraint. If a response fails, the API can retry with feedback until it passes or reaches the configured attempt limit.

VETTING is useful when a product needs a tutor-like assistant that guides users without directly giving away answers, verifies answers against an answer key, or records why a response passed or failed. VETTING endpoints are intentionally non-streaming because the API needs the full response before it can verify it.

Use different models by setting the normal `model` field on `/v1/chat/vetting`, or by setting `config.chatModel.modelId` and `config.verificationModel.modelId` on `/v1/vetting/full`. On `/v1/chat/vetting`, `model` controls the drafted chat response and the verification model is managed internally. On `/v1/vetting/full`, the chat and verification models are explicit and can be different providers.

VETTING uses the same chat model registry as `/v1/chat/completions`. OpenAI chat models, Gemini chat models, VIABLE Lab chat models, VIABLE aliases, and future clearly inferable chat model IDs can be used where a chat model is required. Non-chat models such as embeddings, image generation, speech, and transcription models are rejected on VETTING endpoints with a structured 400 error.

Encrypted prompt values are field-scoped. Automatic `enc::...` decryption is supported only for `/v1/chat/vetting` `vettingConfig.systemPrompt` and system message content, and for `/v1/vetting/full` `prompts.chatSystemPrompt` and `prompts.verificationSystemPrompt`. Other endpoints should receive plain request values unless their docs explicitly add encrypted fields.

### Which VETTING Endpoint To Use

| Endpoint | Use when | Request style | Main response fields |
| --- | --- | --- | --- |
| `POST /v1/chat/vetting` | You already use Chat Completions and want a lightweight verification layer. | OpenAI-compatible chat JSON plus optional `vettingConfig`. | Standard `choices`, `usage`, plus `vetting_info` and `safety_signals`. |
| `POST /v1/vetting/full` | You need answer-key verification, detailed attempt records, separate chat and verification models, or session/question metadata. | Structured JSON with `mode`, `config`, `prompts`, `context`, and `messages`. | `result`, `verification`, `usage`, `metadata`, and `safety_signals`. |

### Lightweight Vetted Chat

Use `/v1/chat/vetting` when the app needs a normal chat response plus `vetting_info`. This is the easiest VETTING integration because it keeps the Chat Completions shape.

Key request fields:

| Field | Required | Notes |
| --- | --- | --- |
| `model` | Optional | Full chat model ID or VIABLE alias. Defaults apply if omitted. Examples: `viable-2`, `gpt-4.1-nano`, `gemini-2.5-flash`. |
| `messages` | Required | Same role/content format as `/v1/chat/completions`; multimodal content is allowed for image-capable chat models. |
| `temperature`, `max_tokens` | Optional | Passed to the chat generation step when supported. |
| `vettingConfig.systemPrompt` | Optional | Guideline checked by the internal verification pass, such as "do not reveal the final answer." |
| `vettingConfig.maxAttempts` | Optional | Maximum generation/verification attempts. Defaults to 3. |

Key response fields:

| Field | Notes |
| --- | --- |
| `choices[0].message.content` | The final vetted assistant response. |
| `usage` | Standard token usage for the returned response. |
| `vetting_info.attemptCount` | Number of internal generation/verification attempts. |
| `vetting_info.stopReason` | `VERIFICATION_PASSED`, `MAX_ATTEMPTS_REACHED`, `GENERATION_ERROR`, `VERIFICATION_ERROR`, or `SAFETY_TRIGGERED`. |
| `vetting_info.lastFailureReason` | Most recent verification failure reason when available. |
| `safety_signals.requires_attention` | True when the response should be reviewed or handled specially. |

```bash
curl https://api.viablelab.org/v1/chat/vetting \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{
    "model": "viable-2",
    "messages": [
      { "role": "user", "content": "Explain photosynthesis without giving away a homework answer." }
    ],
    "vettingConfig": { "maxAttempts": 1 }
  }'
```

### Full VETTING Workflow

Use `/v1/vetting/full` when the app needs answer-key verification and detailed attempt metadata. This endpoint is more explicit and is better for educational products, grading support, tutoring workflows, and research logging.

Modes:

| Mode | Behavior |
| --- | --- |
| `vetting` | Runs chat generation plus verification against `context.items`. Requires `config.verificationModel` and answer-key context. |
| `chat` | Uses the same structured request format but skips verification. `verification` is `null` and `verificationModelUsed` is `null`. |

Key request fields:

| Field | Required | Notes |
| --- | --- | --- |
| `mode` | Required | `vetting` or `chat`. |
| `config.chatModel.modelId` | Required | Chat model or VIABLE alias used to draft the response. Examples: `viable-2`, `gpt-4.1-nano`, `gemini-2.5-flash`, `llama-3.1-70b-instruct`. |
| `config.chatModel.temperature`, `config.chatModel.maxTokens`, `config.chatModel.topP` | Optional | Per-model generation controls. |
| `config.verificationModel.modelId` | Required in `vetting` mode | Chat-capable verification model or VIABLE alias. This can be different from the drafting model and can use a different provider. |
| `config.maxVerificationAttempts` | Optional | Maximum verification loop attempts. Defaults to 3. |
| `prompts` | Required | Prompt container object; individual fields have defaults. |
| `prompts.chatSystemPrompt` | Optional | Tutor or assistant behavior prompt. Defaults to a guided tutor prompt. |
| `prompts.verificationSystemPrompt` | Optional | Evaluator behavior prompt. Defaults are built from the chat prompt and context. |
| `context.sessionId`, `context.userId` | Optional | Passed through to metadata for traceability. |
| `context.items[]` | Required in `vetting` mode | Question and answer-key records used by verification. |
| `context.items[].question.text` | Required in `vetting` mode | The question, task, or prompt being protected. |
| `context.items[].answerKey.correctAnswer` | Required in `vetting` mode | Reference answer used to decide whether the assistant revealed too much. |
| `context.items[].answerKey.keyConcepts` | Optional | Concepts or terms that should not be directly disclosed. |
| `messages` | Required | Conversation messages ending with the latest user request. Put system-level behavior in `prompts`, not in `messages`. |

Key response fields:

| Field | Notes |
| --- | --- |
| `status` | `success` on successful processing. |
| `mode` | The mode used for the response. |
| `result.role`, `result.content` | Final assistant message after the verification loop. |
| `verification.passed` | Whether the final response passed verification. |
| `verification.attemptCount` | Number of chat generation attempts. |
| `verification.stopReason` | Why the loop stopped. |
| `verification.attempts[]` | Per-attempt `chatResponse`, `verificationPassed`, `verificationOutput`, and optional `requiresAttention`. |
| `usage.chatTokens`, `usage.verificationTokens`, `usage.totalTokens` | Aggregated token usage across chat and verification calls. |
| `metadata.processingTimeMs`, `metadata.timestamp`, `metadata.sessionId`, `metadata.userId` | Trace metadata. |
| `metadata.chatModelUsed`, `metadata.verificationModelUsed` | Requested model IDs or aliases used for the workflow. |
| `safety_signals.requires_attention` | True when the response requires special handling or review. |

Minimal full VETTING request:

```bash
curl https://api.viablelab.org/v1/vetting/full \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{
    "mode": "vetting",
    "config": {
      "maxVerificationAttempts": 3,
      "chatModel": { "modelId": "viable-2", "temperature": 0.7 },
      "verificationModel": { "modelId": "gemini-2.5-flash", "temperature": 0.1 }
    },
    "prompts": {
      "chatSystemPrompt": "Guide the student without giving away the final answer.",
      "verificationSystemPrompt": "Pass only responses that guide without revealing the answer key."
    },
    "context": {
      "sessionId": "session-123",
      "userId": "user-456",
      "items": [
        {
          "question": { "id": "q1", "text": "What is 6 x 7?", "subject": "Math" },
          "answerKey": {
            "correctAnswer": "42",
            "keyConcepts": ["multiplication", "42"]
          }
        }
      ]
    },
    "messages": [
      { "role": "user", "content": "Just tell me the answer to this homework question." }
    ]
  }'
```

## Embeddings

```bash
curl https://api.viablelab.org/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{ "model": "nomic-embed-text-v1.5", "input": "Searchable text" }'
```

## Image Generation

```bash
curl https://api.viablelab.org/v1/images/generations \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{
    "model": "flux.2-klein",
    "prompt": "A clean diagram of photosynthesis",
    "size": "1024x1024",
    "n": 1
  }'
```

## Speech

```bash
curl https://api.viablelab.org/v1/audio/speech \
  -H "Content-Type: application/json" \
  -H "X-Platform-ID: your-platform" \
  -d '{
    "model": "gpt-4o-mini-tts",
    "input": "Hello from VIABLE Lab.",
    "voice": "alloy",
    "response_format": "mp3"
  }' \
  --output speech.mp3
```

## Transcription

```bash
curl https://api.viablelab.org/v1/audio/transcriptions \
  -H "X-Platform-ID: your-platform" \
  -F model=gpt-4o-transcribe \
  -F file=@sample.mp3
```

Use `duration_seconds` or `duration_minutes` when known so cost logs can be more accurate.

## Operational Notes

- Include `X-Platform-ID` from server-side calls.
- Use the developer dashboard to manage keys, set limits, build and publish agents, run metered Playground and agent tests, review usage, and export request logs by key, endpoint, model, status, agent, run, or platform.
- Use `stream: true` only on `/v1/chat/completions`.
- VETTING endpoints are intentionally non-streaming.
- Agent runs are non-streaming today. They can return text plus generated asset URLs when the graph produces attachments.
- Request and response text is retained in request logs by default for research replay. Agent inline media is preserved byte-for-byte in private R2 and shown through authenticated asset references instead of raw base64 in D1. Exact external HTTPS provenance is retained on restricted detail/export surfaces. Exports include a sign-in-aware `view_url` for each research asset. When an upstream provider rejects VIABLE Lab credentials, callers receive a provider-neutral service error while authorized request-log detail and exports retain the numeric `upstream_status` for diagnosis. Authorization headers, cookies, API keys, tokens, passwords, client secrets, and private keys are always redacted; no logging mode disables that boundary.
- For current model IDs and pricing notes, read `/v1/models` or `/v1/docs/metadata` instead of hardcoding lists.
