Skip to content

Fix #839 and #843: add explicit timeout to AsyncOpenAI and emit struc…#853

Open
gmrnlg1971 wants to merge 1 commit into
plastic-labs:mainfrom
gmrnlg1971:fix/839-843
Open

Fix #839 and #843: add explicit timeout to AsyncOpenAI and emit struc…#853
gmrnlg1971 wants to merge 1 commit into
plastic-labs:mainfrom
gmrnlg1971:fix/839-843

Conversation

@gmrnlg1971

@gmrnlg1971 gmrnlg1971 commented Jun 27, 2026

Copy link
Copy Markdown

…tured logs on unparseable LLM output

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of structured output parsing failures with clearer errors and better logging details.
    • Requests that exceed the provider’s structured-output support now fail explicitly instead of silently falling back.
    • Added longer timeout handling for OpenAI-based requests to reduce premature timeouts.

…yncOpenAI and emit structured logs on unparseable LLM output
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Structured-output failures now raise StructuredOutputError with error metadata, the OpenAI backend propagates structured parse rejections as that error, the deriver logs parse-failure context before re-raising, and OpenAI client construction uses a 600-second timeout.

Changes

Structured output failure handling and OpenAI timeouts

Layer / File(s) Summary
Error contract and repair flow
src/llm/structured_output.py
StructuredOutputError now stores raw_content and reason, and JSON repair/validation failures raise that error instead of falling back to empty values.
OpenAI parse rejection handling
src/llm/backends/openai.py
OpenAIBackend.complete() now converts structured-output BadRequestError responses into StructuredOutputError with reason="bad_request".
Deriver parse-failure logging
src/deriver/deriver.py
The minimal deriver LLM call catches StructuredOutputError, logs structured parse-failure fields, and re-raises the exception.
OpenAI client timeouts
src/embedding_client.py, src/llm/registry.py
OpenAI AsyncOpenAI construction in the embedding client and registry paths now passes timeout=600.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through parse-fail trails so neat,
With error crumbs and timing sweet.
The backend squeaked, “No more empty sky,”
The deriver logged, “Here’s why!”
And 600 seconds kept my tea warm by.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: adding an explicit AsyncOpenAI timeout and structured logging for unparseable LLM output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/deriver/deriver.py`:
- Around line 167-178: The error logging in deriver.py is too permissive because
Deriver.parse failure currently includes raw_response_excerpt, which may expose
user or inferred PII. Update the logger.error call in the deriver parsing
failure path to avoid emitting raw LLM text by default; instead log
parse_failure_reason plus safe metadata such as excerpt length and a stable
hash/fingerprint of the response. Keep any raw excerpt behind an explicit
diagnostic or PII-approved flag, and ensure the change is applied where e.reason
and e.raw_content are handled.

In `@src/llm/structured_output.py`:
- Around line 13-16: Move the StructuredOutputError custom exception out of
structured_output.py into src/exceptions.py so it becomes the canonical
exception definition, then import and use that class from
src/llm/structured_output.py and any other callers/handlers that reference it.
Keep the expanded __init__ contract (message, raw_content, reason) on the moved
exception class, and update all references to point at the centralized exception
module rather than defining the type locally.
- Around line 50-56: The new raise statements in the structured output error
handling exceed the Black 88-character limit. Reformat the two
StructuredOutputError raises in the exception handlers inside the JSON
repair/validation flow so they wrap cleanly within the line-length rule while
preserving the same message, raw_content, and reason arguments; use the existing
symbols StructuredOutputError, ValidationError, and the final/response_model
validation path to locate the statements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25a16b6a-661c-414b-8d4c-935cca4bb767

📥 Commits

Reviewing files that changed from the base of the PR and between eb386c3 and e44d42a.

📒 Files selected for processing (5)
  • src/deriver/deriver.py
  • src/embedding_client.py
  • src/llm/backends/openai.py
  • src/llm/registry.py
  • src/llm/structured_output.py

Comment thread src/deriver/deriver.py
Comment on lines +167 to +178
logger.error(
"Deriver parse failure",
extra={
"work_unit_id": f"{observed}_{latest_message.session_name}",
"peer_id": observed,
"session_id": latest_message.session_name,
"transport": model_config.transport,
"model": model_config.model,
"structured_output_mode": model_config.structured_output_mode,
"parse_failure_reason": e.reason,
"raw_response_excerpt": e.raw_content[:500] if e.raw_content else "",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw LLM output by default.

raw_response_excerpt can contain user message content or inferred PII from the deriver prompt. Prefer logging length/hash/reason by default, and only include raw excerpts behind an explicit diagnostic/PII-approved setting.

Proposed safer logging shape
                 "structured_output_mode": model_config.structured_output_mode,
                 "parse_failure_reason": e.reason,
-                "raw_response_excerpt": e.raw_content[:500] if e.raw_content else "",
+                "raw_response_length": len(e.raw_content or ""),
+                "raw_response_hash": (
+                    hashlib.sha256(e.raw_content.encode()).hexdigest()
+                    if e.raw_content
+                    else ""
+                ),

This also requires adding:

+import hashlib
📝 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.

Suggested change
logger.error(
"Deriver parse failure",
extra={
"work_unit_id": f"{observed}_{latest_message.session_name}",
"peer_id": observed,
"session_id": latest_message.session_name,
"transport": model_config.transport,
"model": model_config.model,
"structured_output_mode": model_config.structured_output_mode,
"parse_failure_reason": e.reason,
"raw_response_excerpt": e.raw_content[:500] if e.raw_content else "",
}
logger.error(
"Deriver parse failure",
extra={
"work_unit_id": f"{observed}_{latest_message.session_name}",
"peer_id": observed,
"session_id": latest_message.session_name,
"transport": model_config.transport,
"model": model_config.model,
"structured_output_mode": model_config.structured_output_mode,
"parse_failure_reason": e.reason,
"raw_response_length": len(e.raw_content or ""),
"raw_response_hash": (
hashlib.sha256(e.raw_content.encode()).hexdigest()
if e.raw_content
else ""
),
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/deriver/deriver.py` around lines 167 - 178, The error logging in
deriver.py is too permissive because Deriver.parse failure currently includes
raw_response_excerpt, which may expose user or inferred PII. Update the
logger.error call in the deriver parsing failure path to avoid emitting raw LLM
text by default; instead log parse_failure_reason plus safe metadata such as
excerpt length and a stable hash/fingerprint of the response. Keep any raw
excerpt behind an explicit diagnostic or PII-approved flag, and ensure the
change is applied where e.reason and e.raw_content are handled.

