|
7 | 7 |
|
8 | 8 | import pytest |
9 | 9 | from litellm.types.utils import ModelResponse, Usage |
10 | | -from pydantic import ValidationError |
| 10 | +from pydantic import BaseModel, Field, ValidationError |
11 | 11 |
|
12 | 12 | from openhands.sdk.llm.utils.metrics import Metrics |
13 | 13 | from openhands.sdk.llm.utils.telemetry import Telemetry, _safe_json |
@@ -416,7 +416,51 @@ def test_log_completion_with_raw_response(self, mock_metrics, mock_response): |
416 | 416 |
|
417 | 417 | assert "raw_response" in data |
418 | 418 |
|
419 | | - def test_log_completion_model_name_sanitization(self, mock_metrics, mock_response): |
| 419 | + def test_log_completion_with_pydantic_objects_in_context( |
| 420 | + self, mock_metrics, mock_response |
| 421 | + ): |
| 422 | + """ |
| 423 | + Ensure logging works when log_ctx contains Pydantic models with |
| 424 | + excluded fields. This simulates the remote-run case where tools |
| 425 | + (Pydantic models with excluded runtime-only fields like executors) |
| 426 | + are included in the log context. Using Pydantic's model_dump should |
| 427 | + avoid circular references. |
| 428 | + """ |
| 429 | + |
| 430 | + class SelfReferencingModel(BaseModel): |
| 431 | + name: str |
| 432 | + # Simulate an executor-like field that should not be serialized |
| 433 | + executor: object | None = Field(default=None, exclude=True) |
| 434 | + |
| 435 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 436 | + telemetry = Telemetry( |
| 437 | + model_name="gpt-4o", |
| 438 | + log_enabled=True, |
| 439 | + log_dir=temp_dir, |
| 440 | + metrics=mock_metrics, |
| 441 | + ) |
| 442 | + |
| 443 | + # Create a self-referencing instance via an excluded field |
| 444 | + m = SelfReferencingModel(name="tool-like") |
| 445 | + m.executor = m # would create a cycle if serialized via __dict__ |
| 446 | + |
| 447 | + telemetry.on_request({"tools": [m]}) |
| 448 | + |
| 449 | + with warnings.catch_warnings(record=True) as w: |
| 450 | + warnings.simplefilter("always") |
| 451 | + telemetry.log_llm_call(mock_response, 0.25) |
| 452 | + |
| 453 | + # Should not raise circular reference warnings |
| 454 | + msgs = [str(x.message) for x in w] |
| 455 | + assert not any("Circular reference detected" in s for s in msgs) |
| 456 | + |
| 457 | + # Log file should be created and readable JSON |
| 458 | + files = os.listdir(temp_dir) |
| 459 | + assert len(files) == 1 |
| 460 | + with open(os.path.join(temp_dir, files[0])) as f: |
| 461 | + data = json.loads(f.read()) |
| 462 | + assert "response" in data |
| 463 | + |
420 | 464 | """Test that model names with slashes are sanitized in filenames.""" |
421 | 465 | with tempfile.TemporaryDirectory() as temp_dir: |
422 | 466 | telemetry = Telemetry( |
|
0 commit comments