-
-
Notifications
You must be signed in to change notification settings - Fork 11
feat: anthropic / message logger (development only) #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds a file-based Inspector and configuration for the Anthropic translator, validates inspector settings, wires inspector into request/response and streaming flows to optionally persist JSONL request/response entries organised by date and session, and adds docs and startup warnings. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Translator
participant Inspector
participant FS
Client->>Translator: HTTP request
Translator->>Translator: Extract/convert body, derive sessionID
alt Inspector enabled
Translator->>Inspector: LogRequest(sessionID, model, body)
Inspector->>FS: Ensure {outputDir}/{YYYY-MM-DD}/ (0700)
Inspector->>FS: Append JSONL to {session}.jsonl (0600)
note right of Inspector #f9f0c1: emits one-time security warning
end
Translator->>Translator: Forward to backend / stream
Translator->>Translator: Transform backend response
alt Streaming finalised
Translator->>Translator: Reconstruct streaming response
end
alt Inspector enabled
Translator->>Inspector: LogResponse(sessionID, responseJSON)
Inspector->>FS: Append JSONL entry
end
Translator->>Client: Return response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
internal/adapter/translator/anthropic/request.go (1)
39-52: Extract duplicated session ID derivation logic.This session ID derivation logic (lines 41-47) is identical to the code in response.go (lines 72-78). See the comment on response.go for the suggested refactor to extract this into a shared helper method.
🧹 Nitpick comments (4)
internal/adapter/inspector/simple_test.go (1)
14-140: Add test coverage for security and concurrency scenarios.While the basic functionality is well-tested, consider adding tests for:
- Path traversal protection: Test that session IDs like
../../../maliciousare sanitised- Concurrent writes: Test that multiple goroutines writing to the same session file don't corrupt data or cause race conditions
- Invalid session IDs: Test session IDs with special characters, empty strings, very long strings
Example test structure:
t.Run("PathTraversalPrevention", func(t *testing.T) { sessionID := "../../etc/passwd" err := inspector.LogRequest(sessionID, "model", []byte(`{}`)) // Verify file is created in safe location, not traversed path }) t.Run("ConcurrentWrites", func(t *testing.T) { sessionID := "concurrent-test" var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() inspector.LogRequest(sessionID, "model", []byte(fmt.Sprintf(`{"id":%d}`, i))) }(i) } wg.Wait() // Verify all 10 entries exist and are valid JSON })internal/config/types.go (1)
182-187: Consider adding validation for InspectorConfig.The InspectorConfig lacks validation. Consider adding a Validate() method to ensure:
- OutputDir is not empty when Enabled is true
- OutputDir doesn't contain path traversal sequences
- SessionHeader is a valid HTTP header name when provided
// Validate validates the inspector configuration func (c *InspectorConfig) Validate() error { if !c.Enabled { return nil } if c.OutputDir == "" { return fmt.Errorf("output_dir must be specified when inspector is enabled") } // Check for path traversal attempts if strings.Contains(c.OutputDir, "..") { return fmt.Errorf("output_dir must not contain '..' sequences") } return nil }Then call it from AnthropicTranslatorConfig.Validate():
func (c *AnthropicTranslatorConfig) Validate() error { // existing validation... if err := c.Inspector.Validate(); err != nil { return fmt.Errorf("inspector config invalid: %w", err) } return nil }internal/adapter/inspector/simple.go (2)
86-94: Tighten file permissions for sensitive data.The inspector logs may contain sensitive user data (as noted in config.yaml warnings). Consider using more restrictive permissions:
- Line 86: Directory permissions 0755 (world-readable) → 0700 (owner-only)
- Line 94: File permissions 0644 (world-readable) → 0600 (owner-only)
- if err := os.MkdirAll(dirPath, 0755); err != nil { + if err := os.MkdirAll(dirPath, 0700); err != nil { - f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
28-28: Consider per-session mutexes for better concurrency.The single global mutex (line 28) serialises all file writes, creating a bottleneck when multiple sessions are active concurrently. Consider using a sync.Map of per-session mutexes for better throughput.
This is optional since the PR is described as a quick debug port, but for production use:
type Simple struct { logger logger.StyledLogger outputDir string sessionHeader string sessionMutexes sync.Map // map[string]*sync.Mutex enabled bool } func (s *Simple) getSessionMutex(sessionID string) *sync.Mutex { mu, _ := s.sessionMutexes.LoadOrStore(sessionID, &sync.Mutex{}) return mu.(*sync.Mutex) } func (s *Simple) writeEntry(sessionID string, entry Entry) error { mu := s.getSessionMutex(sessionID) mu.Lock() defer mu.Unlock() // ... rest of implementation }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
config/config.yaml(1 hunks)internal/adapter/inspector/simple.go(1 hunks)internal/adapter/inspector/simple_test.go(1 hunks)internal/adapter/translator/anthropic/request.go(2 hunks)internal/adapter/translator/anthropic/response.go(1 hunks)internal/adapter/translator/anthropic/translator.go(3 hunks)internal/config/types.go(2 hunks)
🔇 Additional comments (1)
internal/adapter/translator/anthropic/translator.go (1)
47-61: LGTM! Clean inspector integration.The inspector is properly initialised from configuration and assigned to the translator struct. The integration is straightforward and doesn't alter existing control flow.
| // Log response to inspector if enabled | ||
| if t.inspector.Enabled() { | ||
| sessionID := original.Header.Get(t.inspector.GetSessionHeader()) | ||
| if sessionID == "" { | ||
| sessionID = original.Header.Get("X-Request-ID") | ||
| if sessionID == "" { | ||
| sessionID = "default" | ||
| } | ||
| } | ||
| if respBytes, err := json.Marshal(anthropicResp); err == nil { | ||
| if err := t.inspector.LogResponse(sessionID, respBytes); err != nil { | ||
| t.logger.Warn("Failed to log response to inspector", "error", err) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Extract duplicated session ID derivation logic.
The session ID derivation logic (lines 72-78) is duplicated in both response.go and request.go (lines 41-47). This violates the DRY principle and makes maintenance harder if the fallback logic needs to change.
Consider extracting this to a helper method on the Translator:
// getSessionID extracts session ID from headers with fallback chain
func (t *Translator) getSessionID(r *http.Request) string {
sessionID := r.Header.Get(t.inspector.GetSessionHeader())
if sessionID == "" {
sessionID = r.Header.Get("X-Request-ID")
if sessionID == "" {
sessionID = "default"
}
}
return sessionID
}Then use it in both files:
- sessionID := original.Header.Get(t.inspector.GetSessionHeader())
- if sessionID == "" {
- sessionID = original.Header.Get("X-Request-ID")
- if sessionID == "" {
- sessionID = "default"
- }
- }
+ sessionID := t.getSessionID(original)🤖 Prompt for AI Agents
In internal/adapter/translator/anthropic/response.go around lines 70 to 84, the
session ID derivation (falling back from t.inspector.GetSessionHeader() to
"X-Request-ID" to "default") is duplicated; extract it to a helper method on
Translator, e.g. func (t *Translator) getSessionID(r *http.Request) string that
implements the same fallback chain, add that method to the translator file (or a
shared translator utility), and replace the inline logic here with a call to
t.getSessionID(original); also update
internal/adapter/translator/anthropic/request.go to call the same helper so both
locations use the single implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/adapter/translator/anthropic/streaming.go (1)
306-336: Critical: tool call deltas can be emitted under the wrong content block (indexing bug).content_block_delta for tool arguments uses state.currentIndex instead of the tool’s content block index. If arguments arrive before id/name or multiple tool calls interleave, deltas can be written under a text block or another tool block. Buffering-only until the block exists and using state.toolIndexToBlock is required.
Apply this fix (guard + correct indexing + flush buffered args after block start):
@@ -func (t *Translator) sendToolArgumentsDelta(args string, toolIndex int, state *StreamingState, w http.ResponseWriter, rc *http.ResponseController) error { - state.toolCallBuffers[toolIndex].WriteString(args) +func (t *Translator) sendToolArgumentsDelta(args string, blockIndex int, toolIndex int, state *StreamingState, w http.ResponseWriter, rc *http.ResponseController) error { + // Always buffer for final Input, regardless of streaming emission + state.toolCallBuffers[toolIndex].WriteString(args) @@ - if err := t.writeEvent(w, "content_block_delta", map[string]interface{}{ + if err := t.writeEvent(w, "content_block_delta", map[string]interface{}{ "type": "content_block_delta", - "index": state.currentIndex, + "index": blockIndex, "delta": map[string]interface{}{ "type": "input_json_delta", "partial_json": args, }, }); err != nil { return err } return rc.Flush() } @@ for _, tc := range toolCalls { data, ok := extractToolCallData(tc) if !ok { continue } @@ // initialise buffer if first time seeing this tool index if _, exists := state.toolCallBuffers[data.toolIndex]; !exists { state.toolCallBuffers[data.toolIndex] = &strings.Builder{} } @@ // start block when we get id + name if data.id != "" && data.name != "" { if err := t.initializeToolBlock(data.id, data.name, data.toolIndex, state, w, rc); err != nil { return err } + // If arguments were buffered before the block started, emit them now + if buf := state.toolCallBuffers[data.toolIndex]; buf != nil && buf.Len() > 0 { + if blockIndex, found := state.toolIndexToBlock[data.toolIndex]; found { + if err := t.sendToolArgumentsDelta(buf.String(), blockIndex, data.toolIndex, state, w, rc); err != nil { + return err + } + buf.Reset() + } + } } @@ // buffer args chunks and send as partial_json if data.arguments != "" { - if err := t.sendToolArgumentsDelta(data.arguments, data.toolIndex, state, w, rc); err != nil { - return err - } + // Only emit streaming delta if the block exists; otherwise just buffer + if blockIndex, found := state.toolIndexToBlock[data.toolIndex]; found { + if err := t.sendToolArgumentsDelta(data.arguments, blockIndex, data.toolIndex, state, w, rc); err != nil { + return err + } + } else { + state.toolCallBuffers[data.toolIndex].WriteString(data.arguments) + } } }This preserves correct streaming semantics and ensures final Input is still reconstructed from buffers.
Also applies to: 339-354, 362-386
♻️ Duplicate comments (1)
internal/adapter/translator/anthropic/response.go (1)
70-84: Session ID derivation logic is duplicated.The session ID derivation logic (lines 72-78) is duplicated in both response.go and request.go (lines 41-47). This has been previously identified in review comments.
Consider extracting this to a helper method on the Translator as suggested in the previous review:
// getSessionID extracts session ID from headers with fallback chain func (t *Translator) getSessionID(r *http.Request) string { sessionID := r.Header.Get(t.inspector.GetSessionHeader()) if sessionID == "" { sessionID = r.Header.Get("X-Request-ID") if sessionID == "" { sessionID = defaultSessionID } } return sessionID }Then use it in both files:
- sessionID := original.Header.Get(t.inspector.GetSessionHeader()) - if sessionID == "" { - sessionID = original.Header.Get("X-Request-ID") - if sessionID == "" { - sessionID = defaultSessionID - } - } + sessionID := t.getSessionID(original)
🧹 Nitpick comments (5)
internal/adapter/translator/anthropic/streaming.go (1)
75-80: Consider increasing the scanner token limit for large tool arguments.1 MiB per SSE line may still be tight for complex tool arguments. Suggest 8–16 MiB or switching to bufio.Reader with manual line splitting. This reduces risk of truncated chunks.
internal/adapter/inspector/simple_test.go (1)
103-123: Minor: avoid potential midnight rollover flake.Tests derive “today” via time.Now() after writes. Extremely rare, but a date change at midnight could misalign directories. Optionally capture the date once before logging or inject a clock into the inspector for tests.
internal/adapter/inspector/simple.go (2)
94-101: Use filepath.Rel for robust containment check (avoid HasPrefix pitfalls).HasPrefix on cleaned absolute paths can yield false positives for sibling directories (e.g. /var/log/abc vs /var/log/abcc). Use filepath.Rel to ensure the test path is inside the output directory.
- // try and ensure the resolved path is within the output directory - // uses Clean to normalise paths and prevent ../ bypasses - if !strings.HasPrefix(filepath.Clean(absTestPath), filepath.Clean(absOutputDir)) { - return "", fmt.Errorf("session ID would escape output directory") - } + // Ensure absTestPath resides within absOutputDir + rel, err := filepath.Rel(absOutputDir, absTestPath) + if err != nil || strings.HasPrefix(rel, "..") || strings.HasPrefix(filepath.ToSlash(rel), "../") { + return "", fmt.Errorf("session ID would escape output directory") + }
168-173: Typo in comment (“owtner”).s/owtner/owner/
- // Use 0700 permissions - owtner only access (not world-readable) + // Use 0700 permissions - owner only access (not world-readable)internal/config/types.go (1)
209-236: Confirm intent: deny-list blocks all of /var/, /usr/, etc.validateOutputPath currently rejects any path under common system trees (including /var/log/...). That’s quite restrictive and may surprise operators who expect to point logs to /var/log/olla-inspector.
- If the goal is “don’t overwrite system dirs”, consider allowing subdirectories under those roots (e.g. /var/log/olla-inspector) while still rejecting the root directories themselves.
- Alternative: require a relative path (default ./inspector-logs) unless an explicit allow-list is configured.
Happy to propose a tuned rule set if you confirm desired behaviour.
Also applies to: 246-267
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
docs/content/notes/anthropic-inspector.md(1 hunks)docs/content/notes/overview.md(1 hunks)docs/mkdocs.yml(2 hunks)internal/adapter/inspector/simple.go(1 hunks)internal/adapter/inspector/simple_test.go(1 hunks)internal/adapter/translator/anthropic/constants.go(1 hunks)internal/adapter/translator/anthropic/request.go(2 hunks)internal/adapter/translator/anthropic/response.go(1 hunks)internal/adapter/translator/anthropic/streaming.go(3 hunks)internal/app/services/http.go(1 hunks)internal/config/config_test.go(2 hunks)internal/config/types.go(3 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/content/notes/anthropic-inspector.md
[style] ~12-~12: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1754 characters long)
Context: ...s and troubleshoot integration issues. !!! danger "DEVELOPMENT USE ONLY - DO NOT E...
(EN_EXCESSIVE_EXCLAMATION)
docs/content/notes/overview.md
[uncategorized] ~9-~9: Possible missing comma found.
Context: ...ot warrant a full dedicated area/section but are still useful to know about. * [Ant...
(AI_HYDRA_LEO_MISSING_COMMA)
🪛 markdownlint-cli2 (0.18.1)
docs/content/notes/anthropic-inspector.md
29-29: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
54-54: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
70-70: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
81-81: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
90-90: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
96-96: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
102-102: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
112-112: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
119-119: Code block style
Expected: indented; Actual: fenced
(MD046, code-block-style)
🔇 Additional comments (7)
internal/app/services/http.go (1)
164-168: LGTM! Appropriate warning for experimental feature.The warning mechanism correctly alerts developers when the inspector is enabled, with a clear reference to documentation. The non-exported method placement and timing (after server start) are appropriate.
docs/mkdocs.yml (1)
116-116: LGTM! Documentation navigation properly updated.The new Notes sections are appropriately placed in both Home and Development sections, providing clear navigation to the inspector documentation.
Also applies to: 179-180
internal/adapter/translator/anthropic/request.go (1)
39-52: LGTM! Request logging correctly implemented.The inspector logging properly captures requests with appropriate fallback logic for session identification. Logging failures are handled gracefully without disrupting the request flow.
Note: The session ID derivation logic (lines 41-47) is duplicated in response.go. Consider extracting it to a helper method as suggested in the previous review of response.go to maintain DRY principles.
internal/adapter/translator/anthropic/constants.go (1)
27-31: LGTM! Well-documented fallback constant.The
defaultSessionIDconstant is clearly documented and provides a sensible fallback value for session identification when headers are absent.docs/content/notes/anthropic-inspector.md (1)
1-122: LGTM! Comprehensive and well-structured documentation.The documentation thoroughly covers all aspects of the Anthropic Inspector, including:
- Clear security warnings about production usage
- Detailed configuration and usage examples
- Practical analysis commands with
jqanddiff- Security considerations for log protection
The emphasis on the experimental nature and security implications is appropriate for a dev-only debugging tool.
internal/config/config_test.go (1)
5-5: LGTM! Comprehensive test coverage for inspector configuration.The test suite thoroughly validates:
- Valid and invalid configuration scenarios
- Default value application
- Dangerous path detection (both Unix and Windows)
- HTTP header validation (spaces, colons, special characters)
- Platform-specific tests with appropriate skipping on Windows
The test cases provide excellent coverage for security-critical validation logic.
Also applies to: 636-837
internal/adapter/translator/anthropic/streaming.go (1)
464-468: Inspector logging hook looks good.Logging happens post-finalisation, uses header-derived session with safe fallback, and failures don’t affect the stream. LGTM.
Also applies to: 473-515
|
|
||
| # Notes | ||
|
|
||
| This section covers quite notes and bytes of information about Olla's features (mostly experimental) that may not warrant a full dedicated area/section but are still useful to know about. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo: "quite notes" should be "quick notes".
Apply this diff:
-This section covers quite notes and bytes of information about Olla's features (mostly experimental) that may not warrant a full dedicated area/section but are still useful to know about.
+This section covers quick notes and bytes of information about Olla's features (mostly experimental) that may not warrant a full dedicated area/section, but are still useful to know about.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| This section covers quite notes and bytes of information about Olla's features (mostly experimental) that may not warrant a full dedicated area/section but are still useful to know about. | |
| This section covers quick notes and bytes of information about Olla's features (mostly experimental) that may not warrant a full dedicated area/section, but are still useful to know about. |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~9-~9: Possible missing comma found.
Context: ...ot warrant a full dedicated area/section but are still useful to know about. * [Ant...
(AI_HYDRA_LEO_MISSING_COMMA)
🤖 Prompt for AI Agents
In docs/content/notes/overview.md around line 9, the phrase "quite notes" is a
typo and should be "quick notes"; update the sentence to read "This section
covers quick notes and bytes of information about Olla's features (mostly
experimental) that may not warrant a full dedicated area/section but are still
useful to know about." preserving the rest of the sentence and punctuation.
update token_count and translator strategies
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/adapter/translator/anthropic/streaming.go (1)
340-354: Bug: wrong index used for tool argument deltas when multiple tool calls interleave.content_block_delta currently uses state.currentIndex, which tracks the last-started block. If arguments for tool index i arrive after starting tool index j, deltas for i will reference j’s index, breaking client reconstruction.
Apply this minimal fix to select the correct block index per toolIndex:
func (t *Translator) sendToolArgumentsDelta(args string, toolIndex int, state *StreamingState, w http.ResponseWriter, rc *http.ResponseController) error { - state.toolCallBuffers[toolIndex].WriteString(args) + state.toolCallBuffers[toolIndex].WriteString(args) + + // Resolve the correct content block index for this tool index. + blockIndex := state.currentIndex + if mapped, ok := state.toolIndexToBlock[toolIndex]; ok { + blockIndex = mapped + } if err := t.writeEvent(w, "content_block_delta", map[string]interface{}{ "type": "content_block_delta", - "index": state.currentIndex, + "index": blockIndex, "delta": map[string]interface{}{ "type": "input_json_delta", "partial_json": args, }, }); err != nil { return err }Optional hardening: if the block for toolIndex isn’t started yet, buffer only and skip sending deltas until initialiseToolBlock runs, then flush buffered args.
🧹 Nitpick comments (10)
internal/adapter/translator/anthropic/translator.go (2)
47-54: Inspector initialisation is straightforward; consider lazy/no-op when disabled.Current approach is fine. If you want to shave overhead in prod, you could construct a nil/no‑op inspector when cfg.Inspector.Enabled is false.
81-93: Header constant duplication.You already import internal/core/constants elsewhere. Consider reusing a shared header constant (if one exists) instead of redefining "X-Request-ID" locally.
internal/adapter/translator/anthropic/request.go (2)
298-315: System prompt join semantics.Joining multiple text blocks with no separator can smash words together. Consider joining with a single space (or preserving original separators) to avoid accidental concatenation.
- return strings.Join(textParts, "") + return strings.Join(textParts, " ")
39-46: Add redaction config to InspectorConfig for safer development logging.Raw request bodies are logged directly without filtering in
LogRequest(). Consider extendingInspectorConfigwith optional fields (e.g.,Redact bool,UnsafeFullBody bool) to truncate/mask sensitive content by default, keeping full bodies behind an explicit flag. This prevents accidental exposure of tool arguments or message content if inspector is enabled outside development.internal/adapter/translator/anthropic/token_count.go (6)
78-87: Remove duplicate text handling; delegate to the single counting source.Use countContentBlockChars for all typed blocks to keep logic in one place and avoid drift.
- if block.Type == contentTypeText { - totalChars += len(block.Text) - } - // for other block types, convert to typed block and count - if block.Type != "" && block.Type != contentTypeText { - totalChars += countContentBlockChars(&block) - } + if block.Type != "" { + totalChars += countContentBlockChars(&block) + }
130-139: Support string items within []interface{} for system blocks.Robustly accept arrays that mix strings and blocks.
- for _, block := range systemBlocks { - if normalised, nok := normaliseContentBlock(block); nok { + for _, block := range systemBlocks { + if s, isStr := block.(string); isStr { + if s != "" { + if err := fn(ContentBlock{Type: contentTypeText, Text: s}); err != nil { + return err + } + } + continue + } + if normalised, nok := normaliseContentBlock(block); nok { if err := fn(*normalised); err != nil { return err } } }
162-167: *Add []ContentBlock support for message content (parity with system/tool_result).Covers pointer-shaped message content arrays.
case []ContentBlock: // typed block arrays for i := range content { totalChars += countContentBlockChars(&content[i]) } + case *[]ContentBlock: + if content != nil { + for i := range *content { + totalChars += countContentBlockChars(&(*content)[i]) + } + }
172-213: normaliseContentBlock: accept plain strings as text blocks.Improves resilience when untyped arrays contain strings.
func normaliseContentBlock(block interface{}) (*ContentBlock, bool) { - // already a typed ContentBlock pointer, return as-is + // plain string -> text block + if s, ok := block.(string); ok { + return &ContentBlock{Type: contentTypeText, Text: s}, true + } + // already a typed ContentBlock pointer, return as-is if typedBlock, ok := block.(*ContentBlock); ok { return typedBlock, true }Happy to add unit tests covering: string, []interface{} with mixed types, []ContentBlock, and pointer variants.
230-249: Count runes, not bytes, for text and tool_use name.len(...) overcounts non‑ASCII text; utf8.RuneCountInString improves estimation with multilingual input.
case contentTypeText: - // text blocks contribute their text length - return len(block.Text) + // text blocks contribute their rune length + return utf8.RuneCountInString(block.Text) case contentTypeToolUse: - // tool use blocks contribute name length plus serialised input - return len(block.Name) + countToolInput(block.Input) + // tool use blocks contribute name rune length plus serialised input + return utf8.RuneCountInString(block.Name) + countToolInput(block.Input)Add import:
import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" + "unicode/utf8"
255-296: countToolResultContent: handle single map content and use rune counts for strings.Covers map-shaped content and improves non‑ASCII accuracy.
if contentStr, ok := content.(string); ok { - return len(contentStr) + return utf8.RuneCountInString(contentStr) } +// single untyped block map +if blockMap, ok := content.(map[string]interface{}); ok { + if normalised, ok := normaliseContentBlock(blockMap); ok { + return countContentBlockChars(normalised) + } +}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
internal/adapter/translator/anthropic/request.go(3 hunks)internal/adapter/translator/anthropic/response.go(1 hunks)internal/adapter/translator/anthropic/streaming.go(3 hunks)internal/adapter/translator/anthropic/token_count.go(2 hunks)internal/adapter/translator/anthropic/translator.go(4 hunks)
🔇 Additional comments (6)
internal/adapter/translator/anthropic/response.go (1)
70-78: LGTM: inspector logging with centralised session ID.Use of t.getSessionID(original) removes the prior duplication and the logging is cleanly gated behind Enabled().
internal/adapter/translator/anthropic/translator.go (2)
95-101: LGTM: thin wrapper keeps system-block iteration cohesive.
75-93: Verification complete—centralised session ID logic confirmed.Script output confirms no stray inlined header fallback logic remains elsewhere in production code. The
getSessionIDmethod ininternal/adapter/translator/anthropic/translator.go(lines 75–93) is the sole source of truth for session ID derivation. All other occurrences ofX-Request-IDin the codebase are appropriately scoped to constants, documentation, test headers, and comments. Code changes are sound and maintain readability.internal/adapter/translator/anthropic/streaming.go (3)
65-67: LGTM: passing original request into finalisation enables session-aware logging.
464-469: LGTM: end-of-stream inspector logging is correctly gated.Nice separation: reconstruction happens after final SSE events; failures don’t affect client output.
473-510: LGTM: reconstructed response mirrors non‑streaming shape.Good reuse of mapFinishReasonToStopReason and session derivation via getSessionID(original).
adds back basic request logging for Anthropic for debugging, this isn't designed for production and was a quick port.
Summary by CodeRabbit
New Features
Configuration
Documentation
Tests
Behaviour