|
| 1 | +# Anthropic API Format Support Implementation |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +Successfully implemented proper Anthropic Messages API format support to fix the broken system message injection. The implementation now correctly handles Anthropic's unique top-level `system` array format. |
| 6 | + |
| 7 | +## Changes Made |
| 8 | + |
| 9 | +### 1. Created `lib/fetch-wrapper/formats/anthropic.ts` |
| 10 | + |
| 11 | +Implements the `FormatDescriptor` interface with Anthropic-specific handling: |
| 12 | + |
| 13 | +#### Detection Logic |
| 14 | +```typescript |
| 15 | +detect(body: any): boolean { |
| 16 | + return ( |
| 17 | + body.system !== undefined && |
| 18 | + Array.isArray(body.messages) |
| 19 | + ) |
| 20 | +} |
| 21 | +``` |
| 22 | +- Checks for `body.system` (can be string or array) at the top level |
| 23 | +- Requires `body.messages` array |
| 24 | +- Distinguishes from OpenAI (no top-level system) and Bedrock (has inferenceConfig) |
| 25 | + |
| 26 | +#### System Message Injection |
| 27 | +```typescript |
| 28 | +injectSystemMessage(body: any, injection: string): boolean { |
| 29 | + // Converts string system to array if needed |
| 30 | + if (typeof body.system === 'string') { |
| 31 | + body.system = [{ type: 'text', text: body.system }] |
| 32 | + } else if (!Array.isArray(body.system)) { |
| 33 | + body.system = [] |
| 34 | + } |
| 35 | + |
| 36 | + // Appends injection as text block |
| 37 | + body.system.push({ type: 'text', text: injection }) |
| 38 | + return true |
| 39 | +} |
| 40 | +``` |
| 41 | +- Handles both string and array system formats |
| 42 | +- Converts string to array of text blocks automatically |
| 43 | +- Appends to top-level `body.system` array (NOT in messages) |
| 44 | + |
| 45 | +#### Tool Output Extraction |
| 46 | +```typescript |
| 47 | +extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { |
| 48 | + // Looks for role='user' messages with type='tool_result' blocks |
| 49 | + // Uses tool_use_id field (Anthropic-specific) |
| 50 | +} |
| 51 | +``` |
| 52 | +- Searches user messages for `type: 'tool_result'` content blocks |
| 53 | +- Uses `tool_use_id` field (not `tool_call_id`) |
| 54 | +- Normalizes IDs to lowercase for consistency |
| 55 | + |
| 56 | +#### Tool Output Replacement |
| 57 | +```typescript |
| 58 | +replaceToolOutput(data: any[], toolId: string, prunedMessage: string): boolean { |
| 59 | + // Replaces content field in tool_result blocks |
| 60 | + return { |
| 61 | + ...block, |
| 62 | + content: prunedMessage // Direct string replacement |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | +- Finds matching `tool_result` blocks by `tool_use_id` |
| 67 | +- Replaces the `content` field with pruned message |
| 68 | +- Preserves other block properties (is_error, cache_control, etc.) |
| 69 | + |
| 70 | +### 2. Updated `lib/fetch-wrapper/formats/index.ts` |
| 71 | + |
| 72 | +```typescript |
| 73 | +export { anthropicFormat } from './anthropic' |
| 74 | +``` |
| 75 | + |
| 76 | +Added export for the new Anthropic format descriptor. |
| 77 | + |
| 78 | +### 3. Updated `lib/fetch-wrapper/index.ts` |
| 79 | + |
| 80 | +#### Import Statement |
| 81 | +```typescript |
| 82 | +import { openaiChatFormat, openaiResponsesFormat, geminiFormat, bedrockFormat, anthropicFormat } from "./formats" |
| 83 | +``` |
| 84 | + |
| 85 | +#### Detection Chain Order (CRITICAL) |
| 86 | +```typescript |
| 87 | +// 1. OpenAI Responses API: has body.input (not body.messages) |
| 88 | +if (openaiResponsesFormat.detect(body)) { ... } |
| 89 | + |
| 90 | +// 2. Bedrock: has body.system + body.inferenceConfig + body.messages |
| 91 | +else if (bedrockFormat.detect(body)) { ... } |
| 92 | + |
| 93 | +// 3. Anthropic: has body.system + body.messages (no inferenceConfig) |
| 94 | +else if (anthropicFormat.detect(body)) { ... } |
| 95 | + |
| 96 | +// 4. OpenAI Chat: has body.messages (no top-level system) |
| 97 | +else if (openaiChatFormat.detect(body)) { ... } |
| 98 | + |
| 99 | +// 5. Gemini: has body.contents |
| 100 | +else if (geminiFormat.detect(body)) { ... } |
| 101 | +``` |
| 102 | + |
| 103 | +**Why Order Matters:** |
| 104 | +- `anthropicFormat` MUST come before `openaiChatFormat` |
| 105 | +- Both have `body.messages`, but Anthropic has `body.system` at top level |
| 106 | +- Without proper ordering, Anthropic requests would be incorrectly handled by OpenAI format |
| 107 | +- Bedrock comes before Anthropic because it has more specific fields (inferenceConfig) |
| 108 | + |
| 109 | +### 4. OpenAI Format Compatibility |
| 110 | + |
| 111 | +The existing `openaiChatFormat` has fallback handling for `tool_result` blocks (lines 42-52 in `openai-chat.ts`). This is preserved for: |
| 112 | +- Backward compatibility with hybrid providers |
| 113 | +- Edge cases where providers use mixed formats |
| 114 | +- The detection order ensures true Anthropic requests are caught first |
| 115 | + |
| 116 | +## Key Differences: Anthropic vs OpenAI |
| 117 | + |
| 118 | +| Feature | OpenAI | Anthropic | |
| 119 | +|---------|--------|-----------| |
| 120 | +| System location | In messages array | Top-level `system` field | |
| 121 | +| System format | `{role: "system", content: "..."}` | String or array of blocks | |
| 122 | +| Tool results | `role: "tool"` message | In `user` message with `type: "tool_result"` | |
| 123 | +| Tool ID field | `tool_call_id` | `tool_use_id` | |
| 124 | +| Message roles | system/user/assistant/tool | user/assistant only | |
| 125 | + |
| 126 | +## Testing |
| 127 | + |
| 128 | +Successfully compiled with TypeScript: |
| 129 | +```bash |
| 130 | +npm run build # ✓ No errors |
| 131 | +``` |
| 132 | + |
| 133 | +Generated outputs: |
| 134 | +- `dist/lib/fetch-wrapper/formats/anthropic.js` |
| 135 | +- `dist/lib/fetch-wrapper/formats/anthropic.d.ts` |
| 136 | +- Properly exported in `dist/lib/fetch-wrapper/formats/index.js` |
| 137 | +- Integrated into main wrapper in `dist/lib/fetch-wrapper/index.js` |
| 138 | + |
| 139 | +## Verification Points |
| 140 | + |
| 141 | +1. ✅ Format detection distinguishes Anthropic from OpenAI (checks `body.system`) |
| 142 | +2. ✅ System injection appends to top-level array (not messages) |
| 143 | +3. ✅ Handles both string and array system formats |
| 144 | +4. ✅ Tool extraction uses `tool_use_id` (Anthropic convention) |
| 145 | +5. ✅ Tool replacement targets `tool_result` blocks in user messages |
| 146 | +6. ✅ Detection order prevents OpenAI format from capturing Anthropic requests |
| 147 | +7. ✅ Log metadata tags with `format: 'anthropic'` |
| 148 | +8. ✅ TypeScript compilation successful |
| 149 | + |
| 150 | +## References |
| 151 | + |
| 152 | +- Documentation: `docs/providers/anthropic.md` |
| 153 | +- Similar pattern: `lib/fetch-wrapper/formats/bedrock.ts` (also uses top-level system array) |
| 154 | +- Official API: https://docs.anthropic.com/en/api/messages |
| 155 | + |
| 156 | +## Impact |
| 157 | + |
| 158 | +This fix resolves the issue where Anthropic requests were being incorrectly processed by the OpenAI format handler, which tried to inject system messages into the messages array instead of the top-level system field. The new implementation: |
| 159 | + |
| 160 | +- Properly injects pruning context into Anthropic's system array |
| 161 | +- Correctly identifies and replaces pruned tool outputs |
| 162 | +- Maintains separation between format handlers |
| 163 | +- Preserves backward compatibility with existing OpenAI handling |
0 commit comments