mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 00:19:14 +00:00
1049 lines
32 KiB
Markdown
1049 lines
32 KiB
Markdown
# API Reference
|
|
|
|
This document provides a complete reference for the DeerFlow backend APIs.
|
|
|
|
## Overview
|
|
|
|
DeerFlow backend exposes two sets of APIs:
|
|
|
|
1. **LangGraph-compatible API** - Agent interactions, threads, and streaming (`/api/langgraph/*`)
|
|
2. **Gateway API** - Models, MCP, skills, uploads, and artifacts (`/api/*`)
|
|
|
|
All APIs are accessed through the Nginx reverse proxy at port 2026.
|
|
|
|
For agent conversations, clients can either pre-create a thread
|
|
(`POST /api/langgraph/threads`) or start immediately with the stateless stream
|
|
endpoint (`POST /api/langgraph/runs/stream`). The latter auto-creates a thread
|
|
and returns `thread_id` and `run_id` in the response `Content-Location` header.
|
|
|
|
## LangGraph-compatible API
|
|
|
|
Base URL: `/api/langgraph`
|
|
|
|
The public LangGraph-compatible API follows LangGraph SDK conventions. In the unified nginx deployment, Gateway owns `/api/langgraph/*` and translates those paths to its native `/api/*` run, thread, and streaming routers.
|
|
|
|
### Threads
|
|
|
|
#### Create Thread
|
|
|
|
```http
|
|
POST /api/langgraph/threads
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"metadata": {}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"thread_id": "abc123",
|
|
"created_at": "2024-01-15T10:30:00Z",
|
|
"metadata": {}
|
|
}
|
|
```
|
|
|
|
#### Get Thread State
|
|
|
|
```http
|
|
GET /api/langgraph/threads/{thread_id}/state
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"values": {
|
|
"messages": [...],
|
|
"sandbox": {...},
|
|
"artifacts": [...],
|
|
"thread_data": {...},
|
|
"title": "Conversation Title"
|
|
},
|
|
"next": [],
|
|
"config": {...}
|
|
}
|
|
```
|
|
|
|
### Runs
|
|
|
|
#### Create Run
|
|
|
|
Execute the agent with input.
|
|
|
|
```http
|
|
POST /api/langgraph/threads/{thread_id}/runs
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"input": {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "Hello, can you help me?"
|
|
}
|
|
]
|
|
},
|
|
"config": {
|
|
"recursion_limit": 100,
|
|
"configurable": {
|
|
"model_name": "gpt-4",
|
|
"thinking_enabled": false,
|
|
"is_plan_mode": false
|
|
}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}
|
|
```
|
|
|
|
**Stream Mode Compatibility:**
|
|
- Use: `values`, `messages-tuple`, `custom`, `updates`, `debug`, `tasks`, `checkpoints`
|
|
- Unsupported modes, including `messages`, `events`, and `tools`, return `422` before a run is created. DeerFlow never substitutes `values` for an unsupported mode.
|
|
|
|
**Run Option Compatibility:**
|
|
- Supported concurrency strategies: `reject`, `rollback`, and `interrupt`
|
|
- Compatibility default: `if_not_exists="create"`; this matches DeerFlow's current behavior
|
|
- Artifact delivery is enforced automatically when a run creates or modifies regular files under `/mnt/user-data/outputs`. `present_files` must present at least one path produced by the current run (or a directory containing it), and the terminal receipt must be persisted; presenting only an unrelated file does not satisfy delivery. Runs without changed outputs retain ordinary conversational behavior. `artifact_delivery` is not a client-settable run option.
|
|
- Unsupported options return `422`: `webhook`, `stream_resumable=true`, `after_seconds`, `feedback_keys`, any non-null `on_completion` value (including the SDK values `"complete"` and `"continue"`), `if_not_exists="reject"`, and `multitask_strategy="enqueue"`
|
|
- `stream_resumable=false` is accepted: it is the LangGraph SDK's default and requests the non-resumable stream DeerFlow already serves
|
|
- Undeclared SDK options, including `checkpoint_during` and `durability`, also return `422` instead of being silently discarded
|
|
|
|
When outputs changed during the run, `run.delivery` events retain the Slice 1
|
|
facts (`presented`, `paths`, and `by_tool`) and add `produced_paths`,
|
|
`presented_paths`, `matched_paths`, plus an explicit verdict: `verification`,
|
|
`stage` (`presented`, `mismatched`, or `not_started`), and `satisfied`. Receipts
|
|
for runs without changed outputs keep their existing shape.
|
|
|
|
**Recursion Limit:**
|
|
|
|
`config.recursion_limit` caps the number of graph steps LangGraph will execute
|
|
in a single run. The unified Gateway path defaults to `100` in
|
|
`build_run_config` (see `backend/app/gateway/services.py`), which is a safer
|
|
starting point for plan-mode or subagent-heavy runs. Clients can still set
|
|
`recursion_limit` explicitly in the request body; increase it if you run deeply
|
|
nested subagent graphs. For safety, the Gateway clamps any client-supplied value
|
|
to a configurable server ceiling (`max_recursion_limit` in `config.yaml`,
|
|
default `1000`) so a single run cannot execute unbounded graph steps (runaway
|
|
LLM cost / DoS); invalid or non-positive values fall back to the `100` default.
|
|
|
|
**Configurable Options:**
|
|
- `model_name` (string): Override the default model
|
|
- `thinking_enabled` (boolean): Enable extended thinking for supported models
|
|
- `is_plan_mode` (boolean): Enable TodoList middleware for task tracking
|
|
|
|
**Response:** Server-Sent Events (SSE) stream
|
|
|
|
```
|
|
event: values
|
|
data: {"messages": [...], "title": "..."}
|
|
|
|
event: messages
|
|
data: {"content": "Hello! I'd be happy to help.", "role": "assistant"}
|
|
|
|
event: end
|
|
data: {}
|
|
```
|
|
|
|
#### Get Run History
|
|
|
|
```http
|
|
GET /api/langgraph/threads/{thread_id}/runs
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"runs": [
|
|
{
|
|
"run_id": "run123",
|
|
"status": "success",
|
|
"created_at": "2024-01-15T10:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Stream Run
|
|
|
|
Stream responses in real-time.
|
|
|
|
```http
|
|
POST /api/langgraph/threads/{thread_id}/runs/stream
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Same request body as Create Run. Returns SSE stream.
|
|
|
|
#### Stateless Stream Run
|
|
|
|
Start a conversation without creating a thread first. Gateway auto-creates a
|
|
thread when `config.configurable.thread_id` is omitted, and returns both
|
|
identifiers in the response `Content-Location` header.
|
|
|
|
```http
|
|
POST /api/langgraph/runs/stream
|
|
Content-Type: application/json
|
|
Accept: text/event-stream
|
|
```
|
|
|
|
Through Nginx, `/api/langgraph/runs/stream` is rewritten to the native Gateway
|
|
path `POST /api/runs/stream`.
|
|
|
|
**Request Body:** Same as [Create Run](#create-run). Omit `thread_id` to start a
|
|
new conversation; include it to continue an existing one:
|
|
|
|
```json
|
|
{
|
|
"input": {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "Hello, can you help me?"
|
|
}
|
|
]
|
|
},
|
|
"config": {
|
|
"recursion_limit": 100,
|
|
"configurable": {
|
|
"model_name": "gpt-4",
|
|
"thinking_enabled": false,
|
|
"is_plan_mode": false
|
|
}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}
|
|
```
|
|
|
|
**Response:** Server-Sent Events (SSE) stream with a `Content-Location` header:
|
|
|
|
```http
|
|
Content-Location: /api/threads/{thread_id}/runs/{run_id}
|
|
```
|
|
|
|
Clients should parse `thread_id` and `run_id` from this header (the path ends
|
|
with `/runs/{run_id}`). Persist `thread_id` and send it back on the next turn
|
|
via `config.configurable.thread_id` to keep conversation history.
|
|
|
|
**Continuing a conversation:**
|
|
|
|
```json
|
|
{
|
|
"input": {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "What did I just ask?"
|
|
}
|
|
]
|
|
},
|
|
"config": {
|
|
"configurable": {
|
|
"thread_id": "abc123",
|
|
"model_name": "gpt-4"
|
|
}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Gateway API
|
|
|
|
Base URL: `/api`
|
|
|
|
### Models
|
|
|
|
#### List Models
|
|
|
|
Get all available LLM models from configuration.
|
|
|
|
```http
|
|
GET /api/models
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"models": [
|
|
{
|
|
"name": "gpt-4",
|
|
"display_name": "GPT-4",
|
|
"supports_thinking": false,
|
|
"supports_vision": true
|
|
},
|
|
{
|
|
"name": "claude-3-opus",
|
|
"display_name": "Claude 3 Opus",
|
|
"supports_thinking": false,
|
|
"supports_vision": true
|
|
},
|
|
{
|
|
"name": "deepseek-v3",
|
|
"display_name": "DeepSeek V3",
|
|
"supports_thinking": true,
|
|
"supports_vision": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Get Model Details
|
|
|
|
```http
|
|
GET /api/models/{model_name}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"name": "gpt-4",
|
|
"display_name": "GPT-4",
|
|
"model": "gpt-4",
|
|
"max_tokens": 4096,
|
|
"supports_thinking": false,
|
|
"supports_vision": true
|
|
}
|
|
```
|
|
|
|
### MCP Configuration
|
|
|
|
#### Get MCP Config
|
|
|
|
Get current MCP server configurations.
|
|
|
|
```http
|
|
GET /api/mcp/config
|
|
```
|
|
|
|
Requires an authenticated admin session. Sensitive env/header/OAuth secret
|
|
values are masked in the response.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"mcp_servers": {
|
|
"github": {
|
|
"enabled": true,
|
|
"type": "stdio",
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
"env": {
|
|
"GITHUB_TOKEN": "***"
|
|
},
|
|
"description": "GitHub operations"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Update MCP Config
|
|
|
|
Update MCP server configurations.
|
|
|
|
```http
|
|
PUT /api/mcp/config
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Requires an authenticated admin session. API-managed `stdio` MCP servers may
|
|
only use allowed executable names for `command` (default: `npx`, `uvx`). Set
|
|
`DEER_FLOW_MCP_STDIO_COMMAND_ALLOWLIST` to a comma-separated list when a
|
|
deployment needs additional trusted launchers.
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"mcp_servers": {
|
|
"github": {
|
|
"enabled": true,
|
|
"type": "stdio",
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
"env": {
|
|
"GITHUB_TOKEN": "$GITHUB_TOKEN"
|
|
},
|
|
"description": "GitHub operations"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"mcp_servers": {
|
|
"github": {
|
|
"enabled": true,
|
|
"type": "stdio",
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
"env": {
|
|
"GITHUB_TOKEN": "***"
|
|
},
|
|
"description": "GitHub operations"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Update One MCP Server State
|
|
|
|
Enable or disable one configured MCP server without replacing the full
|
|
extensions configuration.
|
|
|
|
```http
|
|
PATCH /api/mcp/config
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Requires an authenticated admin session. Enabling a `stdio` server validates
|
|
that server's `command` against the same allowlist used by the full `PUT`
|
|
endpoint. Disabling a server does not require its command to be allowlisted, and
|
|
invalid commands on other servers do not block the update. The endpoint
|
|
preserves secrets, environment-variable placeholders, skills, custom server
|
|
fields, and other top-level extensions config. SSE/HTTP targets may use either
|
|
DeerFlow's `type` field or the MCP-spec `transport` field.
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"server_name": "semantic-scholar",
|
|
"enabled": false
|
|
}
|
|
```
|
|
|
|
The response is the full masked MCP configuration, matching `GET` and `PUT`.
|
|
An unknown `server_name` returns `404`; attempting to enable a server with a
|
|
disallowed `stdio` command returns `400`.
|
|
|
|
#### Reset MCP Tools Cache
|
|
|
|
Clear cached MCP tools and persistent MCP sessions process-wide. This affects
|
|
all threads and users in the current Gateway process. Tools are loaded again
|
|
from configured MCP servers on the next agent run or tool lookup.
|
|
|
|
```http
|
|
POST /api/mcp/cache/reset
|
|
```
|
|
|
|
Requires an authenticated admin session.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "MCP tools cache reset. Tools will reload on next use."
|
|
}
|
|
```
|
|
|
|
### Skills
|
|
|
|
#### List Skills
|
|
|
|
Get all available skills.
|
|
|
|
```http
|
|
GET /api/skills
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"skills": [
|
|
{
|
|
"name": "pdf-processing",
|
|
"display_name": "PDF Processing",
|
|
"description": "Handle PDF documents efficiently",
|
|
"enabled": true,
|
|
"license": "MIT",
|
|
"path": "public/pdf-processing"
|
|
},
|
|
{
|
|
"name": "frontend-design",
|
|
"display_name": "Frontend Design",
|
|
"description": "Design and build frontend interfaces",
|
|
"enabled": false,
|
|
"license": "MIT",
|
|
"path": "public/frontend-design"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Get Skill Details
|
|
|
|
```http
|
|
GET /api/skills/{skill_name}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"name": "pdf-processing",
|
|
"display_name": "PDF Processing",
|
|
"description": "Handle PDF documents efficiently",
|
|
"enabled": true,
|
|
"license": "MIT",
|
|
"path": "public/pdf-processing",
|
|
"allowed_tools": ["read_file", "write_file", "bash"],
|
|
"content": "# PDF Processing\n\nInstructions for the agent..."
|
|
}
|
|
```
|
|
|
|
#### Enable Skill
|
|
|
|
```http
|
|
POST /api/skills/{skill_name}/enable
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Skill 'pdf-processing' enabled"
|
|
}
|
|
```
|
|
|
|
#### Disable Skill
|
|
|
|
```http
|
|
POST /api/skills/{skill_name}/disable
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Skill 'pdf-processing' disabled"
|
|
}
|
|
```
|
|
|
|
#### Install Skill
|
|
|
|
Install a skill from a `.skill` file.
|
|
|
|
```http
|
|
POST /api/skills/install
|
|
Content-Type: multipart/form-data
|
|
```
|
|
|
|
**Request Body:**
|
|
- `file`: The `.skill` file to install
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Skill 'my-skill' installed successfully",
|
|
"skill": {
|
|
"name": "my-skill",
|
|
"display_name": "My Skill",
|
|
"path": "custom/my-skill"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Reload Skills
|
|
|
|
Invalidate the skill prompt caches for every user in the current Gateway
|
|
process. Subsequent runs rescan the configured public, custom, and legacy skill
|
|
directories; runs that have already started keep their existing skill snapshot.
|
|
|
|
```http
|
|
POST /api/skills/reload
|
|
```
|
|
|
|
The request has no body and requires an authenticated administrator. For a
|
|
cookie-authenticated request, send the CSRF cookie value in the matching header:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:2026/api/skills/reload \
|
|
-b cookies.txt \
|
|
-H "X-CSRF-Token: <csrf_token-cookie-value>"
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"scope": "process",
|
|
"message": "Skill caches invalidated; subsequent runs in this Gateway process will rescan the latest skills."
|
|
}
|
|
```
|
|
|
|
`success` confirms cache invalidation, not that every file on disk was valid:
|
|
malformed skills retain the existing parser behavior of being skipped and
|
|
logged. The endpoint returns `401` for unauthenticated callers, `403` for
|
|
non-admin users, and a generic `500` if the invalidation mechanism itself
|
|
fails or the process-local background scan does not finish within the cache
|
|
refresh timeout. A loader-level failure, such as an unavailable mounted root,
|
|
does not publish an empty catalog: the last successfully loaded process cache
|
|
remains available. A timed-out scan continues in its daemon worker and can
|
|
still populate the process cache when it finishes.
|
|
|
|
The scope is deliberately process-local. Each Uvicorn worker or Kubernetes Pod
|
|
must be called directly; repeated requests through a load-balanced Service do
|
|
not guarantee that every instance is reached. External MinIO/NFS/CSI writes
|
|
bypass the validation, SkillScan, and history used by the install/edit APIs, so
|
|
the mounted directory must be writable only by trusted operators.
|
|
|
|
### File Uploads
|
|
|
|
#### Upload Files
|
|
|
|
Upload one or more files to a thread.
|
|
|
|
```http
|
|
POST /api/threads/{thread_id}/uploads
|
|
Content-Type: multipart/form-data
|
|
```
|
|
|
|
**Request Body:**
|
|
- `files`: One or more files to upload
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"files": [
|
|
{
|
|
"filename": "document.pdf",
|
|
"size": 1234567,
|
|
"path": ".deer-flow/threads/abc123/user-data/uploads/document.pdf",
|
|
"virtual_path": "/mnt/user-data/uploads/document.pdf",
|
|
"artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf",
|
|
"markdown_file": "document.pdf.md",
|
|
"markdown_path": ".deer-flow/threads/abc123/user-data/.upload-conversions/document.pdf.md",
|
|
"markdown_virtual_path": "/mnt/user-data/.upload-conversions/document.pdf.md",
|
|
"markdown_artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/.upload-conversions/document.pdf.md"
|
|
}
|
|
],
|
|
"message": "Successfully uploaded 1 file(s)"
|
|
}
|
|
```
|
|
|
|
**Supported Document Formats** (auto-converted to Markdown):
|
|
- PDF (`.pdf`)
|
|
- PowerPoint (`.ppt`, `.pptx`)
|
|
- Excel (`.xls`, `.xlsx`)
|
|
- Word (`.doc`, `.docx`)
|
|
|
|
All upload entry points publish complete payloads without replacing an existing name. Concurrent collisions are returned as `document.pdf`, `document_1.pdf`, `document_2.pdf`, and so on. A published filename remains leased through conversion, permission adjustment, sandbox synchronization, and response construction; portable case and Unicode-normalization aliases share the same coordination key so deletion cannot bypass that lifecycle on case-insensitive filesystems. Mounted providers make the exact published paths sandbox-readable; non-mounted providers receive exact private copies for Gateway, embedded-client, and IM-channel ingresses. If a non-mounted sandbox update later fails or the request is cancelled, DeerFlow removes every exact remote path attempted by that request before rolling back its host generations. Gateway cancellation also drains and aborts an in-flight staging creation. Final lease release is the commit point: cancellation newly arriving during release is delayed and the already-built successful response is returned. Basenames matching the internal `.upload-*.part` staging pattern, containing NUL, `<`, or `>`, or containing the reserved model-context boundary markers are rejected before staging so every accepted model-visible filename and path can be rendered losslessly. Embedded multi-file calls are request-atomic: a later failure rolls back every earlier host and remote generation in that call.
|
|
|
|
Generated Markdown is stored outside the primary namespace and is not returned by the list endpoint. Normal conversion names are `<actual-primary-filename>.md`; if that component would exceed 255 UTF-8 bytes, the response contains a deterministic UTF-8-safe prefix plus the full SHA-256 digest and `.md`. Clients must consume the returned `markdown_*` fields rather than derive the path. Mounted AIO sandboxes use a read-only conversion mount, while Local structured file APIs reject writes through a read-only path mapping; Local host bash is outside that boundary. Non-mounted providers receive a private synchronized copy rather than the authoritative host namespace. Deleting `document.pdf` also deletes only its exact generated conversion; an independent `uploads/document.md` is preserved.
|
|
|
|
#### List Uploaded Files
|
|
|
|
```http
|
|
GET /api/threads/{thread_id}/uploads/list
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"files": [
|
|
{
|
|
"filename": "document.pdf",
|
|
"size": 1234567,
|
|
"path": ".deer-flow/threads/abc123/user-data/uploads/document.pdf",
|
|
"virtual_path": "/mnt/user-data/uploads/document.pdf",
|
|
"artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf",
|
|
"extension": ".pdf",
|
|
"modified": 1705997600.0
|
|
}
|
|
],
|
|
"count": 1
|
|
}
|
|
```
|
|
|
|
#### Delete File
|
|
|
|
```http
|
|
DELETE /api/threads/{thread_id}/uploads/{filename}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Deleted document.pdf"
|
|
}
|
|
```
|
|
|
|
If an upload, conversion, or sandbox synchronization still owns this exact filename, the
|
|
delete waits for that lifecycle to finish before removing the primary and its generated
|
|
conversion. Work on unrelated filenames is not serialized.
|
|
|
|
### Thread Cleanup
|
|
|
|
Remove DeerFlow-managed local thread files under `.deer-flow/threads/{thread_id}` after the LangGraph thread itself has been deleted.
|
|
|
|
```http
|
|
DELETE /api/threads/{thread_id}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Deleted local thread data for abc123"
|
|
}
|
|
```
|
|
|
|
**Error behavior:**
|
|
- `422` for invalid thread IDs
|
|
- `500` returns a generic `{"detail": "Failed to delete local thread data."}` response while full exception details stay in server logs
|
|
|
|
### Artifacts
|
|
|
|
#### Get Artifact
|
|
|
|
Download or view an artifact generated by the agent.
|
|
|
|
```http
|
|
GET /api/threads/{thread_id}/artifacts/{path}
|
|
```
|
|
|
|
**Path Examples:**
|
|
- `/api/threads/abc123/artifacts/mnt/user-data/outputs/result.txt`
|
|
- `/api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf`
|
|
|
|
**Query Parameters:**
|
|
- `download` (boolean): If `true`, force download with Content-Disposition header
|
|
|
|
**Response:** File content with appropriate Content-Type
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
All APIs return errors in a consistent format:
|
|
|
|
```json
|
|
{
|
|
"detail": "Error message describing what went wrong"
|
|
}
|
|
```
|
|
|
|
**HTTP Status Codes:**
|
|
- `400` - Bad Request: Invalid input
|
|
- `404` - Not Found: Resource not found
|
|
- `422` - Validation Error: Request validation failed
|
|
- `500` - Internal Server Error: Server-side error
|
|
|
|
---
|
|
|
|
## Authentication
|
|
|
|
DeerFlow supports four HTTP identity sources. They share the same thread/run isolation rules but differ in whether a row is created in `users` and how external identities are mapped. See [AUTH_DESIGN.md](AUTH_DESIGN.md) for the full design.
|
|
|
|
| Model | Entry | `users` table | Isolation key |
|
|
|---|---|---|---|
|
|
| Browser session | `access_token` cookie after login/register | Yes | `users.id` |
|
|
| OIDC / SSO | OAuth callback → cookie | Yes | `users.id` (see [SSO.md](SSO.md)) |
|
|
| IM channel binding | Connect code + `channel_connections` | Bound to registered user | `channel_connections.owner_user_id` |
|
|
| **Internal Auth** | `X-DeerFlow-Internal-Token` + `X-DeerFlow-Owner-User-Id` | **No** | Owner string on `threads_meta.user_id` |
|
|
|
|
**IM channel binding** and **Internal Auth** are both *platform-trust* integrations: DeerFlow trusts the channel/platform to authenticate end users. IM bindings persist the mapping in `channel_connections` / `channel_conversations` and require a DeerFlow `users` row. Internal Auth lets a platform call the Gateway API directly with a deployment-shared token and a per-request owner header—no `users` row, but thread/run/checkpoint isolation works the same way.
|
|
|
|
### Browser session (default)
|
|
|
|
DeerFlow enforces authentication for all non-public HTTP routes. Public routes are limited to health/docs metadata and these public auth endpoints:
|
|
|
|
- `POST /api/v1/auth/initialize` creates the first admin account when no admin exists.
|
|
- `POST /api/v1/auth/login/local` logs in with email/password and sets an HttpOnly `access_token` cookie.
|
|
- `POST /api/v1/auth/register` creates a regular `user` account and sets the session cookie.
|
|
- `POST /api/v1/auth/logout` clears the session cookie.
|
|
- `GET /api/v1/auth/setup-status` reports whether the first admin still needs to be created.
|
|
|
|
The authenticated auth endpoints are:
|
|
|
|
- `GET /api/v1/auth/me` returns the current user.
|
|
- `POST /api/v1/auth/change-password` changes password, optionally changes email during setup, increments `token_version`, and reissues the cookie.
|
|
|
|
Protected state-changing requests also require the CSRF double-submit token: send the `csrf_token` cookie value as the `X-CSRF-Token` header. Login/register/initialize/logout are bootstrap auth endpoints: they are exempt from the double-submit token but still reject hostile browser `Origin` headers.
|
|
|
|
User isolation is enforced from the authenticated user context:
|
|
|
|
- Thread metadata is scoped by `threads_meta.user_id`; search/read/write/delete APIs only expose the current user's threads.
|
|
- Thread files live under `{base_dir}/users/{user_id}/threads/{thread_id}/user-data/` and are exposed inside the sandbox as `/mnt/user-data/`.
|
|
- Memory and custom agents are stored under `{base_dir}/users/{user_id}/...`.
|
|
|
|
Note: MCP outbound connections can still use OAuth for configured HTTP/SSE MCP servers; that is separate from DeerFlow API authentication.
|
|
|
|
### Internal Auth (platform HTTP integration)
|
|
|
|
For server-to-server integrations (e.g. a Feishu or WeCom/Enterprise WeChat bot backend), configure:
|
|
|
|
```bash
|
|
export DEER_FLOW_INTERNAL_AUTH_TOKEN="<long-random-secret>"
|
|
```
|
|
|
|
| Header | Required | Description |
|
|
|---|---|---|
|
|
| `X-DeerFlow-Internal-Token` | Yes | Must match `DEER_FLOW_INTERNAL_AUTH_TOKEN`; missing/invalid → `401` |
|
|
| `X-DeerFlow-Owner-User-Id` | Yes for per-user isolation | Platform user id (e.g. `feishu_ou_alice`, `wecom_user_bob`); omit → `default` bucket |
|
|
|
|
Does **not** use browser cookies or CSRF tokens. Does **not** insert into `users`; sets `threads_meta.user_id` / `runs.user_id` from the owner header. DeerFlow validates only the platform token—not whether the owner id represents a real end user; user validity is entirely the platform's responsibility. See [AUTH_DESIGN.md — Internal Auth](AUTH_DESIGN.md#internal-auth-direct-http) for trust boundaries, persistence, and security notes.
|
|
|
|
Use the standard Gateway thread/run endpoints (`POST /api/threads`, `POST /api/threads/{thread_id}/runs/stream`, etc.) with the headers above on every request.
|
|
|
|
---
|
|
|
|
## Rate Limiting
|
|
|
|
No rate limiting is implemented by default. For production deployments, configure rate limiting in Nginx:
|
|
|
|
```nginx
|
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
|
|
location /api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
proxy_pass http://backend;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Streaming Support
|
|
|
|
Gateway's LangGraph-compatible API streams run events with Server-Sent Events (SSE).
|
|
|
|
**Thread-scoped streaming** (thread must exist):
|
|
|
|
```http
|
|
POST /api/langgraph/threads/{thread_id}/runs/stream
|
|
Accept: text/event-stream
|
|
```
|
|
|
|
**Stateless streaming** (no pre-created thread; Gateway auto-creates one):
|
|
|
|
```http
|
|
POST /api/langgraph/runs/stream
|
|
Accept: text/event-stream
|
|
```
|
|
|
|
Both endpoints return `Content-Location: /api/threads/{thread_id}/runs/{run_id}`.
|
|
The DeerFlow web UI and LangGraph SDK clients rely on this header to discover the
|
|
assigned `thread_id` and `run_id` on the first message of a new chat.
|
|
|
|
### SSE replay retention and gaps
|
|
|
|
Clients may reconnect to a run stream with `Last-Event-ID`. Replay history is
|
|
bounded by `stream_bridge.queue_maxsize` (default `256`) and, for Redis, by the
|
|
rolling `stream_ttl_seconds`. A retained cursor resumes after that event with no
|
|
additional control frame.
|
|
|
|
When a syntactically valid cursor is older than the retained watermark, the
|
|
server sends exactly one `gap` event before any retained data and closes that
|
|
subscription without an `end` event:
|
|
|
|
```text
|
|
event: gap
|
|
data: {"code":"stream_replay_gap","run_id":"run-123","requested_event_id":"1718000000000-1","earliest_available_event_id":"1718000000100-42","latest_available_event_id":"1718000000200-84","recovery":"reload_durable_state"}
|
|
|
|
```
|
|
|
|
The frame deliberately has no SSE `id:`. Consumers must reload durable thread
|
|
state and persisted run events/messages, then may reconnect from
|
|
`latest_available_event_id` to follow newer live events. A gap does not cancel
|
|
the active run. The same signal applies when a no-cursor subscriber has already
|
|
established an empty-stream wait but the first Redis wake-up falls behind before
|
|
delivery; in that case `requested_event_id` is `null`. Malformed cursor handling
|
|
is backend-specific and is not the same as a valid cursor that was evicted.
|
|
|
|
---
|
|
|
|
## SDK Usage
|
|
|
|
### Python (LangGraph SDK)
|
|
|
|
```python
|
|
from langgraph_sdk import get_client
|
|
|
|
client = get_client(url="http://localhost:2026/api/langgraph")
|
|
run_meta: dict[str, str] = {}
|
|
|
|
|
|
def on_run_created(meta) -> None:
|
|
# langgraph-sdk 0.3.x parses Content-Location only when this callback is set.
|
|
if meta.thread_id:
|
|
run_meta["thread_id"] = meta.thread_id
|
|
run_meta["run_id"] = meta.run_id
|
|
|
|
|
|
# Option A: stateless stream — no thread pre-creation
|
|
# Gateway auto-creates a thread and returns thread_id/run_id in Content-Location.
|
|
async for event in client.runs.stream(
|
|
None,
|
|
"lead_agent",
|
|
input={"messages": [{"role": "user", "content": "Hello"}]},
|
|
config={"configurable": {"model_name": "gpt-4"}},
|
|
stream_mode=["values", "messages-tuple", "custom"],
|
|
on_run_created=on_run_created,
|
|
):
|
|
print(event)
|
|
|
|
thread_id = run_meta["thread_id"] # persist before the next turn
|
|
|
|
# Option A (continued): same thread on the next turn
|
|
async for event in client.runs.stream(
|
|
None,
|
|
"lead_agent",
|
|
input={"messages": [{"role": "user", "content": "What did I just ask?"}]},
|
|
config={"configurable": {"thread_id": thread_id, "model_name": "gpt-4"}},
|
|
stream_mode=["values", "messages-tuple", "custom"],
|
|
on_run_created=on_run_created,
|
|
):
|
|
print(event)
|
|
|
|
# Option B: thread-scoped stream — create thread first, then stream
|
|
thread = await client.threads.create()
|
|
async for event in client.runs.stream(
|
|
thread["thread_id"],
|
|
"lead_agent",
|
|
input={"messages": [{"role": "user", "content": "Hello"}]},
|
|
config={"configurable": {"model_name": "gpt-4"}},
|
|
stream_mode=["values", "messages-tuple", "custom"],
|
|
on_run_created=on_run_created,
|
|
):
|
|
print(event)
|
|
```
|
|
|
|
### JavaScript/TypeScript
|
|
|
|
```typescript
|
|
// Using fetch for Gateway API
|
|
const response = await fetch('/api/models');
|
|
const data = await response.json();
|
|
console.log(data.models);
|
|
|
|
function parseRunLocation(contentLocation: string | null) {
|
|
if (!contentLocation) return null;
|
|
const match = /\/threads\/([^/]+)\/runs\/([^/]+)/.exec(contentLocation);
|
|
if (!match) return null;
|
|
return { threadId: match[1], runId: match[2] };
|
|
}
|
|
|
|
// Option A: stateless stream — no thread pre-creation
|
|
let threadId: string | undefined;
|
|
const firstResponse = await fetch("/api/langgraph/runs/stream", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "text/event-stream",
|
|
},
|
|
body: JSON.stringify({
|
|
input: { messages: [{ role: "user", content: "Hello" }] },
|
|
stream_mode: ["values", "messages-tuple", "custom"],
|
|
}),
|
|
});
|
|
|
|
const created = parseRunLocation(firstResponse.headers.get("Content-Location"));
|
|
threadId = created?.threadId;
|
|
console.log("thread_id:", created?.threadId, "run_id:", created?.runId);
|
|
|
|
// Option B: continue the same thread on the next turn
|
|
const followUpResponse = await fetch("/api/langgraph/runs/stream", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "text/event-stream",
|
|
},
|
|
body: JSON.stringify({
|
|
input: { messages: [{ role: "user", content: "What did I just ask?" }] },
|
|
config: { configurable: { thread_id: threadId } },
|
|
stream_mode: ["values", "messages-tuple", "custom"],
|
|
}),
|
|
});
|
|
|
|
// Option C: thread-scoped stream when you already have a thread_id
|
|
const streamResponse = await fetch(`/api/langgraph/threads/${threadId}/runs/stream`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "text/event-stream",
|
|
},
|
|
body: JSON.stringify({
|
|
input: { messages: [{ role: "user", content: "Hello" }] },
|
|
stream_mode: ["values", "messages-tuple", "custom"],
|
|
}),
|
|
});
|
|
|
|
const reader = streamResponse.body?.getReader();
|
|
// Decode and parse SSE frames from reader in your client code.
|
|
```
|
|
|
|
### cURL Examples
|
|
|
|
```bash
|
|
# List models
|
|
curl http://localhost:2026/api/models
|
|
|
|
# Get MCP config
|
|
curl http://localhost:2026/api/mcp/config
|
|
|
|
# Upload file
|
|
curl -X POST http://localhost:2026/api/threads/abc123/uploads \
|
|
-F "files=@document.pdf"
|
|
|
|
# Enable skill
|
|
curl -X POST http://localhost:2026/api/skills/pdf-processing/enable
|
|
|
|
# Stateless stream — no thread pre-creation
|
|
curl -s -D - -N -X POST http://localhost:2026/api/langgraph/runs/stream \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: text/event-stream" \
|
|
-d '{
|
|
"input": {"messages": [{"role": "user", "content": "Hello"}]},
|
|
"config": {
|
|
"recursion_limit": 100,
|
|
"configurable": {"model_name": "gpt-4"}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}'
|
|
# Read Content-Location: /api/threads/{thread_id}/runs/{run_id} from the headers.
|
|
|
|
# Continue the same thread on the next turn
|
|
curl -s -N -X POST http://localhost:2026/api/langgraph/runs/stream \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: text/event-stream" \
|
|
-d '{
|
|
"input": {"messages": [{"role": "user", "content": "What did I just ask?"}]},
|
|
"config": {
|
|
"configurable": {"thread_id": "abc123", "model_name": "gpt-4"}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}'
|
|
|
|
# Thread-scoped flow — create thread first, then stream
|
|
curl -X POST http://localhost:2026/api/langgraph/threads \
|
|
-H "Content-Type: application/json" \
|
|
-d '{}'
|
|
|
|
curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs/stream \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: text/event-stream" \
|
|
-d '{
|
|
"input": {"messages": [{"role": "user", "content": "Hello"}]},
|
|
"config": {
|
|
"recursion_limit": 100,
|
|
"configurable": {"model_name": "gpt-4"}
|
|
},
|
|
"stream_mode": ["values", "messages-tuple", "custom"]
|
|
}'
|
|
```
|
|
|
|
> The unified Gateway path defaults `config.recursion_limit` to 100 for
|
|
> plan-mode and subagent-heavy runs. Clients may still set
|
|
> `config.recursion_limit` explicitly — see the [Create Run](#create-run)
|
|
> section for details.
|