Skip to content

Commit 4686a29

Browse files
author
SqlRush
committed
Fallback when streaming starts fail
1 parent 9e247cd commit 4686a29

4 files changed

Lines changed: 85 additions & 11 deletions

File tree

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ test/parity/ # golden tests against TS/official behavior
135135
- 本轮补充:Anthropic client 构造请求时会把显式 beta headers 与请求内容推导出的 beta headers 合并去重;当 messages/system content 或 tool definitions 中出现 `cache_control`/`cache_reference`,以及 messages 中出现 `cache_edits` payload 时,会自动带上 prompt-caching scope 与 cache-editing beta,避免 prompt-cache/cache-editing 请求依赖外部环境变量手动配置。
136136
- 本轮补充:动态 beta header 推导继续覆盖 strict tool structured-output 请求和 `[1m]` context model 请求;当 request tools 中存在 `strict:true` 时自动追加 structured-outputs beta,当 request model 以 `[1m]` 结尾时自动追加 context-1m beta,并继续和显式 beta 去重合并。
137137
- 本轮补充:Anthropic client 新增可选 access-token provider,非 streaming 与 streaming request 在 401/403 authentication/permission error 时会强制 refresh OAuth access token 并重试一次当前 payload;headless CLI 的 OAuth credentials 路径会把 file/env refresh token provider 传入 client,刷新后复用既有 credential store 持久化。
138+
- 本轮补充:conversation runner 的 streaming request 现在在未收到任何 stream event 前失败时,会用同一请求安全回退到 non-streaming `CreateMessage`;一旦已有 stream event 对外发出,错误会原样返回,避免重复生成半截输出。
138139

139140
### M4: Tool framework、permissions、sandbox
140141

docs/first-second-parity-audit.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Anthropic API 和 conversation:
9393
- Model registry now uses current Claude model IDs and aliases from the source snapshot, including Sonnet 4.6, Opus 4.6, Haiku 4.5, canonical-name rendering, and `[1m]` context variants.
9494
- OAuth support now includes production OAuth config, scope parsing, Claude.ai scope detection, auth URL construction, PKCE verifier/challenge, state generation, and expiry checks.
9595
- Session/history support now includes CC-compatible prompt history references, pasted text/image placeholder parsing, paste-cache hashing and retrieval, `history.jsonl` append/load, current-session-first up-arrow ordering, ctrl+r-style deduped timestamped history, `CLAUDE_CODE_SKIP_PROMPT_HISTORY`, remote session event pagination helpers, lenient transcript loading, legacy progress parent-bridge recovery, compact-boundary pruning, snip removal/relink replay, metadata entry collection, leaf UUID calculation, conversation-chain reconstruction, orphaned parallel tool-result recovery, content-replacement record loading/reconstruction, tombstone metadata delete/relink replay, and tombstone-style transcript message removal with a size guard.
96-
- Anthropic API layer now covers streaming accumulation, usage update/accumulation semantics, non-streaming max token cap, thinking-budget adjustment, retry/backoff with `Retry-After` and `x-should-retry`, context-overflow `max_tokens` retry adjustment, OAuth access-token refresh and retry on unauthorized non-streaming/streaming requests when a refresh provider is configured, beta-header dedupe, request-content beta latching for prompt-cache/cache-editing payloads, strict tool structured-output requests, and `[1m]` context models, custom request headers, basic prompt cache breakpoint/cache-reference/cache-edits placement, prompt dump JSONL capture for init/new user messages/non-streaming responses/stream chunks, and CC-compatible USD cost calculation for known Claude models including cache read/write, web search requests, and Opus 4.6 fast-tier pricing.
96+
- Anthropic API layer now covers streaming accumulation, safe pre-event streaming-to-non-streaming fallback in the conversation runner, usage update/accumulation semantics, non-streaming max token cap, thinking-budget adjustment, retry/backoff with `Retry-After` and `x-should-retry`, context-overflow `max_tokens` retry adjustment, OAuth access-token refresh and retry on unauthorized non-streaming/streaming requests when a refresh provider is configured, beta-header dedupe, request-content beta latching for prompt-cache/cache-editing payloads, strict tool structured-output requests, and `[1m]` context models, custom request headers, basic prompt cache breakpoint/cache-reference/cache-edits placement, prompt dump JSONL capture for init/new user messages/non-streaming responses/stream chunks, and CC-compatible USD cost calculation for known Claude models including cache read/write, web search requests, and Opus 4.6 fast-tier pricing.
9797
- Tool runtime now includes concurrency partitioning, ordered concurrent execution, interrupt behavior/defaults, max result size metadata, oversized result persistence, pre/post/permission-denied/permission-request hook dispatch, settings and local-plugin command-backed/HTTP hook execution for synchronous tool and conversation lifecycle hooks, hook-driven input updates/blocking/permission-request allow-deny, prompt/compact context injection, executor hook phase progress events, lifecycle progress events, retry/fallback model breadcrumbs, final-result/structured-error model attempt lists, structured API error status/type/request-id fields, headless stream-json progress events, snake_case token warning NDJSON payloads, lightweight compact NDJSON metadata, and pre-call cancellation checks.
9898
- Conversation runner can now use streaming clients, aggregate stream events into assistant messages, run tool calls through the orchestrator, preserve transcript append behavior, and apply CC-style per-message aggregate tool-result budget replacement before API requests with persisted replacement records for resume.
9999

@@ -106,7 +106,7 @@ The following items remain incomplete and must not be treated as done:
106106
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
107107
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, complete local/local-jsx slash-command execution wiring, command permission UI/SDK surfacing, forked/remote/MCP/plugin/bundled SkillTool behavior, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
108108
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, mid-call cancellation for concrete tools, background task behavior, telemetry, schema-not-sent hints, and concrete tool-specific semantics.
109-
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and `/issue` integration, cost tracker/session restore integration, streaming-to-non-streaming fallback, gateway/proxy-specific headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
109+
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and `/issue` integration, cost tracker/session restore integration, post-handshake streaming failure semantics, gateway/proxy-specific headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
110110
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.
111111
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, MDM/HKCU settings, managed-settings drop-in loading, settings cache/change detector, schema generation, plugin-only customization enforcement, all strict marketplace validations, and live reload/app-state sync.
112112
- Full session/history parity gaps that remain: large-file optimized transcript loading, preserved-segment edge cases beyond current relink/prune support, content-replacement feature-flag/runtime override and inherited subagent gap-fill details, complete async prompt-history lifecycle parity beyond current lock/buffer/undo paths, full pasted-image processing/runtime integration beyond current prompt image-cache/image-block/metadata path, remaining remote-history edge cases, sidechain/subagent transcript layout, and all session metadata entry types.

internal/conversation/run.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5475,13 +5475,21 @@ func (r Runner) applyToolResultBudget(history []contracts.Message) ([]contracts.
54755475
func (r Runner) createMessage(ctx context.Context, request anthropic.Request) (*anthropic.Response, error) {
54765476
if r.UseStreaming {
54775477
if streamer, ok := r.Client.(StreamingMessageClient); ok {
5478-
request.Stream = true
5478+
streamRequest := request
5479+
streamRequest.Stream = true
54795480
acc := anthropic.NewStreamAccumulator()
5480-
if err := streamer.StreamMessages(ctx, request, func(event anthropic.StreamEvent) error {
5481+
seenStreamEvent := false
5482+
if err := streamer.StreamMessages(ctx, streamRequest, func(event anthropic.StreamEvent) error {
5483+
seenStreamEvent = true
54815484
eventCopy := event
5482-
r.emit(Event{Type: EventStreamEvent, StreamEvent: &eventCopy, Model: request.Model})
5485+
r.emit(Event{Type: EventStreamEvent, StreamEvent: &eventCopy, Model: streamRequest.Model})
54835486
return acc.Add(event)
54845487
}); err != nil {
5488+
if !seenStreamEvent {
5489+
fallbackRequest := request
5490+
fallbackRequest.Stream = false
5491+
return r.Client.CreateMessage(ctx, fallbackRequest)
5492+
}
54855493
return nil, err
54865494
}
54875495
return acc.Finish(), nil

internal/conversation/run_test.go

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ type fakeCall struct {
4343
}
4444

4545
type fakeClient struct {
46-
calls []fakeCall
47-
requests []anthropic.Request
48-
streams [][]anthropic.StreamEvent
46+
calls []fakeCall
47+
requests []anthropic.Request
48+
streams [][]anthropic.StreamEvent
49+
streamErrs []error
4950
}
5051

5152
type fakeRunnerMCPClient struct {
@@ -72,16 +73,24 @@ func (f *fakeClient) CreateMessage(ctx context.Context, req anthropic.Request) (
7273

7374
func (f *fakeClient) StreamMessages(ctx context.Context, req anthropic.Request, handle func(anthropic.StreamEvent) error) error {
7475
f.requests = append(f.requests, req)
75-
if len(f.streams) == 0 {
76+
if len(f.streams) == 0 && len(f.streamErrs) == 0 {
7677
return anthropic.APIError{StatusCode: http.StatusInternalServerError, Type: "test_error", Message: "no fake stream configured"}
7778
}
78-
events := f.streams[0]
79-
f.streams = f.streams[1:]
79+
var events []anthropic.StreamEvent
80+
if len(f.streams) > 0 {
81+
events = f.streams[0]
82+
f.streams = f.streams[1:]
83+
}
8084
for _, event := range events {
8185
if err := handle(event); err != nil {
8286
return err
8387
}
8488
}
89+
if len(f.streamErrs) > 0 {
90+
err := f.streamErrs[0]
91+
f.streamErrs = f.streamErrs[1:]
92+
return err
93+
}
8594
return nil
8695
}
8796

@@ -7075,6 +7084,62 @@ func TestRunnerCanUseStreamingClient(t *testing.T) {
70757084
}
70767085
}
70777086

7087+
func TestRunnerFallsBackToNonStreamingWhenStreamFailsBeforeEvents(t *testing.T) {
7088+
client := &fakeClient{
7089+
streamErrs: []error{anthropic.APIError{StatusCode: http.StatusServiceUnavailable, Type: "overloaded_error", Message: "stream unavailable"}},
7090+
calls: []fakeCall{{
7091+
response: &anthropic.Response{ID: "msg_fallback", Type: "message", Role: "assistant", Model: "sonnet", Content: []contracts.ContentBlock{contracts.NewTextBlock("fallback ok")}},
7092+
}},
7093+
}
7094+
runner := Runner{
7095+
Client: client,
7096+
Model: "sonnet",
7097+
MaxTokens: 64,
7098+
UseStreaming: true,
7099+
}
7100+
result, err := runner.RunTurn(context.Background(), nil, messages.UserText("hello"))
7101+
if err != nil {
7102+
t.Fatal(err)
7103+
}
7104+
if got := result.Assistant.Content[0].Text; got != "fallback ok" {
7105+
t.Fatalf("assistant text = %q", got)
7106+
}
7107+
if len(client.requests) != 2 {
7108+
t.Fatalf("requests = %#v", client.requests)
7109+
}
7110+
if !client.requests[0].Stream {
7111+
t.Fatalf("first request should be streaming: %#v", client.requests[0])
7112+
}
7113+
if client.requests[1].Stream {
7114+
t.Fatalf("fallback request should be non-streaming: %#v", client.requests[1])
7115+
}
7116+
}
7117+
7118+
func TestRunnerDoesNotFallbackAfterStreamEvents(t *testing.T) {
7119+
client := &fakeClient{
7120+
streams: [][]anthropic.StreamEvent{{
7121+
{Type: "message_start", Message: &anthropic.Response{ID: "msg_partial", Type: "message", Role: "assistant", Model: "sonnet"}},
7122+
}},
7123+
streamErrs: []error{anthropic.APIError{StatusCode: http.StatusServiceUnavailable, Type: "overloaded_error", Message: "stream interrupted"}},
7124+
calls: []fakeCall{{
7125+
response: &anthropic.Response{ID: "msg_fallback", Type: "message", Role: "assistant", Model: "sonnet", Content: []contracts.ContentBlock{contracts.NewTextBlock("should not run")}},
7126+
}},
7127+
}
7128+
runner := Runner{
7129+
Client: client,
7130+
Model: "sonnet",
7131+
MaxTokens: 64,
7132+
UseStreaming: true,
7133+
}
7134+
_, err := runner.RunTurn(context.Background(), nil, messages.UserText("hello"))
7135+
if err == nil {
7136+
t.Fatal("expected stream error")
7137+
}
7138+
if len(client.requests) != 1 {
7139+
t.Fatalf("requests = %#v", client.requests)
7140+
}
7141+
}
7142+
70787143
func conversationLSPHelperDefinition() lsppkg.ServerDefinition {
70797144
return lsppkg.ServerDefinition{
70807145
Name: "conversation-lsp-helper",

0 commit comments

Comments
 (0)