ol-llm 0.0.1__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.
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .env
ol_llm-0.0.1/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, MIT Open Learning
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ol_llm-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: ol-llm
3
+ Version: 0.0.1
4
+ Summary: Shared library for LLM calls at MIT Open Learning, with Opik tracing, prompt management, and evaluation built in
5
+ Project-URL: Homepage, https://github.com/mitodl/ol-llm
6
+ Project-URL: Repository, https://github.com/mitodl/ol-llm
7
+ Project-URL: Issues, https://github.com/mitodl/ol-llm/issues
8
+ Author-email: MIT Open Learning <odl-devops@mit.edu>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: llm,observability,opik,pydantic-ai
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpx>=0.28
21
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.38
22
+ Requires-Dist: opentelemetry-sdk>=1.38
23
+ Requires-Dist: opik==2.2.87
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.52; extra == 'anthropic'
26
+ Provides-Extra: bedrock
27
+ Requires-Dist: pydantic-ai-slim[bedrock]<3,>=2.52; extra == 'bedrock'
28
+ Provides-Extra: google
29
+ Requires-Dist: pydantic-ai-slim[google]<3,>=2.52; extra == 'google'
30
+ Provides-Extra: harness
31
+ Requires-Dist: pydantic-ai-harness<0.53,>=0.52; extra == 'harness'
32
+ Provides-Extra: memory
33
+ Requires-Dist: hindsight-client<0.11,>=0.10; extra == 'memory'
34
+ Provides-Extra: mistral
35
+ Requires-Dist: pydantic-ai-slim[mistral]<3,>=2.52; extra == 'mistral'
36
+ Provides-Extra: openai
37
+ Requires-Dist: pydantic-ai-slim[openai]<3,>=2.52; extra == 'openai'
38
+ Provides-Extra: pydantic-ai
39
+ Requires-Dist: pydantic-ai-slim<3,>=2.52; extra == 'pydantic-ai'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # ol-llm
43
+
44
+ The library every LLM call site at MIT Open Learning goes through. It owns the Opik
45
+ integration (tracing, prompt management, evaluation), a single timeout and retry policy, and
46
+ the attribution fields that let cost, quality, and traffic join across applications.
47
+
48
+ Importing `ol_llm` sets `OPIK_SENTRY_ENABLE=false` before anything imports the Opik SDK, which
49
+ otherwise replaces the host application's Sentry client with its own, so import it first.
50
+
51
+ The design is in [mitodl/ol-infrastructure `docs/plans/shared-llm-library-spec.md`](https://github.com/mitodl/ol-infrastructure/blob/main/docs/plans/shared-llm-library-spec.md).
52
+ The rules the code follows are in [`docs/`](https://github.com/mitodl/ol-llm/tree/main/docs):
53
+ the [error contract](https://github.com/mitodl/ol-llm/blob/main/docs/error-contract.md) and the
54
+ [attribution schema](https://github.com/mitodl/ol-llm/blob/main/docs/attribution.md).
55
+
56
+ ## Usage
57
+
58
+ Declare each call site once, as a module-level constant, and build its agent from it:
59
+
60
+ ```python
61
+ from pydantic import BaseModel
62
+
63
+ import ol_llm
64
+
65
+ SUMMARY = ol_llm.UseCase(
66
+ app="mit-learn",
67
+ name="content-summary",
68
+ profile="batch",
69
+ model="openai:gpt-4o-mini",
70
+ fallback_models=("bedrock:global.anthropic.claude-haiku-4-5-20251001-v1:0",),
71
+ )
72
+
73
+
74
+ class Summary(BaseModel):
75
+ text: str
76
+
77
+
78
+ summarizer = ol_llm.build_agent(SUMMARY, output_type=Summary, instructions="Summarize.")
79
+ ```
80
+
81
+ The agent's model has a timeout on every request, retries rate limits and outages and
82
+ nothing else, falls back once the primary has spent its retries, and raises `LLMError`
83
+ subclasses (`LLMRateLimited`, `LLMUnavailable`, `LLMRejected`, ...) with the provider's
84
+ exception as the cause. Build agents once, not per request: each build creates new provider
85
+ clients.
86
+
87
+ Set `OL_LLM_ENVIRONMENT`, or call `ol_llm.configure(ol_llm.Settings(environment=...))` at
88
+ startup. Provider credentials are each SDK's own (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, the AWS
89
+ and Google default credential chains, and so on). Operators can change a use case's limits
90
+ without a release, e.g. `OL_LLM_CONTENT_SUMMARY_TIMEOUT_SECONDS=120`.
91
+
92
+ Call sites not built on PydanticAI (LangGraph, a plain SDK) apply the same numbers with
93
+ `ol_llm.resolve_policy(use_case)`, which needs no extra.
94
+
95
+ The entry points that wrap a run in an Opik trace (`run`, `run_stream`, `complete`) are not
96
+ released yet; until they are, `build_agent` gives the policy and errors without the tracing.
97
+
98
+ ### Testing
99
+
100
+ `ol_llm.testing.override_models` makes `build_model` and `build_agent` use PydanticAI's
101
+ `TestModel` or `FunctionModel` in place of a provider, still under the library's retry and
102
+ error handling, and `tracing_disabled` turns Opik off. Script responses and failures with
103
+ `FunctionModel`; do not commit recorded provider responses.
104
+
105
+ ## Extras
106
+
107
+ The base install is the Opik SDK and OpenTelemetry, which every call site needs. Everything
108
+ else is opt-in:
109
+
110
+ | Extra | Installs | For |
111
+ | --- | --- | --- |
112
+ | `pydantic-ai` | `pydantic-ai-slim` | The default construction path for agents and single calls |
113
+ | `openai`, `anthropic`, `bedrock`, `google`, `mistral` | the matching `pydantic-ai-slim` extra | A provider SDK; each also installs PydanticAI |
114
+ | `harness` | `pydantic-ai-harness` | In-session memory, compaction, and conversation search. Each harness release pins PydanticAI exactly |
115
+ | `memory` | `hindsight-client` | The shared memory service's client. Provisional: the backend is still being evaluated |
116
+
117
+ For example, `ol-llm[bedrock,harness]`.
118
+
119
+ ## Installing without the Opik SDK's heavy dependencies
120
+
121
+ The `opik` package hard-requires litellm, openai, pytest, and several other packages the
122
+ tracing and prompt paths never import. uv consumers can drop them:
123
+
124
+ ```toml
125
+ [tool.uv]
126
+ override-dependencies = [
127
+ "boto3-stubs; sys_platform == 'never'",
128
+ "click; sys_platform == 'never'",
129
+ "litellm; sys_platform == 'never'",
130
+ "openai; sys_platform == 'never'",
131
+ "pytest; sys_platform == 'never'",
132
+ "rapidfuzz; sys_platform == 'never'",
133
+ "tree-sitter; sys_platform == 'never'",
134
+ "tree-sitter-javascript; sys_platform == 'never'",
135
+ "tree-sitter-typescript; sys_platform == 'never'",
136
+ "watchfiles; sys_platform == 'never'",
137
+ ]
138
+ ```
139
+
140
+ Drop `openai` from that list if the application uses the `openai` extra. Opik's evaluation
141
+ metrics need litellm, so the overrides belong in runtime images, not in an environment that
142
+ runs evaluations. CI checks this exact list on every change (`python/smoke/`).
143
+
144
+ Applications that cap how new a dependency may be (`exclude-newer` in uv) should exempt this
145
+ package so releases are usable the day they ship:
146
+
147
+ ```toml
148
+ [tool.uv.exclude-newer-package]
149
+ ol-llm = "0d"
150
+ ```
ol_llm-0.0.1/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # ol-llm
2
+
3
+ The library every LLM call site at MIT Open Learning goes through. It owns the Opik
4
+ integration (tracing, prompt management, evaluation), a single timeout and retry policy, and
5
+ the attribution fields that let cost, quality, and traffic join across applications.
6
+
7
+ Importing `ol_llm` sets `OPIK_SENTRY_ENABLE=false` before anything imports the Opik SDK, which
8
+ otherwise replaces the host application's Sentry client with its own, so import it first.
9
+
10
+ The design is in [mitodl/ol-infrastructure `docs/plans/shared-llm-library-spec.md`](https://github.com/mitodl/ol-infrastructure/blob/main/docs/plans/shared-llm-library-spec.md).
11
+ The rules the code follows are in [`docs/`](https://github.com/mitodl/ol-llm/tree/main/docs):
12
+ the [error contract](https://github.com/mitodl/ol-llm/blob/main/docs/error-contract.md) and the
13
+ [attribution schema](https://github.com/mitodl/ol-llm/blob/main/docs/attribution.md).
14
+
15
+ ## Usage
16
+
17
+ Declare each call site once, as a module-level constant, and build its agent from it:
18
+
19
+ ```python
20
+ from pydantic import BaseModel
21
+
22
+ import ol_llm
23
+
24
+ SUMMARY = ol_llm.UseCase(
25
+ app="mit-learn",
26
+ name="content-summary",
27
+ profile="batch",
28
+ model="openai:gpt-4o-mini",
29
+ fallback_models=("bedrock:global.anthropic.claude-haiku-4-5-20251001-v1:0",),
30
+ )
31
+
32
+
33
+ class Summary(BaseModel):
34
+ text: str
35
+
36
+
37
+ summarizer = ol_llm.build_agent(SUMMARY, output_type=Summary, instructions="Summarize.")
38
+ ```
39
+
40
+ The agent's model has a timeout on every request, retries rate limits and outages and
41
+ nothing else, falls back once the primary has spent its retries, and raises `LLMError`
42
+ subclasses (`LLMRateLimited`, `LLMUnavailable`, `LLMRejected`, ...) with the provider's
43
+ exception as the cause. Build agents once, not per request: each build creates new provider
44
+ clients.
45
+
46
+ Set `OL_LLM_ENVIRONMENT`, or call `ol_llm.configure(ol_llm.Settings(environment=...))` at
47
+ startup. Provider credentials are each SDK's own (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, the AWS
48
+ and Google default credential chains, and so on). Operators can change a use case's limits
49
+ without a release, e.g. `OL_LLM_CONTENT_SUMMARY_TIMEOUT_SECONDS=120`.
50
+
51
+ Call sites not built on PydanticAI (LangGraph, a plain SDK) apply the same numbers with
52
+ `ol_llm.resolve_policy(use_case)`, which needs no extra.
53
+
54
+ The entry points that wrap a run in an Opik trace (`run`, `run_stream`, `complete`) are not
55
+ released yet; until they are, `build_agent` gives the policy and errors without the tracing.
56
+
57
+ ### Testing
58
+
59
+ `ol_llm.testing.override_models` makes `build_model` and `build_agent` use PydanticAI's
60
+ `TestModel` or `FunctionModel` in place of a provider, still under the library's retry and
61
+ error handling, and `tracing_disabled` turns Opik off. Script responses and failures with
62
+ `FunctionModel`; do not commit recorded provider responses.
63
+
64
+ ## Extras
65
+
66
+ The base install is the Opik SDK and OpenTelemetry, which every call site needs. Everything
67
+ else is opt-in:
68
+
69
+ | Extra | Installs | For |
70
+ | --- | --- | --- |
71
+ | `pydantic-ai` | `pydantic-ai-slim` | The default construction path for agents and single calls |
72
+ | `openai`, `anthropic`, `bedrock`, `google`, `mistral` | the matching `pydantic-ai-slim` extra | A provider SDK; each also installs PydanticAI |
73
+ | `harness` | `pydantic-ai-harness` | In-session memory, compaction, and conversation search. Each harness release pins PydanticAI exactly |
74
+ | `memory` | `hindsight-client` | The shared memory service's client. Provisional: the backend is still being evaluated |
75
+
76
+ For example, `ol-llm[bedrock,harness]`.
77
+
78
+ ## Installing without the Opik SDK's heavy dependencies
79
+
80
+ The `opik` package hard-requires litellm, openai, pytest, and several other packages the
81
+ tracing and prompt paths never import. uv consumers can drop them:
82
+
83
+ ```toml
84
+ [tool.uv]
85
+ override-dependencies = [
86
+ "boto3-stubs; sys_platform == 'never'",
87
+ "click; sys_platform == 'never'",
88
+ "litellm; sys_platform == 'never'",
89
+ "openai; sys_platform == 'never'",
90
+ "pytest; sys_platform == 'never'",
91
+ "rapidfuzz; sys_platform == 'never'",
92
+ "tree-sitter; sys_platform == 'never'",
93
+ "tree-sitter-javascript; sys_platform == 'never'",
94
+ "tree-sitter-typescript; sys_platform == 'never'",
95
+ "watchfiles; sys_platform == 'never'",
96
+ ]
97
+ ```
98
+
99
+ Drop `openai` from that list if the application uses the `openai` extra. Opik's evaluation
100
+ metrics need litellm, so the overrides belong in runtime images, not in an environment that
101
+ runs evaluations. CI checks this exact list on every change (`python/smoke/`).
102
+
103
+ Applications that cap how new a dependency may be (`exclude-newer` in uv) should exempt this
104
+ package so releases are usable the day they ship:
105
+
106
+ ```toml
107
+ [tool.uv.exclude-newer-package]
108
+ ol-llm = "0d"
109
+ ```
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "ol-llm"
3
+ version = "0.0.1"
4
+ description = "Shared library for LLM calls at MIT Open Learning, with Opik tracing, prompt management, and evaluation built in"
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "MIT Open Learning", email = "odl-devops@mit.edu" }]
9
+ requires-python = ">=3.11"
10
+ keywords = ["llm", "opik", "pydantic-ai", "observability"]
11
+ classifiers = [
12
+ "Development Status :: 2 - Pre-Alpha",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Typing :: Typed",
19
+ ]
20
+ # The base install is what every call site needs whatever it is built on: the
21
+ # Opik SDK for prompts and the root trace, and OpenTelemetry for the spans a
22
+ # framework emits. Everything framework- or backend-specific is an extra.
23
+ #
24
+ # opik is pinned exactly: consumers slim it with uv override-dependencies, and a
25
+ # release that starts importing a dropped package eagerly would break them at
26
+ # import time. The smoke project in python/smoke/ catches that on every bump.
27
+ dependencies = [
28
+ "httpx>=0.28",
29
+ "opentelemetry-exporter-otlp-proto-http>=1.38",
30
+ "opentelemetry-sdk>=1.38",
31
+ "opik==2.2.87",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ # The default construction path for agents and single calls.
36
+ pydantic-ai = ["pydantic-ai-slim>=2.52,<3"]
37
+ # Provider SDKs, each through PydanticAI's own extra, so a provider extra also
38
+ # installs PydanticAI.
39
+ anthropic = ["pydantic-ai-slim[anthropic]>=2.52,<3"]
40
+ bedrock = ["pydantic-ai-slim[bedrock]>=2.52,<3"]
41
+ google = ["pydantic-ai-slim[google]>=2.52,<3"]
42
+ mistral = ["pydantic-ai-slim[mistral]>=2.52,<3"]
43
+ openai = ["pydantic-ai-slim[openai]>=2.52,<3"]
44
+ # PydanticAI's first-party capabilities: in-session memory, compaction, and
45
+ # conversation search. Each harness release pins pydantic-ai-slim exactly, so
46
+ # this extra also fixes the PydanticAI version. 0.x: pinned to a minor series.
47
+ harness = ["pydantic-ai-harness>=0.52,<0.53"]
48
+ # Client for the shared memory service (cross-session recall and user-scoped
49
+ # facts). Hindsight is the leading candidate and its PoC is still open, so this
50
+ # extra may change backend before the memory interface ships.
51
+ memory = ["hindsight-client>=0.10,<0.11"]
52
+
53
+ [project.urls]
54
+ Homepage = "https://github.com/mitodl/ol-llm"
55
+ Repository = "https://github.com/mitodl/ol-llm"
56
+ Issues = "https://github.com/mitodl/ol-llm/issues"
57
+
58
+ [build-system]
59
+ requires = ["hatchling"]
60
+ build-backend = "hatchling.build"
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/ol_llm"]
@@ -0,0 +1,28 @@
1
+ # A consumer-shaped project: ol-llm installed with the overrides a uv consumer
2
+ # copies to drop the opik SDK's heavy dependencies. CI runs smoke_test.py in this
3
+ # environment on every change, so an opik release that starts importing a dropped
4
+ # package eagerly fails here before any app picks it up.
5
+ [project]
6
+ name = "ol-llm-smoke"
7
+ version = "0"
8
+ requires-python = ">=3.11"
9
+ # The pydantic-ai extra, because the smoke test runs an instrumented agent.
10
+ dependencies = ["ol-llm[pydantic-ai]"]
11
+
12
+ [tool.uv]
13
+ package = false
14
+ override-dependencies = [
15
+ "boto3-stubs; sys_platform == 'never'",
16
+ "click; sys_platform == 'never'",
17
+ "litellm; sys_platform == 'never'",
18
+ "openai; sys_platform == 'never'",
19
+ "pytest; sys_platform == 'never'",
20
+ "rapidfuzz; sys_platform == 'never'",
21
+ "tree-sitter; sys_platform == 'never'",
22
+ "tree-sitter-javascript; sys_platform == 'never'",
23
+ "tree-sitter-typescript; sys_platform == 'never'",
24
+ "watchfiles; sys_platform == 'never'",
25
+ ]
26
+
27
+ [tool.uv.sources]
28
+ ol-llm = { path = "..", editable = true }
@@ -0,0 +1,91 @@
1
+ """Import-and-trace check for ol-llm installed with the opik SDK slimmed.
2
+
3
+ Runs as a plain script, not under pytest: pytest is one of the packages the
4
+ overrides drop. Exits non-zero on any failure.
5
+
6
+ What it proves, in an environment shaped like a consumer's runtime image:
7
+ - ol_llm and opik import with litellm, openai, pytest, and tree-sitter absent;
8
+ - an @opik.track root with an instrumented agent from ol_llm.build_agent inside it
9
+ produces OTel spans that OpikSpanProcessor attaches to the tracked trace;
10
+ - importing ol_llm first leaves no Sentry client running.
11
+
12
+ No Opik server is needed. The SDK is pointed at a closed port, so its background
13
+ sender fails quietly; the assertions read the spans from an in-memory exporter.
14
+ """
15
+
16
+ import os
17
+ import sys
18
+
19
+ os.environ["OPIK_URL_OVERRIDE"] = "http://127.0.0.1:9/api"
20
+ os.environ["OPIK_WORKSPACE"] = "default"
21
+
22
+ # ol_llm before opik, so OPIK_SENTRY_ENABLE is set before opik reads it.
23
+ import ol_llm
24
+ import opik
25
+ import sentry_sdk
26
+ from ol_llm.models import build_agent, override_models
27
+ from opentelemetry.sdk.trace import TracerProvider
28
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
29
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
30
+ InMemorySpanExporter,
31
+ )
32
+ from opik.integrations.otel import OpikSpanProcessor
33
+ from pydantic_ai.capabilities import Instrumentation
34
+ from pydantic_ai.models.instrumented import InstrumentationSettings
35
+ from pydantic_ai.models.test import TestModel
36
+
37
+ DROPPED = ("litellm", "openai", "pytest", "tree_sitter", "click")
38
+ OPIK_TRACE_ID = "opik.trace_id"
39
+ SMOKE = ol_llm.UseCase(
40
+ app="ol-llm-smoke", name="smoke", profile="request", model="openai:smoke"
41
+ )
42
+
43
+
44
+ def main() -> int:
45
+ """Run the checks and return a process exit status."""
46
+ exporter = InMemorySpanExporter()
47
+ provider = TracerProvider()
48
+ provider.add_span_processor(OpikSpanProcessor())
49
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
50
+
51
+ ol_llm.configure(ol_llm.Settings(environment="ci"))
52
+ # The test model stands in for the provider; the policy wrapper, the agent's
53
+ # capabilities, and the import graph are the library's own.
54
+ with override_models(TestModel()):
55
+ agent = build_agent(
56
+ SMOKE,
57
+ capabilities=[
58
+ Instrumentation(
59
+ settings=InstrumentationSettings(tracer_provider=provider)
60
+ )
61
+ ],
62
+ )
63
+
64
+ @opik.track(name="smoke", project_name="ol-llm-smoke")
65
+ def run() -> str:
66
+ return agent.run_sync("ping").output
67
+
68
+ output = run()
69
+ spans = exporter.get_finished_spans()
70
+ attached = [s for s in spans if (s.attributes or {}).get(OPIK_TRACE_ID)]
71
+ imported = [name for name in DROPPED if name in sys.modules]
72
+ failures = []
73
+ if not spans:
74
+ failures.append("the agent emitted no OTel spans")
75
+ if len(attached) != len(spans):
76
+ failures.append(
77
+ f"{len(spans) - len(attached)} of {len(spans)} spans lack {OPIK_TRACE_ID}"
78
+ )
79
+ if imported:
80
+ failures.append(f"dropped packages were imported: {imported}")
81
+ if sentry_sdk.get_client().dsn is not None:
82
+ failures.append("a Sentry client is running after importing opik")
83
+
84
+ print(f"ol-llm {ol_llm.__version__}: output={output!r} spans={len(spans)}")
85
+ for failure in failures:
86
+ print(f"FAIL: {failure}")
87
+ return 1 if failures else 0
88
+
89
+
90
+ if __name__ == "__main__":
91
+ sys.exit(main())