# 概览

<div className="docs-lead-note">
  <p className="font-medium">
    直接运行智能体，或使用你需要的基础能力
  </p>

  <p className="mt-2 text-sm leading-6">
    智能体服务封装模型调用、工具执行等能力，通过智能体 API 完成业务任务。需要直接控制模型、沙箱或搜索时，也可以独立使用对应服务。
  </p>
</div>

## 开始使用 [#开始使用]

<div className="mt-4 grid gap-6 md:grid-cols-3">
  <div>
    <p className="text-sm text-fd-muted-foreground">
      01
    </p>

    <p className="mt-1 font-medium">
      <a href="/docs/developers">选择服务</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      运行智能体，或直接调用模型、沙箱与搜索。
    </p>
  </div>

  <div>
    <p className="text-sm text-fd-muted-foreground">
      02
    </p>

    <p className="mt-1 font-medium">
      <a href="/docs/developers/get-started/concepts">创建云 Key</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      在 Praxis Cloud 控制台创建 API Key，了解鉴权方式与权限范围。
    </p>
  </div>

  <div>
    <p className="text-sm text-fd-muted-foreground">
      03
    </p>

    <p className="mt-1 font-medium">
      <a href="/docs/developers">完成第一次调用</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      打开所选服务的快速开始，完成一个请求并查看结果。
    </p>
  </div>
</div>

## 选择你需要的服务 [#选择你需要的服务]

每项服务都有独立的快速开始、使用指南与 API。

<hr className="my-8 border-fd-border" />

### 智能体 [#智能体]

定义职责、提供资料并运行任务。由智能体服务管理底层能力调用。

<div className="mt-4 grid gap-6 sm:grid-cols-2">
  <div>
    <p className="font-medium">
      快速开始
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      创建并运行第一个智能体
    </p>
  </div>

  <div>
    <p className="font-medium">
      核心概念
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      智能体与运行
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      会话、记忆与协作
    </p>
  </div>

  <div>
    <p className="font-medium">
      使用指南
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      配置职责与能力
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      提供文件与资料
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      使用工具与组织协作
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      接入自己的应用
    </p>
  </div>

  <div>
    <p className="font-medium">
      技术参考
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/get-started/concepts">限制与错误处理</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      智能体 API
    </p>
  </div>
</div>

<hr className="my-8 border-fd-border" />

### 模型 [#模型]

通过统一接口调用模型，生成文本、处理多模态输入。

<div className="mt-4 grid gap-6 sm:grid-cols-2">
  <div>
    <p className="font-medium">
      快速开始
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/model/quickstart">发出第一个模型请求</a>
    </p>
  </div>

  <div>
    <p className="font-medium">
      核心概念
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/model/concepts/models-and-catalog">模型与供应商</a>
    </p>

    <p className="mt-1 text-sm">
      <a href="/docs/developers/model/concepts/protocols">兼容协议与路由</a>
    </p>
  </div>

  <div>
    <p className="font-medium">
      接入与使用
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/cookbook">接入第三方工具</a>
    </p>

    <p className="mt-1 text-sm">
      <a href="/docs/developers/model/guides/streaming">流式响应</a>

      与

      <a href="/docs/developers/model/guides/tool-calling">工具调用</a>
    </p>

    <p className="mt-1 text-sm">
      <a href="/docs/developers/model/guides/structured-output">结构化输出</a>

      与

      <a href="/docs/developers/model/guides/multimodal">多模态</a>
    </p>

    <p className="mt-1 text-sm">
      <a href="/docs/developers/model/concepts/routing">路由与失败重试</a>
    </p>
  </div>

  <div>
    <p className="font-medium">
      技术参考
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/model/limits-and-errors">限制与错误处理</a>
    </p>

    <p className="mt-1 text-sm">
      <a href="/docs/api">模型 API →</a>
    </p>
  </div>
</div>

<hr className="my-8 border-fd-border" />

### 沙箱 [#沙箱]

在隔离环境中执行代码、操作文件和使用浏览器。

<div className="mt-4 grid gap-6 sm:grid-cols-2">
  <div>
    <p className="font-medium">
      快速开始
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      创建沙箱并执行一个任务
    </p>
  </div>

  <div>
    <p className="font-medium">
      核心概念
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      沙箱与生命周期
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      执行环境与资源
    </p>
  </div>

  <div>
    <p className="font-medium">
      使用指南
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      执行命令 · 管理文件
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      使用浏览器与网络
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      回收环境与处理失败
    </p>
  </div>

  <div>
    <p className="font-medium">
      技术参考
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/get-started/concepts">限制与错误处理</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      沙箱 API
    </p>
  </div>
</div>

<hr className="my-8 border-fd-border" />

### 搜索 [#搜索]

查询所需信息，获取搜索结果与来源。

<div className="mt-4 grid gap-6 sm:grid-cols-2">
  <div>
    <p className="font-medium">
      快速开始
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      完成第一次搜索
    </p>
  </div>

  <div>
    <p className="font-medium">
      核心概念
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      搜索范围与查询
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      结果、排序与来源
    </p>
  </div>

  <div>
    <p className="font-medium">
      使用指南
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      设置搜索条件
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      读取与筛选结果
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      在应用中使用搜索
    </p>
  </div>

  <div>
    <p className="font-medium">
      技术参考
    </p>

    <p className="mt-2 text-sm">
      <a href="/docs/developers/get-started/concepts">限制与错误处理</a>
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      搜索 API
    </p>
  </div>
</div>

## 管理你的云服务 [#管理你的云服务]

<div className="mt-4 grid gap-6 sm:grid-cols-2">
  <div id="identity-permissions" className="scroll-mt-24">
    <p className="font-medium">
      <a href="/docs/developers/get-started/concepts">身份与权限</a>
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      管理云 Key 与授权范围
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      保护服务访问凭证
    </p>
  </div>

  <div id="usage-billing" className="scroll-mt-24">
    <p className="font-medium">
      用量与费用
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      计量口径、用量查询
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      账单与费用管理
    </p>
  </div>

  <div id="quotas-limits" className="scroll-mt-24">
    <p className="font-medium">
      <a href="/docs/developers/account/quotas">配额与限制</a>
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      查询服务配额
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      了解请求与资源限制
    </p>
  </div>

  <div id="observability-troubleshooting" className="scroll-mt-24">
    <p className="font-medium">
      观测与故障排查
    </p>

    <p className="mt-2 text-sm text-fd-muted-foreground">
      请求标识、日志与排错
    </p>

    <p className="mt-1 text-sm text-fd-muted-foreground">
      服务异常的定位路径
    </p>
  </div>
</div>

## API [#api]

智能体 / 模型 / 沙箱 / 搜索 —— 查阅请求参数、响应结构、错误码与代码示例。需要验证调用时，在当前页面展开调试面板，配置并发送请求。

<p className="mt-3">
  <a href="/docs/api">浏览所有 API →</a>
</p>

# 第一个请求

## 前提 [#前提]

一个 Praxis 账号。新注册账号自带 **$1 免费额度**，跑完本页绰绰有余，不需要先充值。

## 1. 创建 API Key [#1-创建-api-key]

在 [控制台 → API Keys](https://console.deepraxis.ai/keys) 创建一个 Key。名称必填，有效期默认 90 天，创建后立即复制保存。

## 2. 发出第一个请求 [#2-发出第一个请求]

只需要一个地址：`https://api.deepraxis.ai`。用你已有的 SDK，把 `base_url` 指过来即可。

示范模型 `deepseek-flash`：便宜、快、中英文都好。

```bash
curl https://api.deepraxis.ai/openai/v1/chat/completions \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-flash", "messages": [{"role": "user", "content": "你好"}]}'
```

用你熟悉的语言也行：

**Python**

```python
from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepraxis.ai/openai/v1",
    api_key="sk-praxis-你的Key",
)

completion = client.chat.completions.create(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "你好"}],
)
print(completion.choices[0].message.content)
```

**Node**

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.deepraxis.ai/openai/v1",
  apiKey: process.env.PRAXIS_API_KEY,
});

const res = await client.chat.completions.create({
  model: "deepseek-flash",
  messages: [{ role: "user", content: "你好" }],
});
console.log(res.choices[0].message.content);
```

## 3. 读懂响应 [#3-读懂响应]

响应的 `choices[0].message.content` 是回复文本。`usage&#x60; 里有 token 数——注意 **`cost` 字段直接给出本次调用的费用**。

然后打开 [控制台 → 用量与费用](https://console.deepraxis.ai/usage)：你刚才这一次调用已经记在那里了。

## 下一步 [#下一步]

* [接入方式](/docs/cookbook)：Claude Code、Codex、Cherry Studio……
* [模型目录](https://console.deepraxis.ai/models)：可用模型与定价
* [流式响应](/docs/developers/model/guides/streaming) · [错误与重试](/docs/developers/model/limits-and-errors)

# 选择服务

Praxis Cloud 提供多个服务域。你的场景决定用哪个——很多场景只需要一个。

## 模型（Model） [#模型model]

**你要做什么**：调用大语言模型（对话、写作、翻译、代码、分析）。

**怎么用**：通过 OpenAI 或 Anthropic 兼容 API 直接调用。把现有代码的 `base_url` 指向 Praxis 即可。

**适合**：已有应用要加 AI 能力、写 SDK 代码、接入第三方工具（Cursor、Cherry Studio 等）。

→ [模型快速开始](/docs/developers/model/quickstart) · [模型目录](https://console.deepraxis.ai/models)

## 智能体（Agent） [#智能体agent]

**你要做什么**：让 AI 在云端持续工作——有上下文、有记忆、能调用工具。

**怎么用**：通过 Agent API 创建会话、发送事件、获取回复。智能体可以记住之前的对话、执行多步任务。

**适合**：构建 AI 助手、客服机器人、有状态的工作流。

→ [智能体快速开始](/docs/developers/agent/quickstart)

## 沙箱（Sandbox） [#沙箱sandbox]

**你要做什么**：在安全隔离的环境中运行代码。

**怎么用**：通过 Sandbox API 创建沙箱实例，上传代码并执行，获取结果。

**适合**：代码执行、自动化测试、数据处理管道。

→ [沙箱快速开始](/docs/developers/sandbox/quickstart)

## 搜索（Search） [#搜索search]

**你要做什么**：给 AI 提供网络搜索能力。

**怎么用**：通过 Search API 执行搜索，返回结构化结果。

**适合**：构建有实时信息能力的 AI 应用。

→ [搜索快速开始](/docs/developers/search/quickstart)

## 怎么选 [#怎么选]

| 你的场景          | 推荐                              |
| ------------- | ------------------------------- |
| 只需要调模型（对话/生成） | 模型                              |
| 需要 AI 有记忆和工具  | 智能体                             |
| 需要安全执行代码      | 沙箱                              |
| 需要搜索网络信息      | 搜索                              |
| 组合使用          | 全部可以组合——智能体可以调用模型和搜索，沙箱可以运行任何代码 |

**不确定的话**：从[模型](/docs/developers/model/quickstart)开始——最简单、最直接，3 分钟就能跑通。

# 核心概念

理解这几个概念，使用 Praxis 会更顺手。

## 一个 Key，所有服务 [#一个-key所有服务]

Praxis Cloud 的所有服务（模型、智能体、沙箱、搜索）共用同一个 API Key。你不需要为不同服务创建不同的 Key——一个 Key 就够了。

## 兼容协议 [#兼容协议]

Praxis 提供两种协议入口，你已有的代码不需要换库：

* **OpenAI 兼容**（`https://api.deepraxis.ai/openai/v1`）——OpenAI SDK 改 `base_url` 即用
* **Anthropic 兼容**（`https://api.deepraxis.ai/anthropic`）——Anthropic SDK 改 `base_url` 即用

两种协议背后的模型池是同一个——你可以在 OpenAI 协议下调 Claude 模型，也可以在 Anthropic 协议下调 DeepSeek 模型。

## 模型与路由 [#模型与路由]

Praxis 聚合多家供应商的模型，统一以一个 API 提供。每个模型 ID 背后是具体的供应商路由——你不需要关心用的是哪家的服务，只需要选对模型 ID。

两种模型 ID：

* **滚动入口**（如 `deepseek-flash`）——始终指向最新一代，升级时无需改代码
* **版本快照**（如 `deepseek-v4-flash`）——固定在某一个版本，适合需要可复现性的场景

## 按次计费 [#按次计费]

每次 API 调用按实际 token 用量计费。输入和输出 token 分别计价，推理模型的思维链 token 也计费。不调用不收费——没有月费、没有最低消费。

API 响应中的 `usage.cost` 字段直接返回本次调用的费用，你可以在应用里实时追踪成本。

## 想深入某个服务？ [#想深入某个服务]

* [选择服务](/docs/developers/get-started/choose-a-service)——四域对比
* [第一个请求](/docs/developers/get-started/first-request)——3 分钟跑通
* [术语表](/docs/developers/resources/glossary)——15 个核心术语

# 智能体

让智能体回答产品问题、查询业务数据或完成工具任务。你配置模型、指令与工具，Praxis 管理会话执行和事件记录；你的应用决定谁可以使用、哪些业务操作可以执行。

## 适用对象与结果 [#use-cases]

给已有产品接入助手时，可以使用现成对话界面，把用户身份和当前页面带入会话。需要完全自定义交互、后台任务或非网页客户端时，使用 API 创建会话并读取事件。业务数据通过你授权的工具取得，订单权限仍由业务服务器核验。

如果只需要一次模型调用，或希望自己编写模型与工具循环，从[模型服务](/docs/developers/model)开始。智能体服务适合希望复用配置、持续对话并追踪任务过程的应用。

## 对象如何关联 [#objects]

| 对象   | 负责什么                 | 与其他对象的关系                    |
| ---- | -------------------- | --------------------------- |
| 智能体  | 名称、指令、模型、工具          | 保存配置形成版本；一个智能体可以创建多个会话      |
| 会话   | 一段独立对话及其执行状态         | 创建时固定智能体版本，后续配置修改不会替换该快照    |
| 环境   | 会话运行所需的机器配置          | API 可省略环境选择使用轻型机器；需要沙箱时选择环境 |
| 事件   | 用户消息、智能体回复、工具调用和状态变化 | 属于会话；发送成功表示输入已接收，回复要从事件中读取  |
| 终端用户 | 使用你产品的人              | 嵌入令牌把会话访问绑定到该用户和智能体         |

服务智能体面向你的客户，每个会话使用独立环境；个人智能体绑定持久电脑。本文的接入指南围绕服务智能体展开，详细关系见[智能体与会话](/docs/developers/agent/concepts)。公开智能体 ID 形如 `agent_…`，应直接使用创建或查询响应中的值。

## 选择交付形态 [#delivery]

| 形态        | 你负责                 | 从哪里开始                                            |
| --------- | ------------------- | ------------------------------------------------ |
| API       | 服务端鉴权、消息和事件处理、自有界面  | [运行第一个智能体](/docs/developers/agent/quickstart)    |
| PraxisKit | 在宿主网页挂载组件，接好身份与业务交互 | [嵌入到网页](/docs/developers/agent/embed/quickstart) |
| iframe    | 提供签发端点并加载托管界面       | [一行嵌入](/docs/developers/agent/embed/iframe)      |
| Hosted    | 验证用户身份，签发短期链接并跳转    | [打开托管对话](/docs/developers/agent/embed/hosted)    |

React 是使用 Kit 的一种方式。公开资料问答可使用匿名访客接入；需要账户数据时，由宿主后端验证登录身份再签发令牌。完整比较见[选择接入方式](/docs/developers/agent/embed/overview)。

## 按任务继续 [#tasks]

* 修改回答范围、模型和版本：[配置智能体](/docs/developers/agent/configure)。
* 让助手了解当前页面并调用业务能力：[传入上下文](/docs/developers/agent/embed/context)、[配置工具授权](/docs/developers/agent/embed/tool-authorization)。
* 排查客户问题：[查看会话与用量](/docs/developers/agent/monitor)、[管理终端用户](/docs/developers/agent/end-users)。
* 准备上线：[测试嵌入接入](/docs/developers/agent/embed/testing)、[上线检查清单](/docs/developers/agent/embed/go-live)。

下一步：[API 参考](/docs/api/native/agent) · [文档问答配方](/docs/cookbook/scenarios/docs-qa-assistant) · [限制与错误](/docs/developers/agent/limits-and-errors)

# 运行第一个智能体

用 API 建立一段服务智能体对话，并从事件中验证真实回复。本页只在服务端运行，不需要网页组件或嵌入策略。

准备 Bun 或支持原生 `fetch` 的 Node.js、Develop 组织的 API Key，以及该组织可用的 `deepseek-flash` 模型和余额。在服务端进程设置 `PRAXIS_API_KEY`；密钥创建见[API Key](/docs/developers/account/api-keys)。如果你的 Key 属于其他环境，先统一 API 地址、组织和模型，不能混用环境。

## 1. 创建智能体 [#create-agent]

新建 `first-agent.mjs`，从下面的代码开始。后续三段 JavaScript 按顺序追加到同一个文件，不要分别执行。助手只回答一个算术问题，便于判断首次调用是否完成；业务工具在接通后再添加。

`POST /agent/v1/agents` 使用名称、类型、模型和指令创建配置。`tools: []` 不声明额外工具。请求辅助函数为每次调用检查状态并设置超时，失败时只输出状态和请求标识，不打印 Key。

```js
import assert from "node:assert/strict";
import { setTimeout as delay } from "node:timers/promises";

const { PRAXIS_API_KEY } = process.env;
assert(PRAXIS_API_KEY, "Set PRAXIS_API_KEY in the server process");
const base = "https://api-develop.deepraxis.com/agent/v1";
async function request(path, body) {
  const response = await fetch(`${base}${path}`, {
    method: body === undefined ? "GET" : "POST",
    headers: {
      Authorization: `Bearer ${PRAXIS_API_KEY}`,
      "Content-Type": "application/json",
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    signal: AbortSignal.timeout(30_000),
  });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}; request=${response.headers.get("X-Request-Id") ?? response.headers.get("request-id")}`);
  }
  return response.json();
}
const agent = await request("/agents", {
  name: "First support agent", kind: "service", model: "deepseek-flash",
  system: "Answer the user's question briefly. Do not use tools for this exercise.",
  tools: [],
});
assert(agent.id.startsWith("agent_"));
console.log({ agent: agent.id, version: agent.version });
```

**预期结果：** 创建响应为 HTTP 200，包含公开 `id` 和 `version`。运行脚本时会打印这两个值，后续直接复用，不手写猜测 ID。

## 2. 建立会话 [#create-session]

追加以下代码。`POST /agent/v1/sessions` 把刚创建的版本固定在会话中。省略 `environment_id` 使用轻型机器；这一步不需要先创建沙箱。示例显式设置 100 美分的会话预算，不把预算金额当作预计费用。

```js
const session = await request("/sessions", {
  agent: { type: "agent", id: agent.id, version: agent.version },
  title: "First API conversation",
  budget: { type: "limit", max_list_cost: { amount: "100", currency: "USD" } },
});
assert(session.id);
console.log({ session: session.id });
```

**预期结果：** HTTP 201，响应包含会话 `id`。此时还没有发送用户问题；即使状态为 `idle`，也不代表已经得到回复。预算决定后续请求能否继续，不保证生成特定答案。

## 3. 发送第一条消息 [#send-message]

追加以下代码，调用 `POST /agent/v1/sessions/{session_id}/events`。`events` 是输入数组；这里发送一条 `user.message`，其中只有一个非空文本块。

```js
const accepted = await request(`/sessions/${session.id}/events`, {
  events: [{ type: "user.message", content: [{ type: "text", text: "What is 2 + 2?" }] }],
});
assert(accepted.data.some(event => event.type === "user.message"));
```

**预期结果：** HTTP 200，`data` 中出现已接收的用户事件。这是接收确认；不要把它当作智能体答案，也不要因为暂时没有答案就再次发送同一条消息。

## 4. 读取并验证回复 [#verify]

追加以下代码，然后执行 `node first-agent.mjs` 或 `bun first-agent.mjs`。它用 `GET /agent/v1/sessions/{session_id}/events` 分页读取事件，先找到 `agent.message`，再找到其后的 `session.status_idle`。初始空闲状态不会被误判为成功。

```js
let completed = false;
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
  const events = [];
  let page;
  do {
    const query = new URLSearchParams({ order: "asc", limit: "100" });
    if (page) query.set("page", page);
    const result = await request(`/sessions/${session.id}/events?${query}`);
    events.push(...result.data);
    page = result.next_page;
  } while (page && Date.now() < deadline);
  const reply = events.findIndex(event => event.type === "agent.message");
  const idle = events.findIndex((event, i) => i > reply && event.type === "session.status_idle");
  if (reply >= 0 && idle > reply) {
    const text = events.filter(event => event.type === "agent.message")
      .flatMap(event => event.content ?? []).filter(block => block.type === "text")
      .map(block => block.text).join("\n");
    assert(text.trim(), "Expected a nonempty reply");
    console.log({ reply: text, completed: true });
    completed = true;
    break;
  }
  await delay(1500);
}
assert(completed, `No completed reply before timeout; inspect session ${session.id}`);
```

**完成判定：** 进程正常退出，输出非空 `reply` 和 `completed: true`，答案应表达 4。脚本的 120 秒是本例等待期限，不是服务运行上限。只通过文本非空断言还不足以验证业务答案；上线前为实际问题准备预期结果。

## 失败时检查 [#troubleshoot]

| 症状               | 下一步                                                            |
| ---------------- | -------------------------------------------------------------- |
| HTTP 401         | 检查服务端 Key 是否存在、有效，是否对应当前环境                                     |
| HTTP 400         | 核对字段与模型；错误详情见[限制与错误](/docs/developers/agent/limits-and-errors) |
| HTTP 404         | 使用创建响应里的 ID，确认组织与所选版本一致                                        |
| HTTP 429 或临时上游失败 | 按响应重试提示退避；先查询会话，避免重新创建资源或重复发消息                                 |
| 超时仍无完成回复         | 保留会话 ID，读取事件检查工具等待、预算或运行错误；不要仅依据创建响应的状态判断                      |

重新运行整份脚本会新建智能体和会话。排错时优先查询已输出的 ID。测试记录可在确认不再需要后按 [API 参考](/docs/api/native/agent)处理，不要把仍在运行的会话当作可直接删除的记录。

## 下一步 [#next]

继续[配置智能体](/docs/developers/agent/configure)、了解[智能体与会话](/docs/developers/agent/concepts)，或用 [PraxisKit 接入网页](/docs/developers/agent/embed/quickstart)。

交给你的代理：

```text
按 /docs/developers/agent/quickstart 在服务端运行首次对话。
需要我提供 Develop 组织 API Key，并确认 deepseek-flash 可用。
完成标准：收到非空 agent.message，其后出现 session.status_idle。
不要把 Key 放进浏览器或日志；记录智能体 ID、版本和会话 ID。
```

# 智能体与会话

智能体是一份可复用的配置，会话是使用这份配置开展的一段独立对话。先区分这两个对象，才能判断修改指令会影响谁、消息应该发到哪里，以及如何验证任务结束。

## 配置与运行对象 [#objects]

| 对象    | 包含什么               | 创建与使用                        |
| ----- | ------------------ | ---------------------------- |
| 智能体   | 模型、指令、工具、MCP 配置及版本 | 创建后取得 `agent_…` ID，供多个会话复用   |
| 会话    | 固定版本、消息与工具事件、运行状态  | 以智能体 ID 或带版本的引用创建；后续向该会话发送输入 |
| 环境    | 运行会话的机器配置          | 为需要沙箱的会话选择环境；普通 API 起步可省略    |
| 事件    | 用户输入、回复、工具执行与状态变化  | 写入输入事件，读取历史或订阅事件流            |
| vault | 可供访问工具服务的凭据        | 与配置指令分开管理，不能用提示词代替凭据控制       |

服务智能体为每段会话使用独立运行环境。个人智能体绑定持久电脑，适合保留工作目录的任务；两种类型的选择在创建时确定。API 起步使用服务智能体，不需要先准备个人电脑。

## 版本与会话生命周期 [#lifecycle]

保存智能体配置会创建新版本。创建会话时，`agent` 可以是 ID 字符串，表示取当时最新版本；也可以是 `{ type: "agent", id, version }`，明确选择版本。会话固定这份配置，之后修改智能体不会把已有会话换到新版本。

会话可以处于 `idle`、`running`、`rescheduling` 或 `terminated` 状态。`idle` 表示当前没有运行中的任务，可以继续发送消息；`terminated` 会话不能继续运行。归档是另一项状态：记录仍可查询，但不能继续发送消息。保留记录与停止接收输入是不同需求，清理前先判断需要哪一种。

发送 `user.message` 得到成功响应，只说明输入被接收。读取 `agent.message` 才能取得回复；观察其后的 `session.status_idle` 才能判断这一轮已经回到空闲。需要用户批准工具时，应处理待确认事件，不能将等待确认误判为接口无响应。

## 环境与身份边界 [#boundaries]

API 创建会话省略 `environment_id`，且未选择电脑时，使用轻型机器；选择环境时使用对应沙箱配置。环境配置在会话创建时固定。会话的文件和工具能力取决于配置，不应仅凭“环境”这个名字推断支持任意运行时。

组织 API Key 留在服务端。面向终端用户的网页使用嵌入令牌，由宿主后端从已验证登录态确定 `external_user_id`，再为指定智能体签发。令牌限定智能体、版本及终端用户的访问范围；前端自己填写用户 ID 不能建立可信身份。

公开资料问答可使用可公开 key 获取匿名访客令牌。同一浏览器存储延续匿名身份；这不等于登录账户，也不授予访问订单或其他私有资料的权限。用户切换时销毁旧组件并重新取得凭证，不能只清空消息列表。

工具确认表达用户是否同意本次操作。你的订单或业务服务仍须验证调用凭据和当前用户的资源权限；页面上下文、模型参数和“允许执行”按钮都不能代替这一步。指令和上下文也不适合保存秘密。

## 选择下一步 [#next]

第一次验证 API，用[运行第一个智能体](/docs/developers/agent/quickstart)。要修改配置或选择嵌入版本，读[配置智能体](/docs/developers/agent/configure)；创建与更新请求的字段以 [API 参考](/docs/api/native/agent)为准。

接入客户产品时，先选[交付形态](/docs/developers/agent/embed/overview)，再按[嵌入令牌与用户](/docs/developers/agent/embed/tokens)接好身份。出现执行问题时，从[查看会话与用量](/docs/developers/agent/monitor)定位记录，再按[限制与错误](/docs/developers/agent/limits-and-errors)处理。

# 选择接入方式

选择对话出现的位置，再决定由谁编写界面。PraxisKit、iframe 和 Hosted 提供对话界面；API 适合自己实现界面的应用。

## 比较接入方式 [#compare]

| 方式     | 用户在哪里对话         | 你需要实现          | 外观入口                  | 开始接入                                             |
| ------ | --------------- | -------------- | --------------------- | ------------------------------------------------ |
| Kit 组件 | 你的网页内           | 宿主签发端点、组件初始化   | 宿主代码的 `setOptions`    | [嵌入到网页](/docs/developers/agent/embed/quickstart) |
| iframe | 网页内的独立框架        | 宿主签发端点、一行加载脚本  | 控制台保存，脚本可覆盖布局         | [一行嵌入](/docs/developers/agent/embed/iframe)      |
| Hosted | Praxis 托管的独立对话页 | 宿主验证身份、签发链接并跳转 | 与 iframe 共用保存的外观；全页显示 | [打开托管对话](/docs/developers/agent/embed/hosted)    |
| API    | 你自己的网页、App 或服务  | 会话、事件处理与所需界面   | 由你的应用决定               | [运行第一个智能体](/docs/developers/agent/quickstart)    |

## 按任务选择 [#choose]

需要在网页里调整组件、控制打开时机或连接页面交互时，选择 Kit。Kit 是 Web Component，也能在 React 应用里挂载；你在宿主代码中设置界面选项。

只想插入现成对话界面、把外观交给控制台维护时，选择 iframe。这里的“一行”指加载脚本，登录用户的后端签发端点仍需实现。复制一个裸 iframe 地址不能代替身份验证。

希望用户点按钮进入独立页面时，选择 Hosted。宿主后端为当前用户生成短期链接，浏览器立即跳转；不需要在宿主页安装组件。每次进入都重新取得链接，不能把用户的链接作为公共入口分享。

已有自己的聊天界面，或要从非网页应用接入时，选择 API。产品快速开始先带你从服务端跑通一次会话；向终端用户开放时，再按令牌指南隔离每个人的访问权限。

## 准备身份与配置 [#prepare]

登录用户接入需要一个服务智能体、保存的嵌入策略，以及能够验证宿主登录态的后端。API Key 留在后端；浏览器只取得当前终端用户的嵌入令牌。更换界面形态不应改变你为同一个用户分配的稳定标识。

公开问答站不要求登录时，可选 Kit 的匿名访客接入：使用可公开 key，由 Kit 获取匿名令牌。匿名身份不能代替你的账户登录，也不能据此开放订单等账户数据，见[嵌入令牌与用户](/docs/developers/agent/embed/tokens#anonymous)。

嵌入策略和持久化外观目前只能在控制台配置。Kit 的实例外观则由宿主代码设置。先选一种形态跑通真实问答，再增加上下文、工具或主题。

## 下一步 [#next]

* [嵌入到网页](/docs/developers/agent/embed/quickstart)：完成后端签发、挂载与双用户验证。
* [嵌入令牌与用户](/docs/developers/agent/embed/tokens)：选择身份方式并处理刷新与吊销。
* [智能体 API 参考](/docs/api/native/agent)：查会话与事件操作。
* [Cookbook](/docs/cookbook)：查看完整应用场景。

# 嵌入到网页

在你的网页中加入 PraxisKit，让登录用户与服务智能体对话。本页用本地订单助手跑通后端签发、前端挂载和身份隔离；其他形态见[选择接入方式](/docs/developers/agent/embed/overview)。

准备 Bun、一个可用的服务智能体及运行环境，以及同组织的 API Key。示例连接 Develop，API Key 仅存为后端环境变量 `PRAXIS_API_KEY`。浏览器登录宿主后，由宿主核验身份、向 Praxis 签发令牌，再把令牌交给 Kit。

## 1. 准备智能体 [#prepare]

在 Develop 控制台进入**服务智能体**，选择已有智能体。在**接入与配置 → 配置 → 指令**保存指令；在**配置 → 安全**选择运行环境，打开**允许 localhost**，点**保存安全设置**。本页宿主页为 `http://localhost:4242`。请使用专用测试智能体，来源白名单暂留空，仅启用 localhost；令牌继承非空 HTTPS 来源列表时，本地请求会被拒绝。上线到 HTTPS 网站时，添加该网站的来源。

嵌入策略目前只能在控制台配置。复制公开智能体 ID（`agent_…`）备用。页面显示“已保存，不产生新版本”后，继续实际签发；保存提示本身不能证明对话可用。

## 2. 创建签发端点 [#issue]

新建目录，把代码保存为 `server.ts`。这是仅绑定本机的演示服务器，用 HTTP Basic 验证两个测试账户。`DEMO_PASSWORD` 是你自行设置的测试密码，与 Praxis API Key 无关。服务器从验证成功的账户取用户 ID，不读取浏览器提交的用户 ID。

```ts
import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = "http://localhost:4242";
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.origin !== origin) return new Response("Use localhost:4242", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);
```

将三个环境变量放入服务端进程；`PRAXIS_AGENT_ID` 替换为实际 ID。启动前还需在下一步保存 `index.html`。

```bash
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts
```

期望：终端输出本地网址。未认证请求被宿主拒绝；已认证且同源的请求才会调用 Praxis。Praxis 签发成功返回 HTTP 201；宿主端点只返回 `token`、`expires_at`，并禁止缓存。

接入已有应用时，将 `authenticate` 替换为应用的会话验证，把路由接在登录与 CSRF 保护之后，保留每次签发的身份核验。不要将本地测试账户机制用于线上登录。有效期和刷新见[嵌入令牌与用户](/docs/developers/agent/embed/tokens)。

## 3. 挂载组件 [#mount]

选择一种前端实现。API 地址、CDN 和智能体必须属于同一环境；以下固定使用 Develop 的 Kit 0.2.8。

期望：页面出现高度为 600 像素的对话区。`api.url` 指定 API 地址，`getToken` 返回令牌字符串，`agent.name` 是界面名称。等待元素注册后再调用 `setOptions`。`praxis.ready` 只表示组件挂载；必须收到真实回复才能证明接入成功。

## 4. 验证接入 [#verify]

1. 打开 `http://localhost:4242`，用 `customer_123` 和所设测试密码登录。发送“你好，请介绍你能帮助处理的订单问题”，确认收到回复。
2. 在网络面板检查宿主令牌请求成功，响应没有 API Key；会话请求使用嵌入令牌。不要把令牌复制到日志或截图。
3. 用独立浏览器配置或无痕窗口登录 `customer_456`，期望看不到第一个账户的会话。不能用两个同登录态的标签页代替此检查。
4. 不带凭据请求本地签发端点，验证宿主返回 HTTP 401。已有应用还应验证退出登录后不能再签发。

```bash
curl --silent --output /dev/null --write-out "%{http_code}\n" \
  -X POST http://localhost:4242/api/praxis-token \
  -H "Origin: http://localhost:4242"
```

## 失败时检查 [#troubleshoot]

以下是接口定义的失败分支，按网络面板中的实际响应检查：

* 签发 HTTP 400：确认已保存嵌入策略，运行环境仍可用。
* 签发 HTTP 404：确认 API Key 与服务智能体属于同组织，ID 已替换正确。
* 对话返回 `EMBED_ORIGIN_NOT_ALLOWED`（HTTP 403）：核对实际页面来源；本地应启用 localhost，上线应保存 HTTPS 来源。

## 下一步 [#next]

* [传入上下文](/docs/developers/agent/embed/context)
* [自定义外观](/docs/developers/agent/embed/appearance)
* [智能体 API 参考](/docs/api/native/agent)
* [SaaS 嵌入示例](/docs/cookbook/scenarios/saas-embedded-assistant)

# 一行嵌入

在网页中插入一行加载脚本，显示 Praxis 托管的 iframe 对话界面。外观在控制台保存，用户留在你的网页中对话；“一行”只指前端安装，后端仍需为登录用户签发令牌。

## 准备 HTTPS 宿主 [#prepare]

准备一个可修改页面和后端的 HTTPS 网站、Bun、同组织的 API Key，以及有短号的服务智能体。以下连接 Develop。用于演示的 HTTPS 入口应转发到本机 `127.0.0.1:4242`，并保留原始 `Host` 请求头；证书和域名由你的宿主提供。

在 Develop 控制台打开**服务智能体 → 你的智能体 → 接入与配置 → 配置 → 安全**，选择运行环境，在**来源白名单**添加实际 HTTPS 来源，例如 `https://support.example.com`，点**保存安全设置**。来源不包含路径。嵌入策略目前只能在控制台配置。

仅打开**允许 localhost**不能授权 iframe 嵌入；本页需要已保存的 HTTPS 来源。若只做本机 HTTP 调试，先使用 [Kit 快速开始](/docs/developers/agent/embed/quickstart)。

记录两个标识：签发请求使用公开 ID `agent_…`；脚本的 `data-agent` 使用控制台顶部可复制的短号，格式为 5–8 位字母或数字。不要把完整 ID 填进短号位置。

## 提供签发端点 [#issue]

把以下代码保存为 `server.ts`，放在 HTTPS 入口后。示例使用两个受密码保护的测试账户；上线时将 `authenticate` 换成宿主登录会话验证，并保留 CSRF 保护。`HOST_ORIGIN` 填实际 HTTPS 来源，不带末尾斜线。所有环境变量只配置在后端。

```ts
import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = process.env.HOST_ORIGIN;
if (!origin || new URL(origin).protocol !== "https:") throw new Error("Set HOST_ORIGIN to your HTTPS origin");
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (request.headers.get("Host") !== new URL(origin).host) return new Response("Host rejected", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);
```

在下一节保存 `index.html` 后启动。`DEMO_PASSWORD` 是你自己设置的测试账户密码，不是 API Key；浏览器用 `customer_123` 或 `customer_456` 登录。

```bash
export HOST_ORIGIN="https://support.example.com"
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in the server process.
bun server.ts
```

加载器向 `data-token-endpoint` 发 POST 请求，并携带浏览器凭据。端点必须从已验证登录态取得用户 ID，每次签发都重新核验。Praxis 返回 HTTP 201 后，宿主向加载器返回 `token` 和 `expires_at`。不要在 HTML 中写入 API Key，也不要让令牌进入页面 URL。

## 插入加载脚本 [#install]

保存为 `index.html`。把 `ABC123` 替换为实际短号；若已有宿主页，只需将其中的脚本标签放到需要出现对话的位置。

```html
<!doctype html>
<html lang="zh">
<meta charset="utf-8">
<title>Order assistant</title>
<body>
  <h1>Order support</h1>
  <script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" async></script>
</body>
</html>
```

加载器自动创建 iframe 并完成令牌传递。直接打开或粘贴一个 iframe 地址不会自动获得宿主登录用户的身份。

| 脚本属性                  | 用途                                      |
| --------------------- | --------------------------------------- |
| `data-agent`          | 必填，智能体短号                                |
| `data-token-endpoint` | 必填，宿主签发端点；示例与页面同源                       |
| `data-layout`         | 可选，`inline`、`popup`、`sidebar` 或 `modal` |
| `data-launcher`       | 可选，`bubble`、`none` 或已有按钮的 CSS 选择器       |

未指定布局和启动按钮时使用控制台保存的外观；还没有保存布局时，加载器采用 `popup`。显式设置脚本属性会覆盖对应选项。如果使用 `inline`，脚本所在位置就是插入位置，给外层容器预留足够高度。

## 保存外观 [#appearance]

在**接入与配置 → 接入方式与外观 → iframe**调整名称、欢迎语和布局，点**保存**。期望显示“已保存，嵌入页刷新后生效”；刷新你的宿主页验证。持久化外观目前只能在控制台配置。

如果选择 `data-launcher="none"`，浮层没有默认打开按钮。为已有按钮设置 CSS 选择器，或在宿主代码中控制打开，示例如下。按钮需在加载器执行前存在。

```html
<button id="open-support" type="button">Open support</button>
<script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" data-layout="popup" data-launcher="#open-support" async></script>
```

这是上一节脚本的替代写法，同一智能体在页面中只保留一份加载脚本。

## 验证与排错 [#verify]

打开 HTTPS 宿主页并登录，确认对话界面或启动按钮出现；发送问题并收到回复。再用独立浏览器登录第二个测试账户，确认无法看到第一个账户的会话。

在网络面板确认加载了 `loader.js`，iframe 来自 `chat-develop.praxisusercontent.com`，宿主令牌端点成功返回。API Key 只能出现在宿主到 Praxis 的服务端请求中，不应出现在浏览器网络记录里。

* 框架被浏览器阻止：检查允许来源是否与页面来源一致；宿主 CSP 的 `frame-src` 要允许 `https://chat-develop.praxisusercontent.com`，`script-src` 要允许脚本 CDN。不要禁用整站 CSP 来排错。
* 控制台提示 `Invalid Praxis loader configuration`：检查短号格式、脚本域名以及签发端点是否填写。
* 宿主端点返回 HTTP 401 或 403：先恢复宿主登录或修正同源检查；不要把重试当成重新登录。

验证来源拒绝时，从另一个不在白名单中的 HTTPS 站点加载同一脚本，期望浏览器阻止嵌入。验证签发保护时，在退出宿主登录后再次访问签发端点，期望宿主拒绝请求；不要只检查按钮是否隐藏。

用户退出或切换账户时，重新加载宿主页，让后端按新登录态签发，避免沿用原账户界面。加载器的打开和关闭只影响界面显示，不是账户退出，也不会替你的宿主结束登录会话。

## 下一步 [#next]

* [自定义外观](/docs/developers/agent/embed/appearance)
* [保护嵌入接入](/docs/developers/agent/embed/security)
* [智能体 API 参考](/docs/api/native/agent)
* [Cookbook](/docs/cookbook)

# 打开托管对话

给你的网站添加一个按钮，让已登录用户进入 Praxis 的独立 Hosted 对话页。宿主后端验证用户、签发链接并立即跳转；不需要在宿主页安装聊天组件。

## 准备智能体 [#prepare]

准备 Bun、同组织的 API Key，以及一个已配置指令、运行环境和嵌入策略的服务智能体。智能体必须有短号；在 Develop 控制台的**服务智能体**中打开它，确认顶部显示短号，并复制公开 ID `agent_…`。

尚未保存嵌入策略时，进入**接入与配置 → 配置 → 安全**，选择**运行环境**，点**保存安全设置**。嵌入策略目前只能在控制台配置。以下示例使用 Develop API 和本地宿主 `http://localhost:4242`，对话则在托管域名中打开。

Hosted 是独立页面，不能作为 iframe 嵌进宿主。需要留在当前页面时，选择[一行嵌入](/docs/developers/agent/embed/iframe)或 [Kit 组件](/docs/developers/agent/embed/quickstart)。

## 签发链接并跳转 [#issue]

把以下完整服务器保存为 `server.ts`。它只绑定本机，用 HTTP Basic 验证两个测试账户，并检查表单请求来源。`PRAXIS_API_KEY` 只存在后端；`external_user_id` 来自通过验证的账户，而不是表单字段。

```ts
import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = "http://localhost:4242";
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.origin !== origin) return new Response("Use localhost:4242", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-hosted") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId, hosted: true }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.hosted_url !== "string") throw new Error("Missing URL");
      return new Response(null, {
        status: 302, headers: { ...noStore, Location: data.hosted_url },
      });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);
```

`hosted: true` 让签发响应额外包含 `hosted_url`。成功签发的 HTTP 状态为 201；宿主返回 HTTP 302，并把该 URL 放进 `Location`，浏览器随即进入对话页。宿主不把 API Key 或签发响应中的 `token` 发给表单页面。

把实际智能体 ID、API Key 和你设置的本地测试密码放入服务端环境。下一节保存页面后，从同目录启动：

```bash
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts
```

期望：终端显示本地地址。未经认证或来源不匹配的请求无法签发。用于已有应用时，将测试账户验证换成你的登录会话验证，把路由置于登录与 CSRF 保护之后；不要把这套测试账户机制用于线上登录。

## 添加打开按钮 [#open]

保存为同目录的 `index.html`。普通表单提交可以直接跟随后端的跳转；不要通过后台 `fetch` 请求后就假设浏览器已经导航。

```html
<!doctype html>
<html lang="zh">
<meta charset="utf-8">
<title>Order support</title>
<form action="/api/praxis-hosted" method="post">
  <button type="submit">Open order assistant</button>
</form>
</html>
```

打开 `http://localhost:4242`，以 `customer_123` 和所设测试密码登录，然后点按钮。期望：浏览器进入 `chat-develop.praxisusercontent.com` 下的对话页。每次点击都向宿主重新请求链接，不缓存上一次的结果。浏览器地址变化后，再检查是否出现可输入的对话区。

## 理解链接寿命 [#lifetime]

`hosted_url` 的 `#c=…` 是一次性兑换凭证，不是嵌入令牌。它在签发后 60 秒内有效，只能成功兑换一次。不要记录、收藏或分享带凭证的链接；每个用户应从宿主验证身份后进入。

托管页读取凭证后先移除 URL 片段，再自动向 `POST /agent/v1/embed/exchange` 提交 `code`，取得 `token` 和 `expires_at`。你的宿主不需要手动兑换，否则浏览器拿到的链接已被消费。

链接的 60 秒期限与对话令牌有效期是两件事。兑换成功后，页面在令牌到期前使用该令牌；Hosted 不自动刷新令牌。到期或收到吊销错误后，页面会提示回到原网站重新进入。刷新当前托管页也不能重新使用已经移除的一次性凭证，应回宿主重新点击按钮。

## 验证接入 [#verify]

在服务端可以用下面的请求检查签发形状。它只输出记录 ID、到期时间和是否存在链接，不输出凭证；这是独立的接口检查，不会替你导航浏览器。测试用户 ID 只用于这次受控检查，应用必须使用已验证身份。

```bash
set -o pipefail
curl --fail-with-body --silent --show-error \
  -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"agent":"agent_REPLACE_ME","external_user_id":"customer_123","hosted":true}' \
  | jq '{id, expires_at, has_hosted_url: (.hosted_url | type == "string")}'
```

再从按钮执行完整路径：网络面板应看到宿主 POST 返回 302，随后托管页兑换成功并显示对话。发送一条订单问题，确认收到真实回复；只看到加载界面不算成功。

用独立浏览器以 `customer_456` 进入，确认无法读取第一个账户的会话。无凭据访问宿主端点应返回 HTTP 401。验收链接寿命时，使用单独的测试签发，让链接超过 60 秒后再打开，期望要求回到宿主重新进入，不在原链接上反复重试。

## 失败处理与外观 [#troubleshoot]

兑换接口对过期、已用和未知凭证均返回 `NOT_FOUND`（HTTP 404）。这些情况都应重新从宿主进入。缺少短号或嵌入策略会使签发返回 HTTP 400；在控制台补齐前提后再试。出现 HTTP 429 时按 `Retry-After` 等待，再由宿主签发新链接。

外观在**接入与配置 → 接入方式与外观 → iframe**保存，Hosted 共享名称、主题和欢迎语等设置。持久化外观目前只能在控制台配置。Hosted 始终全页显示，不使用浮窗启动按钮。控制台的 **Hosted → 打开预览**用于预览，不替代宿主对真实登录用户的签发。

## 下一步 [#next]

* [嵌入令牌与用户](/docs/developers/agent/embed/tokens)
* [自定义外观](/docs/developers/agent/embed/appearance)
* [智能体 API 参考](/docs/api/native/agent)
* [Cookbook](/docs/cookbook)

# 配置智能体

修改服务智能体的指令、模型和工具，再选择向终端用户提供哪一个版本。配置保存与嵌入版本选择是两个动作；固定版本后，保存新配置不会自动升级嵌入。

## 准备智能体 [#prepare]

准备一个已有的服务智能体，以及有权管理它的组织账户。打开控制台「服务智能体 → 选择智能体 → 接入与配置 → 配置」。下面的控件名称对应当前中文控制台。

使用 API 时，从创建或查询响应取得公开 ID `agent_…`。在服务器设置 `PRAXIS_API_BASE` 为所用环境的 API 源站地址，`PRAXIS_API_KEY` 为该组织的 API Key；不要把 Key 放入网页。下例还需 Node.js（支持原生 `fetch`），并将 `PRAXIS_AGENT` 设置为真实公开 ID。

## 修改并保存配置 [#save]

## 单独保存嵌入设置 [#embed-settings]

嵌入安全策略、凭据库选择与嵌入版本固定**目前只能在控制台配置**。

在「配置 → 安全」选择可用运行环境，填写你的网站来源，并设置令牌有效期与预算，然后点击「保存安全设置」。有效期界面单位为分钟，预算单位为美元；不要直接把接口的秒数或美分数填入界面。来源规则见[保护嵌入接入](/docs/developers/agent/embed/security)。

需要凭据库时，在「MCP 与凭据」勾选后点击「保存凭据库」。&#x2A;*预期结果：** 两种保存均显示「已保存，不产生新版本」。它们不属于底部保存栏的配置草稿。

## 选择嵌入版本 [#versions]

进入「接入与配置 → 版本」。点击版本号查看与上一版本的差异，再选择「设为嵌入版本」或旧版本的「回滚到此版本」，确认后检查「嵌入中」标记。需要恢复跟随最新时点击「取消固定」。没有安全策略时，先完成上一步。

签发时显式指定的 `version` 优先于控制台固定版本；省略时使用固定版本，未固定才取最新版本。**已经签发的令牌保持原版本**，回滚不会改变它们。验证升级或回滚时，应重新签发令牌并开始新会话。

## 验证与下一步 [#verify]

进入「试用调试」，选择刚保存的版本，发送一个能区分新旧指令的问题，检查真实回复。随后在宿主网页重新取得令牌，开始新会话，并在「运营与管理 → 会话」核对版本。只看到界面加载不能证明新配置已被使用。

服务智能体目前不支持按消息切换模型、工具模式、语音输入和按终端用户的记忆；「配置 → 开场与外观」仍是占位，外观操作见[自定义外观](/docs/developers/agent/embed/appearance)。

下一步：[测试嵌入接入](/docs/developers/agent/embed/testing) · [智能体 API](/docs/api/native/agent) · [Cookbook](/docs/cookbook)

# 嵌入令牌与用户

嵌入令牌让终端用户访问指定智能体及自己名下的会话。登录用户的令牌由宿主后端签发，浏览器消费；本页说明身份、有效期、刷新和单张令牌吊销。

准备同组织的服务智能体、已保存的嵌入策略和服务端 API Key。策略在控制台的**接入与配置 → 配置 → 安全**保存，目前只能在控制台配置。示例使用 Develop；完整网页接入见[嵌入到网页](/docs/developers/agent/embed/quickstart)，一次性跳转见 [Hosted](/docs/developers/agent/embed/hosted)。

## 身份与访问范围 [#identity]

宿主后端用自己的登录系统确认用户，再以 `external_user_id` 传入稳定标识。同一组织、同一智能体、同一标识对应同一个终端用户；它不是组织成员 ID，也不是昵称。标识长 1–128 字符，仅允许字母、数字、下划线、点、冒号、`@` 和连字符。

| 凭证               | 谁使用        | 用途                |
| ---------------- | ---------- | ----------------- |
| API Key          | 宿主后端       | 签发、查看和吊销令牌；不交给浏览器 |
| 嵌入令牌             | 终端用户客户端    | 访问指定智能体下自己的会话     |
| 可公开 key          | 匿名接入页面     | 获取匿名令牌，可写入网页源码    |
| `visitor_secret` | 同一匿名访客的浏览器 | 延续访客身份；必须保密       |

刷新令牌不会改变用户身份。更换登录账户则应移除旧组件，按新身份重新创建；界面中的确认按钮不能代替工具服务端的账户授权。

## 签发令牌 [#issue]

生产签发由宿主后端调用 `POST /agent/v1/embed_tokens`。以下是服务端独立检查，测试用户固定为 `customer_123`；真实宿主必须从验证后的登录会话取值，不接受浏览器自报身份。控制台的**试用调试 → 模拟 → 重新签发**只用于调试身份。

期望：HTTP 201，响应含 `id`、`token`、`expires_at`。`id` 标识本次签发，供后续吊销；`token` 是浏览器凭证；`expires_at` 是 ISO 8601 UTC 到期时间。示例只输出脱敏检查结果。每次调用都会新建签发记录，重复请求不幂等。

可选 `ttl_seconds` 省略时取策略默认值；范围从 60 秒到策略上限与 3600 秒中的较小值。`budget_cents` 不能超过策略单会话预算上限；`allowed_origins` 只能取策略已有条目的子集。版本按显式 `version`、策略固定版本、最新版本的次序选择，签发后固定。完整字段见 [API 参考](/docs/api/native/agent)。

## 刷新与结束访问 [#refresh]

Kit 调用 `getToken(current)` 时，首次传入 `null`，刷新时传入当前令牌。把以下回调放进快速开始的 `api.getToken`；宿主端点每次都验证用户仍已登录，再返回新令牌。

```js
async function getToken(current) {
  const response = await fetch("/api/praxis-token", {
    method: "POST", credentials: "same-origin", cache: "no-store",
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  const data = await response.json();
  if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
  return data.token;
}
```

回调不需要把当前令牌发回宿主来证明身份。Kit 负责调用时机并在内存中持有令牌；不要把一个固定令牌写死在回调中。期望：刷新后仍访问同一用户的会话。

宿主退出登录时，先结束宿主会话，再移除旧 Kit 元素；移除会关闭它的连接，但不会在服务器吊销已签发的令牌。需要立即禁止原凭证访问时，另行吊销。Hosted 没有这条刷新路径，到期后必须回宿主重新进入。

## 匿名访客 [#anonymous]

公开问答不要求登录时，在**接入与配置 → 密钥 → 可公开 key → 新建可公开 key**创建 key，设置匿名限流和预算，并复制只显示一次的完整值。来源仍在**配置 → 安全**管理。这些设置目前只能在控制台配置；可公开 key 与服务端 API Key 不能互换。

以下是可独立保存的 `index.html`。替换 `PRAXIS_PUBLISHABLE_KEY` 为实际可公开 key，并从允许来源的网站提供此文件。它不需要登录用户的宿主签发端点；不要同时配置 `getToken`。

```html
<!doctype html>
<html lang="zh">
<meta charset="utf-8">
<title>Public documentation assistant</title>
<script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<script type="module">
  await customElements.whenDefined("praxis-agent");
  document.querySelector("praxis-agent").setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      publishableKey: "PRAXIS_PUBLISHABLE_KEY",
    },
    agent: { name: "Documentation assistant" },
  });
</script>
</html>
```

Kit 向 `POST /agent/v1/embed_tokens/anonymous` 提交 `publishable_key`，后续带上已有的 `visitor_secret`。接口不需要 API Key，成功返回 HTTP 200、`token`、`expires_at`；仅新访客另收到 `visitor_secret`。浏览器不能选择 `external_user_id`、预算、指令或 Hosted 选项。

Kit 将访客凭据保存在本地存储，存储不可用时退回页面内存。不要公开或共享它。期望：同一存储中的访客刷新后延续身份；独立浏览器存储产生独立身份。匿名身份不授予读取实名订单等账户数据的权限。

## 查看与吊销 [#revoke]

只吊销需要停止使用的那张令牌。下面两种路径完成同一任务。

期望：单张吊销返回带 `revoked_at` 的记录；重复吊销返回原记录。该令牌的事件流收到 `session.error`，错误码为 `EMBED_TOKEN_REVOKED`，随后关闭；其他令牌不受影响。用户级批量撤销、封禁和解封见[管理终端用户](/docs/developers/agent/end-users)。

## 错误与下一步 [#next]

到期后重新验证宿主登录；吊销后停止原访问；来源不允许时修正配置；用户被封禁时交由管理者处理。不要无限重签来绕过拒绝。具体错误码、HTTP 状态和恢复条件见[嵌入限制与错误](/docs/developers/agent/embed/limits-and-errors)。

* [传入上下文](/docs/developers/agent/embed/context)
* [管理终端用户](/docs/developers/agent/end-users)
* [打开托管对话](/docs/developers/agent/embed/hosted)
* [智能体 API 参考](/docs/api/native/agent)

# 传入上下文

把当前课程、选中的订单或用户偏好交给智能体，让回答贴合用户此刻的任务。页面状态用 readables 随消息发送；后端核实的信息用宿主上下文传入。

## 准备接入 [#prepare]

准备一个已能回复的 Kit 页面、服务智能体和嵌入策略。下面的 HTML 由你的宿主网站提供；同源 `POST /api/praxis-token` 是你按[嵌入令牌与用户](/docs/developers/agent/embed/tokens)实现的后端路由，验证登录态后返回 `{ token }`，不是 Praxis 的公开接口。示例使用 Develop 的 Kit 和 API，页面来源须已获准接入。

## 随消息传入页面状态 [#page-context]

把下面的页面保存为网站中的 HTML。下拉框变化时更新登记值，发送消息时 Kit 取当前快照。

```html
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8" />
<title>课程助手</title>
<label>当前课程
  <select id="lesson">
    <option value="第三课：配送规则">第三课：配送规则</option>
    <option value="第四课：退货流程">第四课：退货流程</option>
  </select>
</label>
<button id="clear">停止传入课程</button>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  const lesson = document.querySelector("#lesson");
  const update = () => chat.setReadable("lesson", { title: lesson.value });
  update();
  lesson.addEventListener("change", update);
  document.querySelector("#clear").addEventListener("click", () => {
    chat.removeReadable("lesson");
  });
  chat.addEventListener("praxis.log", event => console.warn(event.detail));
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "课程助手" },
  });
</script>
</html>
```

**预期结果：** 询问「我正在看哪一课」，智能体收到第三课的信息；切换课程再发消息，新消息携带第四课。点击「停止传入课程」后，后续消息不再附带该项。

`setReadable(name, value)` 接受字符串或可 JSON 序列化的值；名称须匹配 `[A-Za-z0-9_.:-]{1,64}`。同名登记覆盖当前值，`removeReadable(name)` 删除登记。非法名称、不能序列化的值会抛错，原登记保持不变。

每条消息最多 32 项，整个上下文 JSON 最多 4096 UTF-8 字节。超出时 Kit 从最早登记的项开始丢弃本次快照中的数据，并通过 `praxis.log` 发出 `warn`，`data.names` 列出丢弃项。登记本身保留；缩小值后可再次发送。

页面上下文附在本轮用户消息里，标记为不可信。每轮取新快照，但移除登记不会抹去旧消息中的信息。它不能证明用户身份、账户余额或订单归属。

## 传入后端核实的信息 [#host-context]

在宿主后端签发令牌时传 `system_message`。下面是服务端请求：替换公开智能体 ID，测试用户 ID 换成宿主登录态确定的 ID；不要把 `PRAXIS_API_KEY` 或这段命令放进网页。

```bash
curl --fail-with-body https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -X POST \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "agent": "agent_example",
    "external_user_id": "customer_123",
    "system_message": [{"type": "text", "text": "客户偏好中文回复。"}]
  }'
```

**预期结果：** 响应为 HTTP 201，包含 `token`、`id` 和 `expires_at`。让宿主路由只把所需的短期凭据返回浏览器，用该令牌创建新会话，用户偏好会进入模型上下文。不要把令牌打印到共享日志。

`system_message` 是 `{ type: "text", text }` 数组，文本合计最多 8192 UTF-8 字节；使用一个文本块可避免分块分隔符增加体积。它是模型可读信息，不是秘密存储。

已经开始的会话可以由 API Key 持有者通过[发送事件](/docs/api/native/agent/sendEvents)追加 `system.message`；更换令牌不会改写已建立会话的上下文。

## 选择其他输入 [#choose-input]

| 需要传入的内容         | 使用方式                                                              |
| --------------- | ----------------------------------------------------------------- |
| 每位用户都相同的角色和语气   | 控制台「配置 → 指令」                                                      |
| 只对一张令牌更换整套指令    | 签发时传 `agent_override.system`；须先在「配置 → 安全」开启「允许宿主覆盖指令」；`null` 清空指令 |
| 只在被问及时读取的页面数据   | [前端工具](/docs/developers/agent/embed/client-tools)                 |
| 订单等需要鉴权或最新查询的数据 | 服务端工具，按终端用户限定数据范围                                                 |

覆盖未获准时返回 HTTP 400、`EMBED_FIELD_NOT_ALLOWED`。先修正策略或去掉覆盖字段，再重新签发；重复同一请求不会解除限制。

## 验证传入结果 [#verify]

打开浏览器 Network，发送消息，检查请求中的 `praxis_context.items`：应有 `name: "lesson"` 和当前课程的字符串值。切换课程、删除登记后各发一次，确认请求随之变化；不要只凭模型回答判断是否传入成功。

后端上下文用新签发的令牌新建会话，询问回复语言并核对签发请求中的文本。模型回复用于体验验证；身份和权限仍由宿主后端及工具服务端校验。

下一步：[前端工具](/docs/developers/agent/embed/client-tools) · [保护嵌入接入](/docs/developers/agent/embed/security) · [智能体 API](/docs/api/native/agent)。

# 接入前端工具

让智能体按需读取当前页面，或执行页面上的动作。你在智能体里声明工具，在 Kit 页面实现同名回调；Kit 把结果送回会话，智能体继续回答。

## 准备接入 [#prepare]

准备一个能回复的 Kit 页面、服务智能体的公开 ID（`agent_…`），以及宿主后端的 API Key。下面以读取课程选择为例，不需要视频、数据库或额外依赖。宿主网站须已有验证登录态的 `POST /api/praxis-token` 路由，返回 `{ token }`，详见[嵌入令牌与用户](/docs/developers/agent/embed/tokens)。它是你的路由，不是 Praxis API。

前端工具适合按需读取。每条消息都需要的小事实，使用[页面上下文](/docs/developers/agent/embed/context#page-context)。

## 在智能体声明工具 [#declare-tool]

将以下文件保存为 `register-tool.ts`，替换 `agent_example`，在已注入 `PRAXIS_API_KEY` 的服务端运行 `bun register-tool.ts`。它先读取智能体，再通过公开 API 更新 `tools`，保留其他工具，并用当前 `version` 防止覆盖并发修改。

```ts
// register-tool.ts — run on the server with: bun register-tool.ts
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY on the server");
const url = "https://api-develop.deepraxis.com/agent/v1/agents/agent_example";
const headers = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };
const currentResponse = await fetch(url, { headers });
if (!currentResponse.ok) throw new Error(`Read failed: ${currentResponse.status}`);
const current = await currentResponse.json();
const tool = {
  type: "custom",
  name: "read_selected_lesson",
  description: "Read the selected lesson when the user asks which lesson is open.",
  input_schema: { type: "object", properties: {}, additionalProperties: false },
};
const tools = [...(current.tools ?? []).filter(
  (item: { type: string; name?: string }) =>
    !(item.type === "custom" && item.name === tool.name),
), tool];
const response = await fetch(url, {
  method: "POST", headers,
  body: JSON.stringify({ version: current.version, tools }),
});
if (!response.ok) throw new Error(`Update failed: ${response.status}`);
const saved = await response.json();
console.log({ id: saved.id, version: saved.version });
```

**预期结果：** 终端输出智能体 ID 和新版本。让宿主签发新版本的令牌，再开始新会话；已固定旧版本的令牌或已有会话不会自动换到新定义。若嵌入策略固定版本，在控制台更新接入版本，或签发时明确指定新 `version`。

工具名须匹配 `^[a-zA-Z0-9_-]{1,64}$`，不能与其他自定义工具或内置工具重名。`input_schema.type` 必须是 `object`；描述要说明何时调用。仅在页面实现函数，不会把工具自动加入智能体。

## 实现页面回调 [#handle-call]

将下面的 HTML 放到宿主网站。它使用 Develop 的 Kit 和 API；页面来源需要在嵌入策略中获准。

```html
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8" />
<title>课程助手</title>
<label>当前课程
  <select id="lesson">
    <option value="第三课：配送规则">第三课：配送规则</option>
    <option value="第四课：退货流程">第四课：退货流程</option>
  </select>
</label>
<output id="tool-status" aria-live="polite"></output>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  const lesson = document.querySelector("#lesson");
  chat.onClientTool = async ({ name }) => {
    if (name !== "read_selected_lesson") throw new Error(`Unknown tool: ${name}`);
    return { title: lesson.value };
  };
  chat.addEventListener("praxis.tool.change", event => {
    const { name, status, isError } = event.detail;
    document.querySelector("#tool-status").textContent =
      `${name}: ${status}${isError ? " (error)" : ""}`;
  });
  chat.addEventListener("praxis.log", event => console.warn(event.detail));
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "课程助手" },
  });
</script>
</html>
```

`onClientTool` 是元素的 JavaScript 属性，不能放进 HTML 属性或 `setOptions`。回调收到 `{ id, name, input }`，分别是调用编号、工具名和输入；参数应由你的函数校验。对未知工具名抛错，不要返回成功。

字符串返回值原样发送，其他可序列化值转成 JSON 文本，`undefined` 转成空字符串。抛错、Promise 拒绝或序列化失败会作为错误结果返回。不要在结果里放其他用户的数据或凭据。

## 验证调用与恢复 [#verify]

询问「请调用工具告诉我正在看哪一课」。&#x2A;*预期结果：** 页面状态依次显示 `start`、`end`，智能体根据工具返回的课程回答。换课后再次提问，返回当前选择。`end` 表示服务端已接收结果，不代表整轮回复结束。

| 现象                              | 检查与处理                           |
| ------------------------------- | ------------------------------- |
| 没有调用                            | 确认新会话使用含工具定义的版本，描述明确说明触发情境      |
| 返回 `not available on this page` | 在调用前给元素赋上 `onClientTool`        |
| 回调失败                            | 修正输入校验或页面逻辑；错误结果会交给智能体          |
| `praxis.log` 报结果提交失败            | 回调不会自动重跑；核对网络及会话状态，勿盲目重做有副作用的动作 |

服务端最多等待 120 秒；超时记入错误结果后继续。Kit 会认领调用编号，并检查历史结果以避免重复执行，但这不是跨标签页事务保证。会改变业务数据的工具必须由后端鉴权并做幂等处理。

## 授权与下一步 [#next]

前端 `custom` 工具不经过平台的工具确认卡。需要用户确认的页面动作，由你的界面处理；真正的数据权限必须在后端检查，不能依赖浏览器或模型参数。

[配置工具授权](/docs/developers/agent/embed/tool-authorization) · [组件参考](/docs/developers/agent/embed/component-reference) · [更新智能体 API](/docs/api/native/agent/updateAgent)。

# 配置工具授权

为已启用的工具选择直接执行或每次询问，让用户在操作发生前确认意图。本页以订单 MCP 的查询和退货工具为例；业务权限仍须由工具服务端校验。

## 准备工具 [#prepare]

准备一个已能接入的服务智能体，并在「配置 → MCP 与凭据」连接名为 `orders` 的 MCP 服务器。示例要求它提供已启用的 `list_orders` 和 `request_return`。替换为你实际的服务器和工具名称，不要用示例名称配置不存在的工具。

确认卡不能代替身份认证。公网 MCP 服务必须先认证平台调用方，再按终端用户限定数据范围，见[保护嵌入接入](/docs/developers/agent/embed/security#tool-access)。

## 选择执行策略 [#policies]

| 策略             | 对普通已启用工具的效果               |
| -------------- | ------------------------- |
| `always_allow` | 不询问，直接执行                  |
| `always_ask`   | 暂停并等待用户确认，允许后执行；拒绝则不执行该调用 |

目前支持这两档。不要使用 `auto`：即使某处类型接受它，运行时也会拒绝执行。需要禁止使用一个工具时，关闭该工具；「每次询问」仍允许用户同意调用。

策略不绕过工具自身的强制交互、安全边界或业务检查。前端 `custom` 工具由你的页面执行，不经过这套确认策略。

## 设置工具与默认值 [#configure]

解析顺序为单工具 `configs[].permission_policy` → 工具集 `default_config.permission_policy` → 工具默认策略。未设置 MCP 策略时，服务器声明 `annotations.readOnlyHint === true` 才默认放行，否则默认询问。该提示仅影响缺省，显式配置优先。

内置工具集未设置默认策略时为 `always_allow`，但工具自身要求的交互仍然存在。对退款、删除和对外发送等动作，明确设置询问，不要仅依赖服务器提示。

## 验证确认过程 [#verify]

签发使用新版本的嵌入令牌并新建会话；若嵌入策略固定版本，先更新固定版本或在签发时指定新版本。保存智能体不会自动升级已有会话。

先让智能体调用查询工具，再请求一次测试退货。&#x2A;*预期：** 查询直接返回；退货显示 Kit 确认卡。拒绝后工具服务端不应执行退货；再次提出请求并允许后才执行。以工具服务端记录核实调用是否发生，不要只看智能体的回答。

Kit 自带确认卡，无须另写界面。嵌入用户的允许只针对这次调用，不会保存为永久工具权限。完整测试情境见[测试嵌入接入](/docs/developers/agent/embed/testing)。

## 下一步 [#next]

[保护工具服务](/docs/developers/agent/embed/security#tool-access) · [前端工具](/docs/developers/agent/embed/client-tools) · [更新智能体 API](/docs/api/native/agent/updateAgent)。

# 自定义外观

让聊天的配色、开场和输入区与网站一致。iframe 与 Hosted 使用控制台保存的同一份外观；Kit 组件使用你在网页中传入的选项。

## 准备接入 [#prepare]

先准备能正常回复的服务智能体和嵌入策略。Kit 示例要求宿主网站已提供 `POST /api/praxis-token`，验证登录态并返回 `{ token }`；这是你的同源路由，详见[嵌入令牌与用户](/docs/developers/agent/embed/tokens)。示例使用 Develop 地址，页面来源须获准接入。

| 接入方式            | 外观在哪里配置                                    |
| --------------- | ------------------------------------------ |
| iframe / Hosted | 控制台保存，两个页面共用；刷新嵌入页后生效                      |
| Kit 组件          | 网页的 `setOptions`；控制台的 Kit 编辑器只供预览和生成代码，不保存 |

## 保存托管外观 [#saved-appearance]

iframe / Hosted 的持久外观**目前只能在控制台配置**。当前控制台入口为「服务智能体 → 你的智能体 → 接入与配置 → 接入方式与外观 → iframe」。

1. 在「主题」选择配色、强调色、灰阶、圆角和密度。
2. 在「开场」填写欢迎语与推荐问题，在「输入区」设置输入提示和页脚。
3. 点击「保存」。&#x2A;*预期：** 提示「已保存，嵌入页刷新后生效」。刷新 iframe，或重新打开 Hosted，检查同一组文字和配色。

不要去「配置 → 开场与外观」编辑：该位置目前是提示页。公开外观接口只提供读取，没有公开的保存接口。

## 设置 Kit 外观 [#kit-options]

把下面的 HTML 放在宿主网站。它同时设置主题、开场和输入框，并提供一个按钮验证运行中更新。

```html
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8" />
<title>订单助手</title>
<button id="dark">切换深色</button>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "订单助手" },
    theme: {
      colorScheme: "light", accent: "#2563eb", grayscale: "cool",
      radius: "lg", density: "normal", fontFamily: "system-ui, sans-serif",
      fontSize: 16,
    },
    header: { title: "订单服务", subtitle: "配送与退货咨询" },
    startScreen: {
      greeting: "今天想了解什么？",
      prompts: [{ label: "配送规则", prompt: "请介绍配送规则。" }],
    },
    composer: { placeholder: "输入订单问题", attachments: { enabled: false } },
    history: { enabled: true },
    footer: "订单信息以商家确认为准。",
  });
  document.querySelector("#dark").addEventListener("click", () => {
    chat.setOptions({ theme: { colorScheme: "dark" } });
  });
</script>
</html>
```

**预期结果：** 出现蓝色强调色、欢迎语和「配送规则」按钮；输入框不显示附件入口。点击「切换深色」后颜色切换，当前会话和消息保留。

再次调用 `setOptions` 会合并嵌套选项，数组整体替换。因此更新 `theme.colorScheme` 不需要重传令牌回调；更换推荐问题时，传入完整的 `startScreen.prompts` 数组。

## 调整主题与文字 [#options]

| 选项                                             | 可选值或用途                                                      |
| ---------------------------------------------- | ----------------------------------------------------------- |
| `theme.colorScheme`                            | `light`、`dark`、`system`；省略为浅色，`system` 跟随系统配色               |
| `theme.accent` / `theme.surface`               | 强调色 / 背景色；使用明确、不透明的 CSS 颜色，例如 `#2563eb`；不接受 `var(...)` 或透明色 |
| `theme.grayscale`                              | `neutral`、`warm`、`cool`                                     |
| `theme.radius`                                 | `none`、`sm`、`md`、`lg`、`full`                                |
| `theme.density`                                | `compact`、`normal`、`spacious`                               |
| `theme.fontFamily` / `theme.fontSize`          | 字体栈 / 正数像素字号；字体须由你的页面自行加载                                   |
| `startScreen.greeting` / `startScreen.prompts` | 欢迎语 / 最多显示 4 个推荐问题；`label` 是按钮文字，点击后发送 `prompt`             |
| `composer.placeholder`                         | 输入框提示                                                       |
| `composer.attachments.enabled`                 | Kit 附件入口开关，默认开启；不在保存的 iframe / Hosted 外观中提供                 |
| `locale`                                       | `zh` 或 `en`；Kit 未指定时先取宿主页语言，再取浏览器语言                         |

保存的外观 JSON 最多 8192 UTF-8 字节；欢迎语 200 字符、推荐问题最多 4 项，每项 `label` 40 字符、`prompt` 200 字符，输入提示 80 字符。头像和顶栏图片须使用 HTTPS 地址。只改组件配置时，详细属性边界见[组件参考](/docs/developers/agent/embed/component-reference)。

## 验证与下一步 [#verify]

在一个新会话中检查开场文字，点击推荐问题，确认实际发送的是 `prompt`。在已有消息的会话点击深色按钮，确认消息保留；另开页面检查控制台保存的外观，避免把 Kit 的本地配置误认为已保存。

外观更新不改变智能体指令或工具授权。若保存失败，按字段提示缩短文字、减少推荐问题或修正图片地址后重试。

[组件参考](/docs/developers/agent/embed/component-reference) · [保护嵌入接入](/docs/developers/agent/embed/security) · [智能体 API](/docs/api/native/agent)。

# 保护嵌入接入

把嵌入智能体接到网站时，用宿主登录态确定用户，用来源白名单限制浏览器接入，并在工具服务端检查权限。本页配置这些边界；测试情境与上线验收分别见[测试嵌入接入](/docs/developers/agent/embed/testing)和[上线检查清单](/docs/developers/agent/embed/go-live)。

## 准备与责任 [#prepare]

你需要能管理服务智能体的控制台账号、宿主网站的后端，以及修改网站响应头的权限。先完成嵌入策略和[令牌签发](/docs/developers/agent/embed/tokens)，再按下面的责任检查接入。

| 平台边界                 | 宿主必须完成的工作                               |
| -------------------- | --------------------------------------- |
| 嵌入用户只能访问所属智能体下自己的会话  | 从已验证登录态取得 `external_user_id`，拒绝浏览器自报的身份 |
| 跨用户、跨智能体资源访问返回 404   | API Key 仅放后端，签发路由验证登录态并防止跨站请求伪造         |
| 浏览器来源校验与 iframe 嵌入限制 | 登记实际网站来源，并限制宿主页可以加载的脚本                  |

## 保管身份与凭据 [#credentials]

后端用 `PRAXIS_API_KEY` 签发短期嵌入令牌，浏览器只接收需要的令牌。每次签发和续期都重新验证宿主登录态；不要让请求体中的 `external_user_id` 决定用户是谁。不要把 API Key、嵌入令牌放进 URL、HTML 或日志。

Kit 的传输层在内存中持有令牌。匿名接入另有 `visitor_secret`，Kit 会尝试保存在 `localStorage` 以延续访客身份，存储不可用时回退到页面内存。`publishableKey` 可公开，`visitor_secret` 不可公开；匿名身份不等于你网站的登录账户。

登出时停止签发、移除聊天组件，并由后端撤销相应凭据；清空页面并不等于撤销。封禁终端用户会阻止新签发并撤销该用户现有令牌。Kit 可在到期前重新取令牌，Hosted 不自动续期；详见[嵌入令牌与用户](/docs/developers/agent/embed/tokens)。

指令、`system_message`、页面上下文与工具结果都可能被模型复述。仅传入当前用户有权知道的信息，不要把密钥或其他用户的数据当作上下文传入。

## 限定页面来源 [#origins]

来源白名单**目前只能在控制台配置**。在当前控制台打开「服务智能体 → 你的智能体 → 接入与配置 → 配置 → 安全」，点击「添加来源」，填写例如 `https://shop.example.com`，然后点击「保存安全设置」。&#x2A;*预期：** 显示「已保存，不产生新版本」。

`allowed_origins` 最多 50 项，只填来源，不含路径、查询参数或片段。普通来源须为 HTTPS；`https://*.example.com` 只匹配一级子域，不匹配根域名或多级子域，且不支持端口。「允许 localhost」只用于本地 HTTP 调试，默认关闭。

签发时可以用 `allowed_origins` 选择策略中的非空子集，条目必须与策略完全一致。不要传空数组来表达「禁止所有来源」：令牌校验会回退读取策略白名单。修改策略也不等于撤销已签发令牌，需要立即收回访问时执行撤销。

来源不允许时返回 HTTP 403、`EMBED_ORIGIN_NOT_ALLOWED`。先修正来源配置再重新接入。来源检查约束浏览器；没有 Origin 的非浏览器请求不受这项检查保护，所以它不能代替令牌认证。

## 保护工具访问 [#tool-access]

MCP 服务器先验证平台调用凭据，再读取 `X-Praxis-External-User-Id` 作为终端用户身份。平台还会附带 `X-Praxis-Agent-Id` 和 `X-Praxis-Session-Id`；没有终端用户的会话不带这些身份头。

你的服务必须拒绝缺少所需身份的请求，每次查询都限定该用户的数据，修改订单前再验证归属和业务状态。不要相信模型传入的用户 ID，也不要仅凭公网请求带了身份头就信任它。通过 MCP 配置的 `headers` 认证调用方，才能使用平台附带的身份。

工具确认卡帮助确认意图；点击允许不会授予数据访问权。前端工具和页面上下文来自浏览器，不能承担权限校验。

## 配置宿主页保护 [#page-protection]

以下是 Develop 宿主页的响应头示例。适用于初始化脚本放在同源 JavaScript 文件中、Kit 从 CDN 加载的页面；按网站所需资源收窄图片等来源。若保留内联初始化脚本，须另为该脚本配置 CSP nonce 或哈希。

```http
Content-Security-Policy: default-src 'self'; script-src 'self' https://kit-develop.praxisusercontent.com; style-src 'self' 'unsafe-inline' https://kit-develop.praxisusercontent.com; connect-src 'self' https://api-develop.deepraxis.com https://kit-develop.praxisusercontent.com; frame-src https://chat-develop.praxisusercontent.com; font-src 'self' https://kit-develop.praxisusercontent.com data:; img-src 'self' https: data: blob:; frame-ancestors 'none'; base-uri 'self';
```

Kit 主题使用内联样式，因此示例允许内联样式。连接来源要与 `api.url` 一致。`frame-src` 允许加载托管 iframe；`frame-ancestors 'none'` 则禁止其他网站嵌入你的宿主页，两者作用不同。

平台 iframe 的 `frame-ancestors` 来自来源白名单，空白名单禁止嵌入；Hosted 禁止被嵌入。Kit 使用 open Shadow DOM，只隔离样式，同页脚本可以读取它；不要把不可信脚本与聊天组件放在同一页面。

若使用 CDN 完整性校验，复制控制台生成的实际固定版本地址与 `integrity`，保留 `crossorigin="anonymous"`。不要给浮动的 `/v1/` 地址配固定哈希，也不要把示意哈希当作发布值。

## 验证与下一步 [#verify]

保存后从获准的网站重新加载嵌入页，确认能发送消息。用浏览器 Network 检查宿主页响应中的 CSP，Console 中不应出现 Kit 加载或 API 连接被 CSP 阻止的错误。检查签发路由只返回短期凭据，工具日志中的用户范围与登录用户一致。

[测试嵌入接入](/docs/developers/agent/embed/testing) · [上线检查清单](/docs/developers/agent/embed/go-live) · [嵌入限制与错误](/docs/developers/agent/embed/limits-and-errors)。

# 管理终端用户

按宿主应用的用户 ID 查找终端用户，并在需要时停止或恢复其嵌入访问。操作范围是当前组织、当前智能体下的这个用户。

## 准备与选择操作 [#prepare]

准备已有嵌入策略的服务智能体、组织管理权限，以及签发时使用的 `external_user_id`。先用隔离测试用户验证操作；撤销和封禁会中断其正在进行的对话。

| 目的        | 操作   | 后续访问                  |
| --------- | ---- | --------------------- |
| 结束当前凭证的访问 | 吊销令牌 | 撤销该用户的全部现有令牌；宿主仍可签发新的 |
| 持续拒绝用户访问  | 封禁   | 撤销现有令牌，拒绝新签发          |
| 恢复签发资格    | 解封   | 允许新签发；已撤销的旧令牌不会恢复     |

只想撤销一张令牌时，使用[令牌管理](/docs/developers/agent/embed/tokens)，不要执行用户级批量撤销。

## 查找并管理用户 [#manage]

## 验证访问边界 [#verify]

撤销后，用旧令牌再次访问，应被拒绝；宿主重新确认登录身份后可以签发新令牌。封禁后，宿主的签发请求应收到 `EMBED_USER_BLOCKED`（HTTP 403）。解封后重新签发并发送消息，确认恢复；不要以旧令牌重试成功作为解封标准。

同时用另一个用户访问同一智能体，确认其访问未被本次操作影响。用户身份不能取自浏览器任意提交的 ID，应由宿主后端的登录态确定。

## 处理失败 [#failures]

查不到用户时，核对组织、公开智能体 ID 和签发时的完整用户 ID；控制台试用调试的数据不会进入客户用户列表。仅有前缀相似不能证明是同一个用户。

若封禁返回失败，重新读取状态后再决定是否重试：封禁状态可能已经保存，而撤销步骤尚未完成。不要在这种情况下显示“封禁完成”。记录 HTTP 状态和请求 ID，按[限制与错误](/docs/developers/agent/embed/limits-and-errors)处理。

## 下一步 [#next]

[查看会话与用量](/docs/developers/agent/monitor) · [测试嵌入接入](/docs/developers/agent/embed/testing) · [智能体 API](/docs/api/native/agent) · [Cookbook](/docs/cookbook)

# 查看会话与用量

从会话、用户和花费判断嵌入是否正常工作，再打开具体会话检查发生了什么。本页适合已经产生真实宿主访问的服务智能体。

## 准备检查记录 [#prepare]

使用有权管理该智能体的组织账户，打开控制台「服务智能体 → 选择智能体 → 运营与管理」。下文使用当前中文控制台的控件名称。准备要检查的时间、终端用户 ID 和宿主接入方式。

先在宿主网页发送一条可识别的测试消息并收到回复，再查运营数据。控制台「试用调试」的记录不会进入客户运营统计，不能用它证明客户流量或花费已经入表。

## 查看概览 [#overview]

进入「概览」，选择 24 小时、7 天或 30 天。概览统计目前只能在控制台查看，没有对应的公开聚合 API。

| 检查内容               | 用它回答什么                                    |
| ------------------ | ----------------------------------------- |
| 会话、活跃终端用户、花费、每会话花费 | 访问量和每次服务的成本是否变化                           |
| 好评率                | 有评价的回复中，正面评价占比是多少                         |
| 首字延迟 P50、错误率       | 首次响应是否变慢，运行是否出现失败                         |
| 来源与版本分布            | 流量来自 Kit、iframe、Hosted、API 还是自动化；新版本是否被使用 |
| 需要关注、工具调用 Top 5    | 来源被拦、预算触顶或工具调用是否异常                        |

**预期结果：** 选择的时间范围内出现会话趋势和版本分布；没有对应样本的指标显示「—」，不是零。24 小时趋势按小时展示，其他范围按天展示。运行错误、来源拦截和工具计数按所选范围覆盖的 UTC 自然日汇总，首尾不足一天时也包含整日计数。

## 查找与回放会话 [#sessions]

## 检查用量与预算 [#usage]

进入「用量与预算」，查看本月花费、月度上限和触顶动作，再选择 7、30 或 90 天的每日花费。下方展示预算与限流、本月来源拆分及花费最高的终端用户 Top 20。本月按 UTC 自然月计算，界面金额为美元。

这些聚合报表与预算设置目前只能在控制台查看或配置。「修改上限」进入「配置 → 安全」；保存后返回核对。触顶动作为「停止新会话」时会拒绝新建会话；「只告警」不会拦截。「花费构成」尚未提供，不能从占位区域推断费用拆分。

## 验证与后续处理 [#verify]

用同一用户、会话和时间范围核对宿主消息、回放及来源。若版本与预期不符，先检查令牌签发时的版本；固定或回滚版本不改变已签发令牌。没有数据时先核对环境、组织和时间范围，不要反复创建测试会话来填充统计。

出现来源拦截或预算拒绝时，记录响应中的 `X-Request-Id`、时间、HTTP 状态和错误码；不要附上 API Key 或令牌。用户级处理见[管理终端用户](/docs/developers/agent/end-users)。

下一步：[嵌入限制与错误](/docs/developers/agent/embed/limits-and-errors) · [智能体 API](/docs/api/native/agent) · [Cookbook](/docs/cookbook)

# 测试嵌入接入

上线前，用真实宿主网页验证正常对话和拒绝路径。下面列的是要执行的验收情境；控制台出现组件或触发 `praxis.ready` 只证明挂载，不证明认证和模型回复成功。

## 准备测试环境 [#prepare]

准备隔离的服务智能体、可用模型与运行环境、已保存的嵌入策略，以及已经完成接入的宿主页面。将该页面的实际来源加入白名单，设置可承受的预算。使用两个宿主测试账户 A、B，分别由后端登录态得到不同的 `external_user_id`。

先在「服务智能体 → 选择智能体 → 接入与配置 → 试用调试」选择版本、发送问题，排除配置本身的问题。随后执行下表中的宿主测试；控制台模拟不替代宿主登录、域名和令牌端点测试，也不进入客户运营统计。

## 按情境验收 [#scenarios]

| 输入或操作                        | 预期结果与验证方法                                                          |
| ---------------------------- | ------------------------------------------------------------------ |
| A 登录宿主，发送一个课程问题              | 页面出现真实回复；「运营与管理 → 会话」能找到相同消息、用户和来源                                 |
| 未登录时调用你的宿主签发端点               | 宿主拒绝签发，不返回嵌入令牌                                                     |
| 从白名单之外的网站加载                  | 访问被拒绝；接口可返回 `EMBED_ORIGIN_NOT_ALLOWED`（403）。iframe 也可能先被浏览器的嵌入策略拦截 |
| 签发时提交 `ttl_seconds: 59`      | 返回 `INVALID_PARAMS`（400），不产生可用令牌                                   |
| 关闭宿主覆盖指令后提交 `agent_override` | 返回 `EMBED_FIELD_NOT_ALLOWED`（400）；不能靠前端输入改变可信指令                    |
| 用户被封禁后由宿主再次签发                | 返回 `EMBED_USER_BLOCKED`（403）                                       |
| 达到同时运行的会话上限后再建会话             | 返回 `EMBED_RATE_LIMITED`（429）；等待运行结束后再试，不靠删除空闲记录释放名额                |
| 月度上限触顶，动作为「停止新会话」            | 新建会话返回 `EMBED_MONTHLY_CAP_REACHED`（402）；不要在真实用户智能体上制造这项条件          |
| 页面声明了前端工具                    | 工具被调用时有预期页面效果和结果；未知工具、参数无效或无业务权限时失败，不能只看确认卡                        |

可用下面的独立脚本验证有效期下限。在服务器设置 `PRAXIS_API_BASE`（API 源站）、`PRAXIS_API_KEY`（组织 API Key）和 `PRAXIS_AGENT`（真实 `agent_…` ID）。用支持原生 `fetch` 的 Node.js 保存并执行 `node test-token-limit.mjs`。智能体必须已配置好策略，否则会先因缺少配置失败。

```js
import assert from 'node:assert/strict';

const { PRAXIS_API_BASE, PRAXIS_API_KEY, PRAXIS_AGENT } = process.env;
assert(PRAXIS_API_BASE && PRAXIS_API_KEY && PRAXIS_AGENT, 'Set the three environment variables');
assert(PRAXIS_AGENT.startsWith('agent_'), 'Use the public agent ID');
const response = await fetch(`${PRAXIS_API_BASE.replace(/\/$/, '')}/agent/v1/embed_tokens`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${PRAXIS_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ agent: PRAXIS_AGENT, external_user_id: 'docs_test_a', ttl_seconds: 59 }),
});
assert.equal(response.status, 400, 'Invalid TTL must be rejected');
console.log({ status: response.status, requestId: response.headers.get('X-Request-Id') });
```

**预期结果：** 脚本正常退出并输出 `status: 400`；这里只验证拒绝状态，错误体应另核对 `INVALID_PARAMS` 和有效期字段提示。脚本不打印凭证，也不替代成功签发和真实回复测试。

## 验证身份与生命周期 [#lifecycle]

1. A 开始会话后退出，销毁 A 的组件。B 登录并新建组件，检查历史里没有 A 的会话；用 B 的令牌读取 A 的会话也必须被拒绝。只清空界面不等于切换身份。
2. A 保持登录跨过一次令牌刷新，确认 `getToken(current)` 重新检查宿主身份，后续消息仍属于 A。单独用已过期令牌请求，应得到 `EMBED_TOKEN_EXPIRED`（401）。
3. 在「运营与管理 → 签发记录」撤销 A 当前令牌，确认旧访问失败，组件不自动绕过撤销继续刷新。用户级批量撤销、封禁与解封按[管理终端用户](/docs/developers/agent/end-users)验证。
4. 匿名接入用独立浏览器存储测试两位访客，再刷新同一访客页面检查身份延续；不要把 `visitor_secret` 分享给另一位访客。
5. Hosted 接入分别测试兑换链接超过 60 秒、重复兑换，以及已打开页面的令牌到期；前两者被拒绝，后者不会自动刷新，应从宿主重新获取链接。

## 排错与清理 [#cleanup]

记录每项的时间、环境、组件版本、预期与实际结果，以及失败响应的 `X-Request-Id`、状态和错误码。错误事件可能只有 `message`；从浏览器网络面板补齐响应信息，记录中不含 Key、令牌或访客凭据。

完成后撤销本轮测试令牌，归档不再使用的测试会话，恢复临时修改的限额和封禁状态。归档不会删除记录。再用正常配置执行一次成功对话，确认清理没有留下访问障碍。

下一步：[上线检查清单](/docs/developers/agent/embed/go-live) · [限制与错误](/docs/developers/agent/embed/limits-and-errors) · [智能体 API](/docs/api/native/agent)

# 上线检查清单

在嵌入首次对外开放，或更换域名、身份接入方式、组件版本后执行本清单。先完成[测试嵌入接入](/docs/developers/agent/embed/testing)，每项保存可复查的结果，再开放访问。

## 确认上线条件 [#checklist]

* [ ] **配置与版本一致。** 控制台「接入与配置 → 版本」显示预期嵌入版本；宿主新签发令牌产生的会话使用该版本。若签发请求显式传 `version`，确认它没有覆盖你的版本选择。
* [ ] **身份来自宿主登录态。** 未登录请求无法从宿主取得令牌；A、B 两个测试用户互相看不到会话。API Key 仅在服务器，浏览器源码和响应中没有 `PRAXIS_API_KEY` 的真实值。
* [ ] **匿名凭据使用正确。** 若启用匿名访问，页面只公开 publishable key，已设置相应限额；不同浏览器存储的访客相互隔离，`visitor_secret` 不被分享。
* [ ] **目标来源已验证。** 用实际 HTTPS 域名加载并完成回复；白名单外来源失败。iframe 在目标页面可显示；Hosted 使用独立页面，不能嵌入 iframe。上线配置关闭不再需要的 localhost 访问。
* [ ] **组件交付可重复。** 已记录并验证使用的 Kit 版本、脚本地址及适用的完整性值；从空缓存加载成功。页面能发送消息并收到真实回复，而非仅收到 `praxis.ready`。
* [ ] **拒绝路径可恢复。** 到期、撤销、封禁和来源拒绝均已测试。宿主退出后销毁旧组件；Hosted 到期后能从宿主获取新链接，不循环使用失效链接。
* [ ] **工具只执行允许的操作。** 已测试有效输入、无效输入和无业务权限的用户；「每次询问」不会替代工具服务端鉴权。
* [ ] **预算与监控可用。** 核对美元预算、有效期、请求与活跃会话上限；月度触顶动作符合业务预期。在「运营与管理」能找到宿主测试会话、来源和用量。

## 准备回滚与支持 [#rollback]

保留已验证的宿主部署和组件版本。配置异常时，在「版本」选择旧版本并点击「回滚到此版本」，重新签发后验证；已签发令牌不变。需要立即停止某个用户访问时，使用[封禁或批量撤销](/docs/developers/agent/end-users)，并确认旧令牌已失败。

为支持人员保存故障时间、环境、公开智能体 ID、会话 ID、HTTP 状态、错误码与 `X-Request-Id`，不保存令牌或 API Key。按[限制与错误](/docs/developers/agent/embed/limits-and-errors)选择恢复动作，再从宿主完成一次对话验证。

相关参考：[配置智能体](/docs/developers/agent/configure) · [组件参考](/docs/developers/agent/embed/component-reference) · [智能体 API](/docs/api/native/agent)

# 组件参考

`<praxis-agent>` 在网页中呈现智能体对话。入口为 `element.setOptions(options)`；本页按属性、方法、事件排列，完整接入步骤见[嵌入到网页](/docs/developers/agent/embed/quickstart)。

## 初始化 [#initialize]

首次设置必须包含 `api.url`、`agent.name`，以及 `api.getToken` 或 `api.publishableKey`。缺少时抛出 `TypeError`。登录用户选择 `getToken`，匿名访客选择 `publishableKey`，请二选一；同时提供时使用 publishable key，不调用 `getToken`。

下面是 Develop 的匿名页面示例。先在该环境创建可公开 key、保存嵌入策略并允许宿主来源，再替换 `PRAXIS_PUBLISHABLE_KEY`，从该来源提供 HTML。公开 key 不是服务器 API Key。脚本地址、版本和完整性值须使用同一发布版本。

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Course assistant</title>
<script type="module"
  src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js"
  integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU"
  crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<p id="status" role="status"></p>
<script type="module">
  await customElements.whenDefined('praxis-agent');
  const agent = document.querySelector('praxis-agent');
  const status = document.querySelector('#status');
  agent.addEventListener('praxis.ready', () => { status.textContent = 'Interface mounted'; });
  agent.addEventListener('praxis.error', () => { status.textContent = 'Check the failed network request'; });
  agent.setOptions({
    api: {
      url: 'https://api-develop.deepraxis.com',
      publishableKey: 'PRAXIS_PUBLISHABLE_KEY',
    },
    agent: { name: 'Course assistant' },
    locale: 'en',
  });
</script>
</html>
```

**预期结果：** 页面显示聊天界面，发送问题后收到真实回复。`Interface mounted` 只表示挂载；验证还要检查回复和用户隔离。

## 属性 [#properties]

### api 与 agent [#api-agent]

| 属性                   | 类型                                                           | 语义                                                            |
| -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
| `api.url`            | `string`                                                     | 必填，API 源站或以 `/agent/v1` 结尾的基础地址；初始化后不可变                       |
| `api.getToken`       | `(current: string \| null) => Promise<string>`               | 返回嵌入令牌；首次 `current` 为 `null`，刷新时传当前令牌。后端每次重新确认宿主登录态           |
| `api.publishableKey` | `string`                                                     | 匿名令牌入口，替代 `getToken`；初始化后不可变。访客身份由组件保存的私有 `visitor_secret` 延续 |
| `agent.name`         | `string`                                                     | 必填，界面显示名称，不用于选择智能体                                            |
| `agent.avatar`       | `string`                                                     | 头像 URL；实际访问的智能体由凭证决定                                          |
| `onClientTool`       | `(call: { id, name, input }) => unknown \| Promise<unknown>` | 元素属性，不放入 `setOptions`。处理前端工具，未知工具应抛错                          |

### 布局与内容 [#layout-content]

| 属性                                | 类型 / 默认值                                        | 效果                                            |
| --------------------------------- | ----------------------------------------------- | --------------------------------------------- |
| `layout`                          | `inline`、`sidebar`、`popup`、`modal`；默认 `inline`  | 对话容器形态                                        |
| `width`                           | `number`；400 CSS 像素                             | sidebar 宽度                                    |
| `launcher.type`                   | `none`、`bubble`、`mascot`                        | 提供 launcher 时必填；默认 inline 为 none，其他布局为 bubble |
| `launcher.image`、`launcher.label` | `string`                                        | 启动按钮图片与文字                                     |
| `launcher.position`               | `bottom-right`、`bottom-left`                    | 启动按钮位置，默认右下                                   |
| `header.title`、`header.subtitle`  | `string`                                        | 标题与副标题；标题默认使用 `agent.name`                    |
| `header.icon`                     | `string`                                        | 支持的 Lucide 图标名或图片 URL                         |
| `header.actions`                  | `{ id: string; icon: string; label: string }[]` | 自定义顶栏按钮，点击发出事件                                |
| `history.enabled`                 | `boolean`；默认 `true`                             | 历史入口；关闭时 `showHistory()` 不生效                  |
| `startScreen.greeting`            | `string`                                        | 空会话问候语                                        |
| `startScreen.prompts`             | `{ label: string; prompt: string }[]`           | 显示前 4 个；点击发送对应 prompt                         |
| `composer.placeholder`            | `string`                                        | 默认随语言显示输入提示                                   |
| `composer.attachments.enabled`    | `boolean`；默认 `true`                             | 是否允许选择、粘贴或拖入附件                                |
| `disclaimer`、`footer`             | `string`                                        | 对话说明与底部文字                                     |
| `autoScroll`                      | `boolean`                                       | 新内容到达时是否自动滚动                                  |
| `locale`                          | `zh`、`en`                                       | 未指定时依次取宿主页面语言、浏览器语言；以 zh 开头用中文，否则英文；无语言信息用英文  |

### theme [#theme]

| 属性                 | 值 / 限制                                                     |
| ------------------ | ---------------------------------------------------------- |
| `colorScheme`      | `light`、`dark`、`system`；未设置时为浅色，system 跟随浏览器               |
| `accent`、`surface` | 明确且不透明的 CSS 颜色；拒绝 `var()`、`currentcolor`、透明色，抛 `TypeError` |
| `grayscale`        | `neutral`、`warm`、`cool`                                    |
| `radius`           | `none`、`sm`、`md`、`lg`、`full`；基础圆角依次为 0、4、10、18、9999 CSS 像素 |
| `density`          | `compact`、`normal`、`spacious`；基础间距依次为 3、4、5 CSS 像素         |
| `fontFamily`       | 非空字体族字符串，不能含 `; { } < >`，否则抛 `TypeError`                   |
| `fontSize`         | 正的有限数字，单位 CSS 像素；否则抛 `RangeError`                          |

未设置 accent 时，浅色默认 `#167A43`，深色默认 `#30a46c`。颜色前景按对比度选择。

### React 属性 [#react-props]

React 的 `PraxisAgent` 使用以下属性。&#x2A;*当前 React 绑定仍要求顶层 `getToken`**，即使 `options.api` 的类型包含 `publishableKey`；匿名接入可直接使用上面的 Web Component。

| 属性 / 返回对象                      | 语义                                                                          |
| ------------------------------ | --------------------------------------------------------------------------- |
| `getToken`                     | 必填，与 `api.getToken` 相同的函数签名                                                 |
| `options`                      | 上述选项，但不含 `api.getToken`；省略时呈现未配置元素                                          |
| `layout`、`launcher`            | 分别覆盖 `options.layout`、`options.launcher.type`；launcher 取 none/bubble/mascot |
| `className`、`style`、`children` | 宿主元素样式与 React 子元素                                                           |
| `onClientTool`                 | 前端工具回调；与元素属性同义                                                              |
| `ref` / `usePraxisAgent()`     | ref 提供控制句柄；hook 返回稳定的 `{ ref, control }`                                    |

## 方法 [#methods]

| 方法                              | 返回 / 行为                                                                                                     |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `setOptions(partial)`           | 初始化或合并选项                                                                                                    |
| `sendUserMessage(text)`         | `Promise<"accepted" \| "rejected" \| "unknown">`；空白、超长、等待审批或无法提交时可拒绝；accepted 不代表回复成功，unknown 时先检查会话，不要盲目重发 |
| `setComposerValue(text)`        | 替换草稿，不发送                                                                                                    |
| `focusComposer()`               | 聚焦输入框；历史面板打开时不生效                                                                                            |
| `setThreadId(id)`               | 切换会话并关闭历史；`null` 回到空会话，不切换用户身份                                                                              |
| `showHistory()`、`hideHistory()` | 打开、关闭历史面板                                                                                                   |
| `open()`、`close()`、`toggle()`   | 展开、收起或切换面板；inline 下无效果                                                                                      |
| `setReadable(name, value)`      | 注册页面上下文；名称 1–64 字符，仅字母、数字与 `_ . : -`；值为字符串或可 JSON 序列化值                                                      |
| `removeReadable(name)`          | 删除该页面上下文值                                                                                                   |

React 控制句柄包含上表从 `sendUserMessage` 到 `toggle` 的方法。`onReady` 前或卸载后调用会抛出未就绪错误，不会排队。React 页面上下文与工具用 `usePraxisReadable`、`usePraxisTool`，需要共享注册范围时使用 `PraxisProvider`，见下方指南。

## 事件 [#events]

事件均为可冒泡、可穿过 Shadow DOM 的 `CustomEvent`。React 回调收到同一个事件对象，通过 `event.detail` 读取内容。

| 事件 / React 回调                                                                   | detail 与时机                                                          |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `praxis.ready` / `onReady`                                                      | 无；界面已挂载，不代表认证或网络成功                                                  |
| `praxis.error` / `onError`                                                      | `KitError`（code、status、retryable、message）或仅 `{ message }`；先检查字段是否存在 |
| `praxis.thread.change` / `onThreadChange`                                       | `{ threadId: string \| null }`；当前会话变化                               |
| `praxis.response.start` / `onResponseStart`                                     | 无；开始忙碌处理                                                            |
| `praxis.response.end` / `onResponseEnd`                                         | 无；结束忙碌处理，不是成功断言                                                     |
| `praxis.open`、`praxis.close` / `onOpen`、`onClose`                               | 无；面板展开、收起                                                           |
| `praxis.history.open`、`praxis.history.close` / `onHistoryOpen`、`onHistoryClose` | 无；历史面板打开、关闭                                                         |
| `praxis.header.action` / `onHeaderAction`                                       | `{ id: string }`；点击顶栏自定义按钮                                          |
| `praxis.tool.change` / `onToolChange`                                           | `{ id, name, status: "start" \| "end", isError? }`；前端工具执行变化         |
| `praxis.log` / `onLog`                                                          | `{ level: "info" \| "warn" \| "error", message, data? }`；上下文裁剪或工具诊断 |
| `praxis.frame.close`                                                            | 无；请求宿主关闭 iframe；React 无对应回调属性                                       |

## 默认行为与生命周期 [#defaults]

`setOptions` 递归合并对象、整体替换数组；显式 `undefined` 重置字段。`api.url` 和 `api.publishableKey` 初始化后不能改，变更会抛 `TypeError`；换后端或用户时销毁旧元素并重新创建。移除元素会释放连接与刷新计时器，不会撤销已签发令牌。

页面上下文快照最多 32 项、4096 UTF-8 字节，超出时从最早注册项开始省略并发出 log。Kit 的这些运行时选项与控制台保存的 iframe/Hosted 外观不是同一个配置入口，持久化外观有自己的字段与大小限制。

## 使用指南 [#guides]

[嵌入令牌与用户](/docs/developers/agent/embed/tokens) · [传入上下文](/docs/developers/agent/embed/context) · [接入前端工具](/docs/developers/agent/embed/client-tools) · [自定义外观](/docs/developers/agent/embed/appearance)

# 嵌入限制与错误

本页适用于嵌入令牌、Kit、iframe 与 Hosted。表中的「来源」区分固定校验、控制台策略和组件行为；策略值以该智能体保存的配置为准。

## 限制 [#limits]

| 项目                             | 值与单位                                                              | 来源                      |
| ------------------------------ | ----------------------------------------------------------------- | ----------------------- |
| `external_user_id`             | 1–128 字符；仅英文字母、数字及 `_ . : @ -`                                    | 签发校验                    |
| `ttl_seconds`                  | 60–3600 秒，同时不超过策略 `ttl_max_seconds`；省略用策略默认值                      | 签发校验与策略                 |
| `system_message`               | 所有 text 块文本合计 ≤8192 UTF-8 字节                                      | 签发校验                    |
| `metadata`                     | ≤16 个键；键 1–64 字符，字符串值 ≤512 字符                                     | 签发校验                    |
| `budget_cents`                 | 0 至策略会话预算上限，单位美分；省略用策略默认值                                         | 签发校验与策略                 |
| 签发 `allowed_origins`           | 只能取策略来源条目的子集，逐项精确匹配                                               | 签发校验                    |
| 策略来源                           | ≤50 条 HTTPS 来源；通配只匹配一级子域且不含端口                                     | 嵌入策略                    |
| `allow_localhost`              | 默认 false；开启后允许 HTTP localhost / 127.0.0.1（可带端口）                   | 嵌入策略                    |
| `max_active_sessions_per_user` | 1–100 个活跃会话/用户；空闲历史不计入                                            | 嵌入策略                    |
| `per_user_rate_per_min`        | 1–600 次/分钟/用户                                                     | 嵌入策略                    |
| `blocked_end_users`            | ≤1000 个用户 ID                                                      | 嵌入策略                    |
| `monthly_cap_cents`            | 非负整数美分；null 不限；按 UTC 自然月                                          | 嵌入策略与月度统计               |
| 页面上下文                          | ≤32 项，完整 JSON ≤4096 UTF-8 字节                                      | 接口校验；Kit 超出时裁剪最早项       |
| 会话标题 / 初始事件                    | 标题 ≤500 字符；初始事件 ≤50 条纯文本用户消息                                      | 嵌入会话创建校验                |
| 附件选择器                          | 每条消息最多 5 个；每个 ≤10 MiB（10,485,760 字节）；PNG、JPEG、GIF、WebP、PDF、TXT、MD | Kit 输入校验；新会话首条消息仍只接受纯文本 |
| `startScreen.prompts`          | 显示前 4 个                                                           | Kit 组件                  |
| Hosted 兑换凭证                    | 60 秒内使用，仅能兑换 1 次                                                  | Hosted 兑换校验             |

月度触顶动作 `stop_new_sessions` 拒绝新会话，`alert_only` 不拦截。控制台金额输入为美元，API 的预算字段为美分。会话预算耗尽后不能继续提交新的工作；保留工具确认和中断入口。

## 服务端错误 [#server-errors]

下列状态用于普通 HTTP 失败响应。事件流已经建立后，错误通过流内事件到达，不能再更改已发送的 HTTP 状态。「可重试」指原请求；修复配置或换取合法凭证后属于新的尝试。

### EMBED\_TOKEN\_EXPIRED [#embed-token-expired]

**401 · 访问阶段 · 不可直接重试。** 令牌到期。可刷新 Kit 会重新取令牌，并对到期响应最多重放一次请求；Hosted 不刷新，须从宿主获取新链接。

### EMBED\_TOKEN\_REVOKED [#embed-token-revoked]

**401 · 访问/事件流 · 不可重试。** 令牌已撤销，Kit 停止刷新。结束旧访问，由宿主重新核验是否仍允许该用户进入；不要自动重签绕过撤销。

### EMBED\_ORIGIN\_NOT\_ALLOWED [#embed-origin-not-allowed]

**403 · 来源检查 · 不可重试。** 当前来源不被允许。核对实际 scheme、host、port，以及策略和签发时收紧的来源列表；修改后重新签发再验证。

### EMBED\_USER\_BLOCKED [#embed-user-blocked]

**403 · 签发/新建会话 · 不可重试。** 用户已封禁。由管理者在「运营与管理 → 终端用户」核查；确需恢复时解封并签发新令牌。

### EMBED\_RATE\_LIMITED [#embed-rate-limited]

**429 · 请求/新建会话/匿名签发 · 可延后重试。** 请求频率、活跃会话数或匿名每日会话额度触顶。分别等待限流窗口恢复、运行结束或下个 UTC 日；不能统一立即重试。

### EMBED\_BUDGET\_EXHAUSTED [#embed-budget-exhausted]

**402 · 会话工作/匿名签发 · 不可重试。** 会话预算或匿名每日花费用尽。由管理者核对预算；获准后以新令牌、新会话继续，或等待每日额度恢复，不循环重签规避上限。

### EMBED\_MONTHLY\_CAP\_REACHED [#embed-monthly-cap-reached]

**402 · 新建会话 · 不可重试。** 月度上限已到且策略要求停止新会话。由管理者决定是否调整上限或触顶动作，或等到下个 UTC 月。

### EMBED\_FIELD\_NOT\_ALLOWED [#embed-field-not-allowed]

**400 · 签发/新建会话 · 不可重试。** 使用了不允许的字段或初始事件。移除未获准的 `agent_override`；嵌入会话创建仅传 `title`、`time_zone`、`initial_events`，初始事件为纯文本 `user.message`（可带页面上下文）。

### 其他拒绝 [#other-errors]

| 码                      | HTTP / 阶段           | 恢复动作                                | 可重试 |
| ---------------------- | ------------------- | ----------------------------------- | --- |
| `INVALID_PARAMS`       | 400 / 参数校验          | 按字段提示修正类型、范围或大小                     | 否   |
| `UNAUTHENTICATED`      | 401 / 鉴权            | 检查凭证是否缺失、失效；公开 key 是否停用             | 否   |
| `NOT_FOUND`            | 404 / 资源或 Hosted 兑换 | 核对组织、公开 ID、策略；失效或已用的 Hosted 链接需重新签发 | 否   |
| `UPSTREAM_UNAVAILABLE` | 502 / 上游调用          | 延后重试；管理写操作先核对是否部分完成                 | 是   |

会话权限、归档和事件内容错误见[智能体限制与错误](/docs/developers/agent/limits-and-errors)。

## Kit 错误与重试 [#kit-errors]

`praxis.error` 的 detail 可能是含 `code`、`status`、`retryable`、`message` 的 `KitError`，也可能只有 `{ message }`。`status: 0` 表示没有 HTTP 响应。

| 码                                     | 状态 / 阶段         | 恢复动作与可重试性                          |
| ------------------------------------- | --------------- | ---------------------------------- |
| `network_error`、`stream_disconnected` | 0 / 网络或事件流      | 恢复连接后可重试                           |
| `http_error`                          | 响应状态 / HTTP     | 检查原始响应；无明确 retryable 时 429、5xx 可重试 |
| `invalid_token`                       | 0 / 取令牌         | getToken 返回空值；修复返回值，可重试            |
| `invalid_response`                    | 响应状态 / JSON 或历史 | 检查返回格式、分页是否前进；不可自动重试               |
| `invalid_stream`                      | 响应状态 / 事件流      | 检查 SSE 内容与 JSON；不可自动重试             |
| `event_cursor_not_found`              | 200 / 历史恢复      | 重新加载历史定位会话；不可自动重试                  |
| `callback_error`                      | 0 / 回调          | 修复宿主异常；不可重试                        |
| `aborted`、`disposed`                  | 0 / 生命周期        | 请求取消或组件已销毁；不重试旧实例                  |

非 Hosted 取令牌仅对可重试失败按 1、2、4 秒退避，最多重试 3 次；撤销或预算拒绝停止该重试。传输层自行恢复事件流时最多重连 5 次；界面可能接管恢复，不应另叠无限重试。

## 诊断入口 [#diagnostics]

保留失败时间、环境、公开 `agent_…` ID、HTTP 状态、错误码和 `X-Request-Id`，不记录凭证。在「运营与管理 → 签发记录」核对令牌状态，再用[会话回放](/docs/developers/agent/monitor)定位发生阶段。修复后按[测试嵌入接入](/docs/developers/agent/embed/testing)复测；字段契约见[智能体 API](/docs/api/native/agent)。

# 智能体限制与错误

本页用于排查智能体配置和会话 API。嵌入令牌、来源、匿名配额与前端组件另见[嵌入限制与错误](/docs/developers/agent/embed/limits-and-errors)；组织余额与配额见[用量与费用](/docs/developers/account/billing)。

## 请求与运行限制 [#limits]

| 项目                    | 值             | 单位与含义                                   |
| --------------------- | ------------- | --------------------------------------- |
| 智能体名称 `name`          | 1–256         | 字符                                      |
| 指令 `system`           | 100,000       | 字符上限                                    |
| 智能体 / 会话 `metadata`   | 16 / 64 / 512 | 键值对 / 键字符数 / 值字符数                       |
| 会话标题 `title`          | 500           | 字符上限                                    |
| 初始输入 `initial_events` | 50            | 事件上限                                    |
| 会话与事件列表 `limit`       | 1–100; 20     | 范围；默认条数                                 |
| 会话预算 `budget`         | 2000          | 默认美分（20 美元）                             |
| 会话时区 `time_zone`      | UTC           | 默认；传入时为 IANA 名称                         |
| 自定义工具                 | 32 / 16 KiB   | 数量上限 / 单个 input\_schema 的 UTF-8 JSON 上限 |

预算对象使用 `type: "limit"`，`max_list_cost.currency: "USD"`，`max_list_cost.amount` 为正整数字符串，单位是美分；`null` 移除预算。它控制后续模型请求，不是费用预测。字符串长度按 JavaScript 字符串长度校验，非所有字符都占一个单位；工具 schema 的大小则按 UTF-8 字节计算。

## 按错误处理 [#errors]

原生 API 的错误体包含 `error.type`、`error.message` 和 `request_id`。服务端已分类的错误还可提供 `error.code` 与 `error.retryable`；请求解析阶段可能只有类型和消息。下列代码以实际响应为准，不能假设每个相同 HTTP 状态都表示同一个原因。

### INVALID\_PARAMS [#invalid-params]

HTTP 400。请求字段、格式或不支持的选项不合法。按错误消息修正请求，原样重试不会解决。

### UNAUTHENTICATED [#unauthenticated]

HTTP 401。凭证缺失或无效。确认当前环境和组织的 Key，重新认证后再请求。

### SESSION\_FORBIDDEN [#session-forbidden]

HTTP 403。当前身份无权访问该会话。核对所属用户与组织，不自动重试，也不要通过换 ID 探测其他用户。

### AGENT\_NOT\_FOUND [#agent-not-found]

HTTP 404。智能体或版本不存在，或对当前身份不可见。使用创建响应中的 ID，并确认版本。

### SESSION\_NOT\_FOUND [#session-not-found]

HTTP 404。会话不存在或不可见。核对返回的会话 ID 与调用身份，不能靠重试创建响应来恢复。

### MODEL\_NOT\_ALLOWED [#model-not-allowed]

HTTP 400。选择的模型不在固定版本允许的模型集合中。使用允许的模型，或更新配置后创建新会话。

### SESSION\_CONFLICT [#session-conflict]

HTTP 409。会话状态与当前操作冲突。重新读取会话或待确认工具，再决定是否提交；不要重复提交已完成的工具结果。

### SESSION\_ARCHIVED [#session-archived]

HTTP 409。会话已归档，不能继续发送消息。保留历史用于查询，新对话创建新会话。

### INVALID\_STATE [#invalid-state]

HTTP 409。当前状态不允许操作，例如删除仍在运行的会话。先中断或等到空闲、终止，再核对状态后操作。

### INVALID\_PAGE\_TOKEN [#invalid-page-token]

HTTP 400。分页游标无效或不匹配当前查询。重新从第一页开始；后续原样传回 next\_page，并保持筛选条件。

### UPSTREAM\_UNAVAILABLE [#upstream-unavailable]

HTTP 502。上游暂时不可用，可在退避后重试。对创建或发消息先查询已有结果，避免重复产生任务。

### SANDBOX\_UNAVAILABLE [#sandbox-unavailable]

HTTP 503。运行环境暂时不可用，可在退避后重试。持续失败时记录请求标识并联系支持。

## 何时重试 [#retry]

`error.retryable: false` 时先处理原因。收到 HTTP 429 时读取 `Retry-After`，不要无间隔循环；临时上游或环境错误可以退避后重试。网络断开不能证明请求未执行，创建资源和发送消息前应先查询已知会话及事件。

事件流已经开始后，运行失败可能通过事件返回，不能只看建立连接时的 HTTP 成功状态。检查回复、工具待确认和会话状态；不要把接收消息成功等同于任务完成。

## 记录诊断信息 [#diagnostics]

保留发生时间、环境、智能体与会话 ID、HTTP 状态、错误类型与代码，以及 `X-Request-Id`；若响应仅提供 `request-id` 或 `request_id`，同时记录该值。不要记录 Key、嵌入令牌或访客凭据。

随后[查看会话与用量](/docs/developers/agent/monitor)，对照 [API 参考](/docs/api/native/agent)检查请求。需要复现时，从[快速开始](/docs/developers/agent/quickstart)的最小请求缩小范围。

# 自动化

自动化把平台能力组合成脚本或工作流，由你主动运行，或由触发器自动启动。

| 对象  | 用途                                |
| --- | --------------------------------- |
| 脚本  | 一段 TypeScript 从头执行到尾，返回一个结果。      |
| 工作流 | YAML 描述的多步骤流程，保存检查点，支持等待和暂停。      |
| 动作  | 有输入输出约定的可复用能力，由脚本或工作流调用。          |
| 运行  | 一次脚本或工作流的执行，记录输入、输出、状态和调用。        |
| 触发器 | 在指定时间或收到 webhook 时启动一个已发布的脚本或工作流。 |

## 选择入口 [#选择入口]

一次计算、批量处理或组合 API 调用，用脚本；跨时间等待、需要步骤检查点的任务，用工作流。把多个编排都会使用的能力封装成动作。动作没有独立运行入口，在脚本或工作流里调用它。

脚本失败后重跑会重新执行整段代码；已发生的效果不会回滚。需要自动执行时，在已发布目标上配置触发器。

## 先运行一次 [#先运行一次]

在 [Develop Console](https://console-develop.deepraxis.ai/keys) 创建 API Key，设置为 `PRAXIS_API_KEY`。在装有 curl 和 jq 的 Bash 中执行，使用 Bearer 认证完成一次加一计算，不调用模型：

```bash
export PRAXIS_API_URL="https://api-develop.deepraxis.ai"
: "${PRAXIS_API_KEY:?Set PRAXIS_API_KEY to your develop API Key}"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"source":"export default async (input: { n: number }) => ({ answer: input.n + 1 });","language":"ts","input":{"n":41},"wait":60}' \
  | jq '{id,status,output,error}'
```

期望：`status` 为 `completed`，`output` 为 `{"answer":42}`。若仍在排队或运行，将返回的 `id` 存为 `RUN_ID`，再[读取结果](/docs/developers/automation/quickstart#read-result)。继续按[快速开始](/docs/developers/automation/quickstart)保存和发布脚本。

## 下一步 [#下一步]

* [快速开始](/docs/developers/automation/quickstart)：用 curl 得到第一个结果，再发布脚本。
* [概念](/docs/developers/automation/concepts)：版本、运行树、幂等与费用。
* [编写脚本](/docs/developers/automation/guides/write-script)：输入输出、平台能力、动作和日志。
* [触发器](/docs/developers/automation/guides/triggers)：定时与 webhook。
* [限制与错误](/docs/developers/automation/limits-and-errors)：容量、超时和排错。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 快速开始

本教程使用 develop、Bash、curl 和 jq。所有命令在同一个 Bash 会话中按顺序执行；不需要安装 SDK。脚本只做加法，不调用计费能力。

<a id="environment" />

## 1. 创建 API Key [#1-创建-api-key]

打开 [Develop Console → API Keys](https://console-develop.deepraxis.ai/keys)，创建 Key，并将其保存到环境变量 `PRAXIS_API_KEY`。Key 的管理说明见[平台管理](/docs/developers/account)。

```bash
export PRAXIS_API_URL="https://api-develop.deepraxis.ai"
: "${PRAXIS_API_KEY:?Set PRAXIS_API_KEY to your develop API Key}"
command -v curl
command -v jq
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs?limit=1" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq -e '.data | type == "array"'
```

期望：两个命令都能找到，最后输出 `true`，退出码为 0；这同时验证 Key 能访问自动化运行列表。

<a id="run-source" />

## 2. 临时运行脚本 [#2-临时运行脚本]

[创建运行](/docs/api/native/automation/createRun)接受源码和输入。`wait` 表示本次 HTTP 请求等待运行终态的最长秒数，范围 0–60，默认 0。`wait: 60` 最多等待 60 秒，运行提前结束就提前返回；等待结束不会取消运行。HTTP 200 表示已到终态（也可能失败），202 表示仍在排队或执行，两者都要检查 `status`。

```bash
SOURCE='export default async (input: { n: number }) => ({ answer: input.n + 1 });'
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg source "$SOURCE" \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
jq -e '.id | startswith("run_")' <<< "$RUN"
```

期望：输出 `true`，`RUN_ID` 是新运行的 ID。源码接收 `n: 41`，返回 `answer: 42`。

HTTP 202 的响应仍是 Run 对象。下面是字段节选（`id` 以实际响应为准）：

```json
{
  "id": "run_...",
  "status": "running",
  "output": null,
  "error": null
}
```

保留 `RUN_ID`，按下一节 GET `/automation/v1/runs/$RUN_ID`，直到 `completed`、`faulted` 或 `cancelled`。不要因为等待结束就重复创建运行。

<a id="read-result" />

## 3. 读结果 [#3-读结果]

用[读取运行](/docs/api/native/automation/getRun)检查终态。下面的函数在约 3 分钟内轮询；脚本失败时打印真实错误并返回非零退出码。

```bash
read_result() {
  local result
  for attempt in $(seq 1 90); do
    result=$(curl --fail-with-body -sS \
      "$PRAXIS_API_URL/automation/v1/runs/$1" \
      -H "Authorization: Bearer $PRAXIS_API_KEY") || return 1
    case "$(jq -r '.status' <<< "$result")" in
      completed) jq -e '.output.answer == 42' <<< "$result"; return $? ;;
      faulted|cancelled) jq '{status,error}' <<< "$result"; return 1 ;;
    esac
    sleep 2
  done
  echo 'Still pending: retrieve the run again using its ID' >&2
  return 1
}
read_result "$RUN_ID"
```

期望：输出 `true` 且退出码为 0，证明运行已 `completed`，输出确实为 42。若仍未完成，保留 `RUN_ID`，再次读取；不要因为客户端等待结束就重复创建运行。

<a id="publish-script" />

## 4. 保存为脚本并发布 [#4-保存为脚本并发布]

把同一份源码保存为[脚本草稿](/docs/api/native/automation/createScript)，读取当前 `draft.revision` 后[发布版本](/docs/api/native/automation/publishScriptVersion)。每次教程使用不同的名称。

命名空间由你自行取名，例如自己的项目名。下面的 `docs-samples` 只是示例；修改 `NAMESPACE` 后，后续请求会使用同一个命名空间。

```bash
NAMESPACE="docs-samples"
SCRIPT_NAME="hello-$(date +%s)-$RANDOM"
SCRIPT=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/scripts" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg namespace "$NAMESPACE" --arg name "$SCRIPT_NAME" --arg source "$SOURCE" \
    '{namespace:$namespace,name:$name,draft:$source}')")
jq -e '.id | startswith("scr_")' <<< "$SCRIPT"
DRAFT=$(curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/scripts/$NAMESPACE/$SCRIPT_NAME" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq -er '.draft.revision')
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/scripts/$NAMESPACE/$SCRIPT_NAME/versions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg revision "$DRAFT" \
    '{version:"1.0.0",expected_draft_revision:$revision}')" \
  | jq -e '.version == "1.0.0"'
```

期望：两次检查均输出 `true`；版本 `1.0.0` 已发布。若返回 `PRECONDITION_FAILED`，报错原文「草稿已修改」，重新读取草稿，确认内容后用最新 revision 发布。

<a id="run-version" />

## 5. 按版本运行 [#5-按版本运行]

```bash
TARGET="$NAMESPACE/$SCRIPT_NAME@1.0.0"
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" \
    '{target:$target,input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"
```

期望：再次输出 `true`，结果仍为 42。保留 `TARGET`，下一步可以给它[添加触发器](/docs/developers/automation/guides/triggers)。用完后可[归档脚本](/docs/api/native/automation/archiveScript)；归档会保留名称和历史。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 概念

脚本和工作流描述要做什么；运行记录某一次执行；触发器决定何时执行。动作是执行期间调用的可复用能力。

## 定义、版本与标签 [#定义版本与标签]

脚本与工作流共用 `namespace/name`，一个名字只能属于其中一种。草稿可以修改；发布的版本不可变，版本号不能复用。同版本同内容再次发布会返回已有版本。弃用版本保留历史，不等于删除。

版本和 dist-tag 遵循 npm 的引用方式：`docs-samples/hello@1.0.0` 固定版本，`@^1.2.0`、`@~1.2`、`@1.x` 选择范围，`@latest` 跟随标签。省略 `@` 等价于 `@latest`。每次运行记录实际解析的版本。

正式版发布会按版本选择规则更新 `latest`；发布较低版本不会自动把它退回。预发布版本需显式指定标签，例如 `beta`。`stable`、`beta` 等标签可以移动；标签名不能是版本号或版本范围；`latest` 不能删除。移动标签影响之后解析它的运行，不改写已发布版本。

用[发布版本](/docs/api/native/automation/publishScriptVersion)管理不可变内容，用[设置标签](/docs/api/native/automation/setScriptTag)把标签指向已有版本。写标签不需要 `If-Match`。修改草稿则需要最新 ETag，发布草稿需要 `expected_draft_revision`。

动作没有语义版本号：兼容更新立即生效，调用记录保存实际修订。输入输出不兼容时使用新名字，避免破坏既有调用方。工作流发布时锁定脚本和子工作流依赖的精确版本，动作不锁定。

## 运行生命周期 [#运行生命周期]

| 状态          | 含义                 |
| ----------- | ------------------ |
| `pending`   | 已受理，等待开始。          |
| `running`   | 正在执行。              |
| `waiting`   | 工作流正在等待事件、定时器或子运行。 |
| `suspended` | 工作流被暂停。            |
| `completed` | 成功结束，读取 `output`。  |
| `faulted`   | 执行失败，读取 `error`。   |
| `cancelled` | 已取消。               |

脚本没有 `waiting` 和 `suspended`。取消请求尚未结束时，运行可能仍为 `running`，并带 `cancel_requested_at`。超时是 `faulted`，错误类型为 `timeout`，不是另一个状态。终态不会恢复；[重跑](/docs/api/native/automation/rerunRun)创建新的根运行，`rerun_of` 指向原运行。

HTTP 成功表示请求被处理，不代表脚本成功。分别检查 `status`、`output` 和 `error`。脚本可查看[调用](/docs/api/native/automation/listRunCalls)与[日志](/docs/api/native/automation/listRunLogs)，工作流可查看[步骤](/docs/api/native/automation/listRunTasks)与[等待点](/docs/api/native/automation/listRunWaits)。

## 子运行与来源 [#子运行与来源]

脚本可以通过 `praxis.automation.runs.create` 启动子运行。子运行的 `initiator` 始终保留最初发起者（人、API Key 或触发器）；父子关系由 `parent_run_id` 表达。按来源筛选子运行用 `channel=run`，按指定父运行查找用 `parent_run_id` 过滤；公开详情中的 `parent` 提供父运行摘要，`root_run_id` 指向根运行。

父运行被取消时向下级联；父正常结束时子运行可以继续。子运行 deadline 不会超出父运行剩余时间。同一目标在调用链上再次出现会被循环检测拒绝；深度和扇出上限见[限制](/docs/developers/automation/limits-and-errors)。

## 幂等与重试 [#幂等与重试]

[创建运行](/docs/api/native/automation/createRun)接受 `Idempotency-Key`。作用域为同一 owner、HTTP 方法、路径和 key，保留 24 小时；请求指纹不包含 `wait`。网络重试应复用原 key 和请求内容；重放返回当前运行，并带 `Idempotent-Replayed: true`。有意再跑一次时使用新 key。

同 key 不同请求返回 `IDEMPOTENCY_KEY_MISMATCH`；尚在处理时返回 `IDEMPOTENCY_IN_FLIGHT`，详见创建运行的错误表。幂等不提供事务回滚，失败前已经完成的调用可能产生了效果。

## 费用归因 [#费用归因]

运行本身不单独收费。运行中调用模型、搜索等能力产生费用，按这些能力的原价计费，并归因到运行与触发器上显示。没有能力调用的纯计算脚本不会产生模型或搜索费用。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 编写脚本

脚本默认导出一个接收输入的函数；返回值成为运行的 `output`。

执行本页前，先在同一个 Bash 会话完成快速开始第 1–3 步：[设置 `PRAXIS_API_URL` 与 `PRAXIS_API_KEY`](/docs/developers/automation/quickstart#environment)、[定义 `SOURCE` 并临时运行](/docs/developers/automation/quickstart#run-source)、[定义并调用 `read_result` 辅助动作](/docs/developers/automation/quickstart#read-result)。该动作检查结果是否为 42，与本页的加一示例对应。

## 入口与 schema [#入口与-schema]

把下面的内容保存为 `script.ts`：

```typescript
export default async (input: { n: number }): Promise<{ answer: number }> => {
  console.log("Adding one", input.n);
  return { answer: input.n + 1 };
};
```

输入 schema 从参数类型推导；输出 schema 从显式返回类型推导，`Promise` 会展开。支持基本类型、对象、数组、联合类型及可解析的本地类型别名。不能推导的类型会得到空 schema 和警告；不要把空 schema 当成已经验证了字段。省略返回类型时，不应依赖返回表达式自动推导出输出约束。

通过[校验定义](/docs/api/native/automation/validateDefinition)检查实际生成的 schema，再发布：

```bash
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/validate" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source script.ts '{kind:"script",source:$source}')" \
  | jq -e '.valid == true and .input_schema.properties.n.type == "number" and .output_schema.properties.answer.type == "number"'
```

期望：输出 `true`，退出码 0。读取完整校验响应中的 `errors` 与 `warnings` 可以定位不能推导的类型。

## 运行与日志 [#运行与日志]

```bash
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source script.ts \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs/$RUN_ID/logs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

期望：结果断言为 `true`；日志响应包含 `Adding one 41`。`console.log/info/debug` 记录为 `info`，`console.warn` 和 `console.error` 保留各自级别。日志不是返回值；需要交给调用方的数据必须 `return`。可通过[日志接口](/docs/api/native/automation/listRunLogs)读取，或使用 `Accept: text/event-stream` 订阅。

## 全局 praxis [#全局-praxis]

运行时已经提供 `praxis`，无需 import、API Key 或安装包。它提供以下 SDK 入口；方法参数采用参考页中的 `path`、`query`、`body` 形状，例如 `praxis.automation.runs.create({body:{target,input}})`。能否调用仍取决于发起者权限。

| 入口                    | 能力参考                                          |
| --------------------- | --------------------------------------------- |
| `praxis.agent`        | [Agent](/docs/api/native/agent)               |
| `praxis.model`        | [Model](/docs/api/native/model)               |
| `praxis.file`         | [File](/docs/api/native/file)                 |
| `praxis.sandbox`      | [Sandbox](/docs/api/native/sandbox)           |
| `praxis.search`       | [Search](/docs/api/native/search)             |
| `praxis.database`     | [Database](/docs/api/native/database)         |
| `praxis.automation`   | [Automation](/docs/api/native/automation)     |
| `praxis.organization` | [Organization](/docs/api/native/organization) |
| `praxis.meta`         | [Meta](/docs/api/native/meta)                 |

脚本不能直接访问外部网络。用平台提供的能力完成操作，并 `await` 调用以取得结果及处理异常。

## 调用平台内置动作 [#调用平台内置动作]

`call("命名空间/名字", 输入)` 接收 Action 引用和输入对象，返回 Action 的输出。它是当前运行里的一个步骤：有自己的输入、输出，并在[调用记录](/docs/api/native/automation/listRunCalls)里占一条，不会产生子运行。`praxis` 是平台命名空间；省略命名空间的裸名也只解析到平台 Action。调用总使用 Action 的当前修订，暂不支持 `@版本`；不再接受 `命名空间::名字` 写法。

调用失败时抛出错误，带 `code`、`message`、`hint`、调用记录序号 `seq` 和 `request_id`。多个 `call` 可以用 `Promise.all` 并发。`praxis.*` 只表示平台 SDK，`call(...)` 和 `praxis.ocr(...)` 这类写法已删除，运行时会报错并提示改用 `call`。

把下面内容保存为 `chat.ts`；它调用 `praxis/chat`。模型由动作内置并由维护者选择，调用时不选模型；`praxis/ocr`、`praxis/image`、`praxis/embed` 也一样。旧 `model` 字段已废弃，传入的值会被忽略。此调用会产生模型费用：

```typescript
export default async (input: { prompt: string }) => {
  return await call("praxis/chat", {
    messages: [{ role: "user", content: input.prompt }],
    stream: false,
  });
};
```

```bash
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source chat.ts \
    '{source:$source,language:"ts",input:{prompt:"Say hello in one sentence."},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs/$RUN_ID" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq '{id,status,output,error}'
```

期望：终态为 `completed`，`output` 为模型结果，`error` 为 null。若仍是 `pending` 或 `running`，重复最后一条 GET，直到终态；`faulted` 时读取错误，不能把 HTTP 成功当成模型成功。动作输入输出的完整约定可用[读取动作](/docs/api/native/automation/getAction)查看 `praxis/chat`。

## 调用自建动作 [#调用自建动作]

以下示例[创建动作](/docs/api/native/automation/createAction)，再用它的完整名字调用。生成的脚本源码中引用是字符串字面量，便于发布前校验。

```bash
ACTION_NAME="add-one-$(date +%s)-$RANDOM"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/actions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg name "$ACTION_NAME" '{
    namespace:"docs-samples",name:$name,
    input_schema:{type:"object",properties:{n:{type:"number"}},required:["n"],additionalProperties:false},
    output_schema:{type:"object",properties:{answer:{type:"number"}},required:["answer"],additionalProperties:false},
    implementation:{kind:"code",language:"ts",source:"export default async (input: { n: number }) => ({ answer: input.n + 1 });"}
  }')" | jq -e '.id | startswith("act_")'
SOURCE="export default async (input: { n: number }) => await call(\"docs-samples/$ACTION_NAME\", input);"
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg source "$SOURCE" \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"
```

期望：动作创建与运行结果均输出 `true`，结果为 42。用完后可[归档动作](/docs/api/native/automation/archiveAction)。动作可复用，但不能在动作内部再编排其他动作、脚本或工作流。

## 调用另一个脚本或工作流 [#调用另一个脚本或工作流]

`invoke("命名空间/名字@版本", 输入)` 把另一个已发布的脚本或工作流作为**子运行**启动，等它结束后返回子运行的输出；省略 `@版本` 用最新版。和 `call` 的区别：`call` 是当前运行里的一个步骤，不产生子运行；`invoke` 产生一个有自己 ID、状态和日志的子运行，并在[调用记录](/docs/api/native/automation/listRunCalls)里用 `child_run_id` 指向它。

```typescript
export default async (input: { urls: string[] }) => {
  // 多个 invoke 用 Promise.all 并发；子运行经队列启动，不受单次请求链长度限制。
  const pages = await Promise.all(input.urls.map(url => invoke("team/fetch-page", { url })));
  return { pages };
};
```

* **等待有上限**：`invoke` 同步等待子运行结束，等待时间受父运行 `deadline` 限制。到期时抛出 `INVOKE_TIMEOUT`，消息里带子运行 ID；子运行不会因此被取消，可以用该 ID 另行读取结果。需要等很久的子运行，请在工作流里用 `run:` 步骤——工作流在等待期间不占用计算。
* **失败**：子运行失败、被取消或无法创建时抛出错误，`code` 是子运行的错误码，`message` 带子运行 ID。目标不存在或已归档时立即失败，不会排队。
* **取消与超时**：父运行被取消或失败时，仍在等待中的子运行会一起被取消。
* **查看子运行**：[列出运行](/docs/api/native/automation/listRuns)时用 `parent_run_id` 筛选，得到每个子运行的 ID、目标、状态和耗时。
* **限制**：每个运行最多 100 个子运行，同时执行的最多 20 个，多出的排队等待；嵌套最多 10 层，不允许自己调用自己。

脚本不能自行指定独立运行（detach）的子运行；需要“启动后不等结果”时，调用 [创建运行](/docs/api/native/automation/createRun)。

## 返回值和时间预算 [#返回值和时间预算]

返回 JSON 可序列化的值；不要返回 `undefined`、BigInt 或循环引用。序列化后的 UTF-8 返回值最多 1 MiB，超过会使运行失败。大结果应保存到文件等存储中，返回引用。

脚本墙钟时间默认最多 5 分钟；CPU 时间最多 30 秒，并受剩余 deadline 进一步约束。等待平台调用与纯计算的耗时不同，增加等待时长不能解决死循环。全部数值见[限制与错误](/docs/developers/automation/limits-and-errors)。

## 有依据的错误与修法 [#有依据的错误与修法]

| code                 | 报错原文                              | 修法                                |
| -------------------- | --------------------------------- | --------------------------------- |
| `SCRIPT_ERROR`       | `脚本执行出错`                          | 阅读 detail 中的原始异常及用户代码行号，修正后创建新运行。 |
| `CPU_LIMIT_EXCEEDED` | `脚本 CPU 时间超出上限`                   | 终止无限循环，缩小每批纯计算量。                  |
| `PAYLOAD_TOO_LARGE`  | `Payload exceeds 1 MiB`           | 缩小 JSON 返回值，返回存储引用。               |
| `INPUT_INVALID`      | `Value must be JSON serializable` | 返回明确的 JSON 值，如对象或 null。           |

运行失败看 `Run.error`；创建请求被拒绝看 HTTP problem 响应。两者不能混用。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 触发器

触发器把“何时执行”与脚本、工作流的内容分开。

执行本页前，先在同一个 Bash 会话完成快速开始第 1–3 步：[设置 `PRAXIS_API_URL` 与 `PRAXIS_API_KEY`](/docs/developers/automation/quickstart#environment)、[定义 `SOURCE` 并临时运行](/docs/developers/automation/quickstart#run-source)、[定义并调用 `read_result` 辅助函数](/docs/developers/automation/quickstart#read-result)。该函数检查结果是否为 42，与本页的加一示例对应。再完成第 4–5 步：[保存并发布脚本](/docs/developers/automation/quickstart#publish-script)、[设置 `TARGET`](/docs/developers/automation/quickstart#run-version)。`TARGET` 必须指向这个接收 `n`、返回 `answer` 的已发布脚本。

## 定时执行 [#定时执行]

每个 schedule 只能选择一种规则：

| 规则                  | 用途                                     |
| ------------------- | -------------------------------------- |
| `cron: "0 9 * * *"` | 每天当地时间 09:00；五段，分钟精度，支持 L、W、#。         |
| `every: "PT5M"`     | 每隔五分钟；最短一分钟，按经过的时长计算。                  |
| `at`                | RFC 3339 绝对时间，必须带时区偏移或 Z；不能早于现在或晚于一年后。 |

`timezone` 使用 IANA 名称，API 默认 UTC。当地时间 cron 在夏令时春季跳过不存在的时刻，秋季重复的时刻只运行一次。`every` 按时长、`at` 按绝对时间执行，不使用这条 cron 夏令时规则。

先[预览下次时间](/docs/api/native/automation/previewTriggerSchedule)：

```bash
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers/preview" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"schedule":{"cron":"0 9 * * *"},"timezone":"Asia/Shanghai"}' \
  | jq -e '{next_fire_times} | select(.next_fire_times | length == 5)'
```

期望：输出五个未来时间，例如：

```json
{
  "next_fire_times": [
    "2026-10-13T01:00:00.000Z",
    "2026-10-14T01:00:00.000Z",
    "2026-10-15T01:00:00.000Z",
    "2026-10-16T01:00:00.000Z",
    "2026-10-17T01:00:00.000Z"
  ]
}
```

日期随预览时间变化。末尾的 `Z` 表示 UTC。例如 `2026-10-13T01:00:00.000Z` 换算为**上海时间（UTC+8）10 月 13 日 09:00**。`timezone` 决定 cron 按哪个当地时区计算，返回的时间统一用 UTC 表达。

再[创建触发器](/docs/api/native/automation/createTrigger)，并立即触发一次检查目标：

```bash
TRIGGER=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"daily-hello",target:$target,kind:"schedule",input:{n:41},
    schedule:{cron:"0 9 * * *"},timezone:"Asia/Shanghai",
    overlap:"skip",catchup_window_seconds:300
  }')")
SCHEDULE_ID=$(jq -er '.id' <<< "$TRIGGER")
jq -e '.state == "enabled" and (.next_fire_times | length == 5)' <<< "$TRIGGER"
FIRING=$(curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/fire" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}')
jq -e '.outcome == "started"' <<< "$FIRING"
read_result "$(jq -er '.run_id' <<< "$FIRING")"
```

期望：三次断言输出 `true`。`started` 只表示启动成功；`read_result` 用返回的 `run_id` [读取运行](/docs/api/native/automation/getRun)，确认终态成功且结果为 42。

`target` 写 `@1.0.0` 固定版本，写 `@latest` 跟随标签；每条触发记录保存实际版本。创建和修改时检查静态输入，触发时再次检查，避免新版本不再接受原输入。

## 重叠、延迟与有效时间 [#重叠延迟与有效时间]

默认 `overlap: "skip"`：上一次运行未结束时，本次记录为 `skipped_overlap`；`allow` 允许重叠启动，仍受运行并发限制。没有排队补齐、取消旧运行或历史 backfill 策略。

`catchup_window_seconds` 默认 300。调度延迟超过窗口记为 `missed`，不会启动；这不是恢复后重放所有错过任务的承诺。暂停期间不执行，恢复后继续未来调度。

用 `start_at` 与 `end_at` 设置有效时间范围（RFC 3339）。一次性 `at` 执行后、或达到结束时间后变为 `expired`。连续 5 次触发失败会自动暂停。通过[暂停](/docs/api/native/automation/pauseTrigger)和[恢复](/docs/api/native/automation/resumeTrigger)控制后续触发。

暂停上面创建的触发器，再恢复：

```bash
curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/pause" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "paused" and (.next_fire_times | length == 0)'
curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/resume" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "enabled" and (.next_fire_times | length == 5)'
```

期望：两次请求都输出 `true`；恢复后返回这个每日计划的下五次执行时间。

工作流 YAML 中的 cron/every schedule 在发布为 latest 时管理一个 `source=definition` 触发器；修改要重新发布定义，不能直接 PATCH 这个触发器。可以暂停或恢复它。`schedule.after` 与 `schedule.on` 暂不支持。

## Webhook [#webhook]

创建时选 `input_mode: "body"`，收到的 JSON 就是运行输入；非 JSON 请求体作为字符串。以下示例把 `n:41` 传给快速开始的脚本：

```bash
HOOK=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"webhook-hello",target:$target,kind:"webhook",webhook:{input_mode:"body"}
  }')")
WEBHOOK_ID=$(jq -er '.id' <<< "$HOOK")
WEBHOOK_URL=$(jq -er '.webhook_url' <<< "$HOOK")
curl --fail-with-body -sS "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "webhook-id: hello-$(date +%s)-$RANDOM" \
  --data '{"n":41}' | jq -e '.id | startswith("frg_")'
```

期望：HTTP 202，断言输出 `true`，响应含触发记录 ID。异步启动结果需要再查触发历史。

完整 URL 只在创建或[轮换](/docs/api/native/automation/rotateTriggerWebhook)时返回；其中的 token 是凭据，无需 API Key，不要公开。旧 URL 在轮换后失效。错误 token、暂停或不存在的触发器都返回 404。请求体最多 1 MiB；相同 `webhook-id` 在 24 小时内去重。

## 签名头与 envelope [#签名头与-envelope]

当前 webhook 入口验证 secret URL，不自动验证 Standard Webhooks、GitHub 或 Stripe 的 HMAC 签名。传入 `webhook-signature` 不代表它已经通过验签。

若目标需要读取头部，创建时使用 `webhook: {input_mode:"envelope"}`，并让目标的输入 schema 接受下列形状（这不是快速开始的 `n` 输入）：

```json
{
  "body": { "n": 41 },
  "headers": { "content-type": "application/json", "webhook-id": "delivery-1" },
  "webhook_id": "delivery-1",
  "received_at": "2026-10-11T12:00:00.000Z"
}
```

`webhook_id` 未提供时为 null。只保留以下头：`content-type`、`user-agent`、`webhook-id`、`webhook-timestamp`、`webhook-signature`、`x-github-event`、`x-github-delivery`、`x-hub-signature-256`、`stripe-signature`；Authorization、Cookie 不会转交。

JSON body 会先被解析，envelope 不包含独立的原始请求字节。对于必须签署原始字节的协议，不能用 `JSON.stringify(body)` 代替原文验签。需要这种验证时，在你接收原始请求的服务中按发送方协议先验证，再转发到 secret URL。当前没有可直接套用到此 JSON envelope 的通用验签示例。

## 查看触发记录与清理 [#查看触发记录与清理]

```bash
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq -e '.data | length > 0'
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq '.data[] | {outcome,run_id,resolved_version,error}'
```

期望：第一个请求输出 `true`，第二个列出记录。若异步处理尚未完成，稍后重新读取。`started` 表示已启动，`failed` 看 error，`missed` 表示超过窗口，`skipped_overlap` 表示前次未结束，`deduped` 表示命中去重。按 `run_id` 继续查看运行终态，记录保留 30 天。

测试完成后删除本页创建的两个触发器，避免继续定时执行：

```bash
for id in "$SCHEDULE_ID" "$WEBHOOK_ID"; do
  curl --fail-with-body -sS -X DELETE \
    "$PRAXIS_API_URL/automation/v1/triggers/$id" \
    -H "Authorization: Bearer $PRAXIS_API_KEY"
done
```

期望：curl 均退出 0。删除不影响已经启动的运行。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 限制与错误

以下默认值约束单次运行及其子运行；调用方设置的更短 deadline 也约束后续执行。并发超限时排队，不会增加执行容量。

## 执行与数据 [#执行与数据]

| 限制                       | 数值                       |
| ------------------------ | ------------------------ |
| 嵌套深度                     | 10                       |
| 每运行子运行总数 / 同时活跃子运行       | 100 / 20                 |
| 运行树总节点                   | 1,000                    |
| 每 owner / 每定义并发运行        | 100 / 20                 |
| 动作 / 沙箱动作超时              | 30 s / 10 min            |
| 脚本墙钟超时                   | 5 min                    |
| 工作流默认 / 最大超时             | 24 h / 7 days            |
| 创建运行 wait 上限             | 60 s                     |
| 载荷软阈值 / 硬上限              | 256 KiB / 1 MiB          |
| 调用载荷内联阈值 / 检查点内联阈值       | 32 KiB / 256 KiB         |
| 每运行任务执行数（含循环与重试）         | 4,000                    |
| 每运行持久执行步骤预算              | 9,000                    |
| 每 fork 分支 / 嵌套并发效果       | 10 / 25                  |
| 幂等保留时间                   | 24 h                     |
| 运行日志保留期                  | 30 days                  |
| 每页记录数                    | 100                      |
| 标签数量 / key 长度 / value 长度 | 50 / 40 / 500 characters |
| 描述长度                     | 1,000 characters         |
| SSE 心跳间隔                 | 20 s                     |

{/* Sources: services/automation/src/api/contract.ts:42-70; services/automation/src/runs/workflow/primitives.ts:7-12. */}

载荷软阈值不是拒绝边界；硬上限为 1 MiB，按 JSON 编码后的 UTF-8 字节计算。内联阈值决定数据的存储方式，不代表脚本可以返回超过硬上限的结果。

## 脚本运行时 [#脚本运行时]

| 限制          | 数值                         |
| ----------- | -------------------------- |
| CPU 时间      | 最多 30 秒，并取剩余 deadline 中较小者 |
| 每脚本连接器请求    | 1,000 次，包含日志和进度            |
| 同时执行的连接器操作  | 20                         |
| 单条日志        | 4,096 UTF-8 字节，超出截断        |
| 调用摘要        | 512 字符                     |
| 脚本错误 detail | 1,000 字符                   |
| 保留的用户堆栈帧    | 最多 5 条                     |

{/* Sources: services/automation/src/script-runtime/core.ts:8-91; index.ts CPU limit; connectors.ts Gate(20). */}

脚本输出必须可序列化为 JSON，编码失败会使运行失败。脚本没有检查点或事务回滚；具体报错原文与修法见[编写脚本](/docs/developers/automation/guides/write-script)。

## 触发器 [#触发器]

| 限制              | 数值     |
| --------------- | ------ |
| 每 owner 启用中的触发器 | 100    |
| every 最短间隔      | 1 分钟   |
| cron 精度         | 五段，分钟  |
| 一次性 at 时间范围     | 当前至一年后 |
| 默认补跑窗口          | 300 秒  |
| 预览未来时间          | 最多 5 个 |
| 连续触发失败后自动暂停     | 5 次    |
| webhook 请求体     | 1 MiB  |
| webhook-id 去重   | 24 小时  |
| 触发记录保留期         | 30 天   |

{/* Sources: services/automation/TRIGGERS.md sections 2-3; services/automation/src/triggers/core.ts; webhook.ts:4. */}

## 找到对应错误 [#找到对应错误]

HTTP 错误表示请求被拒绝；运行状态 `faulted` 表示执行失败，即使读取它的 HTTP 响应为 200。排查时保存运行 ID 与响应头 `X-Request-Id`。

* [创建运行错误表](/docs/api/native/automation/createRun)：输入、目标解析、幂等与配额。
* [发布错误表](/docs/api/native/automation/publishScriptVersion)：草稿已变、版本冲突与定义校验。
* [触发器错误表](/docs/api/native/automation/createTrigger)与[调度错误表](/docs/api/native/automation/previewTriggerSchedule)。
* [Webhook 错误表](/docs/api/native/automation/receiveWebhook)：请求体大小、输入与不可用 URL。
* [鉴权与通用错误](/docs/api/errors)。

例如 `INTERVAL_TOO_SHORT` 的报错原文为 `every must be at least one minute`，把间隔改为 `PT1M` 或更长；不要原样重试无效调度。

[API 参考 →](/docs/api/native/automation) · [Cookbook →](/docs/cookbook)

# 模型

统一接口访问 LLM、图像、语音、Embedding 模型；兼容 OpenAI 与 Anthropic 协议，现有 SDK 零改动接入。

## 出口 [#出口]

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 快速开始

用一个云 API Key 调用 `deepseek-flash`，读取实付费用，再找到同一次调用的记录。示例会产生少量费用。

## 1. 准备 Key 与终端 [#1-准备-key-与终端]

在[控制台 → API Keys](https://console.deepraxis.ai/keys)点击「创建 Key」，填写名称并创建，将 Key 安全注入当前终端的 `PRAXIS_API_KEY` 环境变量。不要写入代码或日志。已有 Key 可直接使用。

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)，在 macOS / Linux 终端执行：

```bash
test -n "${PRAXIS_API_KEY:-}"
```

期望：退出码 0；退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`；切换环境时，将 `PRAXIS_API_BASE` 设为对应 API 域名，不带末尾斜杠或 API 路径。

## 2. 发出请求并读取费用 [#2-发出请求并读取费用]

保存为 `quickstart.py`，在该文件所在目录执行 `uv run -q --no-project --with openai --with anthropic --with google-genai python quickstart.py`。

```python
import json
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
    timeout=90.0,
    max_retries=0,
)
raw = client.chat.completions.with_raw_response.create(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "Reply with exactly: Hello Praxis."}],
    max_tokens=256,
)
reply = raw.parse()
usage = reply.usage.model_dump()
request_id = raw.headers.get("x-request-id")
generation_id = raw.headers.get("x-praxis-generation-id")
assert raw.status_code == 200 and reply.choices[0].message.content
assert request_id and generation_id
assert isinstance(usage.get("cost"), (int, float)) and usage["cost"] >= 0
print(reply.choices[0].message.content)
print("usage:", usage)
print("X-Request-Id:", request_id)
print("X-Praxis-Generation-Id:", generation_id)
Path("quickstart-result.json").write_text(json.dumps({
    "request_id": request_id, "generation_id": generation_id, "cost": usage["cost"],
}))
```

期望：退出码 0，输出非空回答（通常为 `Hello Praxis.`）、`usage` 和两个 ID，并生成 `quickstart-result.json`。该文件只保存本次调用的 ID 与费用，不含 Key。

`usage` 包含输入和输出 token 数；`usage.cost` 是本次实付美元金额，不是每百万 token 的单价。不要把缺失费用当成零。字段定义见 [Chat Completions API](/docs/api/compatible/openai/createChatCompletion)。

## 3. 找到这次调用 [#3-找到这次调用]

打开[控制台 → 模型 → 调用日志](https://console.deepraxis.ai/call-logs)。在「按 X-Request-Id / Generation ID 查找」输入框粘贴上一步的 `X-Request-Id`，按 Enter。此路径与控件名核对于 2026-10-13 的 Console。

期望：出现模型为 `deepseek-flash` 的本次调用，可查看状态、用量与费用。记录可能延迟几秒；使用与 Key 相同的账号及环境，并确保时间范围包含刚才的调用。

不使用浏览器时，可用下面的等价硬验证。保存为 `verify-generation.py`，在同一目录执行 `uv run -q --no-project --with openai --with anthropic --with google-genai python verify-generation.py`。

```python
import json
import os
import time
from decimal import Decimal
from pathlib import Path
import httpx

saved = json.loads(Path("quickstart-result.json").read_text())
base = os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com")
with httpx.Client(timeout=60.0) as client:
    for attempt in range(10):
        response = client.get(
            base + "/model/v1/generation",
            params={"id": saved["generation_id"]},
            headers={"Authorization": "Bearer " + os.environ["PRAXIS_API_KEY"]},
        )
        if response.status_code != 404:
            break
        time.sleep(3)
    response.raise_for_status()
    record = response.json()
    assert record["id"] == saved["generation_id"]
    assert record["model"] == "deepseek-flash"
    assert abs(Decimal(str(record["cost"])) - Decimal(str(saved["cost"]))) <= Decimal("0.0000005")
    print("Verified generation:", record["id"])
    print("cost:", record["cost"])
    print("X-Request-Id:", response.headers.get("x-request-id"))
```

期望：退出码 0，输出 `Verified generation` 和按美元六位小数记录的费用；脚本允许相应舍入差异。查询使用 `X-Praxis-Generation-Id`，不是 `X-Request-Id` 或响应正文的 `id`；详见[生成记录 API](/docs/api/native/model/getGeneration)。

## 失败分支 [#失败分支]

* `404` 查询记录：记录尚未可查时，脚本每 3 秒重试，最多 10 次；仍失败则保留两个 ID，检查 Key、环境及生成 ID 是否来自同一次调用，再查询。
* `Model "gpt-4o" does not exist.`：模型 ID 不自动映射；使用本页的 `deepseek-flash` 或[目录](/docs/developers/model/catalog)中的 ID。

接下来可使用[流式响应](/docs/developers/model/guides/streaming)或[工具调用](/docs/developers/model/guides/tool-calling)。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 模型目录

模型与价格会变化，请从[控制台 → 模型目录](https://console.deepraxis.ai/models)或实时 API 读取，不依赖静态清单。

## 1. 准备查询 [#1-准备查询]

准备 Python 3.10+ 与 [uv](https://docs.astral.sh/uv/getting-started/installation/)。原生目录 `GET /model/v1/models` 无需 Key；下面同时查询 OpenAI 兼容目录，需要按[快速开始](/docs/developers/model/quickstart)设置 `PRAXIS_API_KEY`。

期望：已有 Key 环境变量。示例默认使用 `https://api.deepraxis.com`；切换环境时，设置 `PRAXIS_API_BASE` 为 API 域名，不带末尾斜杠或 API 路径。

## 2. 核对模型与协议 [#2-核对模型与协议]

保存为 `catalog.py`，执行 `uv run -q --no-project --with openai --with anthropic --with google-genai python catalog.py`。

```python
import os
import httpx

base = os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com")
with httpx.Client(timeout=60.0) as client:
    response = client.get(base + "/model/v1/models")
    response.raise_for_status()
    models = response.json()["data"]
    model = next(item for item in models if item["id"] == "deepseek-flash")
    assert "openai.chat.completions" in model["supported_apis"]
    for field in ("id", "type", "supported_apis", "context_window", "pricing"):
        print(field + ":", model[field])
    print("X-Request-Id:", response.headers.get("x-request-id"))

    filtered = client.get(
        base + "/openai/v1/models",
        params={"api": "responses"},
        headers={"Authorization": "Bearer " + os.environ["PRAXIS_API_KEY"]},
    )
    filtered.raise_for_status()
    ids = [item["id"] for item in filtered.json()["data"]]
    assert "deepseek-flash" in ids
    print("Responses models:", ids)
    print("X-Request-Id:", filtered.headers.get("x-request-id"))
```

期望：退出码 0，打印 `deepseek-flash` 的类别、协议、上下文和价格，以及包含该 ID 的 Responses 列表。示例核对于 2026-10-13；以查询时的结果为准。若断言失败，不要向所选协议发送请求，先从最新列表选择支持它的模型。

## 3. 根据结果选择 [#3-根据结果选择]

先按 `type` 找任务类别，再用 `supported_apis` 确认要调用的协议；不能从模型名称推断协议。原生列表还可用 `supported_api=openai.responses` 筛选；OpenAI 兼容列表的 `api=chat,embeddings` 表示匹配任一协议。

再核对上下文和输出限制，以及输入、输出、缓存或非 token 计量的单价。[模型与目录概念](/docs/developers/model/concepts/models-and-catalog)解释计费单位及可用率；[列出模型 API](/docs/api/native/model/listModels)提供字段定义。

期望：所选 ID 支持应用需要的协议，限制能容纳请求，价格单位与任务一致。最后按[快速开始](/docs/developers/model/quickstart)发起真实调用并核对实付费用。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 概念

一次模型调用由模型 ID 指定要用的模型，由协议决定请求和响应的格式；平台在符合要求的线路中完成调用。

* [模型与目录](/docs/developers/model/concepts/models-and-catalog)：区分模型类别、协议支持与计费单位。
* [协议兼容](/docs/developers/model/concepts/protocols)：理解同一个地址下不同协议的适用范围。
* [路由](/docs/developers/model/concepts/routing)：理解线路选择、失败换线与参数要求。
* [会话亲和](/docs/developers/model/concepts/session-affinity)：理解重复使用同一线路与缓存的关系。
* [BYOK](/docs/developers/model/concepts/byok)：查看自带凭证的开放状态。

第一次调用从[快速开始](/docs/developers/model/quickstart)进入；选择模型从[模型目录](/docs/developers/model/catalog)进入。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 模型与目录

模型目录描述可以选择什么；一次调用的记录描述实际用了多少、付了多少。两者用途不同。

## 模型 ID 与能力 [#模型-id-与能力]

请求中的 `model` 使用目录返回的完整 `id`，例如 `deepseek-flash`。显示名称不是 ID；相同 ID 也不表示底层版本永远不变，滚动版本应以模型说明为准。

`type` 区分语言、图像、语音、视频、向量等模型类别，不能据此推断它支持所有相关接口。`supported_apis` 才是已开放协议的列表；选择 Chat、Responses 或 Messages 前分别核对它。输入和输出模态说明能处理什么内容，不等于所有文件格式都能原生读取。

上下文窗口限制一次输入与生成可容纳的内容，不是每次必然能输出的长度。原生目录的字段为 `context_window`；OpenAI 兼容目录使用 `context_length`。输出上限需另看模型说明与 API 参考。

## 标价与实付 [#标价与实付]

目录 `pricing` 的金额为美元，单位由价格项决定。token 单价以每个 token 表达，展示为每百万 token 时乘以一百万；输入与输出分别定价。`input_cache_read` 是缓存命中时的输入读价，不保证每次都会命中。

并非所有模型按 token 计费：视频可按秒、图像可按张、OCR 可按页、转写可按音频分钟。价格项缺失不表示免费，不要把所有数值都乘以一百万。具体价格项与字段定义见[列出模型 API](/docs/api/native/model/listModels)。

Chat 响应的 `usage.cost` 是本次实付美元金额；用目录单价乘以 token 数只是估算，可能忽略缓存或其他计量。需要核对实际调用时，使用[快速开始中的生成记录查询](/docs/developers/model/quickstart)。

## 可用率 [#可用率]

可用率衡量平台与上游能否完成有效请求：平台或上游原因的失败计入，调用方参数等原因造成的拒绝不计入。它不表示任意参数都能成功，也不保证下一次请求必然成功。

[模型目录](/docs/developers/model/catalog)说明如何获取当前目录；本页不维护静态模型与价格清单。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 路由

你选择模型，平台从支持该模型及所用协议的线路中选择一次调用的去向。同一个模型可以有多条线路；线路数量与状态会变化。

## 从请求到线路 [#从请求到线路]

平台先检查模型、协议和参数要求，再结合健康状态与优先级选择可用线路。备选线路必须支持同一种协议；Chat、Responses、Messages 之间不会为了换线而偷偷转换。

同优先级的可用线路还可能受[会话亲和](/docs/developers/model/concepts/session-affinity)影响。优先级不是对某个供应商的永久绑定，也不是每次都选择最低价格的保证。

## 失败换线 [#失败换线]

当一次尝试因可重试的上游故障、限流或线路不可用而失败时，平台可自动尝试其他合适的线路。没有可用备选或尝试仍失败时，请求返回错误；不是无限重试，也不保证已开始的流式回答可以无缝续接。

调用方参数错误不靠换线修复。应用仍需检查完成状态，并按[错误与限制](/docs/developers/model/limits-and-errors)处理失败。

## 参数要求与供应商选择 [#参数要求与供应商选择]

`provider.require_parameters` 要求线路声明支持请求中的相关工具与采样参数，避免把要求交给不支持它的线路。它限制候选线路，不会为模型增加能力，也不保证模型一定调用工具或生成某种回答；没有符合要求的线路时可拒绝请求。具体参数支持以 [Chat API 参考](/docs/api/compatible/openai/createChatCompletion)为准。

`provider` 中指定供应商的 `order`、`only`、`ignore` 是 BYOK 专属选项；`require_parameters` 这种能力约束不属于供应商选择限制。平台模型由平台选线。[BYOK 当前即将推出](/docs/developers/model/concepts/byok)，不要依赖它指定平台供应商。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 协议兼容

协议兼容让你沿用客户端的请求格式；不表示每个模型支持所有协议或官方的全部功能。以模型的 `supported_apis` 和各端点支持说明为准。

只有一个地址 `https://api.deepraxis.com`，按协议分入口：

| 入口           | 协议                |
| ------------ | ----------------- |
| `/openai`    | OpenAI 系协议        |
| `/anthropic` | Anthropic 系协议     |
| `/google`    | Google Gemini API |

## OpenAI Chat Completions [#openai-chat-completions]

`POST /openai/v1/chat/completions`。常用的对话协议；流式（SSE）、工具调用与多模态输入是否可用取决于模型。 模型能力与字段差异见 [Chat API](/docs/api/compatible/openai/createChatCompletion)；会话标识的作用见[会话亲和](/docs/developers/model/concepts/session-affinity)。

## OpenAI Responses [#openai-responses]

`POST /openai/v1/responses` 及 retrieve / delete / cancel / input\_items。OpenAI 新一代有状态协议；Codex 等 Agent 走这里。支持范围见 [Responses API](/docs/api/compatible/openai/createResponse)。压缩响应与输入 token 计数端点当前未实现，调用返回 501。

## OpenAI Files [#openai-files]

`/openai/v1/files`：上传 `user_data` 文件供后续请求引用；可列出、读元数据、下载、删除。

## OpenAI Images [#openai-images]

`POST /openai/v1/images/generations`：使用目录声明支持图像生成的模型。

## Anthropic Messages [#anthropic-messages]

`POST /anthropic/v1/messages`。Claude Code、Anthropic SDK 走这里。 具体支持范围见 [Messages API](/docs/api/compatible/anthropic/createMessage)。

## Anthropic Files [#anthropic-files]

`/anthropic/v1/files`：上传、列出、读元数据、删除；**上传后不可下载**（与官方一致）。

## Google Gemini API（generateContent） [#google-gemini-apigeneratecontent]

`POST /google/v1beta/models/{model}:generateContent`。目前用于 Gemini 原生协议的图像生成与编辑。

## 模型目录 [#模型目录]

用 `GET /openai/v1/models?api=responses` 查找支持 Responses 的模型，`api=chat,embeddings` 匹配任一协议。原生目录 `GET /model/v1/models` 及模型详情会返回每个模型的 `supported_apis`；原生列表可用 `?supported_api=openai.responses` 筛选。

`GET /openai/v1/models`：列出当前 Key 可调用的模型，OpenAI 风格返回；Kilo Code 等客户端靠它自动发现模型。

Anthropic 客户端使用 `GET /anthropic/v1/models`，Google 客户端使用 `GET /google/v1beta/models`；分别返回对应协议的模型列表。

## 标准化地址 [#标准化地址]

以上每个入口还有一个标准化长路径 `/v1/router/<vendor>/...`，语义完全相同，见 [API 参考](/docs/api/compatible)。

→ 深入：[OpenAI 兼容接口](/docs/developers/model/guides/openai-compatible) · [Anthropic 兼容接口](/docs/developers/model/guides/anthropic-compatible)

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 会话亲和

会话亲和让同一会话后续请求，在条件允许时优先复用之前成功的线路。它是一种选路偏好，不是锁定供应商，也不是替应用保存对话历史。

## 为什么有用 [#为什么有用]

多轮对话常重复系统提示、工具定义和早期消息。重复前缀留在同一条支持缓存的线路上，更有机会命中上游缓存，降低重复输入的费用与延迟。相同会话 ID 不保证命中缓存；内容变化、缓存失效或换线都可能让它失效。

## 如何识别同一会话 [#如何识别同一会话]

客户端可用请求体的 `session_id` 或请求头 `x-session-id` 标识会话；两者同时存在时，请求体优先。没有显式会话 ID 时，还可能使用 `prompt_cache_key` 或从对话前缀推导标识。自动推导受内容变化影响，因此长期多轮对话应保持显式标识一致，不同对话使用不同标识。

## 何时生效 [#何时生效]

缓存亲和要求该线路支持本次模型与协议、输入得到保留，并且缓存读取价格低于普通输入价格。成功完成的调用才更新亲和记录；后续请求仍须通过当前的健康与能力检查。

亲和只在同优先级范围内调整顺序，不绕过冷却或参数要求。距上次成功已空闲 10 分钟、原线路不再合适，或亲和记录不可用时，按正常路由继续；因此应用不能依赖它保证状态永远留在同一上游。

Chat 请求仍应携带需要的消息历史。有状态 Responses 的续写属于独立的协议能力，不能用会话 ID 代替 `previous_response_id`。参数说明见 [Chat API](/docs/api/compatible/openai/createChatCompletion) 与 [Responses API](/docs/api/compatible/openai/createResponse)。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# BYOK：自带模型凭证

BYOK（自带模型供应商凭证）即将推出，Console 尚未开放配置入口。目前请使用 Praxis 云 API Key 调用[模型目录](/docs/developers/model/catalog)中的平台模型，由平台管理线路与计费；首次调用见[快速开始](/docs/developers/model/quickstart)。

[API 参考 →](/docs/api/native/model) · [Cookbook →](/docs/cookbook)

# 指南

| 你想做什么                | 去哪                                                |
| -------------------- | ------------------------------------------------- |
| 把 Praxis 接进 SDK / 框架 | [Cookbook → SDK](/docs/cookbook/sdk)              |
| 自带模型凭证（BYOK）         | [BYOK](/docs/developers/model/guides/byok)        |
| 处理错误与限流              | [错误与限流](/docs/developers/model/limits-and-errors) |

# OpenAI 兼容接口

OpenAI SDK 使用 `https://api.deepraxis.com/openai/v1`。请求按同协议转发；兼容不表示所有官方端点、字段和模型都可用。

## 选对地址 [#选对地址]

| 客户端              | 基础地址                                  | 模型目录                                                   |
| ---------------- | ------------------------------------- | ------------------------------------------------------ |
| OpenAI SDK       | `https://api.deepraxis.com/openai/v1` | `/openai/v1/models`                                    |
| Anthropic SDK    | `https://api.deepraxis.com/anthropic` | `/anthropic/v1/models`                                 |
| Google GenAI SDK | `https://api.deepraxis.com/google`    | `/google/v1beta/models`                                |
| Praxis 原生 API    | `https://api.deepraxis.com/model/v1`  | 见[原生 API](/docs/api/native/model)，包含 `providerOptions` |

各兼容目录只列声明提供该协议的模型。出现在 OpenAI 目录里不等于同时支持 Chat Completions 和 Responses；未声明的操作返回 `404 model_protocol_not_offered`。

## 字段差异去哪里核对 [#字段差异去哪里核对]

| 应用使用的内容                   | 发请求前核对                                                                                            |
| ------------------------- | ------------------------------------------------------------------------------------------------- |
| `messages`、工具、采样、思考、供应商偏好 | [Chat Completions 字段支持](/docs/api/compatible/openai/createChatCompletion)                         |
| `input`、响应存储、响应工具         | [Responses 字段支持](/docs/api/compatible/openai/createResponse)                                      |
| `usage.cost`、生成 ID        | [迁移页费用处理](/docs/cookbook/migration/from-openai#行为差异)与[生成记录](/docs/api/native/model/getGeneration) |
| 上传、文件用途                   | [上传文件](/docs/api/compatible/openai/createFile)                                                    |
| 对话以外的端点                   | [操作支持情况](/docs/api/compatible/openai)与[不支持清单及替代](/docs/cookbook/migration/from-openai#不支持的端点与替代)  |

未知顶层字段可能原样转发给同协议上游；请求被接受不证明字段已生效。已声明字段仍会校验。依赖一个选项前，请查看对应端点的字段支持说明。

## 1. 准备 [#1-准备]

需要 Python 3.10+、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和一个 Praxis Key。在[控制台 → API Keys](https://console.deepraxis.ai/keys)创建 Key，将它安全注入当前终端的 `PRAXIS_API_KEY` 环境变量；不要把 Key 写进代码或日志。保留原服务的配置与 Key，供回滚使用。以下命令在 macOS / Linux 的 Bash 中执行。

```bash
test -n "${PRAXIS_API_KEY:-}"
```

期望：退出码 0；退出码 1 表示变量未设置，先设置再继续。

## 2. 验证 Responses [#2-验证-responses]

示例使用 `deepseek-flash`。截至 2026-10-12，`glm-5.3-flash` 也提供 Responses；`gpt-5.6-sol` 提供 Chat Completions，但不提供 Responses。使用前在[模型目录](https://console.deepraxis.ai/models)确认所需操作。

```bash
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
uv run -q --no-project --with openai --with anthropic python - <<'PY'
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepraxis.com/openai/v1",
    api_key=os.environ["PRAXIS_API_KEY"],
    timeout=90.0,
    max_retries=0,
)
models = client.models.list()
assert "deepseek-flash" in {m.id for m in models.data}
raw = client.responses.with_raw_response.create(
    model="deepseek-flash",
    input="Reply OK.",
    max_output_tokens=1024,
)
reply = raw.parse()
text = reply.output_text
print("HTTP", raw.status_code)
print("X-Request-Id:", raw.headers.get("x-request-id"))
print("X-Praxis-Generation-Id:", raw.headers.get("x-praxis-generation-id"))
print(text)
assert raw.status_code == 200 and text and text.strip()
assert raw.headers.get("x-request-id")
PY
```

期望：退出码 0，输出 `HTTP 200`、非空正文（通常为 `OK.`）和 `X-Request-Id`。断言同时检查模型存在与真实回复；不是只检查能否连接。保留请求 ID 便于排查，不要记录 Key。示例会产生少量调用费用。

期望：`output_text` 是最终正文，不把思考输出当作正文。若返回 `model_protocol_not_offered`，换支持 Responses 的模型；若提示 `does not exist`，从目录复制准确 ID。已有应用回滚时同时恢复原地址、Key 与模型，再运行原冒烟请求。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook：从 OpenAI 迁移 →](/docs/cookbook/migration/from-openai)

# Anthropic 兼容接口

Anthropic SDK 使用 `https://api.deepraxis.com/anthropic`。SDK 会追加 `/v1/messages`，基础地址不要再加 `/v1`。请求按同协议转发，可用能力取决于模型的声明。

## 兼容覆盖什么 [#兼容覆盖什么]

`/anthropic/v1/models` 只列 Anthropic 兼容模型。不要假设其他协议目录里的 ID 在这里也可用。未声明的操作返回 `404`（`anthropic.messages is not offered for …`）；换地址不会把不支持的模型能力转换出来。

## 字段差异去哪里核对 [#字段差异去哪里核对]

| 应用使用的内容                           | 需要核对的差异                                                                                                    |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `thinking`、`output_config.effort` | 思考选项取决于模型；部分模型不能关闭思考，见 [Messages](/docs/api/compatible/anthropic/createMessage)                            |
| 缓存控制、工具、元数据、beta 或 workspace 请求头  | [Messages 字段支持](/docs/api/compatible/anthropic/createMessage)，不要假设所有官方扩展可用                                 |
| `count_tokens`                    | 带 `x-praxis-token-count: estimated` 的校准估算，见[统计 token](/docs/api/compatible/anthropic/countTokens)          |
| 模型限额与能力                           | [模型列表](/docs/api/compatible/anthropic/listModels)，未知限额可能为 `null`                                           |
| 文件上传、过期时间                         | [上传文件](/docs/api/compatible/anthropic/createFile)                                                          |
| 费用                                | [Messages 费用处理](/docs/cookbook/migration/from-anthropic#行为差异)与[生成记录](/docs/api/native/model/getGeneration) |

未知顶层字段可传到同协议上游，由上游决定是否接受；已声明字段会校验。成功响应不代表所有选项都已执行。操作支持见 [API 列表](/docs/api/compatible/anthropic)，缺失操作的处理见[迁移替代方案](/docs/cookbook/migration/from-anthropic#不支持的操作与替代)。

## 1. 准备 [#1-准备]

需要 Python 3.10+、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和一个 Praxis Key。在[控制台 → API Keys](https://console.deepraxis.ai/keys)创建 Key，将它安全注入当前终端的 `PRAXIS_API_KEY` 环境变量；不要把 Key 写进代码或日志。保留原服务的配置与 Key，供回滚使用。以下命令在 macOS / Linux 的 Bash 中执行。

```bash
test -n "${PRAXIS_API_KEY:-}"
```

期望：退出码 0；退出码 1 表示变量未设置，先设置再继续。

## 2. 验证 Messages 与 token 统计 [#2-验证-messages-与-token-统计]

```bash
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
uv run -q --no-project --with openai --with anthropic python - <<'PY'
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.deepraxis.com/anthropic",
    api_key=os.environ["PRAXIS_API_KEY"],
    timeout=90.0,
    max_retries=0,
)
models = client.models.list()
assert "deepseek-flash" in {m.id for m in models.data}
raw = client.messages.with_raw_response.create(
    model="deepseek-flash",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Reply OK."}],
)
reply = raw.parse()
text = "".join(block.text for block in reply.content if block.type == "text")
print("HTTP", raw.status_code)
print("X-Request-Id:", raw.headers.get("x-request-id"))
print("X-Praxis-Generation-Id:", raw.headers.get("x-praxis-generation-id"))
print(text)
assert raw.status_code == 200 and text and text.strip()
assert raw.headers.get("x-request-id")
count = client.messages.with_raw_response.count_tokens(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "Reply OK."}],
)
print("count_tokens:", count.parse().input_tokens)
print("X-Request-Id:", count.headers.get("x-request-id"))
print("x-praxis-token-count:", count.headers.get("x-praxis-token-count"))
assert count.status_code == 200
assert count.headers.get("x-praxis-token-count") == "estimated"
PY
```

期望：退出码 0，输出 `HTTP 200`、非空正文（通常为 `OK.`）和 `X-Request-Id`。断言同时检查模型存在与真实回复；不是只检查能否连接。保留请求 ID 便于排查，不要记录 Key。示例会产生少量调用费用。

期望：即使第一块是思考，仍能从 `type == "text"` 的块取得正文。第二次调用输出 token 估算与 `estimated`；不能把估算当成计费用量。

若返回 `400`（`cannot disable thinking`），移除关闭思考的要求或改选允许关闭的模型；若返回 `anthropic.messages is not offered for …`，选择 Anthropic 兼容且支持该操作的模型。已有应用回滚时恢复原地址、Key、模型与思考选项，再运行原 Messages 请求。

[API 参考 →](/docs/api/compatible/anthropic) · [Cookbook：从 Anthropic 迁移 →](/docs/cookbook/migration/from-anthropic)

# 流式响应

流式响应把一条回答拆成多个事件。显示收到的文本后，还要读取终态和用量，才能确认本轮完成。

本页使用 `POST /openai/v1/chat/completions` 的 `deepseek-flash`，并提供 `POST /anthropic/v1/messages` 的 `claude-opus-5` 示例；两者于 2026-10-12 验证通过。

## 1. 准备终端 [#1-准备终端]

将 Praxis 云 API Key 放入环境变量 `PRAXIS_API_KEY`；获取方式见 [Key 与鉴权](/docs/developers/account/api-keys)。不要把 Key 写入代码。准备 Python 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)；TypeScript 示例使用 [Bun](https://bun.sh/docs/installation)。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

将每段 Python 保存为标明的文件名，以 `uv run -q --no-project --with openai --with anthropic python <文件名>` 执行。各段代码独立运行。

## 2. 用 Python 接收文本和末块用量 [#2-用-python-接收文本和末块用量]

保存为 `streaming.py`，运行 `uv run -q --no-project --with openai --with anthropic python streaming.py`。

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
text, finish, usage = '', None, None
with client.chat.completions.with_streaming_response.create(
    model='deepseek-flash', messages=[{'role':'user','content':'Reply with exactly: Hello streaming.'}],
    max_tokens=256, stream=True, stream_options={'include_usage':True},
) as response:
    print('X-Request-Id:', response.headers.get('x-request-id'))
    for chunk in response.parse():
        if chunk.usage is not None:
            usage = chunk.usage
        for choice in chunk.choices:
            piece = choice.delta.content or ''
            text += piece
            print(piece, end='', flush=True)
            if choice.finish_reason is not None:
                finish = choice.finish_reason
print('\nfinish_reason:', finish)
assert text.strip() and finish == 'stop', 'Incomplete text response'
assert usage is not None, 'No final usage received'
print('usage:', usage.model_dump())
```

期望：逐块输出 `Hello streaming.`，随后输出 `finish_reason: stop` 和包含输入、输出 token 数的 `usage`；退出码为 0。

Chat Completions 的读取顺序是：

1. 收集 `choices[].delta.content`；role、思考或空增量可能没有正文，不应当作文本显示。
2. 记录 `finish_reason`。本例的完整文本回答以 `stop` 结束；`tool_calls` 应进入[工具调用](/docs/developers/model/guides/tool-calling)流程。
3. 设置 `stream_options.include_usage=true` 后，继续读取末块 `usage`。这一块的 `choices` 可以为空，不能直接访问 `choices[0]`。
4. HTTP 事件流以 `data: [DONE]` 收尾；SDK 负责处理该标记并结束迭代。

不要在收到 `finish_reason` 时立即跳出循环，否则会漏掉之后的用量。断开连接不等于模型完成；没有终态或末块用量时，不应把部分文本和未知用量记为完整结果。

## 3. 用 cURL 检查事件顺序 [#3-用-curl-检查事件顺序]

在同一终端执行：

```bash
curl --fail-with-body --no-buffer --silent --show-error --include \
  "${PRAXIS_API_BASE:-https://api.deepraxis.com}/openai/v1/chat/completions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-flash","messages":[{"role":"user","content":"Reply with exactly: Hello streaming."}],"max_tokens":256,"stream":true,"stream_options":{"include_usage":true}}'
```

期望：HTTP 200，响应类型为 `text/event-stream`；正文增量后出现 `finish_reason: "stop"`、带 `usage` 的块，最后是 `data: [DONE]`；cURL 退出码为 0。`--no-buffer` 避免 cURL 缓冲显示，但单独退出码 0 不能证明回答完整，仍需检查终态。

## 4. 使用 Anthropic Messages [#4-使用-anthropic-messages]

Messages 依次发送 `message_start`、内容块的 `content_block_start` / `content_block_delta` / `content_block_stop`、`message_delta` 和 `message_stop`，中间可能有 `ping`。不要将它按 Chat Completions 的 `choices` 解析。输入用量在起始消息中，后续用量更新由 SDK 汇总；`get_final_message()` 返回完整结果。

保存为 `streaming-anthropic.py`，运行 `uv run -q --no-project --with openai --with anthropic python streaming-anthropic.py`。

```python
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
with client.messages.stream(model='claude-opus-5', max_tokens=256,
    messages=[{'role':'user','content':'Reply with exactly: Hello streaming.'}]) as stream:
    print('X-Request-Id:', stream.response.headers.get('x-request-id'))
    for text in stream.text_stream:
        print(text, end='', flush=True)
    message = stream.get_final_message()
assert message.stop_reason == 'end_turn'
assert any(block.type == 'text' and block.text.strip() for block in message.content)
print('\nusage:', message.usage.model_dump())
```

期望：输出同样的文本和最终用量，`stop_reason` 为 `end_turn`，退出码为 0。

## 实测失败与处理 [#实测失败与处理]

把 `text-embedding-3-small` 用在本页聊天请求中时，实测返回 HTTP 404：

> openai.chat.completions is not offered for text-embedding-3-small

这是模型不提供聊天端点，不是等待更多事件可以解决的问题。改用本页已验证的 `deepseek-flash` 后，文本、终态与末块用量均正常返回。

协议细节：[OpenAI](https://developers.openai.com/api/docs/guides/streaming-responses) · [Anthropic](https://platform.claude.com/docs/en/build-with-claude/streaming)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 工具调用

模型返回工具名称和参数，由你的程序执行工具，再把结果交回模型。收到工具调用还不是完成回答。

本例查询本地演示数据：Paris 的温度固定为 22°C，不访问天气服务。`deepseek-flash` 在 `GET /openai/v1/models` 的 `supported_parameters` 中列有 `tools` 和 `tool_choice`；下面的两轮调用于 2026-10-12 验证通过。

## 1. 准备终端 [#1-准备终端]

将 Praxis 云 API Key 放入环境变量 `PRAXIS_API_KEY`；获取方式见 [Key 与鉴权](/docs/developers/account/api-keys)。不要把 Key 写入代码。准备 Python 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)；TypeScript 示例使用 [Bun](https://bun.sh/docs/installation)。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

将每段 Python 保存为标明的文件名，以 `uv run -q --no-project --with openai --with anthropic python <文件名>` 执行。各段代码独立运行。

## 2. 定义工具并完成两轮调用 [#2-定义工具并完成两轮调用]

保存为 `tool-calling.py`，运行 `uv run -q --no-project --with openai --with anthropic python tool-calling.py`。

代码先声明 `get_temperature` 的名称、描述和参数，再要求模型调用它；验证参数后执行本地查询，将结果和原始 `tool_call_id` 一起回填，最后请求文字回答。两轮都设置 `reasoning_effort="none"`。

```python
import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
tools = [{'type':'function','function':{
    'name':'get_temperature', 'description':'Read the demo temperature for Paris.',
    'parameters':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
first = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice={'type':'function','function':{'name':'get_temperature'}}, max_tokens=1024)
print('X-Request-Id:', first._request_id)
message = first.choices[0].message
assert first.choices[0].finish_reason == 'tool_calls' and message.tool_calls
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
    assert call.function.name == 'get_temperature'
    args = json.loads(call.function.arguments)
    assert args == {'city':'Paris'}, args
    result = {'city':args['city'], 'temperature_c':22}
    messages.append({'role':'tool','tool_call_id':call.id,'content':json.dumps(result)})
second = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice='none', max_tokens=1024)
print('X-Request-Id:', second._request_id)
answer = second.choices[0].message.content or ''
assert second.choices[0].finish_reason == 'stop' and '22' in answer, answer
print(answer)
```

期望：第一轮 `finish_reason=tool_calls`，函数参数是 `{"city":"Paris"}`；第二轮回答包含 `22`，`finish_reason=stop`；退出码为 0。

保留第一轮完整 assistant 消息，为每个工具调用返回一条 `role="tool"` 消息。不要重造调用 ID，也不要只把结果作为普通用户文字发回。参数是 JSON 字符串，需要解析和校验；不要直接执行模型提供的任意函数名。这里用 `tool_choice="none"` 明确结束工具阶段；使用 `auto` 的实际应用应允许多轮工具调用，并设置轮数上限。

## 3. 用 TypeScript 执行同一流程 [#3-用-typescript-执行同一流程]

保存为 `tool-calling.ts`，运行 `bun tool-calling.ts`；使用内置 `fetch`，无需安装 SDK。

```typescript
import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
async function chat(body: object) {
  const response = await fetch(`${base}/openai/v1/chat/completions`, {
    method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
    body: JSON.stringify({model: 'deepseek-flash', max_tokens: 1024, reasoning_effort: 'none', ...body}),
  });
  console.log('X-Request-Id:', response.headers.get('x-request-id'));
  const data = await response.json();
  assert(response.ok, JSON.stringify(data));
  return data;
}
const tools = [{type: 'function', function: {name: 'get_temperature',
  description: 'Read the demo temperature for Paris.',
  parameters: {type: 'object', properties: {city: {type: 'string', enum: ['Paris']}},
    required: ['city'], additionalProperties: false}}}];
const messages: any[] = [{role: 'user', content: 'Use get_temperature for Paris, then tell me the temperature in Celsius.'}];
const first = await chat({messages, tools, tool_choice: {type: 'function', function: {name: 'get_temperature'}}});
const choice = first.choices[0];
assert.equal(choice.finish_reason, 'tool_calls');
assert(choice.message.tool_calls?.length);
messages.push(choice.message);
for (const call of choice.message.tool_calls) {
  assert.equal(call.function.name, 'get_temperature');
  const args = JSON.parse(call.function.arguments);
  assert.deepEqual(args, {city: 'Paris'});
  messages.push({role: 'tool', tool_call_id: call.id,
    content: JSON.stringify({city: args.city, temperature_c: 22})});
}
const second = await chat({messages, tools, tool_choice: 'none'});
assert.equal(second.choices[0].finish_reason, 'stop');
assert(second.choices[0].message.content.includes('22'));
console.log(second.choices[0].message.content);
```

期望：完成相同的工具调用与回填，最终回答包含 `22`，退出码为 0。

## 4. 使用 Anthropic Messages [#4-使用-anthropic-messages]

Messages 用 `input_schema` 定义参数，返回 `tool_use` 内容块和 `stop_reason="tool_use"`。将完整 assistant 内容带回后，在下一条 `role="user"` 消息中放入 `tool_result`，用 `tool_use_id` 对应原调用。本例的 `claude-opus-5` 已实测通过。

保存为 `tool-calling-anthropic.py`，运行 `uv run -q --no-project --with openai --with anthropic python tool-calling-anthropic.py`。

```python
import json
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
tools = [{'name':'get_temperature','description':'Read the demo temperature for Paris.',
    'input_schema':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'tool','name':'get_temperature'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
first = raw.parse()
assert first.stop_reason == 'tool_use'
messages.append({'role':'assistant','content':[block.model_dump(exclude_none=True) for block in first.content]})
results = []
for block in first.content:
    if block.type == 'tool_use':
        assert block.name == 'get_temperature' and block.input == {'city':'Paris'}
        results.append({'type':'tool_result','tool_use_id':block.id,
            'content':json.dumps({'city':'Paris','temperature_c':22})})
assert results
messages.append({'role':'user','content':results})
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'none'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
second = raw.parse()
answer = ''.join(block.text for block in second.content if block.type == 'text')
assert second.stop_reason == 'end_turn' and '22' in answer, answer
print(answer)
```

期望：第一轮请求工具，第二轮返回包含 `22` 的文本，`stop_reason=end_turn`，退出码为 0。

## 实测失败与处理 [#实测失败与处理]

强制指定函数、但没有关闭思考时，`deepseek-flash` 返回 HTTP 400：

> tool\_choice a named function is not supported while thinking is enabled on this model. Set reasoning effort to "none" or use tool\_choice "auto".

本页采用已验证的解决办法：显式设置 `reasoning_effort="none"`。仅第一轮设置、第二轮省略时，还曾收到 HTTP 400：

> The selected thinking setting is not supported on this model route.

两轮都保留 `reasoning_effort="none"` 后通过。不要在重试时丢掉该参数。

协议细节：[OpenAI](https://developers.openai.com/api/docs/guides/function-calling) · [Anthropic](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 结构化输出

结构化输出将回答约束为指定的 JSON 结构；接收端仍需检查终态、解析 JSON 并验证业务字段。

## 选择支持的模型 [#选择支持的模型]

先查看 `GET /openai/v1/models` 中的 `supported_parameters`：候选模型应列有 `response_format` 和 `structured_outputs`。2026-10-12，`gpt-5.6-sol` 的 Chat Completions `json_schema` + `strict` 和 `claude-opus-5` 的 Messages `output_config` 均实测通过，下面分别使用它们。

`deepseek-flash` 虽列有上述参数，本次 `json_schema` 请求仍返回 400，因此本页不使用它演示 schema 约束。目录声明不能替代实际响应验证。`json_object`（JSON mode）只表达 JSON 格式要求，不等于 schema 约束；需要固定字段和类型时使用本页的 `json_schema`。

## 1. 准备终端 [#1-准备终端]

将 Praxis 云 API Key 放入环境变量 `PRAXIS_API_KEY`；获取方式见 [Key 与鉴权](/docs/developers/account/api-keys)。不要把 Key 写入代码。准备 Python 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)；TypeScript 示例使用 [Bun](https://bun.sh/docs/installation)。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

将每段 Python 保存为标明的文件名，以 `uv run -q --no-project --with openai --with anthropic python <文件名>` 执行。各段代码独立运行。

## 2. 使用 json\_schema 和 strict [#2-使用-json_schema-和-strict]

保存为 `structured-output.py`，运行 `uv run -q --no-project --with openai --with anthropic python structured-output.py`。

`response_format.json_schema` 中同时给出 `name`、`strict: true` 和 `schema`。本例将所有属性列入 `required`，并用 `additionalProperties: false` 禁止额外字段。

```python
import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
schema = {'type':'object', 'properties':{
    'city':{'type':'string'}, 'temperature':{'type':'integer'}},
    'required':['city','temperature'], 'additionalProperties':False}
response = client.chat.completions.create(model='gpt-5.6-sol', max_tokens=1024,
    messages=[{'role':'user','content':'Return JSON for Paris with temperature 22.'}],
    response_format={'type':'json_schema','json_schema':{
        'name':'weather','strict':True,'schema':schema}})
print('X-Request-Id:', response._request_id)
choice = response.choices[0]
assert choice.finish_reason == 'stop' and not choice.message.refusal
value = json.loads(choice.message.content)
assert set(value) == {'city','temperature'}
assert isinstance(value['city'], str) and type(value['temperature']) is int
assert value == {'city':'Paris','temperature':22}, value
print(value)
```

期望：输出 `{'city': 'Paris', 'temperature': 22}`，退出码为 0。代码不剥除 Markdown 围栏；只有直接可解析、字段和类型符合要求的 JSON 才通过。

先检查 `finish_reason` 和 `refusal`，再解析内容。被截断或拒绝的回答不能当成符合 schema 的业务结果；流式回答应先收齐文本再解析。

## 3. 用 TypeScript 验证相同结果 [#3-用-typescript-验证相同结果]

保存为 `structured-output.ts`，运行 `bun structured-output.ts`；无需安装 SDK。

```typescript
import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
const schema = {type: 'object', properties: {
  city: {type: 'string'}, temperature: {type: 'integer'}},
  required: ['city', 'temperature'], additionalProperties: false};
const response = await fetch(`${base}/openai/v1/chat/completions`, {
  method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
  body: JSON.stringify({model: 'gpt-5.6-sol', max_tokens: 1024,
    messages: [{role: 'user', content: 'Return JSON for Paris with temperature 22.'}],
    response_format: {type: 'json_schema', json_schema: {name: 'weather', strict: true, schema}}}),
});
console.log('X-Request-Id:', response.headers.get('x-request-id'));
const data = await response.json();
assert(response.ok, JSON.stringify(data));
const choice = data.choices[0];
assert.equal(choice.finish_reason, 'stop');
assert(!choice.message.refusal);
const value = JSON.parse(choice.message.content);
assert.deepEqual(value, {city: 'Paris', temperature: 22});
console.log(value);
```

期望：同样得到 `city=Paris`、整数 `temperature=22`，没有额外字段，退出码为 0。

## 4. 使用 Messages 的 output\_config [#4-使用-messages-的-output_config]

Messages 将 schema 放在 `output_config.format` 下，`type` 为 `json_schema`；不要把 Chat Completions 的 `response_format` 或 `json_schema.name` 外壳搬过来。这里也不添加 OpenAI 的 `strict` 字段。

保存为 `structured-output-anthropic.py`，运行 `uv run -q --no-project --with openai --with anthropic python structured-output-anthropic.py`。

```python
import json
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
schema = {'type':'object', 'properties':{
    'city':{'type':'string'}, 'temperature':{'type':'integer'}},
    'required':['city','temperature'], 'additionalProperties':False}
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    messages=[{'role':'user','content':'Return JSON for Paris with temperature 22.'}],
    output_config={'format':{'type':'json_schema','schema':schema}})
print('X-Request-Id:', raw.headers.get('x-request-id'))
message = raw.parse()
assert message.stop_reason == 'end_turn'
value = json.loads(''.join(block.text for block in message.content if block.type == 'text'))
assert set(value) == {'city','temperature'}
assert isinstance(value['city'], str) and type(value['temperature']) is int
assert value == {'city':'Paris','temperature':22}, value
print(value)
```

期望：`stop_reason=end_turn`，JSON 验证通过并输出相同对象，退出码为 0。

## 实测失败与处理 [#实测失败与处理]

`deepseek-flash` 的 schema 请求返回 HTTP 400，错误消息的固定部分为：

> This response\_format type is unavailable now

消息还会附带当次请求标识。切换为本页已实测通过的 `gpt-5.6-sol` 后通过。不要将 HTTP 200、Markdown 中的一段 JSON 或能解析的普通 JSON 当成 schema 约束已生效；保留示例中的完整验证。

协议细节：[OpenAI](https://developers.openai.com/api/docs/guides/structured-outputs) · [Anthropic](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 调整思考强度

同一个“低”档，在 Chat、Messages 和 Google 中使用不同字段。先选模型支持的协议与档位，再查看响应中的思考用量；档位是提示，实际消耗以 usage 为准。

## 选择协议与档位 [#选择协议与档位]

打开[控制台 → 模型目录](https://console.deepraxis.ai/models)，点进模型详情的「协议与思考档位」。期望：看到该模型支持的调用方式和思考选项；没有“关闭”时，不要发送关闭思考的设置。

用 `GET /model/v1/models` 的 `supported_apis` 确认协议；Chat 目录 `GET /openai/v1/models?api=chat` 的 `reasoning.supported_efforts` 和 `reasoning.mandatory` 可用于程序检查。2026-10-13 的 Chat 目录将 `glm-5.3`、`glm-5.3-flash` 和 `kimi-k3` 标为 `mandatory=true`，即不能关闭思考；调用前仍需重查目录。不要假设每个模型都有全部档位。下列用量字段是本页所选模型实测返回的字段，不保证所有模型都提供明细。

| 调用方式                   | 设置思考的入口                                                                 | 本页读取的用量                                            |
| ---------------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |
| Chat Completions       | `reasoning_effort`；也接受 `reasoning` 对象                                   | `usage.completion_tokens_details.reasoning_tokens` |
| Anthropic Messages     | `thinking` 控制模式，`output_config.effort` 选择档位                             | `usage.output_tokens_details.thinking_tokens`      |
| Google generateContent | `generationConfig.thinkingConfig` 中的 `thinkingBudget` 或 `thinkingLevel` | `usageMetadata.thoughtsTokenCount`                 |

Chat 的档位范围从 `none` 到 `max`，实际可选项以模型为准；`none` 表示关闭，不是“低”。Messages 的 `enabled`、`adaptive`、`disabled` 分别表达预算思考、自适应思考和关闭，不能把 `thinking.effort` 当作档位字段。Google 的预算和档位随模型而异，不要同时发送两种控制。

完整字段定义见 [Chat](/docs/api/compatible/openai/createChatCompletion)、[Messages](/docs/api/compatible/anthropic/createMessage)、[Google](/docs/api/compatible/google/generateContent)。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/developers/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. Chat：设为低并读取用量 [#2-chat设为低并读取用量]

`deepseek-flash` 支持 Chat 和 `low`。

保存为 `reasoning-chat.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-chat.py`。

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
response = client.chat.completions.create(
    model='deepseek-flash', reasoning_effort='low', max_tokens=2048,
    messages=[{'role': 'user', 'content': 'What is 17 times 23? Answer briefly.'}])
assert response.choices[0].finish_reason == 'stop'
assert '391' in (response.choices[0].message.content or '')
details = response.usage.completion_tokens_details
assert details is not None and details.reasoning_tokens is not None, 'Reasoning usage was not reported'
print('X-Request-Id:', response._request_id)
print('answer:', response.choices[0].message.content)
print('reasoning_tokens:', details.reasoning_tokens)
```

期望：回答包含 `391`，终态为 `stop`，打印实际 `reasoning_tokens`，退出码 0。数字会随调用变化。

## 3. Messages：设为低并读取用量 [#3-messages设为低并读取用量]

这里选用 `claude-opus-5`，因为本次验证中它会返回思考 token 明细。`deepseek-flash` 的 Messages 响应未返回该明细；缺失不等于 0，也不能用 `output_tokens` 代替。

保存为 `reasoning-messages.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-messages.py`。

```python
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
raw = client.messages.with_raw_response.create(
    model='claude-opus-5', max_tokens=2048,
    thinking={'type': 'adaptive'}, output_config={'effort': 'low'},
    messages=[{'role': 'user', 'content': 'What is 17 times 23? Answer briefly.'}])
response = raw.parse()
answer = ''.join(block.text for block in response.content if block.type == 'text')
assert response.stop_reason == 'end_turn' and '391' in answer
usage = response.usage.model_dump()
details = usage.get('output_tokens_details') or {}
assert details.get('thinking_tokens') is not None, 'Thinking usage was not reported'
print('X-Request-Id:', raw.headers.get('x-request-id'))
print('answer:', answer)
print('thinking_tokens:', details['thinking_tokens'])
```

期望：回答包含 `391`，终态为 `end_turn`，打印实际 `thinking_tokens`，退出码 0。

## 4. Google：按模型选预算或档位 [#4-google按模型选预算或档位]

以下依次调用两个模型：`gemini-2.5-flash` 使用预算，`gemini-3.1-flash-lite-preview` 使用低档。使用 HTTP 是为了直接展示 Google 的字段名。

保存为 `reasoning-google.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-google.py`。

```python
import json
import os
import httpx

base = os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com')
with httpx.Client(timeout=120) as client:
    for model, thinking in [
        ('gemini-2.5-flash', {'thinkingBudget': 1024}),
        ('gemini-3.1-flash-lite-preview', {'thinkingLevel': 'low'}),
    ]:
        response = client.post(base + '/google/v1beta/models/' + model + ':generateContent',
            headers={'Authorization': 'Bearer ' + os.environ['PRAXIS_API_KEY']},
            json={'contents': [{'role': 'user', 'parts': [{'text': 'What is 17 times 23? Answer briefly.'}]}],
                  'generationConfig': {'maxOutputTokens': 2048, 'thinkingConfig': thinking}})
        print('X-Request-Id:', response.headers.get('x-request-id'))
        response.raise_for_status()
        data = response.json()
        candidate = data['candidates'][0]
        answer = ''.join(part.get('text', '') for part in candidate['content']['parts'])
        assert candidate['finishReason'] == 'STOP' and '391' in answer
        assert 'thoughtsTokenCount' in data['usageMetadata'], 'Thinking usage was not reported'
        print(model, answer, json.dumps(data['usageMetadata']))
```

期望：两次回答都包含 `391`，终态为 `STOP`，各自打印 `thoughtsTokenCount`，退出码 0。

## 如何比较结果 [#如何比较结果]

保持模型和问题一致，多次采样，比较实际思考 token、完整回答、耗时与质量。预算不是跨模型统一的硬上限；“低”不保证每次都比“高”少用 token，也不保证更快。不要根据一条响应推断单调关系。用量字段缺失时，保留“未报告”，不要填 0。

## 实测失败与处理 [#实测失败与处理]

`glm-5.3` 设置 `reasoning_effort="none"` 返回 HTTP 400，错误码 `thinking_disable_unsupported`：

> glm-5.3 cannot disable thinking; omit the thinking setting or choose a level.

省略关闭设置或选择模型支持的档位，再请求。不要靠换成另一个协议的“关闭”字段绕过限制。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# Responses API

只有声明 `openai.responses` 的模型才能使用 Responses。支持 Chat Completions 不代表支持 Responses。用 `GET /openai/v1/models?api=responses` 查询；2026-10-13 返回 `deepseek-flash` 和 `glm-5.3-flash`。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/developers/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. 接着上一条响应提问 [#2-接着上一条响应提问]

平台会展开 `previous_response_id` 引用的历史。第二次调用只发送新问题和第一条响应 ID。两次使用相同 Key 和模型；示例检查第二条回答能取回未出现在新问题中的代码。

保存为 `responses.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python responses.py`。

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
models = client.models.list(extra_query={'api': 'responses'})
print('X-Request-Id:', models._request_id)
assert 'deepseek-flash' in [model.id for model in models]
first = client.responses.create(model='deepseek-flash',
    input='Remember this code: LIME-742. Reply OK.',
    reasoning={'effort': 'none'}, max_output_tokens=512)
print('X-Request-Id:', first._request_id)
assert first.status == 'completed' and first.id
second = client.responses.create(model='deepseek-flash',
    previous_response_id=first.id, input='What was the code? Reply with only the code.',
    reasoning={'effort': 'none'}, max_output_tokens=512)
print('X-Request-Id:', second._request_id)
assert second.status == 'completed' and 'LIME-742' in second.output_text
assert second.usage is not None
print('answer:', second.output_text)
print('usage:', second.usage.model_dump())
```

期望：两条响应均为 `completed`，第二条回答包含 `LIME-742` 并打印用量，退出码 0。复用历史不意味着输入免费，应逐轮查看 usage。

## 限制与实测失败 [#限制与实测失败]

`POST /openai/v1/responses/compact` 与 `POST /openai/v1/responses/input_tokens` 当前返回 HTTP 501，错误码 `not_implemented`。不要依赖它们压缩对话或预先计数；由应用控制历史长度，完成后读取实际用量。

用 `gpt-5.6-sol` 请求实测返回 HTTP 404，错误码 `model_protocol_not_offered`。改选 Responses 筛选目录中的模型。若示例目录断言失败，先重查该目录再重试。其他网关的响应 ID 不随地址迁移，应重建必要的历史。

参数见[生成响应](/docs/api/compatible/openai/createResponse)，各操作支持状态见 [OpenAI API 参考](/docs/api/compatible/openai)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 读取图片、PDF 和文本文件

先确认模型同时支持所选协议和输入类型。查看 `GET /model/v1/models` 的 `supported_apis`、`architecture.input_modalities` 和 `x_praxis.input_modalities`：`native` 表示原生输入，`converted` 表示经过转换，`unsupported` 表示不可用。支持图片不代表支持音频或视频。

本例用 `deepseek-flash` 读取内联图片，用 `qwen3.8-flash` 读取 PDF 和文本的 `file_id` 引用；后者声明原生 PDF 输入。文件上传和生命周期细节见[文件服务指南](/docs/developers/file)。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/developers/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. 发送已知内容并校验回答 [#2-发送已知内容并校验回答]

脚本在本地构造红色 PNG、单页 PDF 和文本文件。先校验图片颜色，再以 `purpose=user_data` 上传两个文档并询问校验码。每个上传文件都在 `finally` 中删除，包括回答检查失败的情况，无需自备文件。

保存为 `multimodal.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python multimodal.py`。

```python
import base64
import os
import struct
import zlib
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')

# Build a 128 x 128 red PNG using only the Python standard library.
def chunk(kind, data):
    return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data))
image = (b'\x89PNG\r\n\x1a\n'
    + chunk(b'IHDR', struct.pack('>IIBBBBB', 128, 128, 8, 2, 0, 0, 0))
    + chunk(b'IDAT', zlib.compress((b'\x00' + b'\xff\x00\x00' * 128) * 128))
    + chunk(b'IEND', b''))
response = client.chat.completions.create(model='deepseek-flash', reasoning_effort='none', max_tokens=256,
    messages=[{'role': 'user', 'content': [
        {'type': 'text', 'text': 'Name the main color in this image in English.'},
        {'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,' + base64.b64encode(image).decode()}},
    ]}])
print('X-Request-Id:', response._request_id)
answer = response.choices[0].message.content or ''
assert response.choices[0].finish_reason == 'stop' and 'red' in answer.lower(), answer
print('image:', answer)

# Build a one-page PDF with selectable text.
text = b'The verification code is LIME-742.'
stream = b'BT /F1 18 Tf 50 100 Td (' + text + b') Tj ET'
objects = [b'<< /Type /Catalog /Pages 2 0 R >>',
    b'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
    b'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
    b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
    b'<< /Length ' + str(len(stream)).encode() + b' >>\nstream\n' + stream + b'\nendstream']
pdf, offsets = b'%PDF-1.4\n', [0]
for index, obj in enumerate(objects, 1):
    offsets.append(len(pdf))
    pdf += f'{index} 0 obj\n'.encode() + obj + b'\nendobj\n'
xref = len(pdf)
pdf += b'xref\n0 6\n0000000000 65535 f \n'
pdf += b''.join(f'{offset:010d} 00000 n \n'.encode() for offset in offsets[1:])
pdf += f'trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n'.encode()

for name, data, mime in [('code.pdf', pdf, 'application/pdf'), ('code.txt', text, 'text/plain')]:
    file = client.files.create(file=(name, data, mime), purpose='user_data')
    print('upload X-Request-Id:', file._request_id)
    try:
        response = client.chat.completions.create(model='qwen3.8-flash', reasoning_effort='none', max_tokens=256,
            messages=[{'role': 'user', 'content': [
                {'type': 'text', 'text': 'What is the verification code in the file? Reply with only the code.'},
                {'type': 'file', 'file': {'file_id': file.id}},
            ]}])
        print('chat X-Request-Id:', response._request_id)
        answer = response.choices[0].message.content or ''
        assert response.choices[0].finish_reason == 'stop' and 'LIME-742' in answer, answer
        print(name, answer)
    finally:
        deleted = client.files.delete(file.id)
        print('delete X-Request-Id:', deleted._request_id)
        assert deleted.deleted
```

期望：图片回答包含 `red`，`code.pdf` 和 `code.txt` 都返回 `LIME-742`，两个删除操作成功，退出码 0。仅 HTTP 成功不够，内容断言也必须通过。

## 实测失败与处理 [#实测失败与处理]

同一 PDF 使用 `deepseek-flash` 时，HTTP 200，但回答说：

> I wasn't able to read the attached PDF

该模型目录标注 PDF 为转换输入，这次未能证明成功读取。将文档请求切到原生 PDF 输入的 `qwen3.8-flash` 后，内容校验通过。遇到无法读取时，选已验证模型或直接提供相关文本；不要仅凭回答看似合理就认为文件已读到。请求结束前保持文件可用。

本页验证的 `file_id` 路径接受 PDF 和文本，不限于图片；这不代表所有模型支持所有文件类型。参数见 [Chat Completions](/docs/api/compatible/openai/createChatCompletion)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 生成与编辑图像

使用 OpenAI Images API 的 `gpt-image-2` 生成和编辑图片。调用前查 `GET /openai/v1/models?api=images`；原生模型目录分别声明 `openai.images.generations` 和 `openai.images.edits`。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/developers/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. 生成并编辑 [#2-生成并编辑]

在可写目录运行。示例会产生两次图像调用费用：生成一次、编辑一次，并保存 `circle.png` 与 `circle-edited.png`；使用新目录避免覆盖同名文件。示例关闭自动重试。

保存为 `images.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python images.py`。

```python
import base64
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1',
    timeout=180, max_retries=0)
models = client.models.list(extra_query={'api': 'images'})
print('X-Request-Id:', models._request_id)
assert 'gpt-image-2' in [model.id for model in models]

def save(response, filename):
    print('X-Request-Id:', response._request_id)
    assert len(response.data) == 1 and response.data[0].b64_json
    image = base64.b64decode(response.data[0].b64_json, validate=True)
    assert image.startswith(b'\x89PNG\r\n\x1a\n'), 'Expected PNG output'
    Path(filename).write_bytes(image)
    assert response.usage is not None, 'Usage was not reported'
    print(filename, len(image), 'bytes; usage:', response.usage.model_dump())

save(client.images.generate(model='gpt-image-2',
    prompt='A single blue circle centered on a plain white background.',
    n=1, size='1024x1024', quality='low'), 'circle.png')
with open('circle.png', 'rb') as image:
    save(client.images.edit(model='gpt-image-2', image=image,
        prompt='Change the blue circle to a red circle. Keep the white background.',
        n=1, size='1024x1024', quality='low'), 'circle-edited.png')
```

期望：两个文件都是非空 PNG，每次响应包含一张图片和 token 用量，退出码 0。打开图片：第一张应为白底蓝圆，第二张应为白底红圆。文件签名检查验证格式，不代替画面检查。

## 3. 查看图像费用 [#3-查看图像费用]

`gpt-image-2` 按 token 计价，不是每张固定价。文本输入、图像输入、图像输出分别计价；编辑还会产生输入图像 token。查看响应中的用量，以及[模型目录](https://console.deepraxis.ai/models/gpt-image-2)中的实时价格；账户汇总见[用量与费用](/docs/developers/account/billing)。

若目录断言失败，先停止生成，选择当前可用的图像模型。若输出断言失败，保留请求 ID 和响应错误，不要为排查文件问题反复生成。

操作细节：[生成](/docs/api/compatible/openai/createImage)与[编辑](/docs/api/compatible/openai)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 在对话中引用文件

上传、过期、下载与删除见[文件服务](/docs/developers/file)。

对话中可用 `file_id` 引用以 `purpose=user_data` 上传的图片、文本或 PDF，通过消息中的 `{"type":"file","file":{"file_id":"上传返回的 ID"}}` 传入。文件被接受不代表所选模型能读出内容；用途为 `vision` 或 `assistants` 的文件不作为这里的图片输入。

2026-10-13 在 Develop 复核：`qwen3.8-flash` 能从文本和 PDF 中读出指定短语。`deepseek-flash` 引用同一 PDF 时返回 HTTP 200，但回答明确表示转换失败（`provider_rejected`）、未读取文件。遇到这种情况，换用已验证可读的模型，或把提取出的文本直接放入消息；不要把 HTTP 200 当成读取成功。

图片输入仍需选择支持图片的模型；下例使用 `qwen3-vl-plus`。

安装 `openai`，设置 `PRAXIS_API_KEY`。使用 Develop 时，另设 `PRAXIS_API_BASE=https://api-develop.deepraxis.ai`。示例上传一张小 PNG，获取描述后删除文件。

```python
import base64
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
)
image = base64.b64decode(
    "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABWklEQVR4nO3OQQ0AMBAEofVv+iqDxzRBALvtg/wgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwg7gEgaMOyrMtNTwAAAABJRU5ErkJggg=="
)
file = client.files.create(file=("pixel.png", image, "image/png"), purpose="user_data")
try:
    response = client.chat.completions.create(
        model="qwen3-vl-plus",
        max_tokens=80,
        messages=[{"role": "user", "content": [
            {"type": "text", "text": "Describe this image in one sentence."},
            {"type": "file", "file": {"file_id": file.id}},
        ]}],
    )
    print(response.choices[0].message.content)
finally:
    client.files.delete(file.id)
```

期望：输出一句图片描述；请求结束后文件被删除。

# 插件

插件在模型调用前后处理请求或响应，让模型可以使用经过转换的输入。它们不会改变模型本身的原生能力。`file-parser` 是第一个插件：在需要时，把文档或图片转换为文字，再交给你选择的模型。

同一套 `plugins` 参数适用于 `/model/v1/chat/completions`、`/model/v1/messages` 和 `/model/v1/responses` 的创建请求，支持流式和非流式。参数由平台消费，不会作为模型参数传给上游。

## 配置与默认行为 [#配置与默认行为]

在请求体中加入 `plugins` 数组，每个配置用 `id` 指定插件：

```json
{
  "plugins": [
    { "id": "file-parser", "enabled": true }
  ]
}
```

未传 `plugins` 或传 `plugins: []` 都表示沿用默认配置，**不表示关闭**。当前文件解析默认开启；模型能原生处理的 PDF 和图片，在符合模型限制时直接传递。否则，文字 PDF 和 Office 文档在本地提取文字，扫描页使用 OCR，图片使用 OCR 和视觉描述。提取出的文字占用主模型的上下文窗口。

关闭转换使用：

```json
{
  "plugins": [
    { "id": "file-parser", "enabled": false }
  ]
}
```

关闭后，原生支持的输入仍可使用；模型不支持的输入会被拒绝。配置会检查插件 ID、字段和取值，未知配置不会被静默忽略。

### PDF 处理方式 [#pdf-处理方式]

| `pdf.engine` | 行为                                               |
| ------------ | ------------------------------------------------ |
| `auto`（默认模式） | 优先原生处理；需要转换时提取文字，并对扫描页或文字不足的页使用 OCR。             |
| `native`     | 只允许原生 PDF 输入。模型不支持或超过原生限制时拒绝，不自动转为文字。            |
| `ocr`        | 强制走文字转换，即使模型支持原生 PDF；有可用文字的页仍本地提取，需要识别的页才调用 OCR。 |

请求中**省略 `pdf.engine` 会继承默认配置**；没有租户或平台覆盖时即为自动模式。当前请求参数只接受显式的 `native` 或 `ocr`，不能用字符串 `"auto"` 覆盖已有默认。可在控制台将租户 PDF 默认方式设为自动。

```json
{
  "plugins": [
    { "id": "file-parser", "pdf": { "engine": "ocr" } }
  ]
}
```

`pdf.engine` 只影响 PDF。可用 `"image": { "describe": false }` 关闭非原生图片转换；它不关闭原生视觉输入，也不影响文档。

### 配置优先级 [#配置优先级]

从高到低：**平台策略 > 租户锁定 > 本次请求 > 租户默认 > 插件默认**。

平台策略决定的值不可被覆盖。租户锁定后，请求尝试改变锁定值会返回 `PLUGIN_SETTING_LOCKED`。没有锁定的字段可以在本次请求中覆盖，且不会修改租户默认配置。文件大小、页数等平台限制不可通过请求调整。

## 费用 [#费用]

* 本地文字提取不产生额外模型用量；转换后的文字仍按主模型输入计费。
* OCR 按实际处理页数计量，图片描述按描述模型的输入、缓存输入和输出用量计量；单价以所用模型的价格为准。
* OCR、图片描述和主模型是各自的用量记录。主模型失败不会抹掉已经发生的转换用量。
* 命中解析缓存不发起新的转换子调用，因此不新增这部分用量；下一轮主模型仍按实际输入和输出计费。

## 解析注明与下一轮带回 [#解析注明与下一轮带回]

转换成功的响应会携带文件注明（annotations）。保留完整对象，不要修改哈希或解析内容：

```json
{
  "type": "file",
  "file": {
    "hash": "<原样保留服务返回的哈希>",
    "name": "notes.pdf",
    "content": [{ "type": "text", "text": "解析出的文字" }]
  }
}
```

| 入口                 | 非流式位置                                                          | 流式位置                                                                   |
| ------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Chat Completions   | `choices[].message.annotations`                                | `choices[].delta.annotations`，每个 choice 返回一次                           |
| Anthropic Messages | 响应消息顶层 `annotations`                                           | `message_start.message.annotations`                                    |
| Responses          | `output[]` 中 message 的 `content[]` 内 `output_text.annotations` | `response.content_part.done`、`response.output_item.done` 和响应终态中的对应文字快照 |

已有的引用注明及其下标会保留；Responses 的未完成快照不插入文件注明，完成快照才附加。Messages 的顶层字段和 Responses 中的 `type: "file"` 是平台扩展；不要把文件注明变成 Anthropic content block。只有实际转换才产生这些注明，原生直传不产生。Responses 若只返回工具调用、拒绝块或空输出，不会为携带注明而合成文字消息。

下一轮保留原始用户输入和上一轮 assistant 内容，把注明一同带回：

```js
// Chat Completions
messages.push(response.choices[0].message);

// Anthropic Messages：保留顶层扩展字段
messages.push({
  role: "assistant",
  content: response.content,
  annotations: response.annotations,
});

// Responses：保留 output_text 的 annotations
input.push(...response.output);
```

然后追加新的用户问题。流式客户端同样保留聚合结果中的注明；若自行组装消息，需从上述事件位置收集它们。SDK 类型未声明扩展字段时，使用其额外字段机制或读取原始 JSON。

带回内容与服务端解析结果、原文件、配置及模型能力匹配时，会跳过重新解析。修改内容、切换配置或缓存不可用时，不保证复用；客户端注明不能自行证明“已解析”。即使不再携带原文件，注明中的文字也会作为普通对话上下文送给模型。单独传 `previous_response_id` 不等于显式带回文件注明。

## 查看模型支持方式 [#查看模型支持方式]

`GET /model/v1/models` 的 `architecture.input_modalities` 使用 OpenRouter 形状，只列原生模态。原生 PDF 能力表示为 `file`，不会因启用插件而加入这一数组。

扩展字段 `x_praxis.input_modalities` 按输入类型标注 `native`、`converted` 或 `unsupported`：

```json
{
  "architecture": { "input_modalities": ["text"] },
  "x_praxis": {
    "input_modalities": {
      "text": "native",
      "image": "converted",
      "pdf": "converted",
      "docx": "converted",
      "pptx": "converted",
      "xlsx": "converted",
      "audio": "unsupported",
      "video": "unsupported"
    }
  }
}
```

这是一个仅支持原生文字的对话模型的示例。标注由模型能力和已注册插件推导；`converted` 表示存在转换路径，实际请求还受租户设置、请求配置和限额约束。嵌入、OCR 等非对话调用不因此获得对话插件。音频、视频目前没有转换插件，只有模型原生支持时才可输入。

## 限制 [#限制]

* 需要转换的文件必须提供内联内容，或使用当前入口支持的已授权文件引用；插件不下载任意外部 URL。
* 当前支持 PDF、DOCX、PPTX、XLSX 和图片转换。Office 提取正文、幻灯片文字及单元格值，不解析嵌入对象或其中的图片。
* 单文件最多 10 MiB，每请求累计最多 20 MiB；PDF 解析最多 100 页，付费 OCR 最多 20 页，每批最多 10 页；每请求最多 4 个付费子调用、同时最多 2 个。
* 转换总时限为 90 秒；OCR 子调用最多 45 秒，图片描述最多 30 秒。流式转换期间可能先收到 `: PRAXIS PROCESSING` 保活注释，它不是模型输出。
* 提取文字最多 1,000,000 字符，表格每 sheet 最多 1,000 行。模型自身的上下文和输入限制仍然适用。
* Responses 仅在创建时转换新提交的输入；retrieve、cancel 等操作不重新转换。需要转换的 `background: true` 请求返回 `PLUGIN_BACKGROUND_UNSUPPORTED`。
* 并发请求同时未命中缓存时，可能各自发起付费转换。主模型失败时，当前接口不保证返回已解析的文件注明。

# 会话亲和

同一对话的请求复用 `x-session-id`，新对话使用新 ID。会话亲和帮助符合条件的请求复用供应商，但不保证固定供应商或缓存命中。

会话 ID 不会替你保存 Chat Completions 的历史。每轮仍需发送必要消息；如需按响应 ID 接续，使用 [Responses](/docs/developers/model/guides/responses-api)。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/developers/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. 两轮使用同一个 ID [#2-两轮使用同一个-id]

示例生成一个 UUID，两轮都发送该请求头，并在第二轮带上第一轮的消息。

保存为 `session-affinity.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python session-affinity.py`。

```python
import os
import uuid
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
session_id = str(uuid.uuid4())
messages = [{'role': 'user', 'content': 'Remember this code: LIME-742. Reply OK.'}]
for turn in range(2):
    response = client.chat.completions.create(model='deepseek-flash',
        messages=messages, reasoning_effort='none', max_tokens=256,
        extra_headers={'x-session-id': session_id})
    print('X-Request-Id:', response._request_id)
    assert response.choices[0].finish_reason == 'stop'
    answer = response.choices[0].message.content or ''
    assert answer
    print('turn', turn + 1, answer)
    if turn == 0:
        messages.append({'role': 'assistant', 'content': answer})
        messages.append({'role': 'user', 'content': 'What was the code? Reply with only the code.'})
    else:
        assert 'LIME-742' in answer, answer
```

期望：得到两条完成的响应、两个请求 ID，第二轮回答包含 `LIME-742`，退出码 0。这验证了请求和历史传递，不证明某个供应商或缓存命中；缓存用量另按 [Prompt Cache 指南](/docs/developers/model/guides/prompt-cache)检查。

## 实测失败与处理 [#实测失败与处理]

请求体传空 `session_id` 实测返回 HTTP 400：`session_id must not be empty`。使用非空会话 ID。请求体同时提供 `session_id` 时优先于请求头；避免在两个位置维护不同值，也不要让无关对话共用一个全局 ID。

字段细节：[Chat Completions](/docs/api/compatible/openai/createChatCompletion)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# Prompt Cache 与成本

Prompt Cache 复用重复输入的前缀计算，不复用上一条回答。是否命中要看返回的缓存 token 数，不能凭回答相同或速度更快判断。

本页的 OpenAI 示例使用 `deepseek-flash`，不添加缓存控制参数。2026-10-12 实测：26,016 个输入 token 的第二次请求报告 25,856 个缓存 token；这是一组观测值，不是每次调用的保证。

## 1. 准备终端 [#1-准备终端]

将 Praxis 云 API Key 放入环境变量 `PRAXIS_API_KEY`；获取方式见 [Key 与鉴权](/docs/developers/account/api-keys)。不要把 Key 写入代码。准备 Python 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)；TypeScript 示例使用 [Bun](https://bun.sh/docs/installation)。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。示例默认使用 `https://api.deepraxis.com`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

将每段 Python 保存为标明的文件名，以 `uv run -q --no-project --with openai --with anthropic python <文件名>` 执行。各段代码独立运行。

## 2. 连续请求相同前缀 [#2-连续请求相同前缀]

将不常变的指令和资料放在前面，当前问题放在后面，保持模型、前缀内容和顺序一致。下面生成较长的演示资料并连续请求两次，会产生两次模型调用费用。

保存为 `prompt-cache.py`，运行 `uv run -q --no-project --with openai --with anthropic python prompt-cache.py`。

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
prefix = '\n'.join(f'Record {i}: Paris is in France. The demo temperature is 22 Celsius.' for i in range(1500))
hits = []
for turn in range(2):
    response = client.chat.completions.create(model='deepseek-flash',
        messages=[{'role':'system','content':prefix},
            {'role':'user','content':'What country is Paris in? Reply with one word.'}],
        reasoning_effort='none', max_tokens=64)
    print('X-Request-Id:', response._request_id)
    assert response.choices[0].finish_reason == 'stop'
    usage = response.usage
    assert usage and usage.prompt_tokens_details is not None
    cached = usage.prompt_tokens_details.cached_tokens
    assert cached is not None
    hits.append(cached)
    print(turn + 1, 'prompt_tokens:', usage.prompt_tokens, 'cached_tokens:', cached)
assert hits[1] > 0, 'No cache hit observed on the second request'
```

期望：输出两次输入和缓存 token 数，第二次 `cached_tokens > 0`，退出码为 0。重复运行时第一次也可能命中，不要求第一次为零。

`usage.prompt_tokens_details.cached_tokens` 是输入中的缓存部分，已包含在 `prompt_tokens` 内，不能再相加。缓存只减少符合条件的输入费用，不使输出免费；成本按所选模型的缓存输入、普通输入和输出单价计算，不套用固定折扣。缓存最短前缀和保留时间由模型决定。

## 3. 用 TypeScript 检查命中 [#3-用-typescript-检查命中]

保存为 `prompt-cache.ts`，运行 `bun prompt-cache.ts`；无需安装 SDK。

```typescript
import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
const prefix = Array.from({length: 1500}, (_, i) =>
  `Record ${i}: Paris is in France. The demo temperature is 22 Celsius.`).join('\n');
const hits: number[] = [];
for (let turn = 0; turn < 2; turn++) {
  const response = await fetch(`${base}/openai/v1/chat/completions`, {
    method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
    body: JSON.stringify({model: 'deepseek-flash', max_tokens: 64, reasoning_effort: 'none',
      messages: [{role: 'system', content: prefix},
        {role: 'user', content: 'What country is Paris in? Reply with one word.'}]}),
  });
  console.log('X-Request-Id:', response.headers.get('x-request-id'));
  const data = await response.json();
  assert(response.ok, JSON.stringify(data));
  assert.equal(data.choices[0].finish_reason, 'stop');
  const cached = data.usage.prompt_tokens_details?.cached_tokens;
  assert.equal(typeof cached, 'number');
  hits.push(cached);
  console.log(turn + 1, 'prompt_tokens:', data.usage.prompt_tokens, 'cached_tokens:', cached);
}
assert(hits[1] > 0, 'No cache hit observed on the second request');
```

期望：第二次报告正数 `cached_tokens`，退出码为 0。

## 4. 观察 Anthropic cache\_control [#4-观察-anthropic-cache_control]

Messages 可在稳定文本块上设置 `cache_control: {type: "ephemeral"}`。本例使用 `claude-opus-5`，连续调用后分别读取 `cache_creation_input_tokens`（写入）和 `cache_read_input_tokens`（读取）。Messages 的 `input_tokens` 是未计入读写缓存部分的输入；总输入为这三项之和。

本次实测请求被接受，但两次读写缓存数均为零，因此此例用于观察缓存状态，不承诺命中。`GET /openai/v1/models` 的 `supported_parameters` 可查询参数声明，但不是每次缓存命中的证明；最终仍以用量字段为准。

保存为 `prompt-cache-anthropic.py`，运行 `uv run -q --no-project --with openai --with anthropic python prompt-cache-anthropic.py`。

```python
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
prefix = '\n'.join(f'Record {i}: Paris is in France. The demo temperature is 22 Celsius.' for i in range(1500))
hits = []
for turn in range(2):
    raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=64,
        system=[{'type':'text','text':prefix,'cache_control':{'type':'ephemeral'}}],
        messages=[{'role':'user','content':'What country is Paris in? Reply with one word.'}])
    print('X-Request-Id:', raw.headers.get('x-request-id'))
    message = raw.parse()
    assert message.stop_reason == 'end_turn'
    usage = message.usage
    assert usage.cache_read_input_tokens is not None
    assert usage.cache_creation_input_tokens is not None
    hits.append(usage.cache_read_input_tokens)
    print(turn + 1, 'input:', usage.input_tokens, 'write:', usage.cache_creation_input_tokens,
        'read:', usage.cache_read_input_tokens)
print('HIT' if hits[1] > 0 else 'MISS: no cached input reported')
```

期望：两次请求完成，输出读写统计和 `HIT` 或 `MISS: no cached input reported`，退出码为 0。只有读取 token 为正数才算观察到命中；退出码 0 只证明请求和统计读取成功。

## 实测未命中时怎么处理 [#实测未命中时怎么处理]

本次 Messages 示例的实际结果是 `cache_creation_input_tokens=0`、`cache_read_input_tokens=0`，没有 HTTP 报错。不要把请求被接受等同于缓存生效。需要复现已验证的缓存命中时，运行上面的 `deepseek-flash` OpenAI 示例，并检查正数 `cached_tokens`。

为保持前缀可复用，确认两次请求使用相同模型和完整前缀；不要在前缀中加入时间戳或每次变化的标识。保留用量和 `X-Request-Id` 以便核对，不能用相同回答代替命中证据。

协议细节：[OpenAI](https://developers.openai.com/api/docs/guides/prompt-caching) · [Anthropic](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# BYOK：接入你自己的模型密钥

BYOK 即将推出：控制台 → 模型 → BYOK 当前不可点击，标为「即将推出」，暂时无法在此导入自己的供应商密钥。当前可使用 [Praxis 云 API Key](/docs/developers/account/api-keys)，从[模型目录](https://console.deepraxis.ai/models)选择模型，按[模型快速开始](/docs/developers/model/quickstart)调用。不要把供应商的 Key 当作 `PRAXIS_API_KEY`。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)

# 错误与限流

## 错误码速查 [#错误码速查]

| HTTP | 含义        | 你该做什么                        |
| ---- | --------- | ---------------------------- |
| 400  | 请求参数有误    | 检查请求体格式、字段名、模型 ID            |
| 401  | 认证失败      | 检查 API Key 是否完整、是否已过期        |
| 403  | 权限不足或余额耗尽 | 确认 Key 有效；检查账户余额             |
| 404  | 路径或模型不存在  | 检查 API 地址后缀；模型 ID 是否在目录中     |
| 429  | 速率限制      | 按 `X-RateLimit-Reset` 头等待后重试 |
| 500  | 服务端错误     | 稍后重试                         |

## 常见 400 错误 [#常见-400-错误]

**STREAMING\_UNPROVEN**：该模型路由暂未开放流式。去掉 `stream: true` 或换模型。

**模型不存在**：到[控制台 → 模型](https://console.deepraxis.ai/models)复制确切 ID。

**缺少 max\_tokens**（Anthropic 协议）：加 `"max_tokens": 256`。

## 重试策略 [#重试策略]

**可重试**：429（等重置）、500（指数退避 1s→2s→4s 最多 3 次）、网络瞬断（直接重试）。

**不可重试**：400（修正请求）、401（修复 Key）、404（修正地址或模型）。

## 速率限制 [#速率限制]

| 响应头                              | 含义       |
| -------------------------------- | -------- |
| `X-RateLimit-Limit-Requests`     | 每分钟最大请求数 |
| `X-RateLimit-Remaining-Requests` | 本窗口剩余次数  |
| `X-RateLimit-Reset-Requests`     | 距离重置秒数   |

# 沙箱

## 它是什么 [#它是什么]

沙箱提供安全隔离的代码执行环境。你在沙箱里上传代码、运行脚本、安装依赖、访问文件——所有操作都在云端隔离实例中进行，不影响你的本地系统。

典型用途：

* 让智能体执行代码并获取结果
* 运行数据处理管道
* 自动化测试

## 与其他服务的关系 [#与其他服务的关系]

* **模型**：沙箱里的代码可以调用模型 API
* **智能体**：智能体可以在沙箱中执行代码作为工具
* **搜索**：沙箱里的代码可以调用搜索 API

## 出口 [#出口]

[快速开始](/docs/developers/sandbox/quickstart) · [概念](/docs/developers/sandbox/concepts) · [API 参考 →](/docs/api/native/sandbox)

# 快速开始

沙箱（Sandbox）提供安全隔离的代码执行环境——上传代码、运行、获取结果，全程在云端隔离实例中进行。

## 前提 [#前提]

一个 Praxis API Key。到 [控制台 → API Keys](https://console.deepraxis.ai/keys) 创建。

## 1. 查看可用环境 [#1-查看可用环境]

沙箱支持多种执行环境（内置环境只读，租户可自建）：

```bash
curl https://api.deepraxis.ai/sandbox/v1/environments \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

## 2. 创建沙箱实例 [#2-创建沙箱实例]

通过 API 创建实例，指定环境和初始资源。

## 3. 执行代码 [#3-执行代码]

在沙箱中运行代码并获取输出。

## 4. 释放实例 [#4-释放实例]

使用完后释放，避免持续计费。

## 下一步 [#下一步]

* [沙箱概念](/docs/developers/sandbox/concepts) · [限制与错误](/docs/developers/sandbox/limits-and-errors)
* [API 参考 → Sandbox](/docs/api/native/sandbox)

# 概念

| 概念              | 一句话                            |
| --------------- | ------------------------------ |
| 环境（Environment） | 代码运行的隔离实例，包含预装工具和运行时           |
| 实例（Instance）    | 一个正在运行的沙箱环境                    |
| 生命周期            | 创建 → 使用 → 释放，释放后不可恢复           |
| 网络限制            | 部分环境限制出站网络（limited networking） |

# 指南

| 你想做什么   | 去哪                                                  |
| ------- | --------------------------------------------------- |
| 第一次使用沙箱 | [快速开始](/docs/developers/sandbox/quickstart)         |
| 处理错误和限制 | [限制与错误](/docs/developers/sandbox/limits-and-errors) |

# 限制与错误

通用错误码和重试策略见[模型的错误与限流](/docs/developers/model/limits-and-errors)——同一套 HTTP 语义适用于所有服务。

沙箱特有的注意事项：

| 事项    | 说明                                             |
| ----- | ---------------------------------------------- |
| 实例数上限 | 每个账户空间同时运行的沙箱实例有上限                             |
| 执行超时  | 单次代码执行有超时限制                                    |
| 网络限制  | 部分环境限制出站网络（limited networking）                 |
| 包安装失败 | 会终止会话，错误码 `ENVIRONMENT_PACKAGE_INSTALL_FAILED` |

遇到沙箱特有的错误，参考错误码文档并[联系我们](https://console.deepraxis.ai/support)。

# 搜索

## 它是什么 [#它是什么]

搜索为 AI 应用提供网络搜索能力——传入查询，返回结构化结果。结果可以直接用于 RAG（检索增强生成）管道，或作为智能体的工具。

典型用途：

* 让智能体能获取实时网络信息
* 构建基于网络的问答系统
* 补充知识库的时效性缺口

## 与其他服务的关系 [#与其他服务的关系]

* **模型**：搜索结果作为上下文传给模型生成回复
* **智能体**：搜索作为智能体的可用工具之一
* **沙箱**：沙箱里的代码可以调用搜索 API

## 出口 [#出口]

[快速开始](/docs/developers/search/quickstart) · [概念](/docs/developers/search/concepts) · [API 参考 →](/docs/api/native/search)

# 快速开始

搜索（Search）为 AI 应用提供网络搜索能力——返回结构化结果，可直接用于 RAG 或智能体工具调用。

## 前提 [#前提]

一个 Praxis API Key。到 [控制台 → API Keys](https://console.deepraxis.ai/keys) 创建。

## 1. 执行搜索 [#1-执行搜索]

通过 Search API 执行一次搜索，获取结构化结果。

## 2. 在智能体中使用 [#2-在智能体中使用]

搜索可以作为智能体的工具——智能体自主决定何时搜索、如何整合结果。

## 3. 构建知识库问答 [#3-构建知识库问答]

搜索与模型结合，构建基于网络信息的问答系统。

## 下一步 [#下一步]

* [搜索概念](/docs/developers/search/concepts) · [限制与错误](/docs/developers/search/limits-and-errors)
* [API 参考 → Search](/docs/api/native/search)

# 概念

| 概念         | 一句话                   |
| ---------- | --------------------- |
| 查询（Query）  | 你传入的搜索关键词或问题          |
| 结果（Result） | 结构化的搜索返回，包含标题、摘要、链接   |
| RAG        | 检索增强生成——先搜索再让模型基于结果回答 |

# 指南

| 你想做什么   | 去哪                                                 |
| ------- | -------------------------------------------------- |
| 第一次使用搜索 | [快速开始](/docs/developers/search/quickstart)         |
| 处理错误和限制 | [限制与错误](/docs/developers/search/limits-and-errors) |

# 限制与错误

通用错误码和重试策略见[模型的错误与限流](/docs/developers/model/limits-and-errors)。

搜索特有的注意事项：

| 事项   | 说明                  |
| ---- | ------------------- |
| 搜索频率 | 受账户空间速率限制约束         |
| 结果数量 | 每次搜索返回的结果条数有上限      |
| 内容时效 | 搜索结果反映当前网络内容，不保证时效性 |

遇到搜索特有的错误，参考错误码文档并[联系我们](https://console.deepraxis.ai/support)。

# 云端浏览器

云端浏览器是一个在云上运行的 Chrome，可以用 Playwright 或 Puppeteer 直接连接操控，也可以在 Console 实时观看与接管。

## 三种用法 [#三种用法]

以下示例在 Node.js 中运行。先设置 `PRAXIS_API_KEY`，将代码保存为 `.mjs` 文件，再用 `node` 执行。只需安装客户端包，无需下载本地 Chrome。

```bash
export PRAXIS_API_KEY="你的 API Key"
npm install playwright @browserbasehq/sdk
```

### 一步连接 [#一步连接]

连接 `wss://api-develop.deepraxis.ai/browser/v1/connect` 即可新开一个浏览器。`?session=<id>` 连接已有会话；`?name=<名称>` 有就连接，没有就创建。名称可用 1–128 个字母、数字或 `. _ : -`，不能同时传 `session` 和 `name`。

```javascript
import { chromium } from "playwright";

const headers = { Authorization: `Bearer ${process.env.PRAXIS_API_KEY}` };
const browser = await chromium.connectOverCDP(
  "wss://api-develop.deepraxis.ai/browser/v1/connect",
  { headers },
);
const cdp = await browser.newBrowserCDPSession();
const session = await cdp.send("Praxis.getSession");
console.log(session.sessionId, session.liveViewUrl, session.consoleUrl);
try {
  const page = await browser.contexts()[0].newPage();
  await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
  console.log(await page.textContent("body"));
} finally {
  await browser.close();
  // 用完立即释放，停止计费。
  const closed = await fetch(
    `https://api-develop.deepraxis.ai/browser/v1/sessions/${session.sessionId}`,
    { method: "POST", headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ status: "REQUEST_RELEASE" }) },
  );
  if (!closed.ok) throw new Error(await closed.text());
}
```

新建时还可带 `context`、`persist`、`timeout`（秒）、`idle`（毫秒）、`labels` 和 `viewport`。后两项用 `JSON.stringify` 配合 `URLSearchParams` 编码；连接已有会话时，这些新建参数不生效。创建会话返回的 `connectUrl` 自带凭证，连接时无需再带 API Key；不要公开此地址。

### Browserbase 官方 SDK [#browserbase-官方-sdk]

使用自己的 API Key，把官方 SDK 的 `baseURL` 设为 `https://api-develop.deepraxis.ai/browserbase`。SDK 自动发送 `X-BB-API-Key`。无需 Browserbase 项目，`projectId` 可省略。

```javascript
import Browserbase from "@browserbasehq/sdk";
import { chromium } from "playwright";

const bb = new Browserbase({
  apiKey: process.env.PRAXIS_API_KEY,
  baseURL: "https://api-develop.deepraxis.ai/browserbase",
});
const session = await bb.sessions.create({ timeout: 300, keepAlive: true });
try {
  const browser = await chromium.connectOverCDP(session.connectUrl);
  try {
    const page = await browser.contexts()[0].newPage();
    await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
    console.log(await page.textContent("body"));
  } finally {
    await browser.close();
  }
} finally {
  await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" });
}
```

可调用的端点见 [Browserbase 兼容 API](/docs/api/compatible/browserbase)。代理、反检测、扩展和 Selenium 不可用；`region`、`solveCaptchas` 当前接受但忽略，不会选区域或代解验证码。

### REST 先创建再连接 [#rest-先创建再连接]

`POST /browser/v1/sessions` 返回的 `connectUrl` 包含连接凭证，有效期覆盖会话寿命。直接交给 Playwright 或 Puppeteer，无需再传 API Key。

```javascript
import { chromium } from "playwright";

const base = "https://api-develop.deepraxis.ai/browser/v1";
const headers = {
  Authorization: `Bearer ${process.env.PRAXIS_API_KEY}`,
  "Content-Type": "application/json",
};
const response = await fetch(`${base}/sessions`, {
  method: "POST", headers, body: JSON.stringify({ timeout: 300, keepAlive: true }),
});
if (!response.ok) throw new Error(await response.text());
console.log("X-Request-Id:", response.headers.get("X-Request-Id"));
const session = await response.json();
try {
  const browser = await chromium.connectOverCDP(session.connectUrl);
  try {
    const page = await browser.contexts()[0].newPage();
    await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
    console.log(await page.textContent("body"));
  } finally {
    await browser.close();
  }
} finally {
  const closed = await fetch(`${base}/sessions/${session.id}`, {
    method: "POST", headers, body: JSON.stringify({ status: "REQUEST_RELEASE" }),
  });
  if (!closed.ok) throw new Error(await closed.text());
}
```

完整请求参数见[云端浏览器 API](/docs/api/native/browser)。Puppeteer 使用 `puppeteer.connect({ browserWSEndpoint: session.connectUrl })` 连接同一地址。

## 实时观看与接管 [#实时观看与接管]

打开 [Console](https://console-develop.deepraxis.ai) 的 **浏览器 › 会话**，选择会话即可观看并接管。程序也可通过 CDP 命令 `Praxis.getSession` 取得 `sessionId`、`liveViewUrl` 和 `consoleUrl`。需要用户完成登录或确认操作时，调用[请求人工接管](/docs/api/native/browser/browserRequestHandoff)，完成后调用[完成人工接管](/docs/api/native/browser/browserCompleteHandoff)。

会话状态为 `PENDING`、`RUNNING`、`ERROR`、`TIMED_OUT` 或 `COMPLETED`。API 创建且 `keepAlive=false` 的会话在客户端断开且无人观看后结束；一步连接和 Console 打开的会话默认在无人连接或观看 5 分钟后释放。单次最长 6 小时。没有暂停或休眠；下次继续使用登录态，需要 Context。

## 登录态 [#登录态]

先[创建 Context](/docs/api/native/browser/browserCreateContext)，再在创建会话时传 `browserSettings.context`。`persist: true` 会在会话关闭时保存 cookie 与 localStorage；不保存 IndexedDB。同一登录态正在被写入时，另一会话可能收到 `409 CONTEXT_LOCKED`。

```javascript
// 接上面的 REST 初始化代码；contextId 为创建 Context 返回的 id。
const response = await fetch(`${base}/sessions`, {
  method: "POST", headers,
  body: JSON.stringify({
    timeout: 300,
    browserSettings: { context: { id: contextId, persist: true } },
  }),
});
```

## 计费与限制 [#计费与限制]

* **$0.12 / 浏览器小时**，按秒计费，单会话 1 分钟起。平台原因导致的失败只按实际秒数计费；释放后不再计费。
* 每个账户默认最多同时 **10 个会话**，每个会话最长 **6 小时**。
* 不支持代理、验证码代解、反检测、选区域或扩展。代理、反检测和扩展等不支持的配置返回 `501`；`region`、`solveCaptchas` 当前接受但忽略。
* 部分网站（如 Google、百度）会把云端浏览器识别为机器人。
* 排查请求时保留响应头 `X-Request-Id`。

# 应用

应用把一个站点（静态文件、一段 Worker 代码，或两者）发布到平台上，并给它一个可访问的地址。每次发布产生一个版本，部署后成为线上版本。

## 存储放哪儿 [#存储放哪儿]

订单、会话等运行数据放在应用自带的 `d1`、`kv` 或 `r2` components 中，由应用代码执行业务规则。结构化内容用 Database，文件用 File，可检索知识用 Knowledge：它们是共享内容库，应用可以通过公开 API 读取，但不作为应用的私有运行库。

* [运行时能力](/docs/developers/application/runtime-capabilities)：应用里能做什么、不能做什么，以及各项限制。

# 运行时能力

下表是应用当前的运行时能力。

## 发布与部署 [#发布与部署]

| 能力               | 状态  | 说明                                               |
| ---------------- | --- | ------------------------------------------------ |
| Worker → 版本 → 部署 | 支持  | 创建版本不会上线；部署后才成为线上版本。创建版本时设置 `deploy: true` 可一步完成 |
| 回滚               | 支持  | 对旧版本再创建一次部署，无需重新上传文件                             |
| 整站替换             | 支持  | 每个版本的文件清单必须完整列出站点的全部文件；只有内容变化的文件需要上传，没变的不会重传     |
| 灰度发布             | 不支持 | 一个部署只能让一个版本占 100%                                |
| 保留版本数            | 限制  | 最近 100 个版本                                       |
| 单个脚本大小           | 限制  | 压缩后 10 MiB                                       |
| 部署超时             | 限制  | 10 分钟内没有完成即失败                                    |

## 静态资源 [#静态资源]

| 能力                   | 状态 | 说明                                                                           |
| -------------------- | -- | ---------------------------------------------------------------------------- |
| 内容寻址去重               | 支持 | 相同内容只存一份，重复发布不会重复上传                                                          |
| 路径与大小                | 限制 | 每个版本最多 100000 个文件，单个文件最大 25 MiB                                              |
| `html_handling`      | 支持 | 可选 `auto-trailing-slash`、`force-trailing-slash`、`drop-trailing-slash`、`none` |
| `not_found_handling` | 支持 | 可选 `none`、`404-page`、`single-page-application`                               |
| `_headers`           | 支持 | 仅作用于静态资源的响应，最多 100 条规则，每行最多 2000 字符                                          |
| `_redirects`         | 支持 | 仅作用于静态资源的响应，最多 2000 条静态规则和 100 条动态规则，每行最多 1000 字符                            |

`_headers` 和 `_redirects` 不会作用于 Worker 代码生成的响应。需要跨域访问时，在代码里自己设置 CORS 头：

```js
const CORS = {
  "access-control-allow-origin": "*",
  "access-control-allow-methods": "GET, POST, OPTIONS",
  "access-control-allow-headers": "content-type",
};

export default {
  async fetch(request) {
    if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
    return Response.json({ ok: true }, { headers: CORS });
  },
};
```

## 出站请求 [#出站请求]

| 能力         | 状态 | 说明                        |
| ---------- | -- | ------------------------- |
| 访问公网地址     | 支持 | 代码里的 `fetch()` 可以请求任意公网地址 |
| 访问平台上的其他应用 | 支持 | 请求其他应用的地址，得到的是对方应用的响应     |

## 绑定 [#绑定]

| 类型                                                        | 状态  | 说明                              |
| --------------------------------------------------------- | --- | ------------------------------- |
| `d1`                                                      | 支持  | 在创建版本时声明                        |
| `kv_namespace`                                            | 支持  | 在创建版本时声明                        |
| `r2_bucket`                                               | 支持  | 在创建版本时声明                        |
| `secret_text`                                             | 支持  | 在创建版本时声明；值通过密钥接口写入              |
| `plain_text`                                              | 支持  | 在创建版本时声明                        |
| `json`                                                    | 支持  | 在创建版本时声明                        |
| `praxis_token`                                            | 支持  | 启用 owner 组织的平台 API 访问，不注入 token |
| `praxis_vault_ref`                                        | 不可用 | 当前不能使用                          |
| 其他类型（如 `service`、`queue`、`durable_object_namespace`、`ai`） | 不支持 | 创建版本时返回 `UNSUPPORTED`           |

## 密钥 [#密钥]

| 能力    | 状态  | 说明                                                                     |
| ----- | --- | ---------------------------------------------------------------------- |
| 写入密钥  | 支持  | `PUT /application/v1/workers/{id}/secrets/{name}`。写入后会基于当前版本派生一个新版本并部署 |
| 读回密钥值 | 不支持 | 值只写不读，任何接口都不会返回                                                        |

## 兼容标志 [#兼容标志]

| 能力                             | 状态   | 说明                                                                                         |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------ |
| `compatibility_flags`          | 支持   | 创建版本时传入，会原样生效                                                                              |
| `global_fetch_strictly_public` | 固定开启 | 每个版本都会带上，不能关闭；传入 `global_fetch_private_origin` 或 `no_global_fetch_strictly_public` 会返回 400 |

## 调用平台 API [#调用平台-api]

在要部署的版本中声明绑定：

```json
{ "type": "praxis_token", "name": "PRAXIS_API_TOKEN" }
```

应用可以不带 API Key 调用 Develop API：

```js
export default {
  async fetch() {
    return fetch("https://api-develop.deepraxis.com/application/v1/workers");
  }
};
```

应用拥有与 owner 组织 API Key 相同的组织访问权限。其他组织的资源返回不存在。应用代码不会收到 token 或密钥，也不会生成 `env.PRAXIS_API_TOKEN`。未声明该绑定时，受保护接口返回 401，除非请求自行提供有效凭证。其他目标域名的请求保持原样。

Praxis SDK 使用同一条访问路径：省略 `apiKey`，将 `baseUrl` 设为 Develop API 地址，无需修改 SDK。已有应用需要部署声明该绑定的版本后才能启用。移除绑定会在新部署生效、路由缓存刷新后停止注入身份（最多 10 秒）。

# 文件

文件服务保存被素材库、知识库和对话引用的原始文件。上传后得到 `file_id`，再把它交给需要使用文件的服务。

素材库组织素材及其理解结果；知识库提取文档内容并建立检索索引。只上传文件，不会自动将它加入库，也不会自动让它可检索。

[快速开始](/docs/developers/file/quickstart) · [概念](/docs/developers/file/concepts) · [限额与错误](/docs/developers/file/limits-and-errors)

[API 参考 →](/docs/api/native/file) · [Cookbook →](/docs/cookbook)

# 快速开始

安装 SDK，并将 API Key 设置在环境变量 `PRAXIS_API_KEY` 中。把下面的 Python 代码保存为 `files.py`。

使用 Develop 时，另设 `PRAXIS_API_BASE=https://api-develop.deepraxis.ai`；未设置时使用正式 API。Key 必须属于所选环境。

```bash
python -m pip install openai
python files.py
```

```python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
)
file = client.files.create(
    file=("hello.txt", b"Hello, Praxis!\n", "text/plain"),
    purpose="user_data",
    expires_after={"anchor": "created_at", "seconds": 3600},
)
try:
    print("uploaded:", file.id)
    files = client.files.list(limit=100)
    print("listed:", any(item.id == file.id for item in files))
    content = client.files.content(file.id).read()
    assert content == b"Hello, Praxis!\n"
    print("downloaded:", content.decode().strip())
finally:
    deleted = client.files.delete(file.id)
    print("deleted:", deleted.deleted)
```

期望：输出上传的 ID、`listed: True`、`downloaded: Hello, Praxis!` 和 `deleted: True`。即使列出或下载失败，示例也会删除自己创建的文件。

[API 参考 →](/docs/api/native/file) · [Cookbook →](/docs/cookbook)

# 概念

## purpose 用途 [#purpose-用途]

`purpose` 记录文件的预期用途。上传接受三个值：

| purpose      | 用途与限制                                                                            |
| ------------ | -------------------------------------------------------------------------------- |
| `user_data`  | 通用输入文件。在对话中引用图片时必须使用此值。                                                          |
| `assistants` | 用于智能体或知识库工作流的文件。上传不等于加入智能体或知识库；对话图片引用不接受此用途。                                     |
| `vision`     | 只接受 PNG、JPEG、GIF、WebP 图片（按内容识别），其他内容返回 400 `FILE_TYPE_UNSUPPORTED`；对话图片引用不接受此用途。 |

上传不接受 `batch`、`fine-tune` 和 `evals`。通过 Anthropic 上传的文件统一记为 `user_data`。

## 引用 file\_id [#引用-file_id]

同一账户的 `/file/v1/files`、`/openai/v1/files` 和 `/anthropic/v1/files` 共用文件 ID，可跨接口读取元数据和删除。上传文件的下载使用文件 API 或 OpenAI API；Anthropic 下载仅支持会话输出文件。

* **对话**：图片以 `user_data` 上传，用 `type: "file"` 与 `file.file_id` 引用。不要将 ID 填进 `image_url.url`。文本和 PDF 的文件 ID 当前不能作为对话图片输入。参见[图片引用示例](/docs/developers/model/guides/files)。
* **素材库**：用 `file_id` 导入，生成素材记录。参见[素材 API](/docs/api/native/media/assets)。
* **知识库**：将 `file_id` 加入库后，才会提取内容并建立索引。参见[知识库概念](/docs/developers/knowledge/concepts)。
* **智能体**：用 `type: "file"`、`file_id` 和 `mount_path` 将文件加入会话资源。仅设置 `purpose=assistants` 不会挂载文件。参见[会话资源 API](/docs/api/native/agent)。

会话输出文件的元数据可包含 `scope`、`relative_path` 和 `revision`。用 `scope_id` 列出某个会话的文件；Anthropic 使用该筛选时需要 `anthropic-beta: managed-agents-2026-04-01` 请求头。

## 过期 [#过期]

不设置过期时间的上传文件保留到被删除。`expires_after` 从 `created_at` 起计算秒数，响应中的 `expires_at` 是到期时间。过期文件不能再读取或引用。可用范围见[限额与错误](/docs/developers/file/limits-and-errors)。

## 下载安全 [#下载安全]

下载内容被识别为 HTML、SVG、XML 或 JavaScript 时，以附件下发，并设置 `X-Content-Type-Options: nosniff`。类型按内容识别，不依赖扩展名：上传的 HTML 或 SVG 也可能被识别为纯文本并以纯文本返回。下载不会执行其中的内容。

## 分段下载 [#分段下载]

内容下载支持单个 `Range` 区间：`bytes=0-1023`、`bytes=1024-`（从某处到结尾）或 `bytes=-1024`（最后 1024 字节）。命中时返回 206 和 `Content-Range`；区间超出文件大小返回 416。所有下载响应都带 `Accept-Ranges: bytes`，可用于断点续传和音视频拖动。

[API 参考 →](/docs/api/native/file) · [Cookbook →](/docs/cookbook)

# 限额与错误

## 限额 [#限额]

| 项目                                    | 限额                                                   |
| ------------------------------------- | ---------------------------------------------------- |
| 单文件                                   | 100 MiB（104,857,600 字节）；不接受空文件                       |
| 文件存储配额                                | 20 GiB（21,474,836,480 字节），账户内文件共享                    |
| OpenAI 或文件 API 的 `expires_after`      | `anchor: "created_at"`，3,600–2,592,000 秒（1 小时至 30 天） |
| 文件 API 的 `praxis[expires_in_seconds]` | 3,600–7,776,000 秒（1 小时至 90 天）                        |
| Anthropic 表单 `expires_in_seconds`     | 3,600–7,776,000 秒（1 小时至 90 天）                        |

过期参数只能选一种。Anthropic 的 `expires_in_seconds` 只能放在表单中；放在查询参数里返回 400。参见[上传参数](/docs/api/native/file/createFile)。

## 未知字段 [#未知字段]

兼容接口按各自官方的方式拒绝未知参数，不会静默忽略：

| 接口           | 可用参数                                                                       | 其他参数                                                                                     |
| ------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Anthropic 上传 | 表单 `file`、`expires_in_seconds`；查询参数仅 `beta`                                | 400 `invalid_request_error`，如 `purpose: Extra inputs are not permitted`                  |
| OpenAI 上传    | 表单 `file`、`purpose`、`expires_after[anchor]`、`expires_after[seconds]`；无查询参数 | 400 `invalid_request_error`，如 `Unrecognized request argument supplied: foo`，`param` 为该字段 |
| OpenAI 列表    | 查询参数 `after`、`order`、`limit`、`purpose`                                     | 同上                                                                                       |

`praxis[expires_in_seconds]` 与 `scope_id` 等扩展只在[文件 API](/docs/api/native/file) 中可用。

按内容识别 PDF、DOCX/XLSX/PPTX、PNG/JPEG/GIF/WebP、支持的音视频格式和 UTF-8 文本。改扩展名不能让任意二进制文件变成受支持类型。

## 错误处理 [#错误处理]

| 错误                              | 处理方式                                                                       |
| ------------------------------- | -------------------------------------------------------------------------- |
| `PURPOSE_UNSUPPORTED`           | 传入 `user_data`、`assistants` 或 `vision`。                                    |
| `FILE_EMPTY`                    | 上传非空文件。                                                                    |
| `FILE_TOO_LARGE`                | 将单文件控制在 100 MiB 以内。                                                        |
| `FILE_TYPE_UNSUPPORTED`         | 使用支持的文件内容类型，仅修改扩展名无效；`purpose=vision` 只接受图片；损坏的 Office 文档或 ZIP 压缩包也会返回此错误。 |
| `VALIDATION_ERROR`              | 检查文件名与过期参数。                                                                |
| `QUOTA_EXHAUSTED`               | 删除不再需要的文件后重试上传。                                                            |
| HTTP 404                        | 检查 ID、账户以及文件是否已过期或删除。                                                      |
| `File is not an eligible image` | 在对话中使用支持的图片，并以 `purpose=user_data` 重新上传。                                   |

过大的请求可能先被边缘拒绝，返回 HTTP 413 与 HTML 正文，而不是文件 API 的 JSON 错误。解析错误正文前先检查 HTTP 状态。

[API 参考 →](/docs/api/native/file) · [Cookbook →](/docs/cookbook)

# 素材库

素材库为智能体存放、理解和检索图片、视频、音频与文档。一个库是一个隔离单元，素材、文件夹与检索都限定在库内。官方 Cloudinary SDK 可以直接使用。

[快速开始](/docs/developers/media/quickstart) · [概念](/docs/developers/media/concepts) · [限制与错误](/docs/developers/media/limits-and-errors)

[API 参考 →](/docs/api/native/media)

# 快速开始

准备 API Key，将下方代码保存为 quickstart.mjs，在 Node.js 中运行。示例建库、上传图片、按标签检索，并给出原图与缩略图地址。

```bash
npm install cloudinary@2
# Set PRAXIS_API_KEY in your environment.
node quickstart.mjs
```

```javascript
import { v2 as cloudinary } from "cloudinary";

const base = "https://api.deepraxis.ai";
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY");
async function media(path, method = "GET", body) {
  const response = await fetch(base + "/media/v1" + path, {
    method,
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`Media HTTP ${response.status}`);
  return response.json();
}
const library = await media("/libraries", "POST", { name: "Agent media" });
cloudinary.config({
  cloud_name: library.id,
  oauth_token: key,
  upload_prefix: base + "/cloudinary",
});
const asset = await cloudinary.uploader.upload(
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABWklEQVR4nO3OQQ0AMBAEofVv+iqDxzRBALvtg/wgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwg7gEgaMOyrMtNTwAAAABJRU5ErkJggg==",
  { public_id: "quickstart/pixel", tags: ["docs"] },
);
const found = await cloudinary.search.expression("resource_type:image AND tags:docs").execute();
const delivery = asset.secure_url;
const thumbnail = delivery.replace("/upload/", "/upload/w_160,c_fit/");
const understandings = await media(`/libraries/${library.id}/assets/${asset.asset_id}/understandings`);
const current = await media(`/libraries/${library.id}/assets/${asset.asset_id}`);
const matches = current.semantic === "ready"
  ? await media(`/libraries/${library.id}/search/semantic`, "POST", { query: "a small image", top_k: 5 })
  : null;
console.log({ library_id: library.id, asset_id: asset.asset_id, found, delivery, thumbnail, understandings, matches });
```

理解异步完成。上传响应不含处理状态；用 GET 读取素材，查看 processing 与 semantic。实测量级：图片约半分钟，3 秒视频约 50 秒，raw 文件立即就绪（不做理解）。首次读取可能为空；matches 为 null 时稍后重新读取素材，semantic 为 ready 后再检索。理解与语义检索可用；上传、表达式检索和原文件读取无需等待理解完成。

示例保留库供继续使用。用完后，把 LIBRARY\_ID 设为输出的 library\_id 并删除：

```bash
curl -X DELETE "https://api.deepraxis.ai/media/v1/libraries/${LIBRARY_ID}" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

[API 参考 →](/docs/api/native/media)

# 概念

一个库是素材的隔离单元。检索只会命中当前库的素材。

## 库与素材 [#库与素材]

素材有稳定的 asset\_id。public\_id 是交付地址中的名称，可以带路径。asset\_folder 组织素材；移动文件夹不改变 public\_id。按 asset\_folder 列出素材只匹配这一层，不包含子文件夹；空字符串表示根。display\_name 是显示名称，不决定交付地址。音频使用 resource\_type=video，并带 is\_audio=true。

库名最长 64 个字符，可以重名；库以 library\_id 区分。更新素材的 tags、context 或库的 labels 都是整体替换：传入的值就是新的全部内容，传 {}（tags 传 \[]）即清空。

上传 0 字节文件返回 400 FILE\_EMPTY；声明或识别为图片却读不出宽高（例如被截断）的文件返回 400 Invalid image file。其他任意文件按 raw 存放。

## 理解 [#理解]

理解可以产出描述、标签、图片文字、音频转写与视频帧内容。片段结果带起止时间。记录不可变；新版本可以替代旧记录，默认不返回被替代的记录。

整体状态为 pending（等待）、processing（处理中）、ready（完成）、partial（部分完成）或 failed（失败）。单项状态为 pending、running、ready、failed、unsupported（不支持）或 skipped（跳过）。关闭理解或不支持的内容不会阻止原文件读取。

## 语义检索 [#语义检索]

当 semantic 为 ready，即可自然语言检索。默认混合检索结合关键词与语义，也可只用语义。命中带匹配内容；音视频片段带起止毫秒，方便定位。从未完成理解的素材不进入语义结果；重新处理中的素材继续用上一版结果参与检索，直到新结果就绪。

## 文档与其他文件（raw） [#文档与其他文件raw]

PDF、文本等 raw 文件只存放，不做理解，semantic 为 unavailable，不进入语义检索，只能按元数据（public\_id、display\_name、tags、context 等）用表达式检索。需要按全文内容检索文档，请使用[知识库](/docs/developers/knowledge)。

## 删除与恢复 [#删除与恢复]

删除后立即不可见。库可在 7 天内恢复，到期后无法恢复。单个素材没有恢复接口。

## 与文件的区别 [#与文件的区别]

「文件」存放原始文件；素材库进一步组织、理解和检索内容。从文件导入是拷贝：两份文件独立，之后的修改或删除不会互相同步。

[API 参考 →](/docs/api/native/media)

# 限制与错误

视频超过 100 MiB 时不生成帧与音轨理解，也不生成预览海报；仍可存放和读取原文件。

| 限制                    | 数值         |
| --------------------- | ---------- |
| 每账户库数                 | 100        |
| 每库素材数                 | 1000000    |
| 图片最大字节数（50 MiB）       | 52428800   |
| 视频最大字节数（2 GiB）        | 2147483648 |
| 文档最大字节数（100 MiB）      | 104857600  |
| 单请求最大字节数；更大需分片        | 100000000  |
| 非末片最小字节数；各片等长         | 5242880    |
| 分片会话有效小时数             | 24         |
| 视频帧与音轨理解源文件字节上限       | 104857600  |
| 表达式检索每页上限             | 500        |
| 表达式检索默认条数             | 100        |
| 语义检索命中上限              | 50         |
| 语义候选上限                | 100        |
| 以图搜图上传图片最大字节数（10 MiB） | 10485760   |
| 每份素材文字向量上限（图片向量另计）    | 61         |
| 故事板画面上限               | 12         |
| 每库同时理解的任务数            | 4          |
| 理解超时毫秒数               | 600000     |
| 理解最大重试次数              | 3          |
| 每素材标签上限               | 1000       |
| 标签字符上限                | 255        |
| 上下文字节上限               | 1024       |
| 批量素材 ID 上限            | 1000       |
| 库恢复天数                 | 7          |
| 库名字符上限；允许重名           | 64         |
| bge-m3 语义检索最低余弦相似度    | 0.55       |

[错误码与重试规则 →](/docs/api/native/media/errors)

# 知识库

知识库将文件内容分段并建立索引，让应用按语义、关键词和文件属性查找相关文本，适合问答检索、政策查询和给模型提供参考资料。

| 服务  | 适合保存和查找什么                |
| --- | ------------------------ |
| 文件  | 上传、保存和下载原始文件，提供文件 ID     |
| 素材库 | 组织素材、管理文件夹与标签，并查找图片等素材   |
| 知识库 | 从文件内容中查找相关文本分段，返回来源文件与内容 |

一个源文件可以加入多个知识库。从知识库移除文件不会删除源文件；删除源文件后，它会自动从所有知识库中移出，之后不再被搜索到。

* [快速开始](/docs/developers/knowledge/quickstart)：用官方 openai Python SDK 上传、入库和搜索。
* [概念](/docs/developers/knowledge/concepts)：处理状态、分段、搜索与恢复。
* [限制与错误](/docs/developers/knowledge/limits-and-errors)：容量、文件格式与失败处理。

[知识库 API →](/docs/api/native/knowledge) · [OpenAI 兼容接口 →](/docs/api/compatible/openai/vector-stores)

# 快速开始

在服务端运行 Python，将 develop API Key 放入 `PRAXIS_API_KEY` 环境变量。示例连接 develop，每次创建一个知识库和两个文本文件，并在结束时删除它们。

```bash
python -m pip install openai
```

保存为 `quickstart.py`，运行 `python quickstart.py`。先上传政策文件，再建库并等待处理完成；`upload_and_poll` 将第二个文件的上传、添加和等待合成一次调用。上传成功后仍需检查入库状态。

```python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url="https://api.deepraxis.com/openai/v1",
    timeout=120.0,
)
store = None
file_ids = []
try:
    uploaded = client.files.create(
        file=("refund-policy.txt", b"Refunds are available within 30 days of purchase."),
        purpose="assistants",
    )
    file_ids.append(uploaded.id)
    print("uploaded:", uploaded.id)

    store = client.vector_stores.create(
        name="Knowledge quickstart",
        file_ids=[uploaded.id],
    )
    print("store:", store.id)
    first = client.vector_stores.files.poll(
        uploaded.id, vector_store_id=store.id, poll_interval_ms=1000,
    )
    if first.status != "completed":
        raise RuntimeError(f"Ingestion failed: {first.last_error}")

    attached = client.vector_stores.files.upload_and_poll(
        vector_store_id=store.id,
        file=("refund-timing.txt", b"Refunds are processed in 5 business days."),
        poll_interval_ms=1000,
    )
    file_ids.append(attached.id)
    print("ingestion:", attached.status)
    if attached.status != "completed":
        raise RuntimeError(f"Ingestion failed: {attached.last_error}")

    results = client.vector_stores.search(
        vector_store_id=store.id,
        query="How many business days does a refund take?",
        max_num_results=5,
    )
    assert results.data, "Expected at least one matching chunk"
    for result in results.data:
        print(result.filename, result.score)
        for part in result.content:
            print(part.text)
    print("pagination:", results.has_more, results.next_page)
finally:
    try:
        if store is not None:
            deleted = client.vector_stores.delete(store.id)
            print("store deleted:", deleted.deleted)
    finally:
        for file_id in file_ids:
            deleted = client.files.delete(file_id)
            print("file deleted:", file_id, deleted.deleted)
        client.close()
```

输出包含文件 ID、知识库 ID、`ingestion: completed`、命中文本与清理结果。每个文件一条结果，命中的文本块按相关度排列。搜索不支持翻页；结果没有被截断时最后输出 `pagination: False None`。

2026-10-11 的 develop 实测中，文本、Markdown、PDF、DOCX、CSV、JSON 与 HTML 处理约需 4–21 秒；一张 PNG 约需 87 秒。这些是样例耗时，不是时延保证。服务允许处理至多 30 分钟；失败时查看 `last_error`。

示例删除知识库后，知识库会保留 7 天供恢复；源文件单独删除。若要继续使用这些资料，请在自己的程序中将清理步骤放到使用结束之后。

[查看请求字段 →](/docs/api/compatible/openai/vector-stores) · [理解搜索结果 →](/docs/developers/knowledge/concepts)

# 概念

## 知识库与文件 [#知识库与文件]

知识库使用 `vs_` 开头的 ID。`/knowledge/v1` 和 `/openai/v1/vector_stores` 访问相同数据；前者额外提供恢复、失败重试、分段、事件和搜索解释。所有示例都使用 API Key，从服务端调用。

先通过文件服务上传，得到 `file_id`，再将其加入知识库。知识库内文件 ID 与源文件 ID 相同。重复加入已有文件会返回原记录，不重新处理，也不会应用新的属性或分段策略。批次也一样：已在库中的文件出现在批次里不会重新入库，批次的 attributes 和 chunking\_strategy 只作用于新加入的文件。要改属性，用更新文件接口（`POST /vector_stores/{id}/files/{file_id}`）；要换分段策略，先移除该文件再重新加入。

## 处理状态 [#处理状态]

| status       | 含义     | 下一步                                |
| ------------ | ------ | ---------------------------------- |
| in\_progress | 内容正在处理 | 轮询文件状态或使用 SDK 的 poll 方法            |
| completed    | 内容已可搜索 | 发起搜索                               |
| failed       | 处理失败   | 查看 last\_error，修正文件或通过原生 API retry |
| cancelled    | 已取消处理  | 先移除，再按需要重新添加                       |

图片（PNG、JPEG、GIF、WebP）入库的内容是模型生成的画面描述，不是逐字 OCR：图中的文字可能被概括或遗漏，搜索命中的是这段描述。图片入库比文本慢，实测单张约数秒到数十秒，多个文件同时入库时更久。

原生 API 的 `praxis.stage` 给出 queued、parse、chunk、embed、index 进度。完成后 stage 为 null。知识库的 status 由处理情况与过期状态决定；空库也可为 completed，因此它不表示库内一定有内容。

批次把多个文件添加到同一知识库，最多 500 个。`file_ids` 和逐文件配置的 `files` 二选一。使用 `files` 时忽略顶层 attributes 和 chunking\_strategy。取消只对进行中的批次生效；对已完成、已失败或已取消的批次调用取消，会原样返回当前状态。

## 分段 [#分段]

默认 auto 会转为每段 800 个估算 token、相邻重叠 400 token。static 可设置每段 100–4096 token；重叠不得超过段大小的一半。token 数为估算值，不等同于具体模型 tokenizer 的结果。

原生 API 可读取每段的文本、序号、估算 token、页码、UTF-16 起止位置和相邻重叠量；页码可能为 null。读取文件 content 得到的是分段文本，拼接时需考虑重复内容。`usage_bytes` 是已入库文件的源文件大小（字节），未完成入库的文件为 0；知识库的 `usage_bytes` 是其中所有文件之和。

## 搜索结果 [#搜索结果]

`/openai/v1/vector_stores/{id}/search` 与 OpenAI 一致：**每个文件一条结果**，`content` 按相关度从高到低列出该文件命中的文本块，`score` 取其中最高分，`max_num_results` 限制的是文件数（默认 10、最多 50）。`/knowledge/v1` 的搜索**按块返回**，同一文件可能出现多次，适合直接拼接成模型的参考资料。

`query` 可为字符串或字符串数组；数组会以换行连接后共同搜索。`filters` 筛选文件属性，支持 eq、ne、gt、gte、lt、lte、in、nin 和 and/or 组合。比较按类型匹配，例如数字 1 与字符串 "1" 不相同。

默认混合语义与关键词检索，权重为 0.7/0.3。原生 API 可通过 `ranking_options.hybrid_search` 调整权重并用 `praxis.explain: true` 查看解释；在 OpenAI 兼容接口传 hybrid\_search 会返回 400。只有 ranker 为 default-2024-11-15 时请求重排；none、auto 和省略时使用初始排序。重排的效果尚未单独实测。

score 在 0–1 之间，是同一次搜索内的相对排序依据，不是相关性概率，也没有默认下限：库里不相关的内容也会返回，分数可能与相关内容很接近（实测相关文件 0.70，不相关文件 0.61–0.62）。需要过滤时设置 `ranking_options.score_threshold`，低于该值的结果不返回；OpenAI 同样默认不设阈值。可以从 0.5 起试，再用自己的资料和典型问题看相关与不相关结果的分数，逐步上调到不再误删相关结果为止。

`rewrite_query: true` 时先改写查询再检索，见下方「模型」。`has_more` 表示还有更多结果被 `max_num_results` 截掉；搜索不支持翻页，`next_page` 恒为 null。原生结果附带 chunk\_id、page、span；兼容结果不含这些字段和 praxis。

## 模型 [#模型]

每个知识库有三个模型角色，创建时可在原生 API 的 `models` 里指定，之后用更新接口修改；不指定就用默认模型。读取知识库时 `models` 返回各角色当前生效的模型。

| 角色              | 作用                                                  | 默认模型              |
| --------------- | --------------------------------------------------- | ----------------- |
| `embedding`     | 给分段和查询生成向量                                          | bge-m3            |
| `rerank`        | `ranking_options.ranker` 为 default-2024-11-15 时重排结果 | bge-reranker-base |
| `query_rewrite` | 搜索设置 `rewrite_query: true` 时改写查询                    | qwen3.8-flash     |

库中已有文件后不能更换 `embedding`（已有向量来自原模型），更换会返回 409；其他角色随时可改。模型必须在模型目录中并支持该角色，嵌入模型还必须输出 1024 维向量，否则返回 400。某个角色设为 null 恢复它的默认模型，`models: null` 恢复全部默认。OpenAI 兼容接口不返回 `models`，传入会返回 400（与 OpenAI 对未知参数的处理一致），但搜索同样按知识库设置的模型执行。

`rewrite_query: true` 时，搜索前先用 `query_rewrite` 模型把问题改写成简洁的检索查询（例如「退货以后多久能收到退款？」→「退款到账时间」），用改写结果检索，`search_query` 返回改写后的查询；`query` 为数组时逐条改写。每次改写是一次小模型调用，会产生费用，按模型用量计费，并增加约 1–2 秒延迟。改写失败、超过 5 秒或返回为空时，该条查询回退为原文继续搜索，不会让搜索失败。不传或为 false 时不调用模型。

## 更新、过期与删除 [#更新过期与删除]

name 省略时默认为空字符串；创建时不接受 null，更新时 null 会清空名称。metadata 更新会替换全部内容；null 清空。description 被接受后丢弃，不能用来保存说明。未知写入字段当前会被忽略，拼写错误不一定报错。

`expires_after` 只接受 last\_active\_at 和 1–365 天；搜索和添加文件刷新活动时间，null 取消自动过期。删除后，知识库进入「删除中」：`/knowledge/v1` 读取返回 `status: deleting` 和清除时间 `purge_at`，默认列表不再显示（用 `status=deleting` 可以列出），搜索、更新和添加文件返回 409；7 天内可以 `restore` 恢复，也可以用 `purge` 立即永久删除（不可撤销）。`/openai/v1/vector_stores` 与 OpenAI 一致，删除后读取返回 404。删除知识库不会删除「文件」中的源文件。

[API 参考 →](/docs/api/native/knowledge) · [上限与错误 →](/docs/developers/knowledge/limits-and-errors)

# 限制与错误

## 容量 [#容量]

以下为当前 API 上限。下方耗时来自样例实测，不是服务保证。

| 项目                       | 上限                         |
| ------------------------ | -------------------------- |
| 每个组织的未删除知识库              | 100                        |
| 每个知识库的文件                 | 10,000                     |
| 单个源文件                    | 100,000,000 bytes (100 MB) |
| 单个文件估算 token             | 5,000,000                  |
| 每批文件                     | 1–500                      |
| 知识库名称                    | 256 characters             |
| metadata / attributes 键数 | 16                         |
| 键长度 / 字符串值长度             | 64 / 512 characters        |
| 静态分段大小                   | 100–4096 tokens            |
| 默认分段 / 重叠                | 800 / 400 tokens           |
| 分段重叠上限                   | ≤ ½ chunk size             |
| 搜索返回结果数（兼容接口按文件、原生接口按分段） | 1–50; default 10           |
| 列表每页条数                   | 1–100; default 20          |
| 过期天数                     | 1–365                      |
| JSON 请求体                 | 1,000,000 bytes            |
| 入库截止时间                   | 30 minutes                 |
| 处理重试次数                   | 3                          |
| 删除后保留                    | 7 days                     |

metadata 的值只能是字符串；attributes 的值可以是字符串、数字或布尔值。筛选条件在根条件以下最多嵌套 20 层。语义和关键词检索各取最多 50 个候选；搜索不提供下一页。

## 文件格式与处理耗时 [#文件格式与处理耗时]

知识库可以入库以下类型（按文件服务识别的类型判断）：

| 类型                    | 入库内容               |
| --------------------- | ------------------ |
| PDF                   | 文档文本               |
| DOCX、PPTX、XLSX        | 文档文本               |
| PNG、JPEG、GIF、WebP     | 模型生成的画面描述，不是逐字 OCR |
| HTML                  | 可见文本，去掉脚本和样式       |
| 纯文本、Markdown、CSV、JSON | 原文                 |

能上传不代表能入库。文件服务接受、但知识库不能入库的类型有：音频（MP3、WAV、FLAC、OGG、M4A）、视频（MP4、WebM）、NDJSON、XML、YAML、SVG，以及识别为 Python、JavaScript 的源代码文件。它们加入知识库时不会被拒绝，而是在异步入库阶段失败，文件 status 为 failed，`last_error.code` 为 unsupported\_file。加密的 PDF 或 Office 文件同样在入库阶段失败（invalid\_file）。空文件、ZIP 和其他二进制文件在上传时就被文件服务拒绝。

2026-10-11 develop 实测：文本与文档约 4–21 秒入库完成；单张图片约 4–11 秒，7 个文件同时入库时一张 PNG 用了约 87 秒。

## HTTP 错误 [#http-错误]

| HTTP | code                                                                                                        | 处理建议                               |
| ---- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| 400  | INVALID\_PARAMS / LIMIT\_EXCEEDED                                                                           | 修正参数或减少数量。                         |
| 401  | UNAUTHENTICATED                                                                                             | 检查 API Key；兼容响应 code 为 null。       |
| 404  | VECTOR\_STORE\_NOT\_FOUND / FILE\_NOT\_FOUND / FILE\_NOT\_IN\_STORE / BATCH\_NOT\_FOUND / CHUNK\_NOT\_FOUND | 核对资源 ID 与所属知识库。                    |
| 409  | VECTOR\_STORE\_INVALID\_STATE / FILE\_INVALID\_STATE                                                        | 检查删除、过期或文件处理状态。                    |
| 413  | PAYLOAD\_TOO\_LARGE                                                                                         | JSON 请求超过 1 MB；兼容响应 code 可能为 null。 |
| 429  | RATE\_LIMITED                                                                                               | 稍后重试。                              |
| 500  | — / null                                                                                                    | 服务内部错误；保留请求 ID 并稍后重试。              |
| 501  | NOT\_IMPLEMENTED                                                                                            | 当前不支持该操作。                          |
| 503  | PROVIDER\_UNAVAILABLE                                                                                       | 处理服务暂不可用，稍后重试。                     |

原生错误通常包含 `error.code`、`error.message` 与 `error.details`。兼容错误包含 `error.message`、`error.type`、`error.param` 与 `error.code`，官方 SDK 按 HTTP 状态抛出对应异常。未知路径可能没有 code，401 可能没有 details，路径编码错误也可能返回 HTML。

## 异步失败 [#异步失败]

文件 status 为 failed 时查看 `last_error.code`：unsupported\_file 表示无法解析该格式；invalid\_file 表示内容无效、为空或加密；server\_error 表示处理失败或超时。超过估算 token 上限当前映射为 invalid\_file，但尚未实测超限文件。添加成功后仍应检查文件状态。原生 API 支持重试 failed 文件；本次实测未覆盖失败后的成功重试。

真正过期及删除 7 天后的清除尚未端到端观察。实测也未得到可用于承诺的知识库每秒请求额度。

[完整错误对象 →](/docs/api/native/knowledge/errors) · [搜索限制 →](/docs/developers/knowledge/concepts)

# 数据库

## Database 是什么、不是什么 [#database-是什么不是什么]

Database 是 headless CMS 式的内容库：人、智能体和脚本通过公开 API 直接读写结构化的表与行。它保存行程、目录等内容，独立于使用这些内容的应用。它不是应用的私有运行库，也不是数据仓库。

| 角色    | 谁用                       | 生命周期                    | 存在哪里                                      |
| ----- | ------------------------ | ----------------------- | ----------------------------------------- |
| 内容库   | 人、智能体、脚本；应用也可通过公开 API 读取 | 内容独立于应用发布进行编辑和复用        | 结构化内容用 Database，文件用 File，可检索知识用 Knowledge |
| 应用运行库 | 管理订单、会话和业务规则的应用代码        | 状态随应用运行而变化，由应用自己的业务约束管理 | 应用自带的 `d1`、`kv` 或 `r2` components         |
| 数据仓库  | 分析人员和报表流水线               | 积累历史数据用于分析              | 专门的分析型数据仓库，不使用 Database                   |

典型模式是**内容库 → 构建 → 发布到应用**：从 Database 读取行程内容，构建站点文件，再用 [`praxis.application.publish`](/docs/developers/application) 上传、创建版本并部署。修改源内容不会更新这些已发布的文件；需要再次构建和发布才能包含修改。

每个库是一个隔离单元，包含自己的表、字段、行和事件。

通过 Database API 创建库与表结构；通过官方 supabase-js 读写行。同一库的两个行访问地址共享数据。

* [快速开始](/docs/developers/database/quickstart)：建库、建表，再读写第一行。
* [概念](/docs/developers/database/concepts)：字段类型、关联、版本与恢复。
* [限制与错误](/docs/developers/database/limits-and-errors)：容量和查询边界。

[API 参考 →](/docs/api/native/database) · [Supabase 兼容接口 →](/docs/api/compatible/supabase) · [Cookbook →](/docs/cookbook)

# 快速开始

在 Node.js 22 或更高版本中运行，将 develop API Key 放入 PRAXIS\_API\_KEY 环境变量。每次运行都会创建一个新库。

```bash
npm install @supabase/supabase-js
```

将下面内容保存为 `quickstart.mjs`，运行 `node quickstart.mjs`。

```javascript
import { createClient } from "@supabase/supabase-js";

const api = "https://api.deepraxis.com";
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY");

async function request(path, method, body) {
  const response = await fetch(`${api}/database/v1${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${result.message}`);
  return result;
}

const database = await request("/databases", "POST", {
  name: "Agent tasks",
  labels: { purpose: "docs" },
});
await request(`/databases/${database.id}/tables`, "POST", {
  name: "tasks",
  fields: [
    { name: "title", type: "string", required: true, unique: true },
    { name: "done", type: "boolean" },
  ],
});

const supabase = createClient(`${api}/supabase/${database.id}/`, key, {
  auth: { persistSession: false, autoRefreshToken: false },
});

const inserted = await supabase.from("tasks")
  .insert({ title: "Write a summary", done: false }).select();
if (inserted.error) throw inserted.error;

const selected = await supabase.from("tasks")
  .select("*").order("title").limit(10);
if (selected.error) throw selected.error;
console.log({ database_id: database.id, rows: selected.data });
```

输出包含 `database_id` 和刚写入的行。保存库 ID 供后续请求使用。Supabase 地址末尾的斜杠不可省略。

[继续更新与删除行 →](/docs/api/compatible/supabase/tables) · [理解字段与版本 →](/docs/developers/database/concepts)

用完后，可用下面的请求删除示例库。恢复规则见[概念](/docs/developers/database/concepts)。

```bash
curl -X DELETE "https://api.deepraxis.com/database/v1/databases/${DATABASE_ID}" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

将 `DATABASE_ID` 设为示例输出的库 ID。

# 概念

## Database 是什么、不是什么 [#database-是什么不是什么]

Database 是 headless CMS 式的内容库：人、智能体和脚本通过公开 API 直接读写结构化的表与行。它保存行程、目录等内容，独立于使用这些内容的应用。它不是应用的私有运行库，也不是数据仓库。

| 角色    | 谁用                       | 生命周期                    | 存在哪里                                      |
| ----- | ------------------------ | ----------------------- | ----------------------------------------- |
| 内容库   | 人、智能体、脚本；应用也可通过公开 API 读取 | 内容独立于应用发布进行编辑和复用        | 结构化内容用 Database，文件用 File，可检索知识用 Knowledge |
| 应用运行库 | 管理订单、会话和业务规则的应用代码        | 状态随应用运行而变化，由应用自己的业务约束管理 | 应用自带的 `d1`、`kv` 或 `r2` components         |
| 数据仓库  | 分析人员和报表流水线               | 积累历史数据用于分析              | 专门的分析型数据仓库，不使用 Database                   |

典型模式是**内容库 → 构建 → 发布到应用**：从 Database 读取行程内容，构建站点文件，再用 [`praxis.application.publish`](/docs/developers/application) 上传、创建版本并部署。修改源内容不会更新这些已发布的文件；需要再次构建和发布才能包含修改。

库是隔离单元，表定义记录的结构，字段定义值的类型，行保存实际记录。

## 库、表、字段、行 [#库表字段行]

API Key 决定你能访问哪些库。表和字段可用 ID 或名称寻址；关联字段使用表 ID，因此改名不会改变关联目标。库的标签用于分类和筛选。

默认行只包含主键 `id` 和用户字段，`select=*`、`return=representation` 及 RPC 都遵循这一规则。字段保留名只有 `id`；新字段不能以 `_` 开头。`created_at`、`updated_at`、`version` 可作为普通字段。字段名以小写字母开头，后续只含小写字母、数字和下划线，最多 63 字；表名也允许下划线开头。已有下划线用户字段仍可读写，且优先于同名系统元数据；这时该表无法访问同名元数据。

系统元数据只读，需显式取用：`select=*,_version,_created_at,_updated_at`。`*` 不会展开它们。可用 `_version=eq.3` 过滤，或 `order=_created_at.desc` 排序。不指定 `order` 时，仍按内部创建时间再按 `id` 排序。过滤或排序使用旧元数据名且没有同名用户字段时返回 400：`details` 为 `use _created_at`（或对应的新名字），`hint` 为 `system metadata moved to underscore columns`。

时间字段可定义为 `{"name":"created_at","type":"datetime","default":"now()"}`。插入时省略该字段，就保存服务器当前 UTC 时间；读回字段定义仍显示 `"now()"`。显式 null 和 PATCH 中省略的字段不使用默认值。string/json 字段的 `"now()"` 默认值仍为字面量字符串。给已有表新增这个字段时，回填也会求值。

## 七种字段类型 [#七种字段类型]

| 类型         | 保存的值                                                                                                                                         |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `string`   | 字符串                                                                                                                                          |
| `integer`  | 整数                                                                                                                                           |
| `number`   | 数值                                                                                                                                           |
| `boolean`  | `true` 或 `false`                                                                                                                             |
| `datetime` | RFC 3339 日期时间，例如 `2026-10-11T08:30:00+08:00`；不存在的日期（如 2 月 30 日）返回 400。值统一按 UTC、3 位毫秒保存和读出，例如 `2026-10-11T00:30:00.000Z`；精度为毫秒，超过 3 位的小数秒四舍五入 |
| `json`     | 满足指定 JSON Schema 的 JSON 值                                                                                                                    |
| `link`     | 同一库中另一张表的行 ID，或多个行 ID                                                                                                                        |

`required` 要求写入非空值。新增带 `default` 的字段时，已有行一律用它回填；不带 `default` 的新字段在已有行上为空。在已有行的表上新增 `required` 字段时必须提供 `default`；把已有字段改为 `required` 时，若仍有行在该列为空则返回 400 `VALIDATION_FAILED` 并说明空行数，先补齐这些行再修改。`unique` 要求值唯一。前五种类型默认建立索引；不需要的字段可以设 `indexed: false`。`json` 和 `link` 不能设为索引字段。字段类型创建后不可修改。

## 批量写入与事务 [#批量写入与事务]

一次 POST 最多插入 **500 行**。单次请求是原子的：任何一行失败，整批回滚。跨请求没有事务；需要一起成功的行应放在一次批量 POST 中。有关联依赖时先查询目标，再插入引用它的行；先查后插不能保证并发期间目标不变，仍需处理插入时的关联错误。

同一批次会按所有行的键的并集补齐缺失值为 `null`（PostgREST 行为），因此某行省略必填字段可能导致整批失败。发送 `Prefer: missing=default` 可让缺失的键使用字段默认值；显式传入的 `null` 仍为 `null`，没有默认值的必填字段仍必须提供。

默认 `return=minimal`：成功返回 201，body 为空。需要读回新增行时发送 `Prefer: return=representation`，或在 supabase-js 的 `.insert(...)` 后调用 `.select()`。

## JSON Schema [#json-schema]

`json` 字段必须在 `options.schema` 中提供 JSON Schema。例如，下面的字段只接受包含字符串 `summary` 的对象。

```json
{
  "name": "detail",
  "type": "json",
  "options": {
    "schema": {
      "type": "object",
      "properties": { "summary": { "type": "string" } },
      "required": ["summary"],
      "additionalProperties": false
    }
  }
}
```

更新 schema 约束之后的写入，不会重写已有行。查询时可用 `detail->>summary` 读取字符串值。

## 关联与删除保护 [#关联与删除保护]

`link` 的 `options.linked_table_id` 指向同一库中的目标表，`multiple` 决定保存单个行 ID 还是行 ID 数组。关联目标创建后不可更改。

```json
{
  "name": "project",
  "type": "link",
  "options": { "linked_table_id": "tbl_example", "multiple": false }
}
```

把 `tbl_example` 换成已创建表的 ID。引用不存在的行会被拒绝；删除仍被其它行引用的行也会被拒绝；同一次删除中的行互相引用时可以一起删除。先解除引用，再删除目标，不会自动级联删除。

## 用 \_version 避免覆盖他人的更新 [#用-_version-避免覆盖他人的更新]

新行的 `_version` 为 1，每次更新递增。更新时同时过滤行 ID 和读到的版本；没有返回行表示该条件不再匹配，需要重新读取后决定下一步。

```typescript
const { data: row, error: readError } = await supabase.from("tasks")
  .select("*,_version").eq("id", rowId).single();
if (readError) throw readError;

const { data, error } = await supabase.from("tasks")
  .update({ done: true })
  .eq("id", row.id)
  .eq("_version", row._version)
  .select();
if (error) throw error;
if (data.length === 0) throw new Error("Read the row again before updating");
```

## 删除库后 7 天内恢复 [#删除库后-7-天内恢复]

删除库后不能再读写其中的行。在 7 天内调用[恢复库](/docs/api/native/database/databases#恢复库)可以重新使用库及保留的表和行；超过窗口无法恢复。此恢复能力针对整个库，不是单独删除的表、字段或行。

[API 参考 →](/docs/api/native/database) · [限制与错误 →](/docs/developers/database/limits-and-errors)

# 限制与错误

按以下边界规划库与查询。

| 限制                  | 上限              |
| ------------------- | --------------- |
| 每库最多表数              | 50              |
| 每表最多字段数             | 64              |
| 每表最多索引字段数（含 unique） | 16              |
| 每表最多行数              | 1000000         |
| 单行最多字节数             | 2097152 (2 MiB) |
| 单次插入最多行数            | 500             |
| 单条查询最多参数值数          | 100             |
| 删除库后可恢复天数           | 7               |
| 单次查询最多扫描行数          | 3000000         |

单条请求最长执行 8 秒，超时返回 57014（重试不会成功）；给过滤和排序字段建索引，或缩小查询范围。不带过滤的计数总是很快。不指定 limit 时每次最多返回 1000 行，用 limit / offset 或 Range 翻页。大表上需要批量修改或删除时，按主键或索引字段的范围分批执行，例如每批几万行。单次批量插入可包含上述行数上限，不必将每行的字段数相乘作为查询参数数目。

[错误码与重试属性 →](/docs/api/native/database/errors)
一次批量 POST 最多插入 **500 行**，单次请求是原子的：任何一行失败，整批回滚。跨请求没有事务；需要一起成功的行应放在一次批量 POST 中。有关联依赖时先查后插，并处理并发变化导致的关联错误。

同一批次按所有行的键的并集补齐缺失值为 `null`（PostgREST 行为）。发送 `Prefer: missing=default` 可改用字段默认值；显式 `null` 仍为 null，没有默认值的必填字段仍必须提供。默认 `return=minimal` 返回 201、body 为空；发送 `Prefer: return=representation` 才返回行。

## 按错误细节修正请求 [#按错误细节修正请求]

保留字段名只有 `id`；新字段名不能以 `_` 开头，这个前缀留给系统元数据。`created_at`、`updated_at`、`version` 可用作普通字段名。datetime 字段可用 `"now()"` 作为默认值。

`details` 指出字段或 JSON 路径及实际收到的值，批量校验还包含从 0 开始的行号。值最多显示 80 个字符，不回显整行。`hint` 给出修正方式，没有建议时为 null。下面是省略第 2 行必填 title 的错误示例：

```json
{
  "code": "23514",
  "message": "row validation failed",
  "details": "row 1: title: required value (got null)",
  "hint": "Rows in one insert are filled to the union of their keys with null (PostgREST behavior). Send every key in every row, or send Prefer: missing=default to use field defaults."
}
```

唯一冲突返回 409，例如：

```json
{
  "code": "23505",
  "message": "unique constraint violated",
  "details": "Key (email)=(a@b.c) already exists.",
  "hint": null
}
```

# 保险库

Vault 保管智能体调用第三方服务时需要的凭据，例如 GitHub 令牌、OpenAI API Key 或 Linear 的授权。每个库是一组 Secret，一个 Secret 是一个凭据。

Secret 只写：值写入后任何接口都不会再返回它。每个 Secret 都带一组允许的域名，只有发往这些域名的请求才能用到它。智能体和工具按名字引用 Secret，例如 `GITHUB_TOKEN`。

* [快速开始](/docs/developers/vault/quickstart)：建库、加 Secret，再列出。
* [概念](/docs/developers/vault/concepts)：Secret 的种类、模板、导入、一键授权与续期。
* [限制与错误](/docs/developers/vault/limits-and-errors)：容量上限和错误处理。

[API 参考 →](/docs/api/native/vault) · [Claude 托管智能体凭据库 →](/docs/api/compatible/anthropic/managed-agents) · [Cookbook →](/docs/cookbook)

# 快速开始

把 API Key 放进 `PRAXIS_API_KEY` 环境变量，依次运行下面三个请求。每次运行都会创建一个新库。请先安装 `jq` 以提取 ID；可设置 `VAULT_NAME` 自选显示名称。

## 1. 建库 [#1-建库]

```bash
VAULT_JSON=$(curl --fail-with-body -sS -X POST "https://api.deepraxis.com/vault/v1/vaults" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg name "${VAULT_NAME:-Agent credentials}" '{display_name: $name}')")
export VAULT_ID=$(printf '%s' "$VAULT_JSON" | jq -er '.id')
printf '%s\n' "$VAULT_JSON"
```

期望：HTTP 200，返回 `type` 为 `vault`、`status` 为 `active`、`secret_count` 为 `0` 的对象。命令已把返回的 `vlt_` ID 保存到 `VAULT_ID`。

## 2. 加入 Secret [#2-加入-secret]

```bash
curl -X POST "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/secrets" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "GITHUB_TOKEN", "value": "ghp_example", "allowed_hosts": ["api.github.com"]}'
```

期望：HTTP 200，返回 `type` 为 `vault_secret`、`kind` 为 `static`、`version` 为 `1` 的对象。响应里没有 `value`：值写入后不会再返回。这个 Secret 只能用在发往 `api.github.com` 的请求上。

## 3. 列出 Secret [#3-列出-secret]

```bash
curl "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/secrets" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

期望：HTTP 200，`data` 中有一项，`name` 为 `GITHUB_TOKEN`；`next_cursor` 为 `null`。

## 清理 [#清理]

```bash
curl -X POST "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/archive" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

curl -X DELETE "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"
```

期望：归档返回 HTTP 200、`status: "archived"`，随后删除返回 HTTP 200、`{"id": "<VAULT_ID>", "type": "vault_deleted"}`。删除会一并删除库中的全部 Secret，不可恢复。

[从模板开始、导入 .env 与一键授权 →](/docs/developers/vault/concepts) · [完整请求与对象 →](/docs/api/native/vault)

# 概念

库是一组 Secret；Secret 是一个凭据，由名字、值和允许的域名组成。其余功能都只是产生或更新 Secret 的方式。

## 库与 Secret [#库与-secret]

API Key 决定你能访问哪些库。库有显示名称和最多 16 对 metadata 标签。

Secret 的名字在库内唯一，创建后不能修改，只能使用大写字母、数字和下划线，长度为 1–128 个字符，可以以下划线开头，但不能以数字开头，例如 `GITHUB_TOKEN`。可以用名字或 `vsec_` 开头的 ID 寻址一个 Secret；非法名称查找也返回 404 `SECRET_NOT_FOUND`。

值只写：创建或替换后，任何接口都不再返回它。每次替换值，`version` 加一。

## 允许的域名 [#允许的域名]

每个 Secret 必须声明 `allowed_hosts`，1–16 个；空数组返回 400 `INVALID_PARAMS`。只有发往这些域名的请求才能用到这个 Secret。

| 写法               | 匹配                                           |
| ---------------- | -------------------------------------------- |
| `api.github.com` | 只匹配这个域名                                      |
| `*.example.com`  | 匹配 `example.com` 的任意子域名，不匹配 `example.com` 本身 |
| `192.0.2.1`      | 只匹配这个 IPv4 地址                                |

域名写入时统一转为小写，去掉末尾的点，国际化域名转为 punycode。重复项会去重。不提供“任意域名”。IPv6、单标签主机（包括 `localhost`）、`*.internal`、平台域名，以及回环、链路本地、私网与 CGNAT IPv4 均返回 400 `INVALID_PARAMS`。

## 两种 Secret [#两种-secret]

| kind     | 值从哪来              | 状态                                     |
| -------- | ----------------- | -------------------------------------- |
| `static` | 你填写的值             | `active`                               |
| `oauth`  | 一键授权得到的访问令牌，由平台续期 | `active`、`refreshing`、`refresh_failed` |

两种 Secret 归档后状态都是 `archived`。

## 模板 [#模板]

模板为常用服务预填名字、允许的域名和种类，你只需填写值。`GET /vault/v1/templates` 列出全部模板，static 模板有 `github`、`openai`、`anthropic`、`stripe`，OAuth 模板有 `linear`、`atlassian`。GitHub 预填 `GITHUB_TOKEN` 和 `api.github.com`、`uploads.github.com`。添加 static Secret 时可传 `template_id`，其来源记录为 `metadata.template`。

`metadata.source` 和 `metadata.template` 是系统保留键，调用方不能写入，也不占自定义标签的 16 对限额。更新库或 Secret 的 metadata 按键合并：字符串写入，`null` 删除该键，省略的键保留。Secret 的 `description: null` 清除备注，省略则保留。

## 导入 .env [#导入-env]

`POST /vault/v1/vaults/{vault_id}/secrets:import` 一次创建多个 static Secret。传 `dotenv`（`KEY=VALUE` 多行文本）或 `lines` 数组，二者选一；新建时，`allowed_hosts` 默认值应用到没有自带域名的行。同名 Secret 只替换值，只有该行自身的 `allowed_hosts` 才会改变原域名，顶层默认域名不会覆盖原域名。结果分别列出新建、替换和失败的行，失败行带行号与错误码，其余行照常写入。

## 一键授权 [#一键授权]

Linear（CIMD：客户端元数据文档）和 Atlassian（DCR：动态客户端注册）可以通过一键授权产生 `oauth` Secret，无需事先注册应用。需要应用注册的服务（GitHub、HubSpot、Asana、Vercel）请手动添加凭据；Asana 不在一键授权模板中。

1. 调用 `POST /vault/v1/authorizations`，传支持授权的 `template_id`，或要授权的 `resource_url`。
2. 把返回的 `connect_url` 交给最终用户。这个链接 30 分钟内有效、只能使用一次。
3. 用户在确认页看到发起方、目标服务和申请的权限，同意后跳转到服务方完成授权。
4. 完成后，库中出现名为 `secret_name` 的 `oauth` Secret。读取 `GET /vault/v1/authorizations/{id}` 可查看状态：`pending`、`authorizing`、`completed`、`failed` 或 `expired`。

库中已有同名 Secret 时，授权会原地更新它：ID 不变，`version` 加一。

需要授权后回到你的应用时，传 `return_url`。它必须在回跳地址白名单内：用 `PUT /vault/v1/settings/return_urls` 设置，最多 10 个 https 地址，精确匹配。回跳时会带上 `authorization_id` 和 `status`。

## 续期与校验 [#续期与校验]

`oauth` Secret 的访问令牌由平台在过期前续期。服务方拒绝续期时，Secret 变为 `refresh_failed`，`oauth.last_error` 说明原因；重新授权或替换值后恢复为 `active`。服务方暂时不可用时，Secret 保持 `active` 并稍后重试。

`POST …/secrets/{secret}/validate` 用当前令牌向服务发一次探测，返回 `valid`、`invalid` 或 `unknown`，只适用于 `oauth` Secret。调用方收到服务的 401 后，可以调用 `POST …/secrets/{secret}/refresh` 立即续期；同样仅支持 `oauth` Secret。对 static Secret 校验返回 400 `VALIDATE_UNSUPPORTED`，续期返回 400 `SECRET_KIND_MISMATCH`。

## 审计事件 [#审计事件]

库的事件记录创建、更新、归档、删除、使用、续期和授权结果，不包含 Secret 的值。`GET /vault/v1/vaults/{vault_id}/events` 按时间倒序列出，可用 `secret` 按名称或 ID 过滤；未知 Secret 返回空列表。更新事件的 `detail.changed` 列出修改的字段，例如 `value`、`allowed_hosts`。

事件没有按天自动过期的保留期；归档保留事件，删除库或 Secret 会清除其历史事件，仅留下删除记录。`limit` 默认 50、最大 200；超限或无效 `cursor` 返回 400 `INVALID_PARAMS`。完整事件类型见[事件参考](/docs/api/native/vault/vaults)。

## 归档与删除 [#归档与删除]

归档库会同时归档库内全部 Secret 并清除它们的值，之后不能再写入。删除库会删除全部 Secret，不可恢复。单个 Secret 同样可以归档或删除。

## 与 Claude 托管智能体凭据库兼容 [#与-claude-托管智能体凭据库兼容]

同一批库也可以用 Claude 托管智能体的[官方 SDK 兼容接口](/docs/api/compatible/anthropic/managed-agents)访问（`/anthropic/v1/vaults`）。每个请求需要 API Key、`anthropic-version: 2023-06-01`，以及包含 `managed-agents-2026-04-01` 的 `anthropic-beta`；该头可用逗号分隔包含其它 beta。列表使用 `page` 而非 `cursor` 分页；凭据只能用 `vsec_` ID 寻址。库的 `display_name: null` 返回 400。

错误体为 `{type: "error", error: {type, message}, request_id}`；409 对应 `conflict_error`。409 等不可重试错误带 `x-should-retry: false`，不会触发 SDK 自动重试。

| 凭据类型                   | 对应的 Secret                                                                     |
| ---------------------- | ------------------------------------------------------------------------------ |
| `environment_variable` | 名字为 `secret_name`，允许的域名为 `networking.allowed_hosts`；`networking` 只支持 `limited` |
| `static_bearer`        | 允许的域名为 `mcp_server_url` 的主机，名字自动生成为 `MCP_<主机>`                                 |
| `mcp_oauth`            | 暂不支持创建，返回 501；请改用一键授权                                                          |

通过 `/vault/v1` 创建的 Secret 在该接口中显示为 `environment_variable`，`injection_location` 为只在请求头中替换。

# 限制与错误

按以下边界规划库与 Secret。

| 限制                           | 上限                   |
| ---------------------------- | -------------------- |
| 每库有效 Secret 数                | 50                   |
| Secret 值大小                   | 16384 bytes (16 KiB) |
| 每个 Secret 的允许域名数             | 16                   |
| 单次导入行数                       | 50                   |
| 授权创建 / OAuth 校验（每账户每分钟，各自计数） | 30 / 30              |
| 库名称长度                        | 255                  |
| metadata 键值对数                | 16                   |
| metadata 键 / 值长度             | 64 / 512             |
| 列表每页条数                       | 50 (最大 200)          |

同名 Secret 已存在返回 409 SECRET\_CONFLICT；往已归档的库写入返回 409 VAULT\_INVALID\_STATE；超过有效 Secret 上限返回 400 VAULT\_LIMIT\_EXCEEDED；值超过 16 KiB 返回 413 SECRET\_TOO\_LARGE。一键授权创建与 OAuth 校验各按账户独立计数，每个固定一分钟窗口各最多 30 次；超出返回 429 RATE\_LIMITED，等待下一窗口重试。授权链接过期返回 410 AUTHORIZATION\_EXPIRED，需要重新发起授权。

通过 Claude 托管智能体的凭据库接口访问时，每库最多 20 个有效凭据，错误使用该接口自己的错误格式。

[错误码与重试属性 →](/docs/api/native/vault/errors)

# 账户与计费

管理你的 Praxis 账户——Key、用量、费用和配额。

| 你想做什么         | 去哪                                            |
| ------------- | --------------------------------------------- |
| 创建或管理 API Key | [API Keys](/docs/developers/account/api-keys) |
| 查看调用量和费用      | [用量与费用](/docs/developers/account/usage)       |
| 充值或了解计费模式     | [费用与充值](/docs/developers/account/billing)     |
| 了解速率限制和配额     | [配额与限制](/docs/developers/account/quotas)      |

# API Keys

API Key 是你调用 Praxis API 的凭证——`sk-praxis-` 开头的一串字符。每个 Key 属于一个账户空间，通过它产生的调用都记在那个空间名下。

## 创建 Key [#创建-key]

1. 打开 [控制台 → API Keys](https://console.deepraxis.ai/keys)
2. 点「创建 Key」
3. 填写：

| 字段  | 说明                                   |
| --- | ------------------------------------ |
| 名称  | 必填，用于识别用途（如 `prod-backend`、`my-app`） |
| 有效期 | 30 / 60 / 90 天或永久，默认 90 天            |

4. 点「创建」

创建成功后弹窗会展示完整 Key 并带「复制 Key」按钮——**立即复制保存**。关闭弹窗后列表只显示掩码，但你可以随时点行内复制图标重新获取完整 Key。

## 管理 Key [#管理-key]

在 [API Keys 列表页](https://console.deepraxis.ai/keys)，你可以：

* **复制**：悬停 Key 行，点复制图标取回完整 Key
* **禁用**：临时停用某个 Key（不删除，之后可重新启用）
* **删除**：永久移除——已删除的 Key 立即失效，无法恢复

## 安全实践 [#安全实践]

* **不要把 Key 写进代码**——用环境变量（`export PRAXIS_API_KEY=sk-praxis-…`）
* **不同环境用不同 Key**——开发/测试/生产各自一个，出问题时只吊销受影响的那个
* **定期轮换**——长期运行的系统建议每 90 天换新 Key
* **泄露了怎么办**：立即删除该 Key，创建新的，更新你的应用

## 常见问题 [#常见问题]

| 症状                  | 原因                | 解法                   |
| ------------------- | ----------------- | -------------------- |
| 401 Invalid API key | Key 已删除/禁用，或复制不完整 | 到控制台确认 Key 状态；重新完整复制 |
| Key 到期了怎么办          | 有效期到了             | 创建新 Key 并更新到你的应用     |
| 能创建多少个 Key          | —                 | 每个账户空间可创建多个 Key，按需管理 |

# 定价

Praxis 的价格只有一条规则：以模型厂商的官网价为基准，Praxis 标价低于官网价。每个模型的具体价格在[模型目录](/docs/developers/model/catalog)里，单位是美元每百万 token，不含税。

## 价格怎么定 [#价格怎么定]

* 基准是厂商官网定价页的价格，Praxis 记录来源与日期。
* Praxis 标价对所有客户相同，低于官网价；折扣对输入、输出、缓存创建、缓存读取四种 token 类型一致。
* 厂商调价时，Praxis 标价同步更新；Praxis 主动调整标价提前 30 天通知。

## 怎么付费 [#怎么付费]

* 预付充值，按实际消耗扣减，用多少付多少；订阅也是用量的订阅，不卖许可。
* 未消耗的余额可以兑付。
* 用量与余额在控制台的[用量](/docs/developers/account/usage)与[费用与充值](/docs/developers/account/billing)页查看。

## 经合作伙伴购买 [#经合作伙伴购买]

Praxis 可能与介绍您的合作伙伴存在分润安排，不影响您的价格：经合作伙伴购买不会比直接购买更贵。合作伙伴计划的规则在[合作伙伴](/docs/partners)。

# 用量与费用

每一次 API 调用都有账、可追溯。控制台的用量页提供汇总视图和逐笔明细。

## 查看用量 [#查看用量]

打开 [控制台 → 用量与费用](https://console.deepraxis.ai/usage)。

**汇总视图**（默认）：

* 本月花费
* 用量事件数
* 按资源（模型）聚合的调用量和费用

**逐笔明细**：点「查看用量明细」或点击某个资源行，可以看到每一笔调用的：

* 时间
* 模型
* 输入 / 输出 token 数
* 费用

## 费用怎么算 [#费用怎么算]

按次计费、无闲置费。具体来说：

* **输入 token** 和 **输出 token** 分别计价（各模型单价不同，见[模型目录](https://console.deepraxis.ai/models)）
* **推理 token**（思维链）计入输出 token
* 不调用不收费——没有月费、没有最低消费

## API 层获取费用 [#api-层获取费用]

除了控制台，你还可以在 API 响应中直接获取本次调用的费用：

* **OpenAI 兼容协议**：响应的 `usage.cost` 字段直接返回费用金额
* **Anthropic 协议**：Python SDK 的 `message.usage.cost` 可读（JS SDK 的类型层不声明该字段，但协议层有）

这意味着你可以在自己的应用里实时追踪成本，不需要轮询控制台。

## 常见问题 [#常见问题]

| 症状         | 原因           | 解法                                                          |
| ---------- | ------------ | ----------------------------------------------------------- |
| 用量页显示为空    | 还没有调用过       | 发一次请求后刷新                                                    |
| 明细和我的记录对不上 | 推理 token 也计费 | 看 `usage` 里的 `reasoning_tokens`                             |
| 余额不足会怎样    | —            | 调用会返回余额不足错误，到[资金账户](https://console.deepraxis.ai/billing)充值 |

# 费用与充值

Praxis 采用按次计费模式——每次 API 调用按实际 token 用量计费，不调用不收费。

## 计费模式 [#计费模式]

| 项目       | 说明                      |
| -------- | ----------------------- |
| 计费方式     | 按次按 token，输入和输出分别计价     |
| 闲置费      | 无——不调用不产生费用             |
| 最低消费     | 无                       |
| 推理 token | 计入输出 token（推理模型的思维链也计费） |
| 免费额度     | 新账号自带 $1                |

各模型的具体单价见[模型目录](https://console.deepraxis.ai/models)。

## 充值 [#充值]

1. 打开 [控制台 → 资金账户](https://console.deepraxis.ai/billing)
2. 点「充值」
3. 选择金额和支付方式

充值后余额立即可用。

## 余额不足时 [#余额不足时]

当余额耗尽，API 调用会返回余额不足错误（HTTP 402 或类似错误码），不会静默降级。你的应用应处理这个错误并提示用户充值。

## 监控花费 [#监控花费]

* **实时**：API 响应中的 `usage.cost` 字段直接返回本次调用费用
* **汇总**：[控制台 → 用量与费用](https://console.deepraxis.ai/usage)按月/按资源聚合
* **逐笔**：用量明细页每一笔调用都有时间、模型、token 数和费用

## 常见问题 [#常见问题]

| 症状       | 原因           | 解法                                 |
| -------- | ------------ | ---------------------------------- |
| 调用返回 402 | 余额不足         | 到资金账户充值                            |
| 花费比预期高   | 推理 token 也计费 | 查看 `usage.reasoning_tokens`；换非推理模型 |
| 免费额度用完了  | —            | 充值继续使用                             |

# 配额与限制

## 速率限制 [#速率限制]

Praxis API 对每个账户空间施加速率限制，防止滥用。超限时返回 HTTP 429（Too Many Requests），响应头包含：

| 响应头                              | 含义        |
| -------------------------------- | --------- |
| `X-RateLimit-Limit-Requests`     | 每分钟允许的请求数 |
| `X-RateLimit-Remaining-Requests` | 本窗口剩余请求数  |
| `X-RateLimit-Reset-Requests`     | 距离重置的秒数   |

**处理方式**：收到 429 时按 `Reset` 头等待后重试，或减少并发。

## 模型调用限制 [#模型调用限制]

| 限制    | 说明                                                                     |
| ----- | ---------------------------------------------------------------------- |
| 并发请求  | 每个账户空间同时进行的请求数有上限                                                      |
| 上下文长度 | 每个模型有自己的上下文上限（如 1M token），见[模型目录](https://console.deepraxis.ai/models) |
| 流式支持  | 按模型路由逐个开通（报 `STREAMING_UNPROVEN` 表示该路由暂未开放流式）                          |

## 提升配额 [#提升配额]

需要更高的速率限制或并发数？[联系我们](https://console.deepraxis.ai/support)说明你的使用场景和需要的配额。

## 常见问题 [#常见问题]

| 症状                    | 原因          | 解法                          |
| --------------------- | ----------- | --------------------------- |
| 429 Too Many Requests | 速率限制        | 按 `X-RateLimit-Reset` 等待后重试 |
| 上下文超限                 | 输入超过模型上限    | 截断输入或换大上下文模型                |
| STREAMING\_UNPROVEN   | 该模型路由暂不支持流式 | 去掉 `stream: true` 或换模型      |

# 帮助与参考

这里保留跨章节的帮助资料。API 鉴权、端点、错误与版本已经归入顶层 [API](/docs/api)。

# 术语表

**API Key**——调用 Praxis API 的凭证，`sk-praxis-` 开头。

**Agent**（智能体）——有上下文、有记忆、能调用工具的 AI 服务。

**Anthropic 兼容**——Praxis 支持的协议之一，Anthropic SDK 改 `base_url` 即可迁移。

**BYOK**（Bring Your Own Key）——自带上游 Key，平台统一路由。

**上下文窗口**（Context Window）——模型一次能处理的 token 上限。

**流式**（Streaming）——逐 token 输出。部分路由暂未开放。

**模型**（Model）——大语言模型，Praxis 聚合多家供应商统一提供。

**OpenAI 兼容**——Praxis 支持的协议之一，OpenAI SDK 改 `base_url` 即可迁移。

**推理模型**（Reasoning Model）——回复前先"思考"，响应含 `thinking` 块。

**滚动入口**（Rolling Entry）——始终指向最新版本的模型别名（如 `deepseek-flash`）。

**版本快照**——固定版本的模型 ID（如 `deepseek-v4-flash`）。

**沙箱**（Sandbox）——安全隔离的代码执行环境。

**Session**（会话）——智能体的对话上下文单元。

**Token**——模型处理文本的基本单位。1 个中文字 ≈ 1-2 token。

**用量**（Usage）——API 调用的统计记录，含 token 数和费用。

# 服务状态

状态页：`https://status.deepraxis.ai`（建设中，ticket-3549）。

## 各服务可用性 [#各服务可用性]

| 服务         | 状态 |
| ---------- | -- |
| API（模型调用）  | 正常 |
| 控制台        | 正常 |
| Praxis App | 正常 |

## 服务异常时怎么办 [#服务异常时怎么办]

1. 先确认是不是自己的网络问题——试试 `curl https://api.deepraxis.ai/openai/v1/models`（预期返回 401 认证挑战，说明服务活着）
2. 如果 API 确实不通，到[控制台 → 帮助与反馈](https://console.deepraxis.ai/feedback)报告
3. 附上时间点和错误信息原文，方便我们查日志

## 历史事件 [#历史事件]

（待建设：事件时间线将发布在状态页。）

# 常见问题

## 模型 ID 怎么填 [#模型-id-怎么填]

到[控制台 → 模型](https://console.deepraxis.ai/models)复制确切 ID。`deepseek-flash` 是滚动入口（始终最新 Flash），`deepseek-v4-flash` 是固定版本。

## 国内直连能访问吗 [#国内直连能访问吗]

`api.deepraxis.ai` 通过 Cloudflare 全球网络分发，国内一般可直连。需要代理也支持标准 HTTP 代理。

## 余额为零会怎样 [#余额为零会怎样]

API 返回余额不足错误（HTTP 402），不会静默降级。充值后立即恢复。

## Key 泄露了怎么办 [#key-泄露了怎么办]

到 [API Keys](https://console.deepraxis.ai/keys) 立即删除 → 创建新 Key → 更新应用。

## 流式不出字 [#流式不出字]

可能是 `STREAMING_UNPROVEN`（换模型或去掉流式）、推理模型先思考（等几秒）、或网络瞬断（重试）。

## 免费额度有多少 [#免费额度有多少]

新注册 **$1**。`deepseek-flash` 大约能跑几千次对话。

## 能同时用多个模型吗 [#能同时用多个模型吗]

可以——同一请求换 `model` 字段即可，不需要多个 Key。

## 和 OpenAI / Anthropic 官方什么关系 [#和-openai--anthropic-官方什么关系]

Praxis 提供 OpenAI / Anthropic 兼容协议——已有代码只改 `base_url` 即可迁移。详见[迁移指南](/docs/cookbook/migration)。

## 还有别的问题？ [#还有别的问题]

[联系我们](https://console.deepraxis.ai/support)。

# 支持与反馈

## 控制台反馈 [#控制台反馈]

最直接的渠道：[控制台 → 帮助与反馈](https://console.deepraxis.ai/feedback)。你可以：

* 提交 Bug 报告（附上截图或错误信息更高效）
* 提出功能建议
* 查看你的反馈处理进度

工作日通常当天回复。

## 报 Bug 时请附上 [#报-bug-时请附上]

| 信息               | 为什么需要          |
| ---------------- | -------------- |
| 你做了什么（操作步骤）      | 复现问题           |
| 期望看到什么 vs 实际看到什么 | 确认是 Bug 还是设计如此 |
| 错误信息原文           | 定位根因           |
| 浏览器 / 操作系统       | 排除环境因素         |
| 时间点（大约）          | 查服务端日志         |

## API 问题 [#api-问题]

如果是 API 调用报错，附上：

* 请求的**模型 ID**
* **HTTP 状态码**和**错误响应原文**
* **请求时间**（我们按时间查日志）
* 你的 **API Key 前缀**（`sk-praxis-` 后的前几个字符，不要发完整 Key）

# 安全与合规

## 数据驻留 [#数据驻留]

所有数据默认存储在 &#x2A;*APAC（亚太）** 区域。创建新资源（如 Durable Object）时默认使用 `locationHint: "apac"`。

## 文件与数据保留 [#文件与数据保留]

| 数据类型     | 保留策略         |
| -------- | ------------ |
| 对话记录     | 持久保存，直到你主动删除 |
| API 调用日志 | 按平台日志策略保留    |
| 上传文件     | 持久保存，直到你主动删除 |
| 已删除的数据   | 删除后无法恢复      |

## API 安全 [#api-安全]

* **Key 管理**：API Key 是你的调用凭证，不要写进代码或提交到版本控制。详见 [API Keys](/docs/developers/account/api-keys)。
* **传输加密**：所有 API 通信通过 HTTPS 加密。
* **认证方式**：API Key（`Authorization: Bearer sk-praxis-…`）或 `x-api-key` 头。

## 隐私政策与服务条款 [#隐私政策与服务条款]

* [隐私政策](https://deepraxis.ai/privacy)
* [服务条款](https://deepraxis.ai/terms)

## 备案信息 [#备案信息]

`deepraxis.com` 域名已完成 ICP 备案。境内访问经深圳 ECS 反代进入，备案号显示于页脚。

导出自 Praxis 文档 · 2026-10-22 · 共 101 页
