fix: harden OpenAI-compatible parsing with SSE fallback - #312
Conversation
WalkthroughImplemented streaming support for OpenAI-compatible providers in SpacebotModel::stream by constructing live RawStreamingChoice events. Extended RawStreamingResponse with optional usage field. Added SSE parsing utilities and streaming fallback mechanism with enhanced error handling and usage propagation throughout. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/llm/model.rs`:
- Around line 1006-1010: The code builds a primary_failure_reason by embedding
the raw response body (using truncate_body(&primary_text)) and later logs it at
warn level; change this so sensitive payloads are not included in logs: update
the format! that sets primary_failure_reason in src/llm/model.rs to avoid
inserting the response text (instead include non-sensitive metadata like
response length, a fixed redaction marker, or a short hash/checksum), and update
the warn call that logs primary_failure_reason to only emit that sanitized
metadata. Locate the string construction and the warn! that references
primary_failure_reason and replace the embedded body with a
redacted/metadata-only value.
- Around line 1447-1449: The SSE chunk parsing currently swallows JSON parse
errors where the code does serde_json::from_str::<serde_json::Value>(data) and
continues; change this to fail-fast by returning or propagating an Err (or
logging the error with context and breaking the stream) instead of silently
continuing. Similarly, find the reconstructed tool-argument parsing site that
currently falls back to {} on failure (the block that parses the reconstructed
tool args and uses {} on parse error) and replace the silent fallback with
proper error handling: propagate the parse error or return a descriptive Err/log
entry including the raw payload and tool name so a single corrupt chunk cannot
silently mutate arguments.
| Some(format!( | ||
| "response body was not valid JSON and did not look like SSE. Body: {}", | ||
| truncate_body(&primary_text) | ||
| )) | ||
| } |
There was a problem hiding this comment.
Avoid logging response-body snippets at warn level.
Line 1007 embeds response text into primary_failure_reason, and Line 1015 logs that field at warn. This can leak model/user payload data into centralized logs.
🔧 Proposed fix
- Some(format!(
- "response body was not valid JSON and did not look like SSE. Body: {}",
- truncate_body(&primary_text)
- ))
+ Some("response body was not valid JSON and did not look like SSE".to_string())Also applies to: 1015-1018
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/llm/model.rs` around lines 1006 - 1010, The code builds a
primary_failure_reason by embedding the raw response body (using
truncate_body(&primary_text)) and later logs it at warn level; change this so
sensitive payloads are not included in logs: update the format! that sets
primary_failure_reason in src/llm/model.rs to avoid inserting the response text
(instead include non-sensitive metadata like response length, a fixed redaction
marker, or a short hash/checksum), and update the warn call that logs
primary_failure_reason to only emit that sanitized metadata. Locate the string
construction and the warn! that references primary_failure_reason and replace
the embedded body with a redacted/metadata-only value.
| let Ok(event_body) = serde_json::from_str::<serde_json::Value>(data) else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Fail fast on malformed SSE JSON/tool-argument chunks instead of silently continuing.
Line 1447 currently drops invalid SSE data: payloads, and Line 1606 tolerates malformed reconstructed tool arguments by degrading to {}. A single corrupt chunk can silently mutate a tool call into incorrect arguments.
🔧 Proposed fix
- let Ok(event_body) = serde_json::from_str::<serde_json::Value>(data) else {
- continue;
- };
+ let event_body = serde_json::from_str::<serde_json::Value>(data).map_err(|error| {
+ CompletionError::ProviderError(format!(
+ "{provider_label} streaming response contained invalid JSON event: {error}. Event: {}",
+ truncate_body(data)
+ ))
+ })?;
...
- let arguments =
- parse_openai_tool_arguments(&serde_json::Value::String(tool_call.arguments));
+ let arguments = if tool_call.arguments.trim().is_empty() {
+ serde_json::json!({})
+ } else {
+ serde_json::from_str::<serde_json::Value>(&tool_call.arguments).map_err(|error| {
+ CompletionError::ProviderError(format!(
+ "{provider_label} streaming tool_call arguments were invalid JSON: {error}"
+ ))
+ })?
+ };As per coding guidelines: "Never silently discard errors. No let _ = on Results. Handle them, log them, or propagate them."
Also applies to: 1605-1606
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/llm/model.rs` around lines 1447 - 1449, The SSE chunk parsing currently
swallows JSON parse errors where the code does
serde_json::from_str::<serde_json::Value>(data) and continues; change this to
fail-fast by returning or propagating an Err (or logging the error with context
and breaking the stream) instead of silently continuing. Similarly, find the
reconstructed tool-argument parsing site that currently falls back to {} on
failure (the block that parses the reconstructed tool args and uses {} on parse
error) and replace the silent fallback with proper error handling: propagate the
parse error or return a descriptive Err/log entry including the raw payload and
tool name so a single corrupt chunk cannot silently mutate arguments.
Implements Signal messaging support using the signal-cli daemon HTTP API,
following the existing adapter architecture (Telegram, Discord, Slack, Twitch).
## Implementation Details
### Signal Adapter (`src/messaging/signal.rs`)
- **Inbound:** SSE stream from signal-cli daemon with automatic reconnection
and exponential backoff (2s → 60s). Handles UTF-8 chunk boundaries
and buffer overflow protection (1MB max buffer, 256KB max event).
- **Outbound:** JSON-RPC `send` calls. Critical: DM recipients must be
passed as a JSON **array** (`["+1234567890"]`) per signal-cli requirements.
- **Typing indicators:** JSON-RPC `sendTyping` with repeating task (expire ~5s).
- **Attachments outbound:** Temp files in `{instance_dir}/tmp/`, file paths
passed in `attachments` JSON array, auto-cleaned after send.
- **Attachments inbound:** signal-cli provides file paths on disk (treated as
opaque - message falls back to `[Attachment]` placeholder).
- **Streaming:** Not supported (Signal can't edit messages). StreamStart/
Chunk/End are no-ops.
- **Permissions:** DM allowlist + group filter (None = block all groups).
### Config Changes
- `src/config/types.rs`: Added and `SignalInstance `SignalConfig`Config`
with `SystemSecrets` impl for http_url credential.
- `src/config/toml_schema.rs`: Added TOML deserialization types.
- `src/config/load.rs`: Config loading with env var fallbacks
(`SIGNAL_HTTP_URL`, `SIGNAL_ACCOUNT`).
- `src/config/permissions.rs`: Added `SignalPermissions` with
`from_config()`, `from_instance_config()`, `from_bindings_for_adapter()`.
- `src/config/watcher.rs`: Added hot-reload support for Signal permissions
and adapter hot-start on config changes.
### Wiring
- `src/messaging.rs`: Added `pub mod signal;`.
- `src/main.rs`: Wired adapter registration in `initialize_agents()`,
file watcher, and hot-reload paths. Supports default + named instances.
- `src/secrets/store.rs`: Added `SignalConfig` to system secret registry
for auto-categorization of `SIGNAL_HTTP_URL`.
### Testing
- 23 unit tests covering SSE parsing, permission filtering, metadata
construction, and JSON-RPC parameter building.
## Usage
Add to config.toml:
```toml
[messaging.signal]
enabled = true
http_url = "http://127.0.0.1:8686"
account = "+1234567890"
ignore_stories = true
dm_allowed_users = ["+0987654321"]
[[messaging.signal.instances]]
name = "work"
enabled = true
http_url = "http://127.0.0.1:8687"
account = "+1122334455"
dm_allowed_users = ["+5566778899"]
```
Requires signal-cli daemon running: `signal-cli daemon --http`
Closes spacedriveapp#312
Summary
stream: trueand parses SSE when the primary non-stream response cannot be decoded as JSON (the worker failure path seen with OpenRouter/Moonshot)accept-encoding: identityon these calls to reduce response-body decode failures from compressed/chunked provider responsesSpacebotModel::streamwith a usable streamed wrapper and add coverage for SSE reconstruction (including tool-call deltas) and streaming usage reportingTesting
./scripts/preflight.sh./scripts/gate-pr.sh