epi-recorder 1.0.0__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epi_cli/__main__.py +7 -0
- epi_cli/keys.py +3 -3
- epi_cli/ls.py +160 -0
- epi_cli/main.py +73 -8
- epi_cli/record.py +10 -3
- epi_cli/run.py +318 -0
- epi_cli/verify.py +20 -20
- epi_cli/view.py +68 -13
- epi_core/__init__.py +1 -1
- epi_core/redactor.py +14 -1
- epi_core/schemas.py +34 -3
- epi_recorder/__init__.py +1 -1
- epi_recorder/api.py +217 -20
- epi_recorder/environment.py +21 -0
- epi_recorder/patcher.py +88 -7
- epi_recorder-1.1.1.dist-info/METADATA +569 -0
- epi_recorder-1.1.1.dist-info/RECORD +28 -0
- epi_viewer_static/app.js +77 -1
- epi_recorder-1.0.0.dist-info/METADATA +0 -503
- epi_recorder-1.0.0.dist-info/RECORD +0 -25
- {epi_recorder-1.0.0.dist-info → epi_recorder-1.1.1.dist-info}/WHEEL +0 -0
- {epi_recorder-1.0.0.dist-info → epi_recorder-1.1.1.dist-info}/entry_points.txt +0 -0
- {epi_recorder-1.0.0.dist-info → epi_recorder-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {epi_recorder-1.0.0.dist-info → epi_recorder-1.1.1.dist-info}/top_level.txt +0 -0
epi_recorder/api.py
CHANGED
|
@@ -5,13 +5,15 @@ Provides a context manager for recording EPI packages programmatically
|
|
|
5
5
|
with minimal code changes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import functools
|
|
8
9
|
import json
|
|
10
|
+
import os
|
|
9
11
|
import shutil
|
|
10
12
|
import tempfile
|
|
11
13
|
import threading
|
|
12
14
|
from datetime import datetime
|
|
13
15
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict, List, Optional
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
15
17
|
|
|
16
18
|
from epi_core.container import EPIContainer
|
|
17
19
|
from epi_core.schemas import ManifestModel
|
|
@@ -45,7 +47,13 @@ class EpiRecorderSession:
|
|
|
45
47
|
tags: Optional[List[str]] = None,
|
|
46
48
|
auto_sign: bool = True,
|
|
47
49
|
redact: bool = True,
|
|
48
|
-
default_key_name: str = "default"
|
|
50
|
+
default_key_name: str = "default",
|
|
51
|
+
# New metadata fields
|
|
52
|
+
goal: Optional[str] = None,
|
|
53
|
+
notes: Optional[str] = None,
|
|
54
|
+
metrics: Optional[Dict[str, Union[float, str]]] = None,
|
|
55
|
+
approved_by: Optional[str] = None,
|
|
56
|
+
metadata_tags: Optional[List[str]] = None, # Renamed to avoid conflict with tags parameter
|
|
49
57
|
):
|
|
50
58
|
"""
|
|
51
59
|
Initialize EPI recording session.
|
|
@@ -57,6 +65,11 @@ class EpiRecorderSession:
|
|
|
57
65
|
auto_sign: Whether to automatically sign on exit (default: True)
|
|
58
66
|
redact: Whether to redact secrets (default: True)
|
|
59
67
|
default_key_name: Name of key to use for signing (default: "default")
|
|
68
|
+
goal: Goal or objective of this workflow execution
|
|
69
|
+
notes: Additional notes or context about this workflow
|
|
70
|
+
metrics: Key-value metrics for this workflow (accuracy, latency, etc.)
|
|
71
|
+
approved_by: Person or entity who approved this workflow execution
|
|
72
|
+
metadata_tags: Tags for categorizing this workflow (renamed from tags to avoid conflict)
|
|
60
73
|
"""
|
|
61
74
|
self.output_path = Path(output_path)
|
|
62
75
|
self.workflow_name = workflow_name or "untitled"
|
|
@@ -65,6 +78,13 @@ class EpiRecorderSession:
|
|
|
65
78
|
self.redact = redact
|
|
66
79
|
self.default_key_name = default_key_name
|
|
67
80
|
|
|
81
|
+
# New metadata fields
|
|
82
|
+
self.goal = goal
|
|
83
|
+
self.notes = notes
|
|
84
|
+
self.metrics = metrics
|
|
85
|
+
self.approved_by = approved_by
|
|
86
|
+
self.metadata_tags = metadata_tags
|
|
87
|
+
|
|
68
88
|
# Runtime state
|
|
69
89
|
self.temp_dir: Optional[Path] = None
|
|
70
90
|
self.recording_context: Optional[RecordingContext] = None
|
|
@@ -97,9 +117,9 @@ class EpiRecorderSession:
|
|
|
97
117
|
set_recording_context(self.recording_context)
|
|
98
118
|
_thread_local.active_session = self
|
|
99
119
|
|
|
100
|
-
# Patch LLM libraries
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
# Patch LLM libraries and HTTP
|
|
121
|
+
from epi_recorder.patcher import patch_all
|
|
122
|
+
patch_all()
|
|
103
123
|
|
|
104
124
|
# Log session start
|
|
105
125
|
self.log_step("session.start", {
|
|
@@ -134,16 +154,19 @@ class EpiRecorderSession:
|
|
|
134
154
|
duration = (end_time - self.start_time).total_seconds()
|
|
135
155
|
|
|
136
156
|
self.log_step("session.end", {
|
|
137
|
-
"workflow_name": self.workflow_name,
|
|
138
157
|
"timestamp": end_time.isoformat(),
|
|
139
158
|
"duration_seconds": duration,
|
|
140
159
|
"success": exc_type is None
|
|
141
160
|
})
|
|
142
161
|
|
|
143
|
-
# Create manifest
|
|
144
|
-
# Note: workflow_name and tags are logged in steps, not manifest
|
|
162
|
+
# Create manifest with metadata
|
|
145
163
|
manifest = ManifestModel(
|
|
146
|
-
created_at=self.start_time
|
|
164
|
+
created_at=self.start_time,
|
|
165
|
+
goal=self.goal,
|
|
166
|
+
notes=self.notes,
|
|
167
|
+
metrics=self.metrics,
|
|
168
|
+
approved_by=self.approved_by,
|
|
169
|
+
tags=self.metadata_tags
|
|
147
170
|
)
|
|
148
171
|
|
|
149
172
|
# Pack into .epi file
|
|
@@ -333,9 +356,10 @@ class EpiRecorderSession:
|
|
|
333
356
|
)
|
|
334
357
|
|
|
335
358
|
# Repack the ZIP with signed manifest
|
|
336
|
-
|
|
359
|
+
# CRITICAL: Write to temp file first to prevent data loss
|
|
360
|
+
temp_output = self.output_path.with_suffix('.epi.tmp')
|
|
337
361
|
|
|
338
|
-
with zipfile.ZipFile(
|
|
362
|
+
with zipfile.ZipFile(temp_output, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
339
363
|
# Write mimetype first (uncompressed)
|
|
340
364
|
from epi_core.container import EPI_MIMETYPE
|
|
341
365
|
zf.writestr("mimetype", EPI_MIMETYPE, compress_type=zipfile.ZIP_STORED)
|
|
@@ -346,36 +370,209 @@ class EpiRecorderSession:
|
|
|
346
370
|
arc_name = str(file_path.relative_to(tmp_path)).replace("\\", "/")
|
|
347
371
|
zf.write(file_path, arc_name)
|
|
348
372
|
|
|
373
|
+
# Successfully created signed file, now safely replace original
|
|
374
|
+
self.output_path.unlink()
|
|
375
|
+
temp_output.rename(self.output_path)
|
|
376
|
+
|
|
349
377
|
except Exception as e:
|
|
350
378
|
# Non-fatal: log warning but continue
|
|
351
379
|
print(f"Warning: Failed to sign .epi file: {e}")
|
|
352
380
|
|
|
353
381
|
|
|
354
|
-
|
|
382
|
+
def _auto_generate_output_path(name_hint: Optional[str] = None) -> Path:
|
|
383
|
+
"""
|
|
384
|
+
Auto-generate output path in ./epi-recordings/ directory.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
name_hint: Optional base name hint (script name, function name, etc.)
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Path object for the .epi file
|
|
391
|
+
"""
|
|
392
|
+
# Get recordings directory from env or default
|
|
393
|
+
recordings_dir = Path(os.getenv("EPI_RECORDINGS_DIR", "epi-recordings"))
|
|
394
|
+
recordings_dir.mkdir(parents=True, exist_ok=True)
|
|
395
|
+
|
|
396
|
+
# Generate base name
|
|
397
|
+
if name_hint:
|
|
398
|
+
base = Path(name_hint).stem if "." in name_hint else name_hint
|
|
399
|
+
else:
|
|
400
|
+
base = "recording"
|
|
401
|
+
|
|
402
|
+
# Generate timestamp
|
|
403
|
+
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
|
404
|
+
|
|
405
|
+
# Ensure .epi extension
|
|
406
|
+
filename = f"{base}_{timestamp}.epi"
|
|
407
|
+
|
|
408
|
+
return recordings_dir / filename
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _resolve_output_path(output_path: Optional[Path | str]) -> Path:
|
|
412
|
+
"""
|
|
413
|
+
Resolve output path, adding .epi extension and default directory if needed.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
output_path: User-provided path or None for auto-generation
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Resolved Path object
|
|
420
|
+
"""
|
|
421
|
+
if output_path is None:
|
|
422
|
+
return _auto_generate_output_path()
|
|
423
|
+
|
|
424
|
+
path = Path(output_path)
|
|
425
|
+
|
|
426
|
+
# Add .epi extension if missing
|
|
427
|
+
if path.suffix != ".epi":
|
|
428
|
+
path = path.with_suffix(".epi")
|
|
429
|
+
|
|
430
|
+
return path
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Convenience function for users (supports zero-config)
|
|
355
434
|
def record(
|
|
356
|
-
output_path: Path | str,
|
|
435
|
+
output_path: Optional[Path | str] = None,
|
|
357
436
|
workflow_name: Optional[str] = None,
|
|
437
|
+
tags: Optional[List[str]] = None,
|
|
438
|
+
auto_sign: bool = True,
|
|
439
|
+
redact: bool = True,
|
|
440
|
+
default_key_name: str = "default",
|
|
441
|
+
# New metadata fields
|
|
442
|
+
goal: Optional[str] = None,
|
|
443
|
+
notes: Optional[str] = None,
|
|
444
|
+
metrics: Optional[Dict[str, Union[float, str]]] = None,
|
|
445
|
+
approved_by: Optional[str] = None,
|
|
446
|
+
metadata_tags: Optional[List[str]] = None, # Renamed to avoid conflict
|
|
358
447
|
**kwargs
|
|
359
|
-
) -> EpiRecorderSession:
|
|
448
|
+
) -> Union[EpiRecorderSession, Callable]:
|
|
360
449
|
"""
|
|
361
450
|
Create an EPI recording session (context manager).
|
|
362
451
|
|
|
363
452
|
Args:
|
|
364
|
-
output_path: Path for output .epi file
|
|
453
|
+
output_path: Path for output .epi file (optional - auto-generates if None)
|
|
365
454
|
workflow_name: Descriptive name for workflow
|
|
366
|
-
|
|
455
|
+
tags: Tags for categorization
|
|
456
|
+
auto_sign: Whether to automatically sign on exit (default: True)
|
|
457
|
+
redact: Whether to redact secrets (default: True)
|
|
458
|
+
default_key_name: Name of key to use for signing (default: "default")
|
|
459
|
+
goal: Goal or objective of this workflow execution
|
|
460
|
+
notes: Additional notes or context about this workflow
|
|
461
|
+
metrics: Key-value metrics for this workflow (accuracy, latency, etc.)
|
|
462
|
+
approved_by: Person or entity who approved this workflow execution
|
|
463
|
+
metadata_tags: Tags for categorizing this workflow (renamed from tags to avoid conflict)
|
|
464
|
+
**kwargs: Additional arguments (backward compatibility)
|
|
367
465
|
|
|
368
466
|
Returns:
|
|
369
|
-
EpiRecorderSession context manager
|
|
467
|
+
EpiRecorderSession context manager or decorated function
|
|
370
468
|
|
|
371
469
|
Example:
|
|
372
470
|
from epi_recorder import record
|
|
373
471
|
|
|
374
|
-
|
|
472
|
+
# Zero-config (auto-generates filename in ./epi-recordings/)
|
|
473
|
+
with record():
|
|
474
|
+
# Your code here
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
# With custom name
|
|
478
|
+
with record("my_workflow"):
|
|
479
|
+
# Your code here
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
# With metadata
|
|
483
|
+
with record(
|
|
484
|
+
goal="reduce hallucinations",
|
|
485
|
+
notes="switched to GPT-4",
|
|
486
|
+
metrics={"accuracy": 0.89},
|
|
487
|
+
approved_by="alice@company.com",
|
|
488
|
+
metadata_tags=["prod-candidate"]
|
|
489
|
+
):
|
|
490
|
+
# Your code here
|
|
491
|
+
pass
|
|
492
|
+
|
|
493
|
+
# Decorator usage
|
|
494
|
+
@record
|
|
495
|
+
def main():
|
|
496
|
+
# Your code here
|
|
497
|
+
pass
|
|
498
|
+
|
|
499
|
+
# Decorator with metadata
|
|
500
|
+
@record(goal="decorator test", metrics={"test_score": 0.95})
|
|
501
|
+
def main():
|
|
375
502
|
# Your code here
|
|
376
503
|
pass
|
|
377
504
|
"""
|
|
378
|
-
|
|
505
|
+
# Check if this is being used as a decorator with arguments
|
|
506
|
+
# If the first argument is not a path but keyword arguments are provided,
|
|
507
|
+
# we need to return a decorator function
|
|
508
|
+
if output_path is None and (goal is not None or notes is not None or metrics is not None or
|
|
509
|
+
approved_by is not None or metadata_tags is not None):
|
|
510
|
+
# This is a decorator with arguments, return a decorator function
|
|
511
|
+
def decorator(func):
|
|
512
|
+
@functools.wraps(func)
|
|
513
|
+
def wrapper(*args, **kwargs):
|
|
514
|
+
# Auto-generate path based on function name
|
|
515
|
+
auto_path = _auto_generate_output_path(func.__name__)
|
|
516
|
+
with EpiRecorderSession(
|
|
517
|
+
auto_path,
|
|
518
|
+
workflow_name or func.__name__,
|
|
519
|
+
tags=tags,
|
|
520
|
+
auto_sign=auto_sign,
|
|
521
|
+
redact=redact,
|
|
522
|
+
default_key_name=default_key_name,
|
|
523
|
+
goal=goal,
|
|
524
|
+
notes=notes,
|
|
525
|
+
metrics=metrics,
|
|
526
|
+
approved_by=approved_by,
|
|
527
|
+
metadata_tags=metadata_tags,
|
|
528
|
+
**kwargs
|
|
529
|
+
):
|
|
530
|
+
return func(*args, **kwargs)
|
|
531
|
+
return wrapper
|
|
532
|
+
return decorator
|
|
533
|
+
|
|
534
|
+
# Handle decorator usage: record is called without parentheses
|
|
535
|
+
if callable(output_path):
|
|
536
|
+
func = output_path
|
|
537
|
+
|
|
538
|
+
@functools.wraps(func)
|
|
539
|
+
def wrapper(*args, **kwargs):
|
|
540
|
+
# Auto-generate path based on function name
|
|
541
|
+
auto_path = _auto_generate_output_path(func.__name__)
|
|
542
|
+
with EpiRecorderSession(
|
|
543
|
+
auto_path,
|
|
544
|
+
workflow_name or func.__name__,
|
|
545
|
+
tags=tags,
|
|
546
|
+
auto_sign=auto_sign,
|
|
547
|
+
redact=redact,
|
|
548
|
+
default_key_name=default_key_name,
|
|
549
|
+
goal=goal,
|
|
550
|
+
notes=notes,
|
|
551
|
+
metrics=metrics,
|
|
552
|
+
approved_by=approved_by,
|
|
553
|
+
metadata_tags=metadata_tags,
|
|
554
|
+
**kwargs
|
|
555
|
+
):
|
|
556
|
+
return func(*args, **kwargs)
|
|
557
|
+
|
|
558
|
+
return wrapper
|
|
559
|
+
|
|
560
|
+
# Normal context manager usage
|
|
561
|
+
resolved_path = _resolve_output_path(output_path)
|
|
562
|
+
return EpiRecorderSession(
|
|
563
|
+
resolved_path,
|
|
564
|
+
workflow_name,
|
|
565
|
+
tags=tags,
|
|
566
|
+
auto_sign=auto_sign,
|
|
567
|
+
redact=redact,
|
|
568
|
+
default_key_name=default_key_name,
|
|
569
|
+
goal=goal,
|
|
570
|
+
notes=notes,
|
|
571
|
+
metrics=metrics,
|
|
572
|
+
approved_by=approved_by,
|
|
573
|
+
metadata_tags=metadata_tags,
|
|
574
|
+
**kwargs
|
|
575
|
+
)
|
|
379
576
|
|
|
380
577
|
|
|
381
578
|
# Make it easy to get current session
|
|
@@ -386,4 +583,4 @@ def get_current_session() -> Optional[EpiRecorderSession]:
|
|
|
386
583
|
Returns:
|
|
387
584
|
EpiRecorderSession or None
|
|
388
585
|
"""
|
|
389
|
-
return getattr(_thread_local, 'active_session', None)
|
|
586
|
+
return getattr(_thread_local, 'active_session', None)
|
epi_recorder/environment.py
CHANGED
|
@@ -214,3 +214,24 @@ def get_environment_summary() -> str:
|
|
|
214
214
|
lines.append(f"Working Directory: {env['working_directory']['path']}")
|
|
215
215
|
|
|
216
216
|
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# Backward compatibility alias
|
|
220
|
+
def capture_environment(
|
|
221
|
+
include_all_env_vars: bool = False,
|
|
222
|
+
redact_env_vars: bool = True
|
|
223
|
+
) -> Dict[str, Any]:
|
|
224
|
+
"""
|
|
225
|
+
Alias for capture_full_environment for backward compatibility.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
include_all_env_vars: Whether to include all environment variables
|
|
229
|
+
redact_env_vars: Whether to redact sensitive env vars
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
dict: Complete environment snapshot
|
|
233
|
+
"""
|
|
234
|
+
return capture_full_environment(
|
|
235
|
+
include_all_env_vars=include_all_env_vars,
|
|
236
|
+
redact_env_vars=redact_env_vars
|
|
237
|
+
)
|
epi_recorder/patcher.py
CHANGED
|
@@ -32,7 +32,7 @@ class RecordingContext:
|
|
|
32
32
|
enable_redaction: Whether to redact secrets (default: True)
|
|
33
33
|
"""
|
|
34
34
|
self.output_dir = output_dir
|
|
35
|
-
self.steps: List[StepModel] = []
|
|
35
|
+
# self.steps: List[StepModel] = [] # Removed for scalability
|
|
36
36
|
self.step_index = 0
|
|
37
37
|
self.enable_redaction = enable_redaction
|
|
38
38
|
self.redactor = get_default_redactor() if enable_redaction else None
|
|
@@ -83,8 +83,8 @@ class RecordingContext:
|
|
|
83
83
|
# Write to file
|
|
84
84
|
self._write_step(step)
|
|
85
85
|
|
|
86
|
-
# Store in memory
|
|
87
|
-
self.steps.append(step)
|
|
86
|
+
# Store in memory - REMOVED for scalability
|
|
87
|
+
# self.steps.append(step)
|
|
88
88
|
self.step_index += 1
|
|
89
89
|
|
|
90
90
|
def _write_step(self, step: StepModel) -> None:
|
|
@@ -325,9 +325,91 @@ def _patch_openai_legacy() -> bool:
|
|
|
325
325
|
return False
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
return results
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def patch_requests() -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Patch the requests library to intercept all HTTP calls.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
bool: True if patching succeeded, False otherwise
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
import requests
|
|
340
|
+
from requests.sessions import Session
|
|
341
|
+
|
|
342
|
+
# Store original method
|
|
343
|
+
original_request = Session.request
|
|
344
|
+
|
|
345
|
+
@wraps(original_request)
|
|
346
|
+
def wrapped_request(self, method, url, *args, **kwargs):
|
|
347
|
+
"""Wrapped requests.Session.request with recording."""
|
|
348
|
+
|
|
349
|
+
# Only record if context is active
|
|
350
|
+
if not is_recording():
|
|
351
|
+
return original_request(self, method, url, *args, **kwargs)
|
|
352
|
+
|
|
353
|
+
context = get_recording_context()
|
|
354
|
+
start_time = time.time()
|
|
355
|
+
|
|
356
|
+
# Capture request
|
|
357
|
+
# We don't capture full body by default to avoid massive logs,
|
|
358
|
+
# but we capture metadata
|
|
359
|
+
request_data = {
|
|
360
|
+
"provider": "http",
|
|
361
|
+
"method": method,
|
|
362
|
+
"url": url,
|
|
363
|
+
"headers": dict(kwargs.get("headers", {})),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Log request step
|
|
367
|
+
context.add_step("http.request", request_data)
|
|
368
|
+
|
|
369
|
+
# Execute original call
|
|
370
|
+
try:
|
|
371
|
+
response = original_request(self, method, url, *args, **kwargs)
|
|
372
|
+
elapsed = time.time() - start_time
|
|
373
|
+
|
|
374
|
+
# Capture response
|
|
375
|
+
response_data = {
|
|
376
|
+
"provider": "http",
|
|
377
|
+
"status_code": response.status_code,
|
|
378
|
+
"reason": response.reason,
|
|
379
|
+
"url": response.url,
|
|
380
|
+
"headers": dict(response.headers),
|
|
381
|
+
"latency_seconds": round(elapsed, 3)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Log response step
|
|
385
|
+
context.add_step("http.response", response_data)
|
|
386
|
+
|
|
387
|
+
return response
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
# Log error step
|
|
391
|
+
context.add_step("http.error", {
|
|
392
|
+
"provider": "http",
|
|
393
|
+
"error": str(e),
|
|
394
|
+
"error_type": type(e).__name__,
|
|
395
|
+
"url": url
|
|
396
|
+
})
|
|
397
|
+
raise
|
|
398
|
+
|
|
399
|
+
# Apply patch
|
|
400
|
+
Session.request = wrapped_request
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
except ImportError:
|
|
404
|
+
return False
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print(f"Warning: Failed to patch requests: {e}")
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
|
|
328
410
|
def patch_all() -> Dict[str, bool]:
|
|
329
411
|
"""
|
|
330
|
-
Patch all supported LLM providers.
|
|
412
|
+
Patch all supported LLM providers and HTTP libraries.
|
|
331
413
|
|
|
332
414
|
Returns:
|
|
333
415
|
dict: Provider name -> success status
|
|
@@ -337,9 +419,8 @@ def patch_all() -> Dict[str, bool]:
|
|
|
337
419
|
# Patch OpenAI
|
|
338
420
|
results["openai"] = patch_openai()
|
|
339
421
|
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
# results["gemini"] = patch_gemini()
|
|
422
|
+
# Patch generic requests (covers LangChain, Anthropic, etc.)
|
|
423
|
+
results["requests"] = patch_requests()
|
|
343
424
|
|
|
344
425
|
return results
|
|
345
426
|
|