instrumentation-sdk 1.7.0__tar.gz

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.
Files changed (95) hide show
  1. instrumentation_sdk-1.7.0/PKG-INFO +18 -0
  2. instrumentation_sdk-1.7.0/pyproject.toml +34 -0
  3. instrumentation_sdk-1.7.0/setup.cfg +4 -0
  4. instrumentation_sdk-1.7.0/src/__init__.py +39 -0
  5. instrumentation_sdk-1.7.0/src/api/rest/v1/app.py +16 -0
  6. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/deterministic_sampling.py +30 -0
  7. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/instrumentation.py +73 -0
  8. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/metrics.py +104 -0
  9. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/minilm_embedding.py +32 -0
  10. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/pii_injection.py +33 -0
  11. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/streaming.py +55 -0
  12. instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/token_counting.py +31 -0
  13. instrumentation_sdk-1.7.0/src/api/rest/v1/router.py +18 -0
  14. instrumentation_sdk-1.7.0/src/features/__init__.py +0 -0
  15. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/__init__.py +18 -0
  16. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/domain/__init__.py +0 -0
  17. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/domain/mappers.py +71 -0
  18. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/index.py +70 -0
  19. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/__init__.py +0 -0
  20. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/__init__.py +0 -0
  21. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/anthropic_patcher.py +86 -0
  22. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/base.py +10 -0
  23. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/http_patcher.py +127 -0
  24. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/langchain_patcher.py +96 -0
  25. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/litellm_patcher.py +88 -0
  26. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/openai_patcher.py +88 -0
  27. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/ports.py +15 -0
  28. instrumentation_sdk-1.7.0/src/features/auto_instrumentation/service.py +34 -0
  29. instrumentation_sdk-1.7.0/src/features/deterministic_sampling/__init__.py +3 -0
  30. instrumentation_sdk-1.7.0/src/features/deterministic_sampling/index.py +9 -0
  31. instrumentation_sdk-1.7.0/src/features/deterministic_sampling/infra/adapters/sha256_sampling_adapter.py +21 -0
  32. instrumentation_sdk-1.7.0/src/features/deterministic_sampling/ports.py +5 -0
  33. instrumentation_sdk-1.7.0/src/features/deterministic_sampling/service.py +9 -0
  34. instrumentation_sdk-1.7.0/src/features/manual_instrumentation/__init__.py +3 -0
  35. instrumentation_sdk-1.7.0/src/features/manual_instrumentation/index.py +3 -0
  36. instrumentation_sdk-1.7.0/src/features/manual_instrumentation/service.py +141 -0
  37. instrumentation_sdk-1.7.0/src/features/manual_instrumentation/tests/unit/test_context.py +129 -0
  38. instrumentation_sdk-1.7.0/src/features/metrics/__init__.py +1 -0
  39. instrumentation_sdk-1.7.0/src/features/metrics/index.py +49 -0
  40. instrumentation_sdk-1.7.0/src/features/metrics/infra/__init__.py +1 -0
  41. instrumentation_sdk-1.7.0/src/features/metrics/infra/adapters/__init__.py +1 -0
  42. instrumentation_sdk-1.7.0/src/features/metrics/infra/adapters/prometheus_adapter.py +79 -0
  43. instrumentation_sdk-1.7.0/src/features/metrics/ports.py +27 -0
  44. instrumentation_sdk-1.7.0/src/features/metrics/service.py +105 -0
  45. instrumentation_sdk-1.7.0/src/features/minilm_embedding/__init__.py +3 -0
  46. instrumentation_sdk-1.7.0/src/features/minilm_embedding/index.py +67 -0
  47. instrumentation_sdk-1.7.0/src/features/minilm_embedding/infra/adapters/http_embedding_client_adapter.py +18 -0
  48. instrumentation_sdk-1.7.0/src/features/minilm_embedding/ports.py +5 -0
  49. instrumentation_sdk-1.7.0/src/features/minilm_embedding/service.py +12 -0
  50. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/__init__.py +1 -0
  51. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/index.py +9 -0
  52. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/infra/adapters/aho_corasick.py +48 -0
  53. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/infra/adapters/aho_corasick_scanner_adapter.py +92 -0
  54. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/ports.py +5 -0
  55. instrumentation_sdk-1.7.0/src/features/pii_injection_scan/service.py +9 -0
  56. instrumentation_sdk-1.7.0/src/features/spans/__init__.py +1 -0
  57. instrumentation_sdk-1.7.0/src/features/spans/decorator.py +70 -0
  58. instrumentation_sdk-1.7.0/src/features/spans/globals.py +18 -0
  59. instrumentation_sdk-1.7.0/src/features/spans/index.py +5 -0
  60. instrumentation_sdk-1.7.0/src/features/spans/reporter.py +10 -0
  61. instrumentation_sdk-1.7.0/src/features/spans/tests/__init__.py +0 -0
  62. instrumentation_sdk-1.7.0/src/features/spans/tests/unit/__init__.py +0 -0
  63. instrumentation_sdk-1.7.0/src/features/spans/tests/unit/test_decorator.py +53 -0
  64. instrumentation_sdk-1.7.0/src/features/spans/tests/unit/test_decorator_edge_cases.py +125 -0
  65. instrumentation_sdk-1.7.0/src/features/spans/types.py +118 -0
  66. instrumentation_sdk-1.7.0/src/features/streaming/__init__.py +3 -0
  67. instrumentation_sdk-1.7.0/src/features/streaming/index.py +25 -0
  68. instrumentation_sdk-1.7.0/src/features/streaming/infra/adapters/token_counter_adapter.py +7 -0
  69. instrumentation_sdk-1.7.0/src/features/streaming/ports.py +19 -0
  70. instrumentation_sdk-1.7.0/src/features/streaming/service.py +227 -0
  71. instrumentation_sdk-1.7.0/src/features/token_counting/__init__.py +3 -0
  72. instrumentation_sdk-1.7.0/src/features/token_counting/index.py +11 -0
  73. instrumentation_sdk-1.7.0/src/features/token_counting/infra/__init__.py +1 -0
  74. instrumentation_sdk-1.7.0/src/features/token_counting/infra/adapters/__init__.py +1 -0
  75. instrumentation_sdk-1.7.0/src/features/token_counting/infra/adapters/tiktoken_adapter.py +10 -0
  76. instrumentation_sdk-1.7.0/src/features/token_counting/ports.py +8 -0
  77. instrumentation_sdk-1.7.0/src/features/token_counting/service.py +173 -0
  78. instrumentation_sdk-1.7.0/src/infra/__init__.py +0 -0
  79. instrumentation_sdk-1.7.0/src/infra/adapters/__init__.py +0 -0
  80. instrumentation_sdk-1.7.0/src/infra/adapters/kafka/__init__.py +0 -0
  81. instrumentation_sdk-1.7.0/src/infra/adapters/kafka/adapter.py +24 -0
  82. instrumentation_sdk-1.7.0/src/infra/clients/__init__.py +0 -0
  83. instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/instrumentation_pb2.py +89 -0
  84. instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/instrumentation_pb2_grpc.py +498 -0
  85. instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/span_pb2.py +39 -0
  86. instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/span_pb2_grpc.py +69 -0
  87. instrumentation_sdk-1.7.0/src/infra/metrics/__init__.py +1 -0
  88. instrumentation_sdk-1.7.0/src/infra/metrics/meter.py +27 -0
  89. instrumentation_sdk-1.7.0/src/infra/tracing/middleware.py +16 -0
  90. instrumentation_sdk-1.7.0/src/infra/tracing/tracer.py +35 -0
  91. instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/PKG-INFO +18 -0
  92. instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/SOURCES.txt +93 -0
  93. instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/dependency_links.txt +1 -0
  94. instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/requires.txt +13 -0
  95. instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/top_level.txt +5 -0
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: instrumentation-sdk
3
+ Version: 1.7.0
4
+ Summary: LLM Observability Instrumentation SDK
5
+ Author: LLM Observability Team
6
+ Requires-Dist: pydantic>=2.0.0
7
+ Requires-Dist: tiktoken>=0.5.0
8
+ Requires-Dist: PyYAML>=6.0.0
9
+ Requires-Dist: confluent-kafka>=2.3.0
10
+ Requires-Dist: protobuf>=4.25.0
11
+ Requires-Dist: fastapi>=0.110.0
12
+ Requires-Dist: httpx>=0.27.0
13
+ Requires-Dist: opentelemetry-api>=1.24.0
14
+ Requires-Dist: opentelemetry-sdk>=1.24.0
15
+ Requires-Dist: opentelemetry-exporter-otlp>=1.24.0
16
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.45b0
17
+ Requires-Dist: opentelemetry-exporter-prometheus>=0.45b0
18
+ Requires-Dist: pytest-asyncio>=0.23.0
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "instrumentation-sdk"
3
+ version = "1.7.0"
4
+ description = "LLM Observability Instrumentation SDK"
5
+ authors = [
6
+ { name = "LLM Observability Team" }
7
+ ]
8
+ dependencies = [
9
+ "pydantic>=2.0.0",
10
+ "tiktoken>=0.5.0",
11
+ "PyYAML>=6.0.0",
12
+ "confluent-kafka>=2.3.0",
13
+ "protobuf>=4.25.0",
14
+ "fastapi>=0.110.0",
15
+ "httpx>=0.27.0",
16
+ "opentelemetry-api>=1.24.0",
17
+ "opentelemetry-sdk>=1.24.0",
18
+ "opentelemetry-exporter-otlp>=1.24.0",
19
+ "opentelemetry-instrumentation-fastapi>=0.45b0",
20
+ "opentelemetry-exporter-prometheus>=0.45b0",
21
+ "pytest-asyncio>=0.23.0",
22
+ ]
23
+
24
+
25
+ [build-system]
26
+ requires = ["setuptools>=61.0"]
27
+ build-backend = "setuptools.build_meta"
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
31
+ pythonpath = ["src"]
32
+ markers = [
33
+ "performance: marks tests as load/performance tests (deselect with '-m not performance')",
34
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ from .features.spans import llm_observe, SpanReporter
2
+ from .features.manual_instrumentation import llm_span
3
+ from .features.spans.globals import set_reporter, get_reporter
4
+ from .features.auto_instrumentation import (
5
+ init_auto_instrumentation,
6
+ uninstrument_all,
7
+ instrument_client,
8
+ instrument_http_client,
9
+ detect_llm_call,
10
+ trigger_test_call
11
+ )
12
+ from .features.token_counting import count_tokens, llm_span_with_tokens
13
+ from .features.streaming import llm_streaming_span, wrap_stream, wrap_async_stream
14
+ from .features.pii_injection_scan import scan_prompt
15
+ from .features.metrics import init_metrics_pipeline, record_span_metrics
16
+ from .features.deterministic_sampling import should_sample
17
+
18
+ __all__ = [
19
+ "llm_observe",
20
+ "llm_span",
21
+ "set_reporter",
22
+ "get_reporter",
23
+ "SpanReporter",
24
+ "init_auto_instrumentation",
25
+ "uninstrument_all",
26
+ "instrument_client",
27
+ "instrument_http_client",
28
+ "detect_llm_call",
29
+ "trigger_test_call",
30
+ "count_tokens",
31
+ "llm_span_with_tokens",
32
+ "llm_streaming_span",
33
+ "wrap_stream",
34
+ "wrap_async_stream",
35
+ "scan_prompt",
36
+ "init_metrics_pipeline",
37
+ "record_span_metrics",
38
+ "should_sample",
39
+ ]
@@ -0,0 +1,16 @@
1
+ from fastapi import FastAPI
2
+ import os
3
+
4
+ from .router import api_v1_router
5
+ from ....infra.tracing.middleware import instrument_app
6
+
7
+ def create_app() -> FastAPI:
8
+ app = FastAPI(title="Instrumentation SDK API", version="1.0.0")
9
+ app.include_router(api_v1_router, prefix="/v1")
10
+ instrument_app(app)
11
+ return app
12
+
13
+ # Always define the app instance for ASGI servers like uvicorn
14
+ app = create_app()
15
+
16
+ # Build cache test
@@ -0,0 +1,30 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ import os
4
+ from opentelemetry import trace
5
+ from .....features.deterministic_sampling.index import should_sample
6
+
7
+ router = APIRouter(prefix="/sampling", tags=["Deterministic Sampling"])
8
+
9
+ class SamplingGateRequest(BaseModel):
10
+ span_id: str
11
+
12
+ class SamplingGateResponse(BaseModel):
13
+ is_sampled: bool
14
+
15
+ def _set_span_attributes() -> None:
16
+ span = trace.get_current_span()
17
+ span.set_attribute("service.name", "instrumentation-sdk-api")
18
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
19
+ span.set_attribute("feature.name", "deterministic_sampling")
20
+
21
+ @router.post("/should-sample", response_model=SamplingGateResponse)
22
+ def should_sample_endpoint(request: SamplingGateRequest) -> SamplingGateResponse:
23
+ _set_span_attributes()
24
+ try:
25
+ sampled = should_sample(request.span_id)
26
+ span = trace.get_current_span()
27
+ span.set_attribute("llm.is_sampled", sampled)
28
+ return SamplingGateResponse(is_sampled=sampled)
29
+ except Exception as e:
30
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,73 @@
1
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
2
+ from pydantic import BaseModel
3
+ from typing import Optional, Dict, Any
4
+ import os
5
+ from opentelemetry import trace
6
+ from .....features.auto_instrumentation import (
7
+ init_auto_instrumentation,
8
+ uninstrument_all,
9
+ detect_llm_call,
10
+ trigger_test_call
11
+ )
12
+
13
+
14
+ router = APIRouter(prefix="/instrumentation", tags=["Instrumentation"])
15
+
16
+ class StatusResponse(BaseModel):
17
+ success: bool
18
+ message: str
19
+
20
+ class DetectionRequest(BaseModel):
21
+ url: str
22
+ body: str
23
+
24
+ class DetectionResponse(BaseModel):
25
+ provider: str
26
+ model: str
27
+
28
+ class TestCallRequest(BaseModel):
29
+ method: str
30
+ provider: str
31
+
32
+ def _set_span_attributes():
33
+ span = trace.get_current_span()
34
+ span.set_attribute("service.name", "instrumentation-sdk-api")
35
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
36
+ span.set_attribute("feature.name", "auto_instrumentation")
37
+
38
+
39
+ @router.post("/init", response_model=StatusResponse)
40
+ def init_instrumentation():
41
+ _set_span_attributes()
42
+ try:
43
+ init_auto_instrumentation()
44
+ return StatusResponse(success=True, message="Auto-instrumentation initialized")
45
+ except Exception as e:
46
+ raise HTTPException(status_code=500, detail=str(e))
47
+
48
+ @router.post("/uninstrument", response_model=StatusResponse)
49
+ def uninstrument():
50
+ _set_span_attributes()
51
+ try:
52
+ uninstrument_all()
53
+ return StatusResponse(success=True, message="All instrumentation disabled")
54
+ except Exception as e:
55
+ raise HTTPException(status_code=500, detail=str(e))
56
+
57
+ @router.post("/detect", response_model=DetectionResponse)
58
+ def detect(request: DetectionRequest):
59
+ _set_span_attributes()
60
+ result = detect_llm_call(request.url, request.body)
61
+ return DetectionResponse(**result)
62
+
63
+
64
+
65
+
66
+ @router.post("/test-call", response_model=StatusResponse)
67
+ async def test_call(request: TestCallRequest):
68
+ _set_span_attributes()
69
+ result = await trigger_test_call(request.method, request.provider)
70
+ if not result["success"]:
71
+ raise HTTPException(status_code=500, detail=result["message"])
72
+ return StatusResponse(**result)
73
+
@@ -0,0 +1,104 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import Optional, Dict, Any, List
4
+ import os
5
+ from opentelemetry import trace
6
+ from .....features.metrics.index import record_span_metrics, init_metrics_pipeline
7
+
8
+
9
+ router = APIRouter(prefix="/metrics", tags=["Metrics"])
10
+
11
+
12
+ class MetricsStatusResponse(BaseModel):
13
+ initialized: bool
14
+ message: str
15
+
16
+
17
+ class MetricsInitRequest(BaseModel):
18
+ port: Optional[int] = None
19
+
20
+
21
+ class RecordSpanMetricsRequest(BaseModel):
22
+ model: str = "unknown"
23
+ provider: str = "unknown"
24
+ service_name: str = "unknown"
25
+ prompt_tokens: Optional[int] = None
26
+ completion_tokens: Optional[int] = None
27
+ cost_usd_micro: Optional[int] = None
28
+ latency_ms_total: Optional[int] = None
29
+ latency_ms_ttft: Optional[int] = None
30
+ finish_reason: Optional[str] = None
31
+ status: str = "success"
32
+ pii_detected: bool = False
33
+ injection_attempt: bool = False
34
+ retry_count: int = 0
35
+
36
+
37
+ class RecordSpanMetricsResponse(BaseModel):
38
+ recorded: bool
39
+ cost_usd_micro: Optional[int] = None
40
+ price_version: Optional[str] = None
41
+
42
+
43
+ class BatchRecordRequest(BaseModel):
44
+ spans: List[RecordSpanMetricsRequest]
45
+
46
+
47
+ class BatchRecordResponse(BaseModel):
48
+ recorded_count: int
49
+
50
+
51
+ def _set_span_attributes() -> None:
52
+ span = trace.get_current_span()
53
+ span.set_attribute("service.name", "instrumentation-sdk-api")
54
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
55
+ span.set_attribute("feature.name", "metrics")
56
+
57
+
58
+ @router.post("/init", response_model=MetricsStatusResponse)
59
+ def init_metrics(request: MetricsInitRequest = MetricsInitRequest()):
60
+ _set_span_attributes()
61
+ try:
62
+ init_metrics_pipeline(request.port)
63
+ return MetricsStatusResponse(initialized=True, message="Metrics pipeline initialized")
64
+ except Exception as e:
65
+ raise HTTPException(status_code=500, detail=str(e))
66
+
67
+
68
+ @router.get("/health", response_model=MetricsStatusResponse)
69
+ def metrics_health():
70
+ _set_span_attributes()
71
+ from .....features.metrics.index import _initialized
72
+ return MetricsStatusResponse(
73
+ initialized=_initialized,
74
+ message="Metrics pipeline is active" if _initialized else "Metrics pipeline not initialized"
75
+ )
76
+
77
+
78
+ @router.post("/record", response_model=RecordSpanMetricsResponse)
79
+ def record_metrics(request: RecordSpanMetricsRequest):
80
+ _set_span_attributes()
81
+ try:
82
+ span_data = request.model_dump(exclude_none=True)
83
+ record_span_metrics(span_data)
84
+ return RecordSpanMetricsResponse(
85
+ recorded=True,
86
+ cost_usd_micro=span_data.get("cost_usd_micro"),
87
+ price_version=span_data.get("price_version"),
88
+ )
89
+ except Exception as e:
90
+ raise HTTPException(status_code=500, detail=str(e))
91
+
92
+
93
+ @router.post("/record-batch", response_model=BatchRecordResponse)
94
+ def record_batch(request: BatchRecordRequest):
95
+ _set_span_attributes()
96
+ count = 0
97
+ for span_req in request.spans:
98
+ try:
99
+ span_data = span_req.model_dump(exclude_none=True)
100
+ record_span_metrics(span_data)
101
+ count += 1
102
+ except Exception:
103
+ pass
104
+ return BatchRecordResponse(recorded_count=count)
@@ -0,0 +1,32 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ import os
4
+ from opentelemetry import trace
5
+ from .....features.minilm_embedding.index import get_embedding
6
+
7
+ router = APIRouter(prefix="/embeddings", tags=["MiniLM Embedding"])
8
+
9
+ class EmbeddingRequest(BaseModel):
10
+ text: str
11
+
12
+ class EmbeddingResponse(BaseModel):
13
+ embedding: list[float]
14
+
15
+ def _set_span_attributes() -> None:
16
+ span = trace.get_current_span()
17
+ span.set_attribute("service.name", "instrumentation-sdk-api")
18
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
19
+ span.set_attribute("feature.name", "minilm_embedding")
20
+
21
+ @router.post("/embed", response_model=EmbeddingResponse)
22
+ async def embed_endpoint(request: EmbeddingRequest) -> EmbeddingResponse:
23
+ _set_span_attributes()
24
+ try:
25
+ emb = await get_embedding(request.text)
26
+ if emb is None:
27
+ raise HTTPException(status_code=500, detail="Failed to generate embedding")
28
+ return EmbeddingResponse(embedding=emb)
29
+ except HTTPException:
30
+ raise
31
+ except Exception as e:
32
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,33 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import Union, List, Dict, Any
4
+ import os
5
+ from opentelemetry import trace
6
+ from .....features.pii_injection_scan.index import scan_prompt
7
+
8
+ router = APIRouter(prefix="/pii-injection", tags=["PII & Injection Scan"])
9
+
10
+ class PiiInjectionScanRequest(BaseModel):
11
+ prompt: Union[str, List[Dict[str, Any]]]
12
+
13
+ class PiiInjectionScanResponse(BaseModel):
14
+ pii_detected: bool
15
+ injection_attempt: bool
16
+
17
+ def _set_span_attributes() -> None:
18
+ span = trace.get_current_span()
19
+ span.set_attribute("service.name", "instrumentation-sdk-api")
20
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
21
+ span.set_attribute("feature.name", "pii_injection_scan")
22
+
23
+ @router.post("/scan", response_model=PiiInjectionScanResponse)
24
+ def scan(request: PiiInjectionScanRequest) -> PiiInjectionScanResponse:
25
+ _set_span_attributes()
26
+ try:
27
+ pii, inj = scan_prompt(request.prompt)
28
+ span = trace.get_current_span()
29
+ span.set_attribute("llm.pii_detected", pii)
30
+ span.set_attribute("llm.injection_attempt", inj)
31
+ return PiiInjectionScanResponse(pii_detected=pii, injection_attempt=inj)
32
+ except Exception as e:
33
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,55 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from fastapi.responses import StreamingResponse
3
+ from pydantic import BaseModel
4
+ from typing import List, Optional
5
+ import os
6
+ import asyncio
7
+ from opentelemetry import trace
8
+ from .....features.streaming.index import llm_streaming_span, wrap_async_stream
9
+
10
+ router = APIRouter(prefix="/streaming", tags=["Streaming"])
11
+
12
+ class TestStreamRequest(BaseModel):
13
+ provider: str
14
+ chunks: Optional[List[str]] = None
15
+
16
+ def _set_span_attributes():
17
+ span = trace.get_current_span()
18
+ span.set_attribute("service.name", "instrumentation-sdk-api")
19
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
20
+ span.set_attribute("feature.name", "streaming")
21
+
22
+ async def mock_async_stream(chunks: List[str]):
23
+ for chunk in chunks:
24
+ await asyncio.sleep(0.01)
25
+ yield chunk
26
+
27
+ @router.post("/test-stream-call")
28
+ async def test_stream_call(request: TestStreamRequest):
29
+ _set_span_attributes()
30
+ try:
31
+ chunks = request.chunks
32
+ if not chunks:
33
+ chunks = ["Hello ", "there! ", "This ", "is ", "a ", "mock ", "stream."]
34
+
35
+ span_ctx = llm_streaming_span(
36
+ span_type="llm_call",
37
+ provider=request.provider,
38
+ model="test-stream-model",
39
+ prompt="Triggered via API verification endpoint",
40
+ )
41
+
42
+ wrapped_stream = wrap_async_stream(
43
+ mock_async_stream(chunks),
44
+ span_context=span_ctx,
45
+ model="test-stream-model"
46
+ )
47
+
48
+ async def event_generator():
49
+ async with span_ctx:
50
+ async for chunk in wrapped_stream:
51
+ yield f"data: {chunk}\n\n"
52
+
53
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
54
+ except Exception as e:
55
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,31 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import Union, List, Dict, Any
4
+ import os
5
+ from opentelemetry import trace
6
+ from .....features.token_counting.index import count_tokens
7
+
8
+ router = APIRouter(prefix="/token-counting", tags=["Token Counting"])
9
+
10
+ class TokenCountRequest(BaseModel):
11
+ prompt: Union[str, List[Dict[str, Any]]]
12
+ model: str
13
+
14
+ class TokenCountResponse(BaseModel):
15
+ tokens: int
16
+ method: str
17
+
18
+ def _set_span_attributes():
19
+ span = trace.get_current_span()
20
+ span.set_attribute("service.name", "instrumentation-sdk-api")
21
+ span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
22
+ span.set_attribute("feature.name", "token_counting")
23
+
24
+ @router.post("/count", response_model=TokenCountResponse)
25
+ def count(request: TokenCountRequest):
26
+ _set_span_attributes()
27
+ try:
28
+ tokens, method = count_tokens(request.prompt, request.model)
29
+ return TokenCountResponse(tokens=tokens, method=method)
30
+ except Exception as e:
31
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter
2
+ from .handlers.instrumentation import router as instrumentation_router
3
+ from .handlers.token_counting import router as token_counting_router
4
+ from .handlers.streaming import router as streaming_router
5
+ from .handlers.pii_injection import router as pii_injection_router
6
+ from .handlers.metrics import router as metrics_router
7
+ from .handlers.deterministic_sampling import router as deterministic_sampling_router
8
+ from .handlers.minilm_embedding import router as minilm_embedding_router
9
+
10
+ api_v1_router = APIRouter()
11
+ api_v1_router.include_router(instrumentation_router)
12
+ api_v1_router.include_router(token_counting_router)
13
+ api_v1_router.include_router(streaming_router)
14
+ api_v1_router.include_router(pii_injection_router)
15
+ api_v1_router.include_router(metrics_router)
16
+ api_v1_router.include_router(deterministic_sampling_router)
17
+ api_v1_router.include_router(minilm_embedding_router)
18
+
File without changes
@@ -0,0 +1,18 @@
1
+ from .index import (
2
+ init_auto_instrumentation,
3
+ uninstrument_all,
4
+ instrument_client,
5
+ instrument_http_client,
6
+ detect_llm_call,
7
+ trigger_test_call
8
+ )
9
+
10
+ __all__ = [
11
+ "init_auto_instrumentation",
12
+ "uninstrument_all",
13
+ "instrument_client",
14
+ "instrument_http_client",
15
+ "detect_llm_call",
16
+ "trigger_test_call"
17
+ ]
18
+
@@ -0,0 +1,71 @@
1
+ from typing import Any, Dict, Optional
2
+ from ...spans.types import FinishReason
3
+
4
+ class ProviderMapper:
5
+ @staticmethod
6
+ def map_openai_response(response: Any) -> Dict[str, Any]:
7
+ usage = getattr(response, "usage", None)
8
+ choices = getattr(response, "choices", [])
9
+
10
+ finish_reason = FinishReason.UNSPECIFIED
11
+ if choices:
12
+ reason = choices[0].finish_reason
13
+ if reason == "stop":
14
+ finish_reason = FinishReason.STOP
15
+ elif reason == "length":
16
+ finish_reason = FinishReason.LENGTH
17
+ elif reason == "content_filter":
18
+ finish_reason = FinishReason.CONTENT_FILTER
19
+ elif reason == "tool_calls":
20
+ finish_reason = FinishReason.TOOL_CALLS
21
+
22
+ return {
23
+ "model": response.model,
24
+ "provider": "openai",
25
+ "prompt_tokens": usage.prompt_tokens if usage else 1,
26
+ "completion_tokens": usage.completion_tokens if usage else 0,
27
+ "finish_reason": finish_reason,
28
+ "response_content": choices[0].message.content if choices and hasattr(choices[0].message, "content") else None
29
+ }
30
+
31
+ @staticmethod
32
+ def map_anthropic_response(response: Any) -> Dict[str, Any]:
33
+ usage = getattr(response, "usage", None)
34
+
35
+ finish_reason = FinishReason.UNSPECIFIED
36
+ stop_reason = getattr(response, "stop_reason", None)
37
+ if stop_reason == "end_turn":
38
+ finish_reason = FinishReason.STOP
39
+ elif stop_reason == "max_tokens":
40
+ finish_reason = FinishReason.LENGTH
41
+ elif stop_reason == "stop_sequence":
42
+ finish_reason = FinishReason.STOP
43
+ elif stop_reason == "tool_use":
44
+ finish_reason = FinishReason.TOOL_CALLS
45
+
46
+ content = ""
47
+ if hasattr(response, "content") and response.content:
48
+ content = response.content[0].text if hasattr(response.content[0], "text") else ""
49
+
50
+ return {
51
+ "model": response.model,
52
+ "provider": "anthropic",
53
+ "prompt_tokens": usage.input_tokens if usage else 1,
54
+ "completion_tokens": usage.output_tokens if usage else 0,
55
+ "finish_reason": finish_reason,
56
+ "response_content": content
57
+ }
58
+ @staticmethod
59
+ def map_langchain_response(response: Any, model: str, provider: str) -> Dict[str, Any]:
60
+ usage = getattr(response, "usage_metadata", {})
61
+ if not usage:
62
+ usage = {}
63
+
64
+ return {
65
+ "model": model,
66
+ "provider": f"langchain:{provider}",
67
+ "prompt_tokens": usage.get("input_tokens", 1),
68
+ "completion_tokens": usage.get("output_tokens", 0),
69
+ "finish_reason": FinishReason.STOP if hasattr(response, "content") else FinishReason.UNSPECIFIED,
70
+ "response_content": getattr(response, "content", "")
71
+ }
@@ -0,0 +1,70 @@
1
+ from .service import AutoInstrumentationService
2
+ from .infra.patchers.openai_patcher import OpenAIPatcher
3
+ from .infra.patchers.anthropic_patcher import AnthropicPatcher
4
+ from .infra.patchers.litellm_patcher import LiteLLMPatcher
5
+ from .infra.patchers.langchain_patcher import LangChainPatcher
6
+ from .infra.patchers.http_patcher import HTTPPatcher
7
+
8
+ from typing import Dict, Any, Optional
9
+
10
+ _SERVICE = AutoInstrumentationService(
11
+ patchers=[
12
+ OpenAIPatcher(),
13
+ AnthropicPatcher(),
14
+ LiteLLMPatcher(),
15
+ LangChainPatcher(),
16
+ HTTPPatcher()
17
+ ]
18
+ )
19
+
20
+ def init_auto_instrumentation() -> None:
21
+ _SERVICE.instrument_all()
22
+
23
+ def uninstrument_all() -> None:
24
+ _SERVICE.uninstrument_all()
25
+
26
+ def instrument_client(client: Any, provider: str) -> None:
27
+ _SERVICE.instrument_client(client, provider)
28
+
29
+ def instrument_http_client(client: Any, provider: Optional[str] = None) -> None:
30
+ for patcher in _SERVICE._patchers:
31
+ if isinstance(patcher, HTTPPatcher):
32
+ patcher.patch_instance(client)
33
+ return
34
+
35
+ def detect_llm_call(url: str, body: str) -> Dict[str, str]:
36
+ for patcher in _SERVICE._patchers:
37
+ if isinstance(patcher, HTTPPatcher):
38
+ provider = patcher._detect_provider(url)
39
+ model = patcher._extract_model(body)
40
+ return {"provider": provider or "unknown", "model": model}
41
+ return {"provider": "unknown", "model": "unknown"}
42
+
43
+ async def trigger_test_call(method: str, provider: str) -> Dict[str, Any]:
44
+ import httpx
45
+ import json
46
+
47
+ url_map = {
48
+ "openai": "https://api.openai.com/v1/chat/completions",
49
+ "anthropic": "https://api.anthropic.com/v1/messages"
50
+ }
51
+ url = url_map.get(provider, "https://api.openai.com/v1/chat/completions")
52
+ payload = {"model": "instrumentation-test-model"}
53
+
54
+ try:
55
+ if method == "httpx":
56
+ async with httpx.AsyncClient() as client:
57
+ try:
58
+ await client.post(url, json=payload, timeout=1.0)
59
+ except httpx.HTTPError:
60
+ pass
61
+ elif method == "requests":
62
+ import requests
63
+ try:
64
+ requests.post(url, json=payload, timeout=1.0)
65
+ except requests.RequestException:
66
+ pass
67
+
68
+ return {"success": True, "message": f"Test call triggered via {method} for {provider}"}
69
+ except Exception as e:
70
+ return {"success": False, "message": str(e)}