> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunra.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 输出上限与流生命周期

有三件事会让一次 LLM 响应在模型自己认为写完之前结束：您请求的输出上限、模型自身强制的上限，以及流式请求下网关的流生命周期限制。本页说明这三者，以及提前结束的请求如何计费。

## 请求输出上限

| Endpoint               | 字段                                            |
| ---------------------- | --------------------------------------------- |
| `/v1/chat/completions` | `max_completion_tokens`（推荐）、`max_tokens`（已弃用） |
| `/v1/messages`         | `max_tokens`                                  |
| `/v1/responses`        | `max_output_tokens`                           |

Sunra 会把您传入的上限转发给上游 provider。响应因触及该上限而停止时，会返回 `finish_reason: "length"`（Chat Completions 与 Responses）或 `stop_reason: "max_tokens"`（Messages）——与您直接调用 provider 得到的信号一致。

<Note>
  在 Chat Completions 上，部分 provider 只认 `max_tokens` 而忽略 `max_completion_tokens`——于是您以为设了上限，实际得到的是无上限的补全。对于 `deepseek/` 系列模型，网关会代您把 `max_completion_tokens` 翻译为 `max_tokens`。两个字段同时传入时，取较小者生效，因为两者都是上限语义。

  原生支持 `max_completion_tokens` 的 provider 不做改写，因此您设置的上限只会在一处被执行一次。
</Note>

## 模型输出上限

每个模型都有自己的最大输出长度，与您请求的数值无关。请求超过模型允许的值会被**拒绝，而不是被静默削平**——上游 provider 返回 `400`，并在消息中给出合法区间。例如 `deepseek/deepseek-v4-flash` 与 `deepseek/deepseek-v4-pro` 最高接受 393,216 个输出令牌，超出即拒绝：

```json theme={null}
{
  "error": {
    "message": "Invalid max_tokens value, the valid range of max_tokens is [1, 393216]",
    "type": "invalid_request_error"
  }
}
```

这里返回 `400` 是刻意设计。被静默降低的上限与"正常跑到上限"无法区分：两者都报 `finish_reason: "length"`，`usage` 里也没有任何字段能告诉您发生的是哪一种。

输出上限因模型而异，并随 provider 发布新版本而变化。请把 `400` 当作权威答案，不要在客户端里硬编码某个上限值。

## 流生命周期

流式请求（`stream: true`）在网关侧受两条限制约束：

| 限制     | 数值                       | 拦截的情形                   |
| ------ | ------------------------ | ----------------------- |
| 空闲超时   | 上游 provider 连续 120 秒没有字节 | provider 已经不再产出，但没有关闭连接 |
| 生命周期上限 | 自上游响应开始起 14 分钟           | 仍在产出但不会终止的流             |

只要流在空闲窗口内持续输出令牌，这两条限制都不会触发——长生成不会仅仅因为"长"而被打断。非流式请求不受这两条限制约束，它们有自己固定的 6 分钟整体上限。

### 中止帧

任一限制触发时——或流中途上游连接故障时——网关不会裸断连。它会把仍持有的内容刷出，按您所调用 endpoint 的 SSE 方言发送一个 error 帧，然后干净地关闭流。**该帧之前已投递的所有内容都是有效输出**，可以照常使用。

在 `/v1/chat/completions` 与 `/v1/responses` 上：

```
data: {"error":{"message":"Stream aborted by gateway (stream_lifetime_ceiling); partial output above is complete as delivered","type":"gateway_stream_aborted","code":"stream_lifetime_ceiling"}}

data: [DONE]
```

`[DONE]` 哨兵只属于 Chat Completions 方言；Responses 流在 error 帧之后直接结束，没有 `[DONE]`。

在 `/v1/messages` 上，该帧遵循 Anthropic 方言——一个带类型的 `error` 事件，且没有 `[DONE]`：

```
event: error
data: {"type":"error","error":{"type":"gateway_stream_aborted","message":"Stream aborted by gateway (upstream_idle_timeout); partial output above is complete as delivered"}}
```

`type` 恒为 `gateway_stream_aborted`。在 OpenAI 形状的方言上，`code` 指明触发的是哪条限制：

| Code                      | 是否可重试       | 含义                                          |
| ------------------------- | ----------- | ------------------------------------------- |
| `upstream_idle_timeout`   | 可以——可能是暂时性的 | provider 连续 120 秒没有发送任何内容。                  |
| `stream_lifetime_ceiling` | 原样重试无意义     | 流已达到 14 分钟。同一请求大概率还会撞上——请调低输出上限，或把任务拆成多次请求。 |
| `upstream_stream_error`   | 可以——可能是暂时性的 | 流中途与 provider 的连接发生故障。                      |

没有中止帧的流就是正常结束的流。由于该帧是确定性的，若流被截断**且没有**中止帧，应视为客户端或网络问题，而不是网关中止。

### 被中止的流如何计费

被中止的流按已投递给您的内容计费，绝不会按 provider 内部可能继续生成的完整内容计费。

* 若中止前 provider 已上报最终 usage，则按该上报计费。
* 否则按**保守估计**结算已投递的输出——大约每 4 个字符折算 1 个令牌，并以您请求的输出上限封顶。该笔记录会标记 `usage_source: "conservative_estimate"` 备查。
* 若没有投递任何内容，或 provider 上报了明确的错误，请求标记为失败并释放预留的额度。失败的请求不计费。

## 客户端如何处理

```python theme={null}
for line in response.iter_lines():
    if not line.startswith(b"data: "):
        continue
    payload = line[6:]
    if payload == b"[DONE]":
        break

    event = json.loads(payload)
    if error := event.get("error"):
        if error.get("type") == "gateway_stream_aborted":
            # 保留已收集的内容——它作为"已投递部分"是完整的。
            # error["code"] 说明是否值得重试。
            handle_partial(collected, reason=error["code"])
            break
        raise UpstreamError(error)

    collected.append(event["choices"][0]["delta"].get("content", ""))
```

关键在于保留这部分输出而不是丢弃：无论如何您都要为它付费，而在长生成场景下，它通常已经是答案的大部分。
