DASP/1.0 客户端接入规范
1. 模块定位
本文是 DASP/1.0 的客户端和 SDK 独立接入模块。完整的逐场景接口入参、出参、错误和注意事项见
dasp-api-reference-v1.md;服务端协议、存储、执行和容灾约束见
stateful-session-protocol-v1.md。
客户端只负责:
保存 sid
提交本次新增 input 和 model config
消费七种流事件
把 Item 投影为界面
掉线后从头恢复当前回答
按明确顺序停止、中途补充、编辑或删除
按需分页查询单会话或自动合并的多会话可见历史
客户端不得自行拼接完整模型上下文、计算压缩边界、伪造 Usage、重复提交恢复请求,或把 SID 当成身份凭证。
2. 接口速查
| 业务 | 方法与路径 | SID | 请求体 |
|---|---|---|---|
| 首次提交 | POST /v1/session/responses |
不发送 | Input + Model Config |
| 继续会话 | POST /v1/session/responses |
必须 | Input + Model Config |
| 恢复当前回答 | GET /v1/session/responses |
必须 | 无 |
| 停止当前回答 | POST /v1/session/responses/cancel |
必须 | 无 |
| 中途打断并补充 | POST /v1/session/responses/interrupt |
必须 | 补充 Input + Model Config |
| 编辑最近用户消息 | PUT /v1/session/responses |
必须 | 替换 Input + Model Config |
| 查询或合并历史 | GET /v1/session/history?limit=&order=&cursor= |
1~100 个逗号分隔 SID | 无 |
| 更新 System Prompt | PATCH /v1/session |
必须 | system_prompt |
| 删除会话 | DELETE /v1/session |
必须 | 无 |
所有请求仍需正常携带平台认证信息。SID 只定位会话,不能代替 API Key、JWT 或租户认证。
3. 最小数据类型
export type SessionItem =
| { id?: string; type: "text"; status?: ItemStatus; data: string }
| { id: string; type: "reasoning"; status: ItemStatus; output?: string }
| { id: string; type: "compaction"; status: ItemStatus; output?: string; usage?: Usage }
| { type: "input_file"; file_url: string; filename?: string }
| { id: string; type: "file_analysis"; status: ItemStatus; file_url: string; data?: string; source?: string; usage?: Usage }
| { id: string; type: "function_call"; status: ItemStatus; call_id: string; name: string; arguments: unknown }
| { type: "function_call_output"; call_id: string; output: unknown }
| { id?: string; type: string; [key: string]: unknown };
export type ItemStatus = "running" | "completed" | "failed";
export interface Usage {
input_tokens: number;
output_tokens: number;
total_tokens: number;
cached_tokens: number;
reasoning_tokens: number;
}
export type StreamEvent =
| { type: "start"; id: string; model: string }
| { type: "item"; item: SessionItem }
| { type: "delta"; id: string; data: string }
| ({ type: "usage" } & Usage)
| { type: "done"; id: string; status: "completed" | "canceled" }
| { type: "error"; code: string; message: string }
| { type: "interrupt"; id: string; status: "canceling" | "canceled" | "outcome_unknown"; usage?: Usage }
| { type: string; [key: string]: unknown };
export interface HistoryExchange {
sid: string;
status: "completed" | "canceled";
question_at: string;
question: SessionItem[];
answer_at: string;
answer: SessionItem[];
}
客户端必须保留 Unknown Event/Item 的忽略能力,不能因服务端增加可选 Item 而终止整个流。
4. SID 生命周期
4.1 首次请求
首次 POST 不发送 sid Header。成功响应始终通过响应头返回:
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
浏览器通过 response.headers.get("sid") 读取。服务端会公开 CORS Header。
4.2 本地保存
- SID 应与当前登录主体、环境和 API Base URL 一起保存;
- 用户退出登录、切换租户或切换 API Key 时,不得自动复用旧 SID;
- SID 可以保存在应用数据库、系统安全存储或受控浏览器存储;
- 不把 SID 放入公开 URL、埋点、Referer 或第三方日志;
- 客户端埋点只记录操作类型、耗时、状态码和事件计数,不记录
input、工具输出或完整历史; - SID 泄露不应造成越权,但仍应视为会话隐私标识处理。
4.3 无效 SID
400 invalid_sid:本地值损坏,应停止重试;404 session_not_found:会话不存在、已删除或不属于当前主体,应清理本地 SID;- 不要收到 404 后自动用原消息创建新 SID,除非产品明确要求用户确认。
除 History 外,每个请求只能携带一个 SID。History 可以在同一个 sid Header 中传入 1~100 个
逗号分隔的 SID;这只是读取合并视图,不会把这些会话的 Context 或生命周期合并。
5. 提交与非流式响应
async function submitOnce(baseUrl: string, apiKey: string, sid: string | null, input: string) {
const headers: Record<string, string> = {
"content-type": "application/json",
"x-api-key": apiKey,
};
if (sid) headers.sid = sid;
const response = await fetch(`${baseUrl}/v1/session/responses`, {
method: "POST",
headers,
body: JSON.stringify({ model: "gpt-5.6-sol", input, stream: false }),
});
const nextSid = response.headers.get("sid") ?? sid;
const body = await response.json();
if (!response.ok) throw protocolError(response.status, body);
return { sid: nextSid, response: body };
}
非流请求的浏览器连接断开不取消服务端任务。发生网络异常时,不要重复 POST;应使用同一 SID 调用 恢复 GET,以免把业务上的“继续会话”误当成网络重试。
6. Fetch SSE 解析
浏览器原生 EventSource 不能稳定附带自定义 SID 和认证 Header,因此推荐使用 Fetch 读取 SSE:
async function* readSse(response: Response): AsyncGenerator<StreamEvent> {
if (!response.ok) {
const body = await response.json().catch(() => null);
throw protocolError(response.status, body);
}
if (!response.body) throw new Error("missing_response_body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
buffer += decoder.decode(value, { stream: !done });
let boundary: number;
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, boundary).replace(/\r/g, "");
buffer = buffer.slice(boundary + 2);
const data = frame
.split("\n")
.filter(line => line.startsWith("data:"))
.map(line => line.slice(5).trimStart())
.join("\n");
if (data) yield JSON.parse(data) as StreamEvent;
// `: ping` 没有 data 行,会自然忽略。
}
if (done) break;
}
}
生产 SDK 还应限制单帧、缓冲区和完整响应最大字节数,防止异常代理或服务端导致无界内存增长。
7. 客户端投影状态机
客户端按 Item ID Upsert,不按事件数量创建重复气泡:
interface ActiveProjection {
responseId: string | null;
model: string | null;
order: string[];
items: Map<string, SessionItem>;
usage: Usage | null;
interrupted: { id: string; status: string; usage?: Usage } | null;
status: "idle" | "running" | "interrupting" | "completed" | "canceled" | "failed";
}
function applyEvent(state: ActiveProjection, event: StreamEvent) {
switch (event.type) {
case "start":
state.responseId = event.id;
state.model = event.model;
state.status = "running";
return;
case "item": {
const id = event.item.id;
if (!id) return;
if (!state.items.has(id)) state.order.push(id);
state.items.set(id, structuredClone(event.item));
return;
}
case "delta": {
const item = state.items.get(event.id);
if (!item) return;
if (item.type === "text") item.data = (item.data ?? "") + event.data;
if (item.type === "reasoning") item.output = (item.output ?? "") + event.data;
return;
}
case "usage":
state.usage = event;
return;
case "done":
state.status = event.status;
return;
case "error":
state.status = "failed";
return;
case "interrupt":
state.interrupted = { id: event.id, status: event.status, usage: event.usage };
if (event.status === "canceling") state.status = "interrupting";
return;
default:
return;
}
}
item 是完整 Upsert,后到的 completed Item 可以替换 running Item。delta 只追加到已有 Item;若
因为异常网络顺序先收到 Delta,SDK 可以短暂缓冲,但不能创建错误 Item 类型。
8. 断线恢复
恢复请求:
async function recover(baseUrl: string, apiKey: string, sid: string) {
return fetch(`${baseUrl}/v1/session/responses`, {
method: "GET",
headers: { "x-api-key": apiKey, sid, accept: "text/event-stream" },
});
}
V1 只使用 SID,因此恢复固定从当前响应开头重放。客户端必须:
- 保留已经完成的旧 Exchange;
- 清空当前未完成回答的 Item、Delta 和 Usage;
- 调用恢复 GET;
- 从新的
start重新构建当前回答; - 收到
done或error后停止重连。
网络异常建议使用带抖动指数退避,例如 0.5s、1s、2s、4s,最大 10s。以下情况不自动重试:
400 invalid_sid;401/403认证失败;404 session_not_found;- 已收到终态;
- 用户主动离开且产品不要求后台观察。
恢复 GET 不重新执行模型、不增加 History、不产生模型 Token 费用。
9. 安全停止
await fetch(`${baseUrl}/v1/session/responses/cancel`, {
method: "POST",
headers: { "x-api-key": apiKey, sid },
});
202 {"status":"canceling"} 只是接受取消。按钮进入“正在停止”状态,禁止立即提交下一条消息。
继续读取原流或调用恢复 GET,直到:
{"type":"done","id":"resp_...","status":"canceled"}
取消和正常完成可能竞争。如果 Cancel 返回 200 completed,界面应保留完整回答,不能改画为已取消。
刷新页面、关闭 Fetch Reader 或调用 AbortController.abort() 都只断开观察者,不等于安全停止。
被停止时仍为 running 的 Text、Reasoning 或工具 Item 会以 failed Item Upsert 收口;Exchange
本身仍以 status: canceled 展示,不能把 Item 的 failed 误判为整个 Session 已删除。
10. 中途打断并提交补充信息
用户在回答尚未完成时补充条件,只发一个请求:
const response = await fetch(`${baseUrl}/v1/session/responses/interrupt`, {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": apiKey, sid },
body: JSON.stringify({ model, input: supplement, stream: true }),
});
不要先发 Cancel 再发普通 POST。服务端会原子取消旧 Response、创建后继 Response,并保证后继在旧 调用真实终止后才执行。新 Question 自动由“旧原始 Question + 补充 Input”组成;旧部分 Answer 保留为 canceled History,但不进入后继 Context。
同一 SSE 依次出现:
interrupt(canceling old)
interrupt(canceled old, usage)
start(new)
item / delta / usage
done(new)
把旧 Response 的 Usage 存到 interrupted,不要覆盖或累加到新 Response 的累计 Usage。交接时断线,
仍只调用 GET /v1/session/responses 并携带 SID;恢复流会从第一个 interrupt 事件重放。收到
409 session_busy 表示前一次取消/交接尚未完成,客户端等待或恢复,不重复提交补充消息。若旧响应
在请求到达前恰好完成,服务端按普通续写接受补充,此时没有 interrupt 事件。
11. 编辑最近一条用户消息
正确客户端顺序:
- 当前响应仍在运行:先 Cancel;
- 等待
done(status=canceled); - 本地暂存替换文本,不先永久修改 History;
- 调用
PUT /v1/session/responses; - PUT 被接受后,用新问题替换最近用户消息;
- 剪切该问题之后的旧 Assistant、Reasoning、工具调用、工具结果和部分回答;
- 用 PUT 返回的新流重新生成后续投影。
const response = await fetch(`${baseUrl}/v1/session/responses`, {
method: "PUT",
headers: { "content-type": "application/json", "x-api-key": apiKey, sid },
body: JSON.stringify({ model, input: editedText, stream: true }),
});
收到 409 session_busy 时不能在本地强行剪切;应恢复当前状态或继续等待 Cancel 终态。收到
409 no_editable_message 时恢复原界面内容。
最近一次生成若以 error 结束,服务端仍允许 PUT 替换该次已经持久化的用户 Input;客户端无需把
失败问题重新解释为普通 POST,也不会因此剪切更早的已完成 Exchange。
12. 工具调用循环
收到 completed function_call Item 后,客户端或 Agent Runtime 执行工具,再进行下一次 POST:
{
"model": "gpt-5.6-sol",
"input": [
{
"type": "function_call_output",
"call_id": "call_456",
"output": { "result": "检查完成" }
}
],
"stream": true
}
必须原样返回 call_id。400 function_call_mismatch 表示 Call 已闭合、已被编辑剪切,或 SID
不属于该工具链;客户端不得更换 Call ID 后盲目重试。
13. 历史查询与渲染
History 是当前 Revision 的真实可见 Exchange,不是压缩摘要:
async function readAllHistory(
baseUrl: string,
apiKey: string,
sids: string[],
order: "asc" | "desc" = "asc",
) {
if (sids.length < 1 || sids.length > 100) throw new Error("history requires 1-100 sids");
const sidHeader = [...new Set(sids)].join(",");
const history: HistoryExchange[] = [];
let cursor: string | null = null;
do {
const url = new URL(`${baseUrl}/v1/session/history`);
url.searchParams.set("limit", "50");
url.searchParams.set("order", order);
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, { headers: { "x-api-key": apiKey, sid: sidHeader } });
if (!response.ok) throw protocolError(response.status, await response.json());
history.push(...await response.json() as HistoryExchange[]);
cursor = response.headers.get("has-more") === "true"
? response.headers.get("next-cursor")
: null;
} while (cursor);
return history;
}
- Question 和 Answer 永远按 Item 数组处理;
- 每条 Exchange 固定包含来源
sid,主/子会话展示不得依赖数组位置或本地关系推断; question_at和answer_at是服务端 UTC ISO 8601 时间;服务端已经按question_at做稳定合并, 客户端不得再用本地时钟或各 SID 的 Sequence 重排;- completed Exchange 可以进入后续模型 Context;
- canceled Exchange 可以展示停止前部分输出,但其 Answer 不进入后续 Context;
- PUT 剪切的旧 Revision 不在默认 History 中;
compaction是运行说明,不用摘要替换历史问题和回答;- 默认响应体每页仍是友好数组;显式 Prompt 时间线使用固定 Envelope;分页元数据只读
has-more和next-cursorHeader; - Cursor 是不透明字符串,不解析、不缓存为业务主键;
order默认asc;聊天首屏使用desc可直接读取最近一页,后续 Cursor 继续读取更早内容;- Cursor 同时绑定整个 SID 集合、排序方向、时间线模式及各会话 Revision;翻页时必须发送完全相同的 SID 集合和参数;
- 续页更换
order或include_system_prompt时会收到400 invalid_cursor,应丢弃 Cursor 并按目标参数重新读取; 409 cursor_stale表示 SID 集合变化,或其中任一会话发生编辑/Prompt 更新,应丢弃已拼接页并 使用目标 SID 集合从第一页重读;- UI 应按页增量渲染或虚拟滚动,不一次加载全部 DOM。
- 使用
order=desc的聊天 UI 应把每页按时间正序渲染;首屏定位在页尾,上拉时把下一页插入顶部。
需要显示 Prompt 变更时间线时,显式增加 include_system_prompt=true。该模式返回:
interface PromptHistoryItem {
sid: string;
revision: number;
system_prompt_at: string;
system_prompt: string | null;
}
interface HistoryWithPromptsPage {
history: HistoryExchange[];
system_prompts: PromptHistoryItem[];
}
limit 是两个数组的合计条数。服务端已经按 question_at/system_prompt_at 对两类记录共同分页;客户端
把两个数组按时间重新合并渲染。此模式的 Cursor 不能用于默认数组模式,下一页必须继续携带相同 SID
集合和 include_system_prompt=true。system_prompt: null 表示清空;重复设置相同值不会新增历史。
14. 文件输入
客户端仅提交公开可访问的 HTTP/HTTPS URL:
{
"model": "gpt-5.6-sol",
"input": [
{ "type": "text", "data": "总结附件" },
{ "type": "input_file", "file_url": "https://files.example.com/report.pdf" }
],
"stream": true
}
不要上传 Base64、file_id、本地路径、内网 URL、带认证 Header/Cookie 才能下载的 URL。网关按本次
有效执行路由决定文件路径:可用的原生文件能力直接处理,否则自动完成 MCP 分析。透明路由不会改变
公开模型身份;客户端对两种路径使用同一请求,不应推断内部渠道,只需把可能出现的 file_analysis
当作普通 Item Upsert 渲染,并使用最终累计 Usage。
15. 更新 System Prompt
await fetch(`${baseUrl}/v1/session`, {
method: "PATCH",
headers: { "content-type": "application/json", "x-api-key": apiKey, sid },
body: JSON.stringify({ system_prompt: "你是订单审计助手,回答必须简洁。" }),
});
发送 null、空字符串或纯空白字符串清空 Prompt。运行中、取消中和结果未知状态都可以更新,客户端
不需要先停止当前回答。DASP 在响应被接受时冻结 Prompt:当前正在输出的响应继续使用旧快照,PATCH
成功后才被接受的响应使用新 Prompt。
如果用户希望当前问题立即按新 Prompt 重新回答,客户端按以下顺序执行:
- 等待 PATCH 返回
200 {"status":"updated"}; - 调用
POST /v1/session/responses/interrupt提交必要补充; - 继续按 Interrupt 交接事件恢复和渲染。
同一 SID 的并发 PATCH 由服务端解决冲突,最后成功提交的值生效。客户端无需把 Prompt 重复附加到
每条 Input,也不应在默认 History 中伪造一条 System 消息。需要时间线时使用
include_system_prompt=true 返回的 system_prompts。Prompt 更新会使旧 History Cursor 失效。
服务端内部只在 Prompt State 保留当前值;每个响应使用接收时快照,快照不会进入消息 Context。客户端
不需要读取、保存或回传当前 Prompt 才能续写。
16. 删除
删除前必须保证 Session 已终态:
const response = await fetch(`${baseUrl}/v1/session`, {
method: "DELETE",
headers: { "x-api-key": apiKey, sid },
});
204:立即清理本地 SID、History、当前投影和恢复任务;409 session_busy:先安全停止,等待 canceled,再删除;- 删除不能被实现为
AbortController.abort()或 Cancel; - 删除成功后不得自动用同一 SID 继续提交。
17. 错误处理矩阵
| Code | 客户端行为 |
|---|---|
invalid_request |
标记请求构造错误,不自动重试 |
invalid_sid |
清理损坏 SID,提示用户 |
invalid_file_url |
要求用户改用公开 HTTP/HTTPS URL |
invalid_cursor |
丢弃 Cursor,从第一页读取 |
invalid_limit |
修正为 1–100,不自动重复原请求 |
invalid_order |
修正为 asc 或 desc;续页保持与 Cursor 一致 |
invalid_system_prompt |
缩短 Prompt 后由用户重试 |
session_not_found |
清理本地会话引用,不自动新建 |
response_not_found |
Session 可继续提交,但没有可恢复回答 |
model_not_available |
保留 SID,提示重新选择可用模型后再提交新请求 |
session_busy |
恢复/等待当前响应,不重复 POST |
no_editable_message |
恢复编辑前 UI |
cursor_stale |
丢弃已拼接分页,从第一页重读 |
function_call_mismatch |
终止该工具结果提交,重新读取 History |
rate_limit_exceeded |
按 Retry-After 或指数退避 |
context_compaction_failed |
显示明确失败,不丢弃本地 History |
file_analysis_failed |
保留 SID 和原文件输入,提示分析失败,不重复 POST |
provider_outcome_unknown |
不自动重发原 POST,等待人工/产品策略 |
session_storage_unavailable |
不发起收费重试风暴,稍后重试提交 |
session_unavailable |
暂停新 POST/PUT/Interrupt;已有会话仍可恢复、查询、停止和删除 |
session_internal_error |
本次请求失败;保留 SID,按服务端重试策略稍后重试或上报 |
流开始前错误使用 HTTP JSON;流开始后错误使用 error 事件。SDK 应统一成包含 HTTP Status、Code、
Message、SID 和可重试属性的异常类型。
SDK 建议为每次请求生成 GUID 格式 X-Request-Id 并保存到本地诊断事件。服务端响应会返回同一值,
且所有超过 500ms 的请求会使用该 ID 写结构化慢日志;客户端诊断事件不得记录 API Key、Prompt、
Question、Answer、工具输出或文件分析正文。
18. 客户端并发规则
- 每个 SID 只维护一个写操作队列;
- POST、PUT、Interrupt、Cancel 和 Delete 不能并行发送;
- 多个 UI 组件可以观察同一 SID,但应共享一份投影 Store;
- 页面多标签同时写同一 SID 时,使用 BroadcastChannel 或本地锁协调;
- 即使做了客户端锁,也必须正确处理服务端
409 session_busy; - 不要把恢复 GET 放入写队列,它是只读观察操作。
19. 客户端完成检查表
- 首次 POST 正确读取并持久化 SID;
- 续写只发送新 Input;
- 流解析支持分块边界、CRLF、Heartbeat 和未知事件;
- Item Upsert 不产生重复回答;
- Usage 使用最后累计值;
- 刷新后先清空未完成投影,再从头恢复;
- Cancel 等待 canceled 终态;
- Interrupt 用一个请求完成旧响应停止与补充提交,并分别保存旧/新 Usage;
- Edit 先停止,再剪切,再重新生成;
- Delete 不代替 Cancel;
- 工具结果保持 Call ID;
- History 使用 Cursor 分页并处理 Revision 失效;
- 可选 Prompt 时间线按消息与 Prompt 的服务端时间合并,正确显示清空事件;
- 多 SID History 使用一个逗号分隔
sidHeader,按服务端时序显示并保留每条来源 SID; - History 正确显示
question_at和answer_at,不以设备时间覆盖; - System Prompt 只通过 PATCH 更新;运行中可切换,当前响应保持旧快照,之后接受的响应使用新版本;
- 文件只提交公开 HTTP/HTTPS URL,并能渲染
file_analysis; - 切换用户、租户、环境和 API Key 时隔离 SID;
- 所有自动重试都不会重复 POST 模型调用。
- 每个请求携带可关联的
X-Request-Id,本地日志不含会话正文。
首次创建建议始终携带稳定的 Idempotency-Key,并在拿到 SID 前保持不变。它只解决首个 SID/Response
绑定窗口;已有 SID 的续写、打断和编辑不能使用该 Header。