Comment on lines +13 to +16
def __init__(self, message: str, raw_content: str = "", reason: str = ""):
super().__init__(message)
self.raw_content = raw_content
self.reason = reason

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep custom exceptions in src/exceptions.py.

This change expands the StructuredOutputError contract, but the custom exception still lives in src/llm/structured_output.py. Move the exception type to src/exceptions.py and import it from there so downstream handlers use the canonical exception module. As per coding guidelines, "Define custom exception types in src/exceptions.py and use them throughout the codebase".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llm/structured_output.py` around lines 13 - 16, Move the
StructuredOutputError custom exception out of structured_output.py into
src/exceptions.py so it becomes the canonical exception definition, then import
and use that class from src/llm/structured_output.py and any other
callers/handlers that reference it. Keep the expanded __init__ contract
(message, raw_content, reason) on the moved exception class, and update all
references to point at the centralized exception module rather than defining the
type locally.

Source: Coding guidelines

Comment on lines +50 to +56
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
raise StructuredOutputError(f"Failed to repair JSON: {e}", raw_content=raw_content, reason="empty_after_repair") from e

try:
return response_model.model_validate_json(final)
except ValidationError:
if response_model is PromptRepresentation:
return PromptRepresentation(explicit=[])
raise
except ValidationError as e:
raise StructuredOutputError(f"Validation failed after repair: {e}", raw_content=raw_content, reason="validation_error") from e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrap the new raise statements to satisfy the 88-character limit.

Lines 51 and 56 exceed the project’s Black-compatible line length.

Proposed formatting fix
     except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
-        raise StructuredOutputError(f"Failed to repair JSON: {e}", raw_content=raw_content, reason="empty_after_repair") from e
+        raise StructuredOutputError(
+            f"Failed to repair JSON: {e}",
+            raw_content=raw_content,
+            reason="empty_after_repair",
+        ) from e
 
     try:
         return response_model.model_validate_json(final)
     except ValidationError as e:
-        raise StructuredOutputError(f"Validation failed after repair: {e}", raw_content=raw_content, reason="validation_error") from e
+        raise StructuredOutputError(
+            f"Validation failed after repair: {e}",
+            raw_content=raw_content,
+            reason="validation_error",
+        ) from e

As per coding guidelines, "Enforce 88 character line length (Black compatible) in Python code".

📝 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.

Suggested change
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
raise StructuredOutputError(f"Failed to repair JSON: {e}", raw_content=raw_content, reason="empty_after_repair") from e
try:
return response_model.model_validate_json(final)
except ValidationError:
if response_model is PromptRepresentation:
return PromptRepresentation(explicit=[])
raise
except ValidationError as e:
raise StructuredOutputError(f"Validation failed after repair: {e}", raw_content=raw_content, reason="validation_error") from e
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
raise StructuredOutputError(
f"Failed to repair JSON: {e}",
raw_content=raw_content,
reason="empty_after_repair",
) from e
try:
return response_model.model_validate_json(final)
except ValidationError as e:
raise StructuredOutputError(
f"Validation failed after repair: {e}",
raw_content=raw_content,
reason="validation_error",
) from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llm/structured_output.py` around lines 50 - 56, The new raise statements
in the structured output error handling exceed the Black 88-character limit.
Reformat the two StructuredOutputError raises in the exception handlers inside
the JSON repair/validation flow so they wrap cleanly within the line-length rule
while preserving the same message, raw_content, and reason arguments; use the
existing symbols StructuredOutputError, ValidationError, and the
final/response_model validation path to locate the statements.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant