Skip to content
Merged
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ shunt add upstream https://provider.example/docs --print | claude

## プロバイダー

プロバイダーは、順序付き `[[upstreams]]` エントリまたはレガシーな `[providers.<name>]` TOML テーブルです(YAML では、それぞれ対応する sequence または mapping のエントリ)。2 種類のアダプターでほとんどの上流をカバーします。`kind = "anthropic"`(上流が Anthropic Messages を話す場合。別のキーを付けてパススルー可能)と `kind = "responses"`(上流が OpenAI Responses API を話す場合。shunt が Anthropic Messages ⇄ Responses をストリーミング込みで変換)です。3 つ目のネイティブな種類である `kind = "cursor"` は、Cursor の ConnectRPC/protobuf AgentService をブリッジし、Cursor サブスクリプションを同じ Anthropic Messages インターフェース経由で利用できるようにします。
プロバイダーは、順序付き `[[upstreams]]` エントリまたはレガシーな `[providers.<name>]` TOML テーブルです(YAML では、それぞれ対応する sequence または mapping のエントリ)。2 種類のアダプターでほとんどの上流をカバーします。`kind = "anthropic"`(上流が Anthropic Messages を話す場合。別のキーを付けてパススルー可能)と `kind = "responses"`(上流が OpenAI Responses API を話す場合。shunt が Anthropic Messages ⇄ Responses をストリーミング込みで変換し、Responses API には `stop` パラメーターがないため `stop_sequences` をゲートウェイ側でエミュレート)です。3 つ目のネイティブな種類である `kind = "cursor"` は、Cursor の ConnectRPC/protobuf AgentService をブリッジし、Cursor サブスクリプションを同じ Anthropic Messages インターフェース経由で利用できるようにします。

順序付きアップストリームにより、プロバイダー間のフェイルオーバーが可能になります。宣言順が試行順となり、モデルの `upstream_model` マップが参加するエントリを選択して、公開 id を各バックエンドの id にマッピングします。

Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ shunt add upstream https://provider.example/docs --print | claude

## 프로바이더

프로바이더는 순서가 있는 `[[upstreams]]` 항목 또는 레거시 `[providers.<name>]` TOML 테이블입니다(YAML에서는 각각 해당 sequence 또는 mapping의 항목). 두 가지 어댑터 종류가 대부분의 업스트림을 커버합니다. `kind = "anthropic"`(업스트림이 Anthropic Messages를 사용하며, 필요하면 다른 키로 패스스루)와 `kind = "responses"`(업스트림이 OpenAI Responses API를 사용하며, shunt가 Anthropic Messages ⇄ Responses를 스트리밍 포함하여 변환)입니다. 세 번째 네이티브 종류인 `kind = "cursor"`는 Cursor의 ConnectRPC/protobuf AgentService를 브리지하여 Cursor 구독을 동일한 Anthropic-Messages 인터페이스로 사용할 수 있게 합니다.
프로바이더는 순서가 있는 `[[upstreams]]` 항목 또는 레거시 `[providers.<name>]` TOML 테이블입니다(YAML에서는 각각 해당 sequence 또는 mapping의 항목). 두 가지 어댑터 종류가 대부분의 업스트림을 커버합니다. `kind = "anthropic"`(업스트림이 Anthropic Messages를 사용하며, 필요하면 다른 키로 패스스루)와 `kind = "responses"`(업스트림이 OpenAI Responses API를 사용하며, shunt가 Anthropic Messages ⇄ Responses를 스트리밍 포함하여 변환하고, Responses API에는 `stop` 파라미터가 없으므로 `stop_sequences`를 게이트웨이 측에서 에뮬레이션)입니다. 세 번째 네이티브 종류인 `kind = "cursor"`는 Cursor의 ConnectRPC/protobuf AgentService를 브리지하여 Cursor 구독을 동일한 Anthropic-Messages 인터페이스로 사용할 수 있게 합니다.

순서가 있는 업스트림은 프로바이더 간 페일오버를 지원합니다. 선언 순서가 시도 순서이며, 모델의 `upstream_model` 맵은 참여할 항목을 선택하고 공개 id를 각 백엔드 id에 매핑합니다.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The command is offline and read-only: it prints guidance but never edits files,

## Providers

A provider is either an ordered `[[upstreams]]` entry or a legacy `[providers.<name>]` TOML table (under YAML, an entry in the corresponding sequence or mapping). Two adapter kinds cover most upstreams: `kind = "anthropic"` (the upstream speaks Anthropic Messages; passed through, optionally with a different key) and `kind = "responses"` (the upstream speaks the OpenAI Responses API; shunt translates Anthropic Messages ⇄ Responses, streaming included). A third native kind, `kind = "cursor"`, bridges Cursor's ConnectRPC/protobuf AgentService so a Cursor subscription is reachable through the same Anthropic-Messages interface.
A provider is either an ordered `[[upstreams]]` entry or a legacy `[providers.<name>]` TOML table (under YAML, an entry in the corresponding sequence or mapping). Two adapter kinds cover most upstreams: `kind = "anthropic"` (the upstream speaks Anthropic Messages; passed through, optionally with a different key) and `kind = "responses"` (the upstream speaks the OpenAI Responses API; shunt translates Anthropic Messages ⇄ Responses, streaming included, and emulates `stop_sequences` gateway-side because the Responses API has no `stop` parameter). A third native kind, `kind = "cursor"`, bridges Cursor's ConnectRPC/protobuf AgentService so a Cursor subscription is reachable through the same Anthropic-Messages interface.

Ordered upstreams enable cross-provider failover. Declaration order is the attempt order; a model's `upstream_model` map selects the participating entries and maps its public id to each backend's id:

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ shunt add upstream https://provider.example/docs --print | claude

## 提供方

一个提供方可以是有序的 `[[upstreams]]` 条目,也可以是旧式 `[providers.<name>]` TOML 表(在 YAML 中,分别对应 sequence 或 mapping 中的条目)。两种适配器类型即可覆盖大多数上游:`kind = "anthropic"`(上游讲 Anthropic Messages;透传,可选择换用不同的密钥)和 `kind = "responses"`(上游讲 OpenAI Responses API;shunt 在 Anthropic Messages ⇄ Responses 之间转换,含流式传输)。第三种原生类型 `kind = "cursor"` 桥接 Cursor 的 ConnectRPC/protobuf AgentService,使 Cursor 订阅可通过同一套 Anthropic-Messages 接口访问。
一个提供方可以是有序的 `[[upstreams]]` 条目,也可以是旧式 `[providers.<name>]` TOML 表(在 YAML 中,分别对应 sequence 或 mapping 中的条目)。两种适配器类型即可覆盖大多数上游:`kind = "anthropic"`(上游讲 Anthropic Messages;透传,可选择换用不同的密钥)和 `kind = "responses"`(上游讲 OpenAI Responses API;shunt 在 Anthropic Messages ⇄ Responses 之间转换,含流式传输,并且由于 Responses API 没有 `stop` 参数,`stop_sequences` 由网关侧模拟)。第三种原生类型 `kind = "cursor"` 桥接 Cursor 的 ConnectRPC/protobuf AgentService,使 Cursor 订阅可通过同一套 Anthropic-Messages 接口访问。

有序上游支持跨提供方故障转移。声明顺序就是尝试顺序;模型的 `upstream_model` 映射选择参与的条目,并将其公开 id 映射到各后端的 id:

Expand Down
60 changes: 60 additions & 0 deletions docs/m1-responses-translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,66 @@ instruction on how the agent should proceed. Claude Code surfaces only the error
`detailed_explanation` / `error_type` fields are not forwarded — upstream treats them as
sensitive and never persists them.

## 8b. Emulated `stop_sequences` (issue #605)

The Responses API has **no `stop` parameter** — Chat Completions does, Responses does not — so a
client's Anthropic `stop_sequences` cannot be forwarded. Before #605 the field was simply never
read, and every Responses upstream silently ignored it; Claude Code's auto-mode permission
classifier sends `stop_sequences: ["</block>"]` / `["</severity>"]` and the trailing text past the
stop broke its parser, costing a retry per classification.

shunt emulates them inside the translation instead. `forward` reads the field once
(`adapters/responses/mod.rs`), threads it through `TurnOptions` → `RelayOptions` →
`AnthropicSseMachine::with_stop_sequences`, and **never** adds it to the upstream body.

**Scanner** (`model/stop_sequences.rs`). Assistant text deltas only — reasoning summaries
(`response.reasoning_summary_text.delta`) and tool-call arguments
(`response.function_call_arguments.delta`) are never scanned, so a stop string inside a thinking
block or a tool argument cannot end the turn. Each text delta is appended to a holdback, which is
searched for the earliest occurrence of any stop sequence (earliest start byte wins; ties break by
the client's order). Without a match, everything but the longest holdback suffix that is a *proper*
prefix of some stop sequence is emitted immediately — so at most `max_len - 1` bytes are ever
buffered and streaming is preserved, while a stop split across two deltas is still caught. Slicing
only happens on `char` boundaries, so a multi-byte code point is never split.

**Termination.** On a match the machine emits the text before it, closes the open block, and sends
`message_delta` with `stop_reason: "stop_sequence"` and `stop_sequence: "<matched>"`, then
`message_stop`; `stopped` makes every later event — `response.completed` included — a no-op. Text
after the stop is neither streamed nor accumulated, so the non-streaming `final_json`
reconstruction is truncated at exactly the same point and reports the same `stop_reason` /
`stop_sequence`. A held-back prefix that never completes is ordinary output: `close_any` flushes it
as a normal `text_delta` before the block's `content_block_stop`, on every path that closes a text
block.

**Abort.** Every transport but one drops the upstream the moment the stop fires: `sse_parse.rs`
drops the `reqwest` byte stream with the final chunk (streaming HTTP), and `ws_stream.rs` drops
the `CodexWsEvents` receiver (both websocket paths), which makes the codex_ws reader abandon the
turn and evict the socket — correct, since a half-consumed turn must not be pooled. The exception
is non-streaming HTTP: `json_response` reads the whole body with `upstream.text()` before the
machine sees an event, so it truncates at the match like every other path but cannot cut the
upstream short. That is the pre-existing shape of the non-streaming relay, not something the stop
emulation introduced.

Each transport keys the abort on the stop sequence specifically rather than on "the machine is
stopped": on a *normally* completed turn the upstream is not mid-turn, and aborting it there would
evict a healthy pooled socket (websocket) or close a connection reqwest could otherwise return to
its idle pool (HTTP). So an ordinary terminal — `response.completed` / `response.done` /
`response.incomplete`, or a backend error event — leaves the upstream to finish; on streaming HTTP
the rest of the body goes to `spawn_terminal_drain`, a detached read bounded by
`TERMINAL_DRAIN_BUDGET`, so the connection still returns to the idle pool. The trailing bytes
translate to nothing, since `stopped` already makes every later event a no-op.

**Usage caveat.** The stop makes every later event a no-op, so the upstream's `response.completed`
usage is never applied — on the aborting transports it never arrives, and on non-streaming HTTP it
is read but discarded. `usage_value`'s existing estimate substitution applies instead, so the turn
reports the local input estimate and `output_tokens: 0`: in `message_delta.usage` when streaming,
in the final JSON's `usage` when not. A non-streaming turn emits no `message_start` and normally
skips the estimate altogether, so `forward` widens its gate to cover a request carrying
`stop_sequences` — otherwise a stopped turn would report `input_tokens: 0` for a non-empty prompt.
The estimate stays subject to the same `count_tokens = "tiktoken"` opt-in as every other path.
Tokens the upstream generated between the match and the abort are still billed upstream —
negligible for the short completions stop sequences are used for.

## 9. Test targets (M1)

- `insta` snapshots: request translation for (plain text, multi-turn, tool_use+tool_result
Expand Down
8 changes: 5 additions & 3 deletions site/src/content/docs/ja/providers/anthropic.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,11 @@ classifier_model = "claude-sonnet-5"

このキーは `kind = "anthropic"` のプロバイダーでのみ受け付けられ、**そのプロバイダー内でのモデルの
差し替え**であって、別のプロバイダーへのルートではありません。分類器リクエストは `stop_sequences`
を運びますが、shunt の Responses 変換はこのフィールドを落とします。Sonnet エイリアス経由で Codex
の上流に向けたところ、分類器は第 1 段階をリトライし、パーミッション確認のたびに 5〜7 秒余計に
かかりました。キーを設定しない場合(デフォルト)は、クライアントが要求したモデルのまま動きます。
に依存します。shunt の Responses 変換がこのフィールドを落としていた頃は、Sonnet エイリアス経由で
Codex の上流に向けたところ、分類器は第 1 段階をリトライし、パーミッション確認のたびに 5〜7 秒余計に
かかりました。現在は shunt が [Responses 経路で `stop_sequences` をエミュレート](/ja/providers/openai/#ストップシーケンス)
するため、その失敗はもう当てはまりませんが、このキーは引き続きプロバイダー内での差し替えです。
キーを設定しない場合(デフォルト)は、クライアントが要求したモデルのまま動きます。

ゲートウェイログインのデプロイメントでは、代わりに[マネージド設定](/ja/guides/gateway-login/#管理設定とモデルポリシー)で
エイリアスをクライアントへ配信することもできます。
Expand Down
25 changes: 25 additions & 0 deletions site/src/content/docs/ja/providers/openai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ ChatGPT / Codex バックエンドです — そのためネイティブが自
`tool_search = true` が必要です。ここで `tool_search = false` を設定すると、代わりにテキストシムを強制します。
[ツール検索](/ja/guides/codex/#ツール検索)を参照してください。

## ストップシーケンス

Responses API には **`stop` パラメーターがありません** — Chat Completions にはありますが、Responses
にはありません — そのため、クライアントの Anthropic `stop_sequences` をアップストリームへ渡すことは
できません。代わりに shunt が Responses の変換内でゲートウェイ側からこれをエミュレートします。
すべての Responses プロバイダーと、すべてのトランスポート(HTTP と WebSocket、ストリーミングと
非ストリーミング)が対象です。

- アシスタントのテキストは、いずれかのストップシーケンスが最初に現れた位置で打ち切られ、それ以降の
テキストがクライアントへ届くことはありません。
- ターンは `stop_reason: "stop_sequence"` と、一致した文字列を示す `stop_sequence` で終了します。
ネイティブの Anthropic アップストリームが報告するのとまったく同じ形です。
- 一致した時点でアップストリームのリクエストを中断し、走らせたままにはしません — ストリーミング
HTTP と 2 つの WebSocket パスが対象です。非ストリーミング HTTP は例外で、レスポンスボディを
すべて読み終えてから変換するため、打ち切り自体は他のパスと同じですが、アップストリームは最後
まで実行されます。一致から中断までの間にアップストリームが生成したトークンは、引き続き
アップストリーム側で課金されます。ストップシーケンスを使う典型的な短い応答では、無視できる量
です。

注意点がひとつあります。ストップによって、アップストリームの `response.completed` の使用量が反映
される前にターンが終わるため、shunt は自前のローカル入力推定値で代替し、`output_tokens: 0` を報告
します — ストリーミングのターンでは `message_delta.usage` に、非ストリーミングのターンでは
レスポンスの `usage` に入ります。この推定値にはプロバイダーの `count_tokens = "tiktoken"` の
オプトインが必要で、有効にしていない場合、ストップしたターンは `input_tokens: 0` を報告します。

## 検証

```bash
Expand Down
Loading
Loading