catalyst-tracing 0.0.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.
Files changed (35) hide show
  1. catalyst_tracing/__init__.py +94 -0
  2. catalyst_tracing/agent_span.py +199 -0
  3. catalyst_tracing/anthropic.py +17 -0
  4. catalyst_tracing/claude_agent_sdk.py +21 -0
  5. catalyst_tracing/constants.py +122 -0
  6. catalyst_tracing/env.py +67 -0
  7. catalyst_tracing/errors.py +134 -0
  8. catalyst_tracing/exporter.py +60 -0
  9. catalyst_tracing/installers/__init__.py +0 -0
  10. catalyst_tracing/installers/anthropic.py +49 -0
  11. catalyst_tracing/installers/base.py +48 -0
  12. catalyst_tracing/installers/claude_agent_sdk.py +51 -0
  13. catalyst_tracing/installers/langchain.py +49 -0
  14. catalyst_tracing/installers/langgraph.py +42 -0
  15. catalyst_tracing/installers/langsmith.py +125 -0
  16. catalyst_tracing/installers/openai.py +60 -0
  17. catalyst_tracing/installers/openai_agents.py +62 -0
  18. catalyst_tracing/installers/pydantic_ai.py +43 -0
  19. catalyst_tracing/instrumentation/__init__.py +0 -0
  20. catalyst_tracing/instrumentation/anthropic.py +400 -0
  21. catalyst_tracing/instrumentation/claude_agent_sdk.py +274 -0
  22. catalyst_tracing/instrumentation/langchain.py +474 -0
  23. catalyst_tracing/instrumentation/openai.py +573 -0
  24. catalyst_tracing/instrumentation/stream_accumulator.py +387 -0
  25. catalyst_tracing/langchain.py +17 -0
  26. catalyst_tracing/langgraph.py +7 -0
  27. catalyst_tracing/langsmith.py +7 -0
  28. catalyst_tracing/openai.py +21 -0
  29. catalyst_tracing/openai_agents.py +21 -0
  30. catalyst_tracing/pydantic_ai.py +21 -0
  31. catalyst_tracing/semconv.py +181 -0
  32. catalyst_tracing/setup.py +288 -0
  33. catalyst_tracing-0.0.1.dist-info/METADATA +26 -0
  34. catalyst_tracing-0.0.1.dist-info/RECORD +35 -0
  35. catalyst_tracing-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,94 @@
