|
| 1 | +# Anthropic/ZAI Streaming Translation Fix |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +The Anthropic connector (and by inheritance, the zai-coding-plan connector) was not translating streaming chunks to the internal domain format. This caused Anthropic-formatted SSE chunks to flow through the system untranslated, breaking cross-API compatibility. |
| 6 | + |
| 7 | +### Root Cause |
| 8 | + |
| 9 | +**OpenAI Connector** (`src/connectors/openai.py` lines 629-637): |
| 10 | +- ✅ Translates each streaming chunk using `translation_service.to_domain_stream_chunk()` |
| 11 | +- Converts OpenAI/Responses API format → domain format (OpenAI-compatible) |
| 12 | + |
| 13 | +**Anthropic Connector** (`src/connectors/anthropic.py` lines 530-540): |
| 14 | +- ❌ Did NOT translate streaming chunks |
| 15 | +- Just passed through raw Anthropic SSE chunks wrapped in `ProcessedResponse` |
| 16 | + |
| 17 | +**zai-coding-plan Connector**: |
| 18 | +- Inherits from `AnthropicBackend` |
| 19 | +- Does not override `_handle_streaming_response()` |
| 20 | +- Therefore inherited the broken streaming behavior |
| 21 | + |
| 22 | +## Solution |
| 23 | + |
| 24 | +### 1. Fixed Anthropic Connector Streaming (`src/connectors/anthropic.py`) |
| 25 | + |
| 26 | +Updated the `event_stream()` function to translate each chunk: |
| 27 | + |
| 28 | +```python |
| 29 | +async def event_stream() -> AsyncGenerator[ProcessedResponse, None]: |
| 30 | + try: |
| 31 | + async for chunk in response.aiter_text(): |
| 32 | + _capture_message_id(chunk) |
| 33 | + |
| 34 | + # Translate Anthropic SSE chunk to domain format |
| 35 | + domain_chunk = self.translation_service.to_domain_stream_chunk( |
| 36 | + chunk, "anthropic" |
| 37 | + ) |
| 38 | + yield ProcessedResponse(content=domain_chunk) |
| 39 | + |
| 40 | + # Translate final [DONE] marker |
| 41 | + done_chunk = self.translation_service.to_domain_stream_chunk( |
| 42 | + "data: [DONE]\n\n", "anthropic" |
| 43 | + ) |
| 44 | + yield ProcessedResponse(content=done_chunk) |
| 45 | +``` |
| 46 | + |
| 47 | +### 2. Enhanced Translation Function (`src/core/domain/translation.py`) |
| 48 | + |
| 49 | +Updated `anthropic_to_domain_stream_chunk()` to handle SSE format: |
| 50 | + |
| 51 | +**Before**: Only accepted parsed JSON dicts |
| 52 | +**After**: Accepts both SSE-formatted strings and JSON dicts |
| 53 | + |
| 54 | +Key improvements: |
| 55 | +- Parses multi-line SSE events (with `event:` and `data:` lines) |
| 56 | +- Extracts JSON from `data:` lines |
| 57 | +- Handles all Anthropic event types: |
| 58 | + - `message_start` → sets role |
| 59 | + - `content_block_delta` → extracts text content |
| 60 | + - `message_delta` → maps stop_reason to finish_reason |
| 61 | + - `message_stop` → marks completion |
| 62 | +- Maps Anthropic stop reasons to OpenAI equivalents: |
| 63 | + - `end_turn` → `stop` |
| 64 | + - `max_tokens` → `length` |
| 65 | + - `tool_use` → `tool_calls` |
| 66 | +- Handles `[DONE]` markers |
| 67 | +- Backward compatible with dict format |
| 68 | + |
| 69 | +## Tests Created |
| 70 | + |
| 71 | +### Translation Layer Tests (`tests/unit/core/domain/test_translation_anthropic_streaming.py`) |
| 72 | + |
| 73 | +16 comprehensive tests covering: |
| 74 | +- SSE content deltas |
| 75 | +- Message start/stop events |
| 76 | +- Stop reason mapping |
| 77 | +- [DONE] marker handling |
| 78 | +- Event line parsing |
| 79 | +- Multi-line SSE format |
| 80 | +- Invalid JSON handling |
| 81 | +- Backward compatibility with dict format |
| 82 | +- OpenAI structure preservation |
| 83 | + |
| 84 | +### Connector Tests (`tests/unit/connectors/test_anthropic_streaming_translation.py`) |
| 85 | + |
| 86 | +4 integration tests covering: |
| 87 | +- End-to-end Anthropic streaming translation |
| 88 | +- SSE format handling in connector |
| 89 | +- [DONE] marker translation |
| 90 | +- zai-coding-plan inheritance verification |
| 91 | + |
| 92 | +## Impact |
| 93 | + |
| 94 | +### Fixed |
| 95 | +- ✅ Anthropic connector now emits domain-formatted chunks |
| 96 | +- ✅ zai-coding-plan connector inherits the fix automatically |
| 97 | +- ✅ Cross-API translation works correctly for streaming |
| 98 | +- ✅ Downstream processors receive consistent OpenAI-style format |
| 99 | + |
| 100 | +### Verified |
| 101 | +- ✅ All 20 new tests pass |
| 102 | +- ✅ All 15 existing translation tests still pass |
| 103 | +- ✅ Backward compatibility maintained |
| 104 | + |
| 105 | +## Why Tests Didn't Catch This |
| 106 | + |
| 107 | +The existing tests mocked the translation service or didn't verify the actual format of streaming chunks. The new tests: |
| 108 | +1. Test the actual translation function with SSE input |
| 109 | +2. Test the connector's streaming handler end-to-end |
| 110 | +3. Verify the output format matches OpenAI structure |
| 111 | +4. Ensure zai-coding-plan inherits the correct behavior |
| 112 | + |
| 113 | +## Files Modified |
| 114 | + |
| 115 | +1. `src/connectors/anthropic.py` - Added streaming translation |
| 116 | +2. `src/core/domain/translation.py` - Enhanced SSE parsing |
| 117 | +3. `tests/unit/connectors/test_anthropic_streaming_translation.py` - New connector tests |
| 118 | +4. `tests/unit/core/domain/test_translation_anthropic_streaming.py` - New translation tests |
0 commit comments