docs(plugin): document LLM request policy and SDK cancellation requirements (#142)

Co-authored-by: Hyu <[email protected]>
This commit is contained in:
RockChinQ
2026-10-15 20:07:47 +08:00
committed by GitHub
co-authored by Hyu
parent 905dfea480
commit 6d988f8f18
16 changed files with 472 additions and 14 deletions
+3
View File
@@ -718,6 +718,7 @@
"pages": [
"en/plugin/dev/apis/agent-run",
"en/plugin/dev/apis/common",
"en/plugin/dev/apis/llm-requests",
"en/plugin/dev/apis/platform",
"en/plugin/dev/apis/pipeline-events",
"en/plugin/dev/apis/messages",
@@ -1034,6 +1035,7 @@
"pages": [
"zh/plugin/dev/apis/agent-run",
"zh/plugin/dev/apis/common",
"zh/plugin/dev/apis/llm-requests",
"zh/plugin/dev/apis/platform",
"zh/plugin/dev/apis/pipeline-events",
"zh/plugin/dev/apis/messages",
@@ -1353,6 +1355,7 @@
"pages": [
"ja/plugin/dev/apis/agent-run",
"ja/plugin/dev/apis/common",
"ja/plugin/dev/apis/llm-requests",
"ja/plugin/dev/apis/platform",
"ja/plugin/dev/apis/pipeline-events",
"ja/plugin/dev/apis/messages",
+2
View File
@@ -19,6 +19,8 @@ This page covers the data and methods available through these contexts. Use `sel
`EventContext`, `ExecuteContext`, and `Session` also carry `instance_uuid`, `workspace_uuid`, and `placement_generation`. These fields identify the current instance and workspace for execution; plugins must not treat them as authorization data.
See [LLM request configuration](/en/plugin/dev/apis/llm-requests) for retries, timeouts, numeric reasoning budgets, prompt caching, headers and stream cancellation.
## EventListener
EventListener handles Pipeline events. Its event handler receives an `EventContext`:
+2
View File
@@ -4,6 +4,8 @@ title: "LangBot API"
Use `self.plugin` for models, tools, knowledge bases, and storage. For the current event, replies, and conversation state, see [Context APIs](/en/plugin/dev/apis/agent-run).
See [LLM request configuration](/en/plugin/dev/apis/llm-requests) for retries, timeouts, numeric reasoning budgets, prompt caching, headers and stream cancellation.
## LangBot API
These APIs can be called in any plugin component. Access methods:
+134
View File
@@ -0,0 +1,134 @@
---
title: "LLM request configuration"
description: "Retries, deadlines, reasoning budgets, caching and stream cancellation for plugins and Runners"
---
These options require a Core build containing [Core #2661](https://github.com/langbot-app/LangBot/pull/2661). A merged PR does not imply a published release. Pass them through the existing `extra_args`; plugin and Runner signatures, message types and authorization remain unchanged. See [LangBot API](/en/plugin/dev/apis/common) and [Context APIs](/en/plugin/dev/apis/agent-run).
## Retries and timeouts
The following non-streaming example configures the request and reads complete usage:
```python
from langbot_plugin.api.entities.builtin.provider.message import Message
result = await self.plugin.invoke_llm_with_usage(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
timeout=110,
extra_args={
"request_policy": {
"retries": 1,
"total_timeout": 90,
"first_event_timeout": 30,
"stream_idle_timeout": 30,
"connect_timeout": 10,
"read_timeout": 60,
},
},
)
print(result.message.content)
if result.usage is not None:
print(result.usage.model_dump())
```
Runner `ctx.api.invoke_llm*` accepts the same `extra_args`, subject to run authorization and remaining time. Calls through `self.plugin` also inherit these restrictions inside a run context.
`timeout=` is the SDK/RPC waiting limit (120 seconds by default for non-streaming calls). Core's `request_policy` controls the inner request. Keep its total budget within the enclosing RPC/Runner deadline; raising it cannot extend that deadline. Streaming methods have no `timeout=` argument and remain bounded by SDK/Runner waiting limits.
| `request_policy` field | Default | Meaning |
| --- | --- | --- |
| `retries` | `0` | Additional attempts after the first, integer 0–5 |
| `total_timeout` | `300` | Whole invocation budget, including backoff |
| `first_event_timeout` | `120` | Wait for the first upstream event, not the first visible text |
| `stream_idle_timeout` | `120` | Idle time between stream events |
| `connect_timeout` | `10` | Connection timeout |
| `read_timeout` | `120` | Transport read timeout |
| `write_timeout` | `30` | Transport write timeout |
| `pool_timeout` | `10` | Connection pool acquisition timeout |
| `backoff_base` / `backoff_max` | `0.5` / `8` | Exponential backoff base and cap, with full jitter |
| `attribution` | `True` | Automatic application attribution headers |
| `conversation_header` | `None` | Generated session header name, described below |
Times are seconds; timeouts must be finite and positive. Unknown policy fields are rejected. Precedence: instance `llm_request_policy`, model `extra_args`, then call `extra_args`. Within one layer, `request_policy.retries` wins over legacy `num_retries` / `max_retries`. A call-level legacy value still overrides model policy; `0` disables retries. Legacy numeric `extra_args["timeout"]` sets read, first-event and stream-idle limits without extending the total budget.
Authentication, invalid-request and exhausted-quota failures are not retried. Retries stop after any upstream event, including reasoning or usage. `Retry-After` is bounded by the remaining budget. Core disables LiteLLM and provider SDK retries for managed calls; gateways may independently retry. This adds no tool replay, model fallback or embedding/rerank retries. Legacy third-party requesters still own their transport configuration.
## Reasoning budgets
Existing `reasoning_level` behavior is unchanged. For a numeric budget, pass this object as `extra_args` or save it in model advanced parameters. Omitting `level` means `enabled`:
```json
{
"reasoning_config": {"budget_tokens": 2048},
"max_tokens": 4096
}
```
Do not combine a budget with an effort level such as `medium`, `disabled`, or native reasoning parameters. Direct Anthropic calls require explicit `max_tokens` greater than the budget; Core never raises that limit automatically.
Precedence: persisted model `reasoning_config`, model `extra_args.reasoning_config`, call `extra_args.reasoning_config`, then an explicit Runner level override. Each object replaces the previous one; an effort override clears a lower-priority budget.
Numeric budgets map to native Anthropic, Gemini 2.5, supported Qwen models and OpenRouter routes. Direct OpenAI/Codex and adaptive-only Claude models retain effort control. Check the model's `reasoning_capabilities.budget_tokens` for support and bounds. Unsupported budgets fail before HTTP. Providers/gateways may impose stricter limits; OpenRouter may translate a numeric budget into effort.
## Prompt caching
`extra_args.prompt_caching` is consumed by Core, not forwarded unchanged. Call configuration replaces model configuration. The default `{"mode": "provider_default"}` adds no fields. This is not a local response cache.
| Configuration | Behavior |
| --- | --- |
| `{"mode": "openai", "retention": "in_memory"}` | Generates a model-and-conversation-scoped `prompt_cache_key` when conversation context exists; optional `retention` also accepts `24h` if supported by the target |
| `{"mode": "anthropic", "ttl": "5m"}` | Adds an ephemeral breakpoint to the last text block of the leading system/developer prefix; `ttl` accepts `5m` or `1h` |
OpenAI mode applies to compatible routes and OpenRouter OpenAI models supporting these options. Explicit native `prompt_cache_key`, `prompt_cache_retention` and `prompt_cache_options` win. Anthropic mode supports native Claude and OpenRouter Claude. Existing native cache markers are preserved without adding duplicates. Core does not reorder tools/history or mark user messages, and inserts nothing without an eligible prefix.
Hints do not guarantee hits. Inspect `usage.model_dump()` for provider fields such as `prompt_tokens_details.cached_tokens` or `cache_read_input_tokens`; missing counts are not zero. Minimum prefix lengths, retention eligibility and pricing are provider-controlled. Codex's restricted Responses body does not accept this configuration object; existing server caching remains. General Responses migration and Gemini explicit cache-resource management are outside this change.
## Application and session headers
Direct OpenRouter calls receive `HTTP-Referer: https://langbot.app`, `X-OpenRouter-Title: LangBot`, and, with conversation context, `x-session-id: lb-<opaque scoped hash>`. Custom gateway addresses do not automatically inherit these headers. `request_policy.attribution=False` disables only automatic application attribution. Explicit `extra_headers` remain and take precedence case-insensitively.
A gateway can use an explicitly configured session header:
```json
{
"request_policy": {"conversation_header": "x-litellm-session-id"},
"extra_headers": {
"HTTP-Referer": "https://langbot.app",
"X-OpenRouter-Title": "LangBot"
}
}
```
Supported names: `x-session-id`, `x-litellm-session-id`, `x-langbot-conversation-id`. The last is a custom convention, not a universal provider standard, and provides no automatic routing or caching benefit. Generated identifiers are scoped by workspace/bot/pipeline/session and contain no raw session ID or prompt. They are pseudonymous, not a guarantee of anonymization, and are omitted without context. New API and other gateways may filter or rewrite headers; verify forwarding rules and actual outbound requests.
## Stream cancellation and compatibility
Use `aclosing` when leaving a stream early. Allow task cancellation exceptions to propagate:
```python
from contextlib import aclosing
from langbot_plugin.api.entities.builtin.provider.message import Message
async with aclosing(self.plugin.invoke_llm_stream_events(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
extra_args={"request_policy": {"retries": 0, "total_timeout": 90}},
)) as events:
async for event in events:
if event.usage is not None:
print(event.usage.model_dump())
if event.chunk is not None and event.chunk.content:
print(event.chunk.content)
break
```
This example intentionally stops after the first nonempty content chunk and may miss final usage. For a complete response, remove `break`; a final event can have usage without a chunk. `invoke_llm` still returns `Message`, and `invoke_llm_stream` still yields `MessageChunk`. Choose `invoke_llm_with_usage` or `invoke_llm_stream_events` for accounting. Usage may be `None`; nested cache/reasoning counts and provider-specific fields are preserved.
SDK ≤0.7.11 has a proxy-layer defect that delays upstream cancellation on early close. Deterministic close requires **langbot-plugin ≥0.7.12**, which publishes the fix from [SDK #177](https://github.com/langbot-app/langbot-plugin-sdk/pull/177). A Core dependency pin does not automatically upgrade a separately deployed Runtime or plugin development environment; upgrade those executing environments too. Ordinary signatures, payloads and return types remain compatible. Total deadlines include consumer time, but cannot interrupt a consumer while the generator is suspended at `yield`; expiration is enforced on the next pull.
## Diagnostics
Core monitoring records expose `details.timing`, complete `usage` and structured `error`; legacy SDK exception transport is unchanged. Structured errors include `code`, `status_code`, `retryable`, `retry_after`, `request_id` and `phase`. Do not assume these fields exist on legacy exceptions received by plugins.
Millisecond timings include `preparation_ms`, `transport_setup_ms`, `first_event_ms`, `first_reasoning_ms`, `first_content_ms`, `first_visible_chunk_ms`, `total_ms`, plus `attempts` and `retry_wait_ms`. The first event may be reasoning/control, and the first visible chunk may include reasoning. `request_started_ms` marks the SDK call, not a packet leaving the socket. Missing fields mean unobserved, not zero. Monitoring retains partial usage received before cancellation but cannot reconstruct missing upstream accounting.
+2
View File
@@ -19,6 +19,8 @@ title: "コンテキスト API"
`EventContext`、`ExecuteContext`、`Session` には `instance_uuid`、`workspace_uuid`、`placement_generation` も含まれます。これらは現在の実行に対応するインスタンスとワークスペースを示すためのもので、プラグイン側の認可情報として使用しないでください。
リトライ、タイムアウト、数値思考予算、prompt caching、ヘッダー、ストリーム終了は [LLM リクエスト設定](/ja/plugin/dev/apis/llm-requests)を参照してください。
## EventListener
EventListener はパイプラインイベントを処理します。イベントハンドラーは `EventContext` を受け取ります。
+2
View File
@@ -4,6 +4,8 @@ title: "LangBot API"
モデル、ツール、ナレッジベース、ストレージには `self.plugin` を使用します。現在のイベント、返信、会話の状態については[コンテキスト API](/ja/plugin/dev/apis/agent-run) を参照してください。
リトライ、タイムアウト、数値思考予算、prompt caching、ヘッダー、ストリーム終了は [LLM リクエスト設定](/ja/plugin/dev/apis/llm-requests)を参照してください。
## LangBot API
これらのAPIは、通常のプラグインコンポーネントで呼び出すことができます。アクセス方法:
+134
View File
@@ -0,0 +1,134 @@
---
title: "LLM リクエスト設定"
description: "プラグインと Runner のリトライ、タイムアウト、思考予算、キャッシュ、ストリーム終了"
---
本ページの設定には [Core #2661](https://github.com/langbot-app/LangBot/pull/2661) を含む Core が必要です。マージとリリースは別です。既存の `extra_args` で指定し、プラグインと Runner のシグネチャ、メッセージ型、権限確認は変わりません。[LangBot API](/ja/plugin/dev/apis/common) と[コンテキスト API](/ja/plugin/dev/apis/agent-run)も参照してください。
## リトライとタイムアウト
非ストリーミング呼び出しでリクエスト設定と完全な使用量を扱う例です:
```python
from langbot_plugin.api.entities.builtin.provider.message import Message
result = await self.plugin.invoke_llm_with_usage(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
timeout=110,
extra_args={
"request_policy": {
"retries": 1,
"total_timeout": 90,
"first_event_timeout": 30,
"stream_idle_timeout": 30,
"connect_timeout": 10,
"read_timeout": 60,
},
},
)
print(result.message.content)
if result.usage is not None:
print(result.usage.model_dump())
```
Runner の `ctx.api.invoke_llm*` でも同じ `extra_args` を使えますが、実行の権限と残り時間に制限されます。実行コンテキスト内の `self.plugin` 呼び出しにも同じ制限が適用されます。
`timeout=` は SDK/RPC の待機期限(非ストリーミングでは既定 120 秒)です。`request_policy` は Core 内部のリクエストを制御します。総予算は外側の RPC/Runner の期限内に収めてください。Core の予算を増やしても外側の期限は延長されません。ストリーミングメソッドには `timeout=` 引数がなく、SDK/Runner の待機期限が適用されます。
| `request_policy` フィールド | 既定値 | 意味 |
| --- | --- | --- |
| `retries` | `0` | 初回以外の追加試行数、整数 0–5 |
| `total_timeout` | `300` | リトライ待機を含む呼び出し全体の予算 |
| `first_event_timeout` | `120` | 最初の表示文字ではなく、最初の上流イベントを待つ期限 |
| `stream_idle_timeout` | `120` | ストリームイベント間の待機期限 |
| `connect_timeout` | `10` | 接続タイムアウト |
| `read_timeout` | `120` | 読み取りタイムアウト |
| `write_timeout` | `30` | 書き込みタイムアウト |
| `pool_timeout` | `10` | 接続プールの待機期限 |
| `backoff_base` / `backoff_max` | `0.5` / `8` | full jitter を使う指数バックオフの基数と上限 |
| `attribution` | `True` | アプリ識別ヘッダーの自動付与 |
| `conversation_header` | `None` | 生成するセッションヘッダー名(後述) |
時間は秒単位で、タイムアウトは有限の正数です。未知のフィールドは拒否されます。優先順位はインスタンスの `llm_request_policy`、モデルの `extra_args`、呼び出しの `extra_args` の順です。同じ階層では `request_policy.retries` が旧 `num_retries` / `max_retries` より優先されます。呼び出し側の旧パラメーターでもモデル設定を上書きでき、`0` でリトライを無効化できます。旧数値 `extra_args["timeout"]` は読み取り、最初のイベント、ストリーム待機を設定し、総予算は延長しません。
認証失敗、不正なリクエスト、クォータ不足はリトライしません。思考や usage を含む上流イベントを受信した後も再試行しません。`Retry-After` は残りの総予算内に制限されます。Core は管理対象の LiteLLM/プロバイダー SDK 内蔵リトライを無効化しますが、中継サービスは独自に再試行する場合があります。ツール再実行、モデル切り替え、embedding/rerank のリトライは追加しません。旧サードパーティ requester の内部通信設定はその実装が管理します。
## 思考予算
既存の `reasoning_level` は変わりません。数値予算は以下を `extra_args` またはモデルの詳細パラメーターに指定します。`level` を省略すると `enabled` です:
```json
{
"reasoning_config": {"budget_tokens": 2048},
"max_tokens": 4096
}
```
予算と effort レベル(例:`medium`)、`disabled`、プロバイダー独自の思考パラメーターを併用しないでください。Anthropic 直結では予算より大きい `max_tokens` を明示する必要があり、Core は出力上限を自動で増やしません。
優先順位は保存済みモデル `reasoning_config`、モデル `extra_args.reasoning_config`、呼び出し `extra_args.reasoning_config`、明示的な Runner レベル指定です。各オブジェクトが前の設定を全置換し、effort 指定は低優先度の数値予算を削除します。
数値予算はネイティブ Anthropic、Gemini 2.5、対応 Qwen、OpenRouter 経由に変換されます。OpenAI/Codex 直結と adaptive のみの Claude は effort を使います。モデルの `reasoning_capabilities.budget_tokens` で対応と範囲を確認してください。未対応なら HTTP 送信前にエラーになります。上流の制限がより厳しい場合や、OpenRouter が予算を effort に変換する場合もあります。
## Prompt caching
`extra_args.prompt_caching` は Core 専用で、そのまま上流には送信されません。呼び出し設定がモデル設定を全置換します。既定の `{"mode": "provider_default"}` はフィールドを追加しません。ローカルの回答キャッシュではありません。
| 設定 | 動作 |
| --- | --- |
| `{"mode": "openai", "retention": "in_memory"}` | 会話コンテキストがあるとモデルと会話別の `prompt_cache_key` を生成。`retention` は省略可能で、対応モデルでは `24h` も指定可能 |
| `{"mode": "anthropic", "ttl": "5m"}` | 先頭の system/developer 接頭辞の最後のテキストブロックに一時キャッシュマーカーを追加。`ttl` は `5m` または `1h` |
OpenAI モードは対応する互換ルートおよび OpenRouter の OpenAI モデル向けです。明示したネイティブ `prompt_cache_key`、`prompt_cache_retention`、`prompt_cache_options` が優先されます。Anthropic モードはネイティブ Claude と OpenRouter Claude に対応します。既存マーカーがあれば追加せず、ツール/履歴の並べ替えやユーザーメッセージへの追加もしません。適切な接頭辞がなければ何も追加しません。
キャッシュヒントはヒットを保証しません。`usage.model_dump()` の `prompt_tokens_details.cached_tokens`、`cache_read_input_tokens` などで確認します。未提供はゼロではありません。最小接頭辞長、保持期間の対応、料金はプロバイダー次第です。Codex の制限付き Responses ボディはこの設定を受け付けず、既存のサーバー側キャッシュを維持します。汎用 Responses 移行と Gemini 明示的キャッシュリソース管理は今回の対象外です。
## アプリとセッションのヘッダー
OpenRouter 直結では `HTTP-Referer: https://langbot.app`、`X-OpenRouter-Title: LangBot`、会話コンテキストがあれば `x-session-id: lb-<opaque scoped hash>` を自動付与します。独自の中継 URL には自動付与しません。`request_policy.attribution=False` は自動アプリ識別のみを無効化します。明示した `extra_headers` は保持され、大文字小文字を区別せず優先されます。
中継サービスに合わせてセッションヘッダーを指定できます:
```json
{
"request_policy": {"conversation_header": "x-litellm-session-id"},
"extra_headers": {
"HTTP-Referer": "https://langbot.app",
"X-OpenRouter-Title": "LangBot"
}
}
```
対応名は `x-session-id`、`x-litellm-session-id`、`x-langbot-conversation-id` です。最後は独自規約であり共通標準ではなく、自動的なルーティングやキャッシュ効果はありません。生成値は workspace/bot/pipeline/session ごとに分離され、生の ID やプロンプトを含みませんが、仮名化であり匿名化保証ではありません。コンテキストがなければ生成しません。New API などはヘッダーを削除・上書きする可能性があるため、転送設定と実際の出力を確認してください。
## ストリーム終了と互換性
途中でループを抜けるときは `aclosing` で生成器を確実に閉じ、タスクのキャンセル例外は伝播させてください:
```python
from contextlib import aclosing
from langbot_plugin.api.entities.builtin.provider.message import Message
async with aclosing(self.plugin.invoke_llm_stream_events(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
extra_args={"request_policy": {"retries": 0, "total_timeout": 90}},
)) as events:
async for event in events:
if event.usage is not None:
print(event.usage.model_dump())
if event.chunk is not None and event.chunk.content:
print(event.chunk.content)
break
```
この例は最初の空でない内容で停止するため、最後の usage を受け取れない場合があります。全文取得では `break` を除きます。最後のイベントが chunk なしの usage のみの場合もあります。`invoke_llm` は引き続き `Message`、`invoke_llm_stream` は `MessageChunk` を返します。使用量には `invoke_llm_with_usage` または `invoke_llm_stream_events` を使います。usage は `None` の場合があり、ネストしたキャッシュ/思考使用量や上流独自フィールドは保持されます。
SDK ≤0.7.11 には、プロキシの早期終了が上流へ即時伝播しない問題があります。確実な終了には **langbot-plugin ≥0.7.12** が必要です。この公開版は [SDK #177](https://github.com/langbot-app/langbot-plugin-sdk/pull/177) の修正を含みます。Core の依存固定だけでは別配置の Runtime やプラグイン開発環境は更新されないため、実行する各環境も更新してください。通常のシグネチャ、ペイロード、戻り値は互換です。総期限は消費時間も含みますが、生成器が `yield` で停止中に消費側を中断せず、次の読み取りで期限切れを確認します。
## 診断
Core の監視レコード `details` に `timing`、完全な `usage`、構造化 `error` を記録します。旧 SDK の例外伝送形式は変更しません。構造化エラーは `code`、`status_code`、`retryable`、`retry_after`、`request_id`、`phase` を含みますが、プラグインが受け取る旧例外にこれらがあるとは限りません。
時間はミリ秒で、`preparation_ms`、`transport_setup_ms`、`first_event_ms`、`first_reasoning_ms`、`first_content_ms`、`first_visible_chunk_ms`、`total_ms`、`retry_wait_ms`、および試行数 `attempts` を記録します。最初のイベントは思考や制御の場合があり、最初の表示チャンクも思考を含む場合があります。`request_started_ms` は SDK 呼び出し時刻で、パケット送信時刻ではありません。欠落は未観測でありゼロではありません。キャンセル前の部分 usage は監視に保持されますが、未提供の使用量は復元できません。
+28 -8
View File
@@ -7,6 +7,7 @@ This checks SDK calls and result shapes, not external platform delivery.
from __future__ import annotations
import asyncio
import argparse
import ast
import inspect
import re
@@ -34,6 +35,7 @@ ROOT = Path(__file__).resolve().parents[1]
class Host:
def __init__(self):
self.calls = []
self.closed_streams = 0
async def call_action(self, action, data, timeout=15):
name = action.value
@@ -74,9 +76,13 @@ class Host:
async def call_action_generator(self, action, data, timeout=15):
assert action.value == "invoke_llm_stream"
yield {"chunk": {"role": "assistant", "content": "Hello", "is_final": False}}
yield {"chunk": {"role": "assistant", "content": "!", "is_final": True}}
yield {"usage": {"total_tokens": 8}}
self.calls.append((action.value, data))
try:
yield {"chunk": {"role": "assistant", "content": "Hello", "is_final": False}}
yield {"chunk": {"role": "assistant", "content": "!", "is_final": True}}
yield {"usage": {"total_tokens": 8}}
finally:
self.closed_streams += 1
def context():
@@ -111,7 +117,7 @@ def context():
def snippets(locale, page):
source = (ROOT / locale / f"plugin/dev/apis/{page}.mdx").read_text()
source = (ROOT / locale / f"plugin/dev/apis/{page}.mdx").read_text(encoding="utf-8")
if page == "common":
headings = {
"zh": ("### 模型流式输出与用量", "## 其他资源 API"),
@@ -125,7 +131,7 @@ def snippets(locale, page):
def verify_context_reference():
source = (ROOT / "zh/plugin/dev/apis/agent-run.mdx").read_text()
source = (ROOT / "zh/plugin/dev/apis/agent-run.mdx").read_text(encoding="utf-8")
query_methods = {
"reply",
"get_bot_uuid",
@@ -217,10 +223,10 @@ async def execute(block, namespace):
await namespace["example"]()
async def main():
async def main(pages):
verify_context_reference()
total = 0
for page in ("common", "agent-run", "platform"):
for page in pages:
blocks = snippets("zh", page)
for locale in ("en", "ja"):
assert blocks == snippets(locale, page), f"{page}: {locale} code differs"
@@ -260,6 +266,13 @@ async def main():
}
with bind_invocation(host, api):
await execute(block, namespace)
if page == "llm-requests":
expected = 0 if "aclosing" in block else 1
llm_calls = [data for name, data in host.calls if name.startswith("invoke_llm")]
assert len(llm_calls) == 1
assert llm_calls[0]["extra_args"]["request_policy"]["retries"] == expected
if "aclosing" in block:
assert host.closed_streams == 1, "Early close did not reach the Host stream"
for name, data in host.calls:
if name in {"reply_message", "set_query_var"}:
expected_query = 42 if "event_context" in block else 43
@@ -297,4 +310,11 @@ async def main():
if __name__ == "__main__":
asyncio.run(main())
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--page", action="append",
choices=("common", "agent-run", "platform", "llm-requests"),
help="Verify selected pages (repeatable); defaults to all pages.",
)
args = parser.parse_args()
asyncio.run(main(args.page or ("common", "agent-run", "platform", "llm-requests")))
+21
View File
@@ -2079,6 +2079,27 @@
<xhtml:link rel="alternate" hreflang="ja" href="https://langbot.app/docs/ja/plugin/dev/apis/common" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://langbot.app/docs/en/plugin/dev/apis/common" />
</url>
<url>
<loc>https://langbot.app/docs/en/plugin/dev/apis/llm-requests</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://langbot.app/docs/zh/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="ja" href="https://langbot.app/docs/ja/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
</url>
<url>
<loc>https://langbot.app/docs/zh/plugin/dev/apis/llm-requests</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://langbot.app/docs/zh/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="ja" href="https://langbot.app/docs/ja/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
</url>
<url>
<loc>https://langbot.app/docs/ja/plugin/dev/apis/llm-requests</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://langbot.app/docs/zh/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="ja" href="https://langbot.app/docs/ja/plugin/dev/apis/llm-requests" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://langbot.app/docs/en/plugin/dev/apis/llm-requests" />
</url>
<url>
<loc>https://langbot.app/docs/en/plugin/dev/apis/messages</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://langbot.app/docs/en/plugin/dev/apis/messages" />
+1 -1
View File
@@ -23,7 +23,7 @@ function links(node, result = []) {
}
test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => {
assert.equal(urls.length, 506 + zhOnlyArticleRoutes.length);
assert.equal(urls.length, 509 + zhOnlyArticleRoutes.length);
for (const url of urls) {
const route = relative(url);
const text = await read(`${route}.md`);
+3 -3
View File
@@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", ()
test("canonical MDX and nested OpenAPI sources are discovered", async () => {
const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8"));
const pages = await collectMdxDocuments(root);
assert.equal(pages.length, 332);
assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 105, zh: 122, ja: 105 });
assert.equal(pages.length, 335);
assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 106, zh: 123, ja: 106 });
assert.deepEqual(collectOpenApiSources(docs), [
{ locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" },
{ locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" },
@@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async
const first = await prepareFumapress({ root, outRoot: temp });
const second = await prepareFumapress({ root, outRoot: temp });
assert.deepEqual(first, second);
assert.equal(first.documents, 332);
assert.equal(first.documents, 335);
assert.equal(first.fallbackDefaults, 0);
assert.equal(first.localeOnlyDocuments, 17);
assert.equal(first.redirects, 103);
+1 -1
View File
@@ -46,7 +46,7 @@ function staticHtmlForUrlPath(urlPath) {
test("all canonical localized documents have static HTML", async () => {
const documents = await collectMdxDocuments(root);
assert.equal(documents.length, 332);
assert.equal(documents.length, 335);
const missing = [];
for (const document of documents) {
try {
+1 -1
View File
@@ -36,5 +36,5 @@ test("every localized HTML payload includes the preference client provider", asy
}
for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale));
// 509 canonical content pages plus one framework 404 page per locale.
assert.equal(count, 512);
assert.equal(count, 515);
});
+2
View File
@@ -19,6 +19,8 @@ title: "上下文 API"
`EventContext`、`ExecuteContext` 和 `Session` 还会携带 `instance_uuid`、`workspace_uuid` 和 `placement_generation`。这些字段用于标识当前执行所在的实例和工作区,不应作为插件自行鉴权的依据。
重试、超时、数字思考预算、prompt caching、请求头和流式取消见 [LLM 请求配置](/zh/plugin/dev/apis/llm-requests)。
## EventListener
EventListener 处理流水线事件。事件处理函数接收 `EventContext`:
+2
View File
@@ -4,6 +4,8 @@ title: "LangBot API"
模型、工具、知识库和存储等能力通过 `self.plugin` 访问。当前事件、回复和会话状态的访问方式见[上下文 API](/zh/plugin/dev/apis/agent-run)。
重试、超时、数字思考预算、prompt caching、请求头和流式取消见 [LLM 请求配置](/zh/plugin/dev/apis/llm-requests)。
## LangBot API
这些 API 可在所有组件中通过 `self.plugin` 调用。访问方式如下:
+134
View File
@@ -0,0 +1,134 @@
---
title: "LLM 请求配置"
description: "插件与 Runner 的重试、超时、思考预算、缓存和流式取消"
---
本页对应 [Core #2661](https://github.com/langbot-app/LangBot/pull/2661) 的请求层能力。部署的 Core 必须包含该改动;合并不代表已发布。新配置通过已有的 `extra_args` 传入,不改变插件或 Runner 的方法签名、消息返回类型和权限检查。方法列表见 [LangBot API](/zh/plugin/dev/apis/common),运行上下文见[上下文 API](/zh/plugin/dev/apis/agent-run)。
## 重试与超时
以下示例在一次非流式调用中设置请求策略,并读取完整用量:
```python
from langbot_plugin.api.entities.builtin.provider.message import Message
result = await self.plugin.invoke_llm_with_usage(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
timeout=110,
extra_args={
"request_policy": {
"retries": 1,
"total_timeout": 90,
"first_event_timeout": 30,
"stream_idle_timeout": 30,
"connect_timeout": 10,
"read_timeout": 60,
},
},
)
print(result.message.content)
if result.usage is not None:
print(result.usage.model_dump())
```
Runner 的 `ctx.api.invoke_llm*` 同样接受 `extra_args`,并受当前运行的授权与剩余时间限制。`self.plugin` 在运行上下文内调用也会绑定这些限制。
`timeout=` 是 SDK/RPC 的等待期限,非流式默认 120 秒;`request_policy` 控制 Core 内部请求。总预算应小于外层 RPC 和 Runner 的剩余期限,增加 Core 预算不会延长外层期限。流式方法没有 `timeout=` 参数,仍受 SDK/Runner 的等待期限约束。
| `request_policy` 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `retries` | `0` | 首次尝试之外的重试次数,整数 0–5 |
| `total_timeout` | `300` | 整次调用预算,包含重试等待 |
| `first_event_timeout` | `120` | 等待首个上游事件,而非首个可见文字 |
| `stream_idle_timeout` | `120` | 流式事件之间允许的空闲时间 |
| `connect_timeout` | `10` | 建立连接的超时 |
| `read_timeout` | `120` | 传输层读取超时 |
| `write_timeout` | `30` | 传输层写入超时 |
| `pool_timeout` | `10` | 等待连接池的超时 |
| `backoff_base` / `backoff_max` | `0.5` / `8` | 指数退避基数与上限,使用 full jitter |
| `attribution` | `True` | 自动添加应用身份头 |
| `conversation_header` | `None` | 指定生成的会话头名称,见下文 |
时间单位为秒,超时必须是有限正数,未知策略字段会被拒绝。优先级为实例 `llm_request_policy`、模型 `extra_args`、调用 `extra_args`。同层 `request_policy.retries` 优先于旧的 `num_retries` / `max_retries`;调用层旧参数仍可覆盖模型层策略,`0` 禁用重试。旧的数值 `extra_args["timeout"]` 同时设置读取、首事件和流式空闲超时,不延长总预算。
鉴权、非法请求和额度耗尽不重试;收到任何上游事件后(包括思考或 usage)不再重试。`Retry-After` 受剩余总预算约束。Core 关闭托管调用的 LiteLLM 和供应商 SDK 内置重试,中转站仍可能自行重试。这里不增加工具重放、模型回退或 embedding/rerank 重试。旧第三方 requester 仍负责自己的传输配置。
## 思考预算
已有的 `reasoning_level` 保持不变。需要数字预算时,通过 `extra_args.reasoning_config` 设置,省略 `level` 表示 `enabled`:
```json
{
"reasoning_config": {"budget_tokens": 2048},
"max_tokens": 4096
}
```
将此对象作为调用的 `extra_args`,或保存到模型高级参数。数字预算不能与 effort 等级(如 `medium`)、`disabled` 或供应商原生思考参数混用。直接调用 Anthropic 时必须显式设置大于预算的 `max_tokens`,Core 不会自动提高输出上限。
配置依次来自模型 `reasoning_config`、模型 `extra_args.reasoning_config`、调用 `extra_args.reasoning_config`、显式 Runner 等级覆盖。每个对象整体替换前一个,后设置的 effort 等级会清除低优先级的预算。
数字预算适用于原生 Anthropic、Gemini 2.5、支持该能力的 Qwen 和 OpenRouter 路由;直接 OpenAI/Codex 及仅支持 adaptive 的 Claude 仍使用 effort。根据模型的 `reasoning_capabilities.budget_tokens` 判断支持情况与上下限,不支持时在发请求前报错。供应商或中转站可能有更严格的限制,OpenRouter 也可能将预算转换为 effort。
## Prompt caching
`extra_args.prompt_caching` 是 Core 配置,不会原样发给供应商。调用配置整体替换模型配置,默认 `{"mode": "provider_default"}` 不添加字段。这不是本地回答缓存。
| 配置 | 行为 |
| --- | --- |
| `{"mode": "openai", "retention": "in_memory"}` | 有会话上下文时生成按模型和会话隔离的 `prompt_cache_key`;`retention` 可省略,也可为 `24h`,需要目标模型支持 |
| `{"mode": "anthropic", "ttl": "5m"}` | 在开头 system/developer 前缀最后一个文本块添加临时缓存标记;`ttl` 可为 `5m` 或 `1h` |
OpenAI 模式用于支持相关参数的 OpenAI 兼容路由和 OpenRouter OpenAI 模型。显式原生 `prompt_cache_key`、`prompt_cache_retention`、`prompt_cache_options` 优先。Anthropic 模式支持原生 Claude 和 OpenRouter Claude;已有原生缓存标记时不重复添加,不重排工具/历史、不标记用户消息,没有合适的前缀则不添加。
缓存提示不保证命中。使用 `usage.model_dump()` 检查供应商返回的 `prompt_tokens_details.cached_tokens`、`cache_read_input_tokens` 等字段;缺失不等于零。前缀最小长度、保留时间支持与计费由供应商决定。Codex 的受限 Responses 请求不接受此配置对象,保留现有服务端缓存。此改动不包含通用 Responses 迁移或 Gemini 显式缓存资源管理。
## 应用与会话请求头
直连 OpenRouter 时自动添加 `HTTP-Referer: https://langbot.app`、`X-OpenRouter-Title: LangBot`,有会话上下文时还会添加 `x-session-id: lb-<opaque scoped hash>`。自定义中转地址不会自动继承 OpenRouter 身份头。`request_policy.attribution=False` 仅关闭自动应用身份头;显式 `extra_headers` 仍保留,并以不区分大小写的方式优先。
中转站可按其配置显式选择会话头:
```json
{
"request_policy": {"conversation_header": "x-litellm-session-id"},
"extra_headers": {
"HTTP-Referer": "https://langbot.app",
"X-OpenRouter-Title": "LangBot"
}
}
```
支持 `x-session-id`、`x-litellm-session-id`、`x-langbot-conversation-id`。最后一个是自定义约定,不是通用供应商标准,不会自动带来路由或缓存收益。生成的值按 workspace/bot/pipeline/session 隔离,不发送原始会话标识或提示词;它是化名标识,不是强匿名保证。没有上下文时不生成。New API 等中转站可能过滤或改写头,需核对其转发配置与实际出站请求。
## 流式取消与兼容性
提前退出异步流时,使用 `aclosing` 确定性关闭生成器,任务取消时也应让取消异常继续传播:
```python
from contextlib import aclosing
from langbot_plugin.api.entities.builtin.provider.message import Message
async with aclosing(self.plugin.invoke_llm_stream_events(
llm_model_uuid=model_uuid,
messages=[Message(role="user", content="Hello!")],
extra_args={"request_policy": {"retries": 0, "total_timeout": 90}},
)) as events:
async for event in events:
if event.usage is not None:
print(event.usage.model_dump())
if event.chunk is not None and event.chunk.content:
print(event.chunk.content)
break
```
此例刻意只消费第一个非空内容块,提前结束可能收不到最终 usage。完整读取时不要 `break`,末尾可能只有 usage 而没有 chunk。`invoke_llm` 仍返回 `Message`,`invoke_llm_stream` 仍返回 `MessageChunk`;需要用量时选择 `invoke_llm_with_usage` 或 `invoke_llm_stream_events`,`usage` 可为 `None`,嵌套缓存、思考和供应商附加字段会保留。
SDK ≤0.7.11 存在代理层提前关闭不及时向上游传播的问题。确定性取消需要实际运行环境使用 **langbot-plugin ≥0.7.12**,该版本已发布 [SDK #177](https://github.com/langbot-app/langbot-plugin-sdk/pull/177) 的修复。Core 固定 SDK 依赖不会自动升级独立部署的 Runtime 或插件自己的开发环境;这些环境也需升级到包含修复的版本。普通调用的签名、参数载荷和返回结构保持兼容。总超时包含消费时间,但生成器停在 `yield` 时不会主动中断消费者,下一次拉取时才检查过期。
## 诊断
Core 监控记录的 `details` 提供 `timing`、完整 `usage` 与结构化 `error`,旧 SDK 的异常传输格式保持不变。结构化错误包含 `code`、`status_code`、`retryable`、`retry_after`、`request_id`、`phase`,不要假定插件收到的旧异常对象具有这些字段。
计时单位为毫秒,包括 `preparation_ms`、`transport_setup_ms`、`first_event_ms`、`first_reasoning_ms`、`first_content_ms`、`first_visible_chunk_ms`、`total_ms`、`attempts` 和 `retry_wait_ms`。首事件可能是思考或控制事件,首个可见块也可能包含思考;`request_started_ms` 是 SDK 调用时间,不是发出网络包的时间。缺失字段代表未观测到,不是零。取消前已收到的部分用量会留在监控中,但无法补出供应商未返回的用量。