1
+ """``catalyst_tracing`` — first-party tracing for LLM apps.
2
+
3
+ Drop-in OpenInference-shaped instrumentation for the SDKs your app
4
+ uses (openai, anthropic, langchain, langgraph, langsmith,
5
+ openai-agents, claude-agent-sdk, pydantic-ai), plus a small ergonomic helper layer for cases where
6
+ you need to author spans by hand.
7
+
8
+ Quick start::
9
+
10
+ from catalyst_tracing import setup
11
+ from openai import OpenAI
12
+
13
+ tracing = setup()
14
+ client = OpenAI(api_key=...)
15
+ client.chat.completions.create(model=..., messages=...)
16
+ tracing.shutdown()
17
+
18
+ What this package owns
19
+ ----------------------
20
+
21
+ - **OTLP plumbing** — endpoint, token, service-name resource, batch
22
+ vs. simple processor selection, env-var resolution with sensible
23
+ defaults including legacy ``OTLP_*`` fallback.
24
+ - **First-party OpenInference instrumentation** for openai, anthropic,
25
+ langchain, langgraph, openai-agents, claude-agent-sdk, plus bridges
26
+ for langsmith and pydantic-ai's native OTel. No
27
+ ``openinference-instrumentation-*`` runtime dependency.
28
+ - **Helpers**: :func:`agent_span` for manual AGENT spans;
29
+ :class:`Attr` and :class:`SpanKindValues` for the OI semantic-
30
+ convention constants you'd want when authoring custom spans.
31
+
32
+ What this package doesn't own
33
+ -----------------------------
34
+
35
+ - The SDKs themselves. They're optional extras — pull in only the
36
+ ones you use::
37
+
38
+ pip install 'catalyst-tracing[openai,anthropic]'
39
+
40
+ - Shipping spans somewhere other than OTLP/HTTP/JSON. If you need a
41
+ different transport, build your own exporter and add it to
42
+ ``tracing.provider``.
43
+
44
+ Public API
45
+ ----------
46
+
47
+ * :func:`setup` — bootstrap tracing, returns a
48
+ :class:`CatalystTracing` handle.
49
+ * :func:`agent_span` — context manager that wraps work in an
50
+ OI-shaped AGENT span.
51
+ * :class:`Attr`, :class:`SpanKindValues` — wire-format constants
52
+ shared across instrumentation and consumer-authored spans.
53
+ """
54
+
55
+ from .agent_span import AgentSpanHandle, agent_span
56
+ from .errors import (
57
+ CatalystTracingError,
58
+ CatalystTracingErrorCode,
59
+ CatalystTracingErrorCodes,
60
+ InvalidSdkInputError,
61
+ InvalidTracerProviderError,
62
+ )
63
+ from .semconv import Attr, MessageRole, OpenInferenceAttribute, SpanKindValues
64
+ from .setup import CatalystTracing, InstrumentResult, setup
65
+
66
+ __version__ = "0.0.1"
67
+ PACKAGE_NAME = "catalyst-tracing"
68
+ IMPORT_NAME = "catalyst_tracing"
69
+ COMPANY_NAME = "Inference"
70
+ PLATFORM_NAME = "Catalyst"
71
+ PRODUCT_NAME = "Catalyst by Inference.net"
72
+
73
+ __all__ = [
74
+ "AgentSpanHandle",
75
+ "Attr",
76
+ "CatalystTracing",
77
+ "CatalystTracingError",
78
+ "CatalystTracingErrorCode",
79
+ "CatalystTracingErrorCodes",
80
+ "COMPANY_NAME",
81
+ "IMPORT_NAME",
82
+ "InstrumentResult",
83
+ "InvalidSdkInputError",
84
+ "InvalidTracerProviderError",
85
+ "MessageRole",
86
+ "OpenInferenceAttribute",
87
+ "PACKAGE_NAME",
88
+ "PLATFORM_NAME",
89
+ "PRODUCT_NAME",
90
+ "SpanKindValues",
91
+ "__version__",
92
+ "agent_span",
93
+ "setup",
94
+ ]
@@ -0,0 +1,199 @@
1
+ """:func:`agent_span` — context manager that wraps work in an
2
+ OpenInference-flavored AGENT span.
3
+
4
+ Use this when you have a chunk of "agent work" — a CLI subprocess
5
+ call, a multi-step Pydantic AI run, the outer wrapper around an
6
+ ``agents-sdk`` ``Runner.run()`` — and you want it to show up in
7
+ the trace viewer as a single AGENT-kind row with input/output and
8
+ optional token counts.
9
+
10
+ The context manager:
11
+
12
+ * Sets ``openinference.span.kind=AGENT``, ``agent.name``,
13
+ ``gen_ai.system`` automatically.
14
+ * Runs the body inside the span's active OTel context, so child
15
+ spans created by instrumented LLM SDKs auto-parent to this
16
+ AGENT span. (That's how a full trace tree forms.)
17
+ * Ends with ``OK`` on success, records the exception and ends with
18
+ ``ERROR`` on failure (re-raising so the caller's error handling
19
+ is unaffected).
20
+
21
+ Example
22
+ -------
23
+ Wrapping a CLI subprocess::
24
+
25
+ from catalyst_tracing import agent_span, setup
26
+
27
+ tracing = setup()
28
+ with agent_span(tracing.tracer, name="ClaudeCode", system="anthropic") as span:
29
+ span.set_input(prompt)
30
+ answer = run_cli(prompt)
31
+ span.set_output(answer)
32
+ span.record_tokens(prompt=120, completion=40)
33
+
34
+ Wrapping an `openai-agents` runner so its LLM-call spans nest
35
+ under the AGENT span::
36
+
37
+ from catalyst_tracing import agent_span, setup
38
+ from agents import Agent, Runner
39
+
40
+ tracing = setup()
41
+ agent = Agent(name="SupportAgent", instructions=..., tools=[...])
42
+
43
+ with agent_span(tracing.tracer, name="SupportAgent", system="openai") as span:
44
+ span.set_input(user_message)
45
+ result = await Runner.run(agent, input=user_message)
46
+ span.set_output(str(result.final_output))
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import json
52
+ from collections.abc import Iterator
53
+ from contextlib import contextmanager
54
+ from dataclasses import dataclass
55
+ from typing import Any
56
+
57
+ from opentelemetry.trace import Span, SpanKind, Status, StatusCode, Tracer
58
+
59
+ from .semconv import Attr, SpanKindValues
60
+
61
+
62
+ @dataclass
63
+ class AgentSpanHandle:
64
+ """The handle yielded by :func:`agent_span`.
65
+
66
+ Provides type-safe helpers for the OI attribute set the agent
67
+ span should carry, plus a ``span`` escape hatch for callers
68
+ that need the underlying OTel span (e.g. to add custom
69
+ attributes outside the OI vocabulary).
70
+ """
71
+
72
+ #: The underlying OTel span — escape hatch for advanced use.
73
+ span: Span
74
+
75
+ def set_input(self, value: Any) -> None:
76
+ """Record the agent's input.
77
+
78
+ Strings are stored as-is on ``input.value``. Non-strings
79
+ are JSON-stringified.
80
+ """
81
+ self.span.set_attribute(
82
+ Attr.INPUT_VALUE,
83
+ value if isinstance(value, str) else json.dumps(value),
84
+ )
85
+
86
+ def set_output(self, value: Any) -> None:
87
+ """Record the agent's final output.
88
+
89
+ Same string-vs-JSON rule as :meth:`set_input`.
90
+ """
91
+ self.span.set_attribute(
92
+ Attr.OUTPUT_VALUE,
93
+ value if isinstance(value, str) else json.dumps(value),
94
+ )
95
+
96
+ def record_tokens(
97
+ self,
98
+ *,
99
+ prompt: int | None = None,
100
+ completion: int | None = None,
101
+ total: int | None = None,
102
+ ) -> None:
103
+ """Record token usage on the agent span.
104
+
105
+ Any of ``prompt``, ``completion``, ``total`` may be omitted;
106
+ only the provided fields are written. Use this when you have
107
+ aggregated counts across multiple LLM calls inside the agent
108
+ loop — per-call counts get captured by the LLM-level
109
+ instrumentation separately.
110
+ """
111
+ if prompt is not None:
112
+ self.span.set_attribute(Attr.TOKEN_COUNT_PROMPT, prompt)
113
+ if completion is not None:
114
+ self.span.set_attribute(Attr.TOKEN_COUNT_COMPLETION, completion)
115
+ if total is not None:
116
+ self.span.set_attribute(Attr.TOKEN_COUNT_TOTAL, total)
117
+
118
+ def set_model(self, model: str) -> None:
119
+ """Record the model name (becomes ``llm.model_name``)."""
120
+ self.span.set_attribute(Attr.MODEL_NAME, model)
121
+
122
+
123
+ @contextmanager
124
+ def agent_span(
125
+ tracer: Tracer,
126
+ *,
127
+ name: str,
128
+ system: str | None = None,
129
+ span_kind: SpanKindValues | str = SpanKindValues.AGENT,
130
+ otel_kind: SpanKind = SpanKind.INTERNAL,
131
+ span_name: str | None = None,
132
+ ) -> Iterator[AgentSpanHandle]:
133
+ """Wrap a chunk of agent work in an OI-shaped AGENT span.
134
+
135
+ Parameters
136
+ ----------
137
+ tracer : Tracer
138
+ OpenTelemetry tracer to author the span from. Use the one
139
+ on :class:`CatalystTracing.tracer`.
140
+ name : str
141
+ Logical agent name — e.g. ``"ClaudeCode"``,
142
+ ``"WeatherAgent"``. Becomes the ``agent.name`` attribute
143
+ and the prefix of the default span name.
144
+ system : str, optional
145
+ Identifier of the LLM provider this agent runs on, e.g.
146
+ ``"openai"`` or ``"anthropic"``. Becomes the
147
+ ``gen_ai.system`` attribute. Optional — omit if the agent
148
+ is provider-agnostic.
149
+ span_kind : SpanKindValues or str, default AGENT
150
+ OpenInference span kind. Pass another
151
+ :class:`SpanKindValues` (e.g. ``CHAIN``) when this helper
152
+ is used outside a strictly agent context.
153
+ otel_kind : SpanKind, default INTERNAL
154
+ OTel-level span kind. ``INTERNAL`` because the work itself
155
+ is internal to this process; the LLM-call children get
156
+ their own ``CLIENT`` kind from the per-SDK patchers.
157
+ span_name : str, optional
158
+ Override the auto-generated span name (``f"{name}.run"``).
159
+ Useful when "run" doesn't fit — e.g. our examples use
160
+ ``"claude-code.invocation"`` for one-shot CLI calls.
161
+
162
+ Yields
163
+ ------
164
+ AgentSpanHandle
165
+ Helpers to set input/output/tokens/model on the span.
166
+
167
+ Notes
168
+ -----
169
+ The span is closed automatically when the ``with`` block
170
+ exits. On unhandled exceptions the span ends with ``ERROR``
171
+ status and an ``exception`` event recording the traceback,
172
+ then the exception re-raises so caller error handling is
173
+ unaffected.
174
+ """
175
+ resolved_span_name = span_name or f"{name}.run"
176
+ resolved_kind_value = (
177
+ span_kind.value if isinstance(span_kind, SpanKindValues) else span_kind
178
+ )
179
+
180
+ attributes: dict[str, str] = {
181
+ Attr.SPAN_KIND: resolved_kind_value,
182
+ Attr.AGENT_NAME: name,
183
+ }
184
+ if system is not None:
185
+ attributes[Attr.SYSTEM] = system
186
+
187
+ with tracer.start_as_current_span(
188
+ resolved_span_name,
189
+ kind=otel_kind,
190
+ attributes=attributes,
191
+ ) as span:
192
+ handle = AgentSpanHandle(span=span)
193
+ try:
194
+ yield handle
195
+ span.set_status(Status(StatusCode.OK))
196
+ except Exception as err:
197
+ span.record_exception(err)
198
+ span.set_status(Status(StatusCode.ERROR, str(err)))
199
+ raise
@@ -0,0 +1,17 @@
1
+ """``catalyst_tracing.anthropic`` — granular entry-point import for the
2
+ Anthropic integration.
3
+
4
+ Example::
5
+
6
+ from catalyst_tracing import setup
7
+ from catalyst_tracing.anthropic import install_anthropic
8
+
9
+ tracing = setup()
10
+ install_anthropic(tracing.provider)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .installers.anthropic import install_anthropic
16
+
17
+ __all__ = ["install_anthropic"]
@@ -0,0 +1,21 @@
1
+ """``catalyst_tracing.claude_agent_sdk`` — granular entry-point import
2
+ for the Claude Agent SDK integration.
3
+
4
+ Example::
5
+
6
+ from catalyst_tracing import setup
7
+ from catalyst_tracing.claude_agent_sdk import install_claude_agent_sdk
8
+
9
+ tracing = setup()
10
+ install_claude_agent_sdk(tracing.provider)
11
+
12
+ # `from claude_agent_sdk import query` continues to read the
13
+ # wrapped function. The patch covers both the package-level and
14
+ # submodule bindings.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .installers.claude_agent_sdk import install_claude_agent_sdk
20
+
21
+ __all__ = ["install_claude_agent_sdk"]
@@ -0,0 +1,122 @@
1
+ """Package-level constants shared across setup, installers, and patchers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ PACKAGE_VERSION = "0.0.1"
6
+
7
+
8
+ class DefaultConfig:
9
+ OTLP_ENDPOINT = "http://localhost:8799"
10
+ SERVICE_NAME_PREFIX = "catalyst-app"
11
+ SERVICE_VERSION = PACKAGE_VERSION
12
+
13
+
14
+ class EnvVar:
15
+ CATALYST_OTLP_ENDPOINT = "CATALYST_OTLP_ENDPOINT"
16
+ LEGACY_OTLP_ENDPOINT = "OTLP_ENDPOINT"
17
+ CATALYST_OTLP_TOKEN = "CATALYST_OTLP_TOKEN"
18
+ LEGACY_OTLP_INGEST_TOKEN = "OTLP_INGEST_TOKEN"
19
+ CATALYST_SERVICE_NAME = "CATALYST_SERVICE_NAME"
20
+ LEGACY_SERVICE_NAME = "SERVICE_NAME"
21
+ CATALYST_SERVICE_VERSION = "CATALYST_SERVICE_VERSION"
22
+ CATALYST_DEBUG = "CATALYST_DEBUG"
23
+
24
+
25
+ class MimeType:
26
+ JSON = "application/json"
27
+
28
+
29
+ class HttpHeader:
30
+ AUTHORIZATION = "Authorization"
31
+ CONTENT_TYPE = "Content-Type"
32
+
33
+
34
+ class OtlpPath:
35
+ TRACES = "/v1/traces"
36
+
37
+
38
+ class BatchingMode:
39
+ BATCH = "batch"
40
+ SIMPLE = "simple"
41
+
42
+
43
+ class SpanProcessorDefaults:
44
+ MAX_EXPORT_BATCH_SIZE = 64
45
+ SCHEDULE_DELAY_MILLIS = 1000
46
+
47
+
48
+ class ResourceAttribute:
49
+ SERVICE_NAME = "service.name"
50
+ SERVICE_VERSION = "service.version"
51
+
52
+
53
+ class OpenTelemetryRuntime:
54
+ PROXY_TRACER_PROVIDER = "ProxyTracerProvider"
55
+
56
+
57
+ class IntegrationId:
58
+ OPENAI = "openai"
59
+ ANTHROPIC = "anthropic"
60
+ LANGCHAIN = "langchain"
61
+ LANGGRAPH = "langgraph"
62
+ LANGSMITH = "langsmith"
63
+ OPENAI_AGENTS = "openai-agents"
64
+ CLAUDE_AGENT_SDK = "claude-agent-sdk"
65
+ PYDANTIC_AI = "pydantic-ai"
66
+
67
+
68
+ class InstallerId:
69
+ """Function-name-compatible integration identifiers for errors."""
70
+
71
+ OPENAI = "openai"
72
+ ANTHROPIC = "anthropic"
73
+ LANGCHAIN = "langchain"
74
+ LANGGRAPH = "langgraph"
75
+ LANGSMITH = "langsmith"
76
+ OPENAI_AGENTS = "openai_agents"
77
+ CLAUDE_AGENT_SDK = "claude_agent_sdk"
78
+ PYDANTIC_AI = "pydantic_ai"
79
+
80
+
81
+ class ImportName:
82
+ OPENAI = "openai"
83
+ ANTHROPIC = "anthropic"
84
+ LANGCHAIN_CORE = "langchain_core"
85
+ LANGGRAPH = "langgraph"
86
+ LANGSMITH = "langsmith"
87
+ LANGSMITH_CLIENT = "langsmith.client"
88
+ OPENAI_AGENTS = "agents"
89
+ CLAUDE_AGENT_SDK = "claude_agent_sdk"
90
+ CLAUDE_AGENT_SDK_QUERY = "claude_agent_sdk.query"
91
+ PYDANTIC_AI = "pydantic_ai"
92
+
93
+
94
+ class GenAISystem:
95
+ OPENAI = "openai"
96
+ ANTHROPIC = "anthropic"
97
+
98
+
99
+ class TracerName:
100
+ ROOT = "catalyst_tracing"
101
+ OPENAI = "catalyst_tracing.openai"
102
+ ANTHROPIC = "catalyst_tracing.anthropic"
103
+ LANGCHAIN = "catalyst_tracing.langchain"
104
+ CLAUDE_AGENT_SDK = "catalyst_tracing.claude_agent_sdk"
105
+
106
+
107
+ class SpanName:
108
+ OPENAI_CHAT_COMPLETIONS = "OpenAI Chat Completions"
109
+ OPENAI_RESPONSES = "OpenAI Responses"
110
+ ANTHROPIC_MESSAGES = "Anthropic Messages"
111
+ CLAUDE_AGENT_QUERY = "ClaudeAgentSDK.query"
112
+
113
+
114
+ class AgentName:
115
+ CLAUDE_AGENT_SDK = "ClaudeAgentSDK"
116
+
117
+
118
+ class PatchedMethod:
119
+ CREATE = "create"
120
+
121
+
122
+ PATCH_MARKER_ATTR = "_catalyst_wrapped"
@@ -0,0 +1,67 @@
1
+ """Resolve catalyst-tracing config from kwargs + env vars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import uuid
7
+ from dataclasses import dataclass
8
+
9
+ from .constants import DefaultConfig, EnvVar
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ResolvedConfig:
14
+ endpoint: str
15
+ token: str | None
16
+ service_name: str
17
+ service_version: str
18
+ debug: bool
19
+
20
+
21
+ def _truthy(value: str | None) -> bool:
22
+ if value is None or value == "":
23
+ return False
24
+ return value.lower() not in {"0", "false", "no"}
25
+
26
+
27
+ def resolve_config(
28
+ *,
29
+ endpoint: str | None = None,
30
+ token: str | None = None,
31
+ service_name: str | None = None,
32
+ service_version: str | None = None,
33
+ debug: bool | None = None,
34
+ ) -> ResolvedConfig:
35
+ env = os.environ
36
+ resolved_endpoint = (
37
+ endpoint
38
+ or env.get(EnvVar.CATALYST_OTLP_ENDPOINT)
39
+ or env.get(EnvVar.LEGACY_OTLP_ENDPOINT)
40
+ or DefaultConfig.OTLP_ENDPOINT
41
+ )
42
+ resolved_token = (
43
+ token
44
+ or env.get(EnvVar.CATALYST_OTLP_TOKEN)
45
+ or env.get(EnvVar.LEGACY_OTLP_INGEST_TOKEN)
46
+ )
47
+ resolved_service_name = (
48
+ service_name
49
+ or env.get(EnvVar.CATALYST_SERVICE_NAME)
50
+ or env.get(EnvVar.LEGACY_SERVICE_NAME)
51
+ or f"{DefaultConfig.SERVICE_NAME_PREFIX}-{uuid.uuid4().hex[:8]}"
52
+ )
53
+ resolved_service_version = (
54
+ service_version
55
+ or env.get(EnvVar.CATALYST_SERVICE_VERSION)
56
+ or DefaultConfig.SERVICE_VERSION
57
+ )
58
+ resolved_debug = (
59
+ debug if debug is not None else _truthy(env.get(EnvVar.CATALYST_DEBUG))
60
+ )
61
+ return ResolvedConfig(
62
+ endpoint=resolved_endpoint,
63
+ token=resolved_token,
64
+ service_name=resolved_service_name,
65
+ service_version=resolved_service_version,
66
+ debug=resolved_debug,
67
+ )
@@ -0,0 +1,134 @@
1
+ """Catalyst-tracing error types.
2
+
3
+ Errors raised by this package always extend :class:`CatalystTracingError`
4
+ and carry a stable :attr:`~CatalystTracingError.code` string for
5
+ programmatic handling. Messages are written for humans — they say what
6
+ went wrong AND how to fix it.
7
+
8
+ Use ``isinstance(err, CatalystTracingError)`` for catch-and-log
9
+ policies and the ``code`` field for fine-grained branching.
10
+
11
+ Example::
12
+
13
+ from catalyst_tracing import CatalystTracingError, install_openai
14
+
15
+ try:
16
+ install_openai(provider)
17
+ except CatalystTracingError as err:
18
+ if err.code == "ERR_INVALID_TRACER_PROVIDER":
19
+ ... # actionable handling
20
+ raise
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Final, Literal
26
+
27
+ from .constants import InstallerId
28
+
29
+ CatalystTracingErrorCode = Literal[
30
+ "ERR_INVALID_TRACER_PROVIDER",
31
+ "ERR_INVALID_SDK_INPUT",
32
+ ]
33
+
34
+
35
+ class CatalystTracingErrorCodes:
36
+ INVALID_TRACER_PROVIDER: CatalystTracingErrorCode = "ERR_INVALID_TRACER_PROVIDER"
37
+ INVALID_SDK_INPUT: CatalystTracingErrorCode = "ERR_INVALID_SDK_INPUT"
38
+
39
+
40
+ class CatalystTracingError(Exception):
41
+ """Base class for every error raised by ``catalyst_tracing``.
42
+
43
+ Catch this when you only need "did the package raise?". Subclasses
44
+ exist for the specific failure modes.
45
+ """
46
+
47
+ #: Stable, machine-readable code for ``except``-and-branch logic.
48
+ code: CatalystTracingErrorCode
49
+
50
+ def __init__(self, code: CatalystTracingErrorCode, message: str) -> None:
51
+ super().__init__(message)
52
+ self.code = code
53
+
54
+
55
+ class InvalidTracerProviderError(CatalystTracingError):
56
+ """Raised when an ``install_*`` helper is given something that
57
+ isn't a TracerProvider.
58
+
59
+ The Python install helpers consistently take a TracerProvider
60
+ (matching the OTel SDK's idiom). If you pass a Tracer, a string,
61
+ or ``None``, you get this error pointing at the call site rather
62
+ than a confusing AttributeError deep in the patcher.
63
+ """
64
+
65
+ INTEGRATION_HINTS: Final[dict[str, str]] = {
66
+ InstallerId.OPENAI: "install_openai(provider)",
67
+ InstallerId.ANTHROPIC: "install_anthropic(provider)",
68
+ InstallerId.LANGCHAIN: "install_langchain(provider)",
69
+ InstallerId.LANGGRAPH: "install_langgraph(provider)",
70
+ InstallerId.LANGSMITH: "install_langsmith(provider)",
71
+ InstallerId.OPENAI_AGENTS: "install_openai_agents(provider)",
72
+ InstallerId.CLAUDE_AGENT_SDK: "install_claude_agent_sdk(provider)",
73
+ InstallerId.PYDANTIC_AI: "install_pydantic_ai(provider)",
74
+ }
75
+
76
+ def __init__(self, integration: str, received: Any) -> None:
77
+ hint = self.INTEGRATION_HINTS.get(
78
+ integration,
79
+ f"install_{integration}(provider)",
80
+ )
81
+ super().__init__(
82
+ CatalystTracingErrorCodes.INVALID_TRACER_PROVIDER,
83
+ f"install_{integration}: expected a TracerProvider "
84
+ f"(from `opentelemetry.sdk.trace`), got {_describe(received)}.\n"
85
+ f"Hint: pass `tracing.provider` from `setup()`, or your own "
86
+ f"`TracerProvider`: `{hint}`.",
87
+ )
88
+ self.integration = integration
89
+
90
+
91
+ class InvalidSdkInputError(CatalystTracingError):
92
+ """Raised when an ``install_*`` helper that takes an SDK module
93
+ is given a non-module / wrong-shape argument."""
94
+
95
+ def __init__(
96
+ self,
97
+ *,
98
+ integration: str,
99
+ expectation: str,
100
+ received: Any,
101
+ fix_hint: str,
102
+ ) -> None:
103
+ super().__init__(
104
+ CatalystTracingErrorCodes.INVALID_SDK_INPUT,
105
+ f"install_{integration}: {expectation}, got {_describe(received)}.\n"
106
+ f"Hint: {fix_hint}",
107
+ )
108
+ self.integration = integration
109
+
110
+
111
+ def _describe(value: Any) -> str:
112
+ """Short, human-friendly description of a value for error text.
113
+
114
+ We avoid logging the full value (potentially huge / contains
115
+ secrets); just enough context for the user to recognize what they
116
+ passed.
117
+ """
118
+ if value is None:
119
+ return "None"
120
+ if isinstance(value, type):
121
+ return f"class {value.__name__}"
122
+ if callable(value):
123
+ name = getattr(value, "__name__", None) or "anonymous"
124
+ return f"callable `{name}`"
125
+ if isinstance(value, dict):
126
+ keys = list(value)[:4]
127
+ if not keys:
128
+ return "an empty dict"
129
+ suffix = ", ..." if len(value) > 4 else ""
130
+ return f"dict with keys {keys}{suffix}"
131
+ type_name = type(value).__name__
132
+ if isinstance(value, (str, int, float, bool)):
133
+ return f"{type_name} {value!r}"
134
+ return type_name