orbitrage 0.2.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.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbitrage
3
+ Version: 0.2.0
4
+ Summary: One-line observability + intelligent LLM routing for Python agents.
5
+ Author: Orbitrage
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://orbitrage.xyz
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Monitoring
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: traceloop-sdk<1.0,>=0.50.0
21
+ Requires-Dist: opentelemetry-sdk>=1.27.0
22
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
23
+ Provides-Extra: openai
24
+ Requires-Dist: openai>=1.0; extra == "openai"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.20; extra == "anthropic"
27
+ Provides-Extra: langchain
28
+ Requires-Dist: langchain-openai>=0.2; extra == "langchain"
29
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
30
+
31
+ # orbitrage
32
+
33
+ One-line observability + intelligent LLM routing for Python agents.
34
+
35
+ ```python
36
+ import orbitrage
37
+ orbitrage.init("orb_xxx_yyy") # one line. that's it.
38
+
39
+ # now any OpenAI / Anthropic / LangChain call you make is auto-traced
40
+ # AND can be auto-routed via:
41
+ from openai import OpenAI
42
+ client = OpenAI(base_url="https://orbitrage.xyz/api/v1", api_key="orb_xxx_yyy")
43
+ ```
44
+
45
+ ## What it does
46
+
47
+ - **Zero-latency observability**: non-blocking batch span export — your hot
48
+ path stays at LLM-call speed, not LLM-call + telemetry RTT.
49
+ - **Auto-instruments** OpenAI, Anthropic, LangChain, LlamaIndex, etc. via
50
+ OpenLLMetry under the hood.
51
+ - **One endpoint**: `https://orbitrage.xyz/api/telemetry` — auth via your
52
+ Orbitrage API key.
53
+ - **Decorators** for grouping multi-step workflows:
54
+
55
+ ```python
56
+ from orbitrage import workflow, task
57
+
58
+ @workflow("checkout_flow")
59
+ def checkout(user_id):
60
+ plan = planner.invoke(...)
61
+ out = executor.invoke(...)
62
+ return formatter.invoke(...)
63
+ ```
64
+
65
+ ## Install
66
+
67
+ ```
68
+ pip install orbitrage
69
+ ```
70
+
71
+ ## Redacting prompts (opt-out)
72
+
73
+ Prompt + completion text is captured by default so it renders in the
74
+ Orbitrage dashboard. To redact content (token counts, costs, and routing
75
+ decisions still flow):
76
+
77
+ ```python
78
+ orbitrage.init("orb_xxx", capture_content=False)
79
+ ```
80
+
@@ -0,0 +1,50 @@
1
+ # orbitrage
2
+
3
+ One-line observability + intelligent LLM routing for Python agents.
4
+
5
+ ```python
6
+ import orbitrage
7
+ orbitrage.init("orb_xxx_yyy") # one line. that's it.
8
+
9
+ # now any OpenAI / Anthropic / LangChain call you make is auto-traced
10
+ # AND can be auto-routed via:
11
+ from openai import OpenAI
12
+ client = OpenAI(base_url="https://orbitrage.xyz/api/v1", api_key="orb_xxx_yyy")
13
+ ```
14
+
15
+ ## What it does
16
+
17
+ - **Zero-latency observability**: non-blocking batch span export — your hot
18
+ path stays at LLM-call speed, not LLM-call + telemetry RTT.
19
+ - **Auto-instruments** OpenAI, Anthropic, LangChain, LlamaIndex, etc. via
20
+ OpenLLMetry under the hood.
21
+ - **One endpoint**: `https://orbitrage.xyz/api/telemetry` — auth via your
22
+ Orbitrage API key.
23
+ - **Decorators** for grouping multi-step workflows:
24
+
25
+ ```python
26
+ from orbitrage import workflow, task
27
+
28
+ @workflow("checkout_flow")
29
+ def checkout(user_id):
30
+ plan = planner.invoke(...)
31
+ out = executor.invoke(...)
32
+ return formatter.invoke(...)
33
+ ```
34
+
35
+ ## Install
36
+
37
+ ```
38
+ pip install orbitrage
39
+ ```
40
+
41
+ ## Redacting prompts (opt-out)
42
+
43
+ Prompt + completion text is captured by default so it renders in the
44
+ Orbitrage dashboard. To redact content (token counts, costs, and routing
45
+ decisions still flow):
46
+
47
+ ```python
48
+ orbitrage.init("orb_xxx", capture_content=False)
49
+ ```
50
+
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "orbitrage"
7
+ version = "0.2.0"
8
+ description = "One-line observability + intelligent LLM routing for Python agents."
9
+ readme = "README.md"
10
+ # Aligned with traceloop-sdk 0.50+ which requires Python >= 3.10.
11
+ requires-python = ">=3.10"
12
+ authors = [{name = "Orbitrage"}]
13
+ license = {text = "Apache-2.0"}
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: System :: Monitoring",
25
+ ]
26
+ dependencies = [
27
+ "traceloop-sdk>=0.50.0,<1.0",
28
+ "opentelemetry-sdk>=1.27.0",
29
+ "opentelemetry-exporter-otlp-proto-http>=1.27.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ openai = ["openai>=1.0"]
34
+ anthropic = ["anthropic>=0.20"]
35
+ langchain = ["langchain-openai>=0.2", "langchain-core>=0.3"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://orbitrage.xyz"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ """orbitrage — one-line observability + intelligent routing for Python agents.
2
+
3
+ Usage:
4
+ import orbitrage
5
+ orbitrage.init("orb_xxx_yyy")
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from ._client import (
10
+ init,
11
+ flush,
12
+ shutdown,
13
+ workflow,
14
+ task,
15
+ tool,
16
+ agent,
17
+ set_association_properties,
18
+ )
19
+
20
+ __all__ = [
21
+ "init",
22
+ "flush",
23
+ "shutdown",
24
+ "workflow",
25
+ "task",
26
+ "tool",
27
+ "agent",
28
+ "set_association_properties",
29
+ ]
30
+
31
+ __version__ = "0.2.0"
@@ -0,0 +1,391 @@
1
+ """orbitrage client. Thin wrapper over Traceloop / OpenLLMetry with
2
+ hot-path-safe defaults so users pay close to zero overhead per LLM call.
3
+
4
+ Design goals:
5
+ - **One-line init**: `orbitrage.init("orb_xxx")` — no other config needed.
6
+ - **Zero blocking on the hot path**: spans are queued to a daemon-thread
7
+ BatchSpanProcessor; OTLP exports never run inline with the user's call.
8
+ - **Fail-open**: if the Orbitrage backend is unreachable, the user's
9
+ workflow MUST still succeed. Export errors are swallowed silently.
10
+ - **No surprises**: never call `os._exit`, never install global hooks
11
+ that the user didn't ask for, never print to stdout on success.
12
+
13
+ What we override vs. raw Traceloop:
14
+ - `disable_batch=False` — Traceloop's default is fine, but raw users
15
+ sometimes pass `disable_batch=True` (the docs show it), which makes
16
+ every span export a synchronous HTTPS POST. We force-disable that.
17
+ - Aggressive BatchSpanProcessor tuning via env vars for snappier flush.
18
+ - Default endpoint -> orbitrage.xyz, default headers from the api_key,
19
+ and a metadata-collector function so we don't have to ask the user
20
+ for app_name / workflow_id.
21
+ - `traceloop_sync_enabled=False` — we don't talk to Traceloop's prompt
22
+ sync API ever; users get prompts via Orbitrage's own /v1/prompts.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import atexit
27
+ import logging
28
+ import os
29
+ import re
30
+ import sys
31
+ from typing import Any, Callable, Optional, Set
32
+
33
+ logger = logging.getLogger("orbitrage")
34
+
35
+ # Lazy imports — keep `import orbitrage` itself sub-millisecond.
36
+ _initialized = False
37
+ _client = None
38
+ _tracer_wrapper = None
39
+
40
+
41
+ # ── public env config ─────────────────────────────────────────────────────
42
+ # Endpoint defaults to production. Override via ORBITRAGE_ENDPOINT for
43
+ # self-hosted / staging tests.
44
+ _DEFAULT_ENDPOINT = "https://orbitrage.xyz/api/telemetry"
45
+
46
+
47
+ # Map import names → Traceloop Instruments enum value names. Detection looks
48
+ # at sys.modules first, then falls back to importlib.util.find_spec for
49
+ # packages that are installed but not yet imported. Keeping this list tight:
50
+ # only LLM / agent libraries — vector DBs / loggers / generic HTTP are opt-in.
51
+ _AUTO_INSTRUMENT_MAP: list[tuple[tuple[str, ...], str]] = [
52
+ (("openai",), "OPENAI"),
53
+ (("anthropic",), "ANTHROPIC"),
54
+ (("langchain", "langchain_core", "langchain_openai",
55
+ "langchain_anthropic"), "LANGCHAIN"),
56
+ (("llama_index", "llama_index.core"), "LLAMA_INDEX"),
57
+ (("cohere",), "COHERE"),
58
+ (("groq",), "GROQ"),
59
+ (("mistralai",), "MISTRAL"),
60
+ (("google.generativeai", "google_generativeai"), "GOOGLE_GENERATIVEAI"),
61
+ (("ollama",), "OLLAMA"),
62
+ (("together",), "TOGETHER"),
63
+ (("replicate",), "REPLICATE"),
64
+ (("crewai",), "CREWAI"),
65
+ (("haystack",), "HAYSTACK"),
66
+ (("agno",), "AGNO"),
67
+ (("openai_agents",), "OPENAI_AGENTS"),
68
+ (("mcp",), "MCP"),
69
+ ]
70
+
71
+
72
+ def _autodetect_instruments() -> Optional[set]:
73
+ """Return the Instruments set Traceloop should load.
74
+
75
+ Strategy — only enable instruments for libraries the user is actually
76
+ using (sys.modules), PLUS the two raw LLM clients (openai, anthropic)
77
+ even if not yet imported. This handles the common pattern where
78
+ `orbitrage.init()` is called at the very top of the file BEFORE the
79
+ LLM client import — we still want the openai call to be traced.
80
+
81
+ We deliberately exclude heavy/fragile instrumentors (langchain,
82
+ llama_index, transformers) from the always-on set: their
83
+ instrumentation typically wraps the underlying openai/anthropic
84
+ client anyway, so the raw-client instrumentor catches the same span.
85
+
86
+ Returns None if Traceloop isn't importable (caller will skip init).
87
+ """
88
+ try:
89
+ from traceloop.sdk.instruments import Instruments
90
+ except Exception:
91
+ return None
92
+ out = set()
93
+ # Always-on raw LLM clients — safe, fast, near-zero startup cost.
94
+ for enum_name in ("OPENAI", "ANTHROPIC"):
95
+ v = getattr(Instruments, enum_name, None)
96
+ if v is not None:
97
+ out.add(v)
98
+ # Modules already imported by the user — definitely in use.
99
+ for modules, enum_name in _AUTO_INSTRUMENT_MAP:
100
+ if any(m in sys.modules for m in modules):
101
+ v = getattr(Instruments, enum_name, None)
102
+ if v is not None:
103
+ out.add(v)
104
+ return out
105
+
106
+
107
+ def _resolve_endpoint(explicit: Optional[str]) -> str:
108
+ """Resolve endpoint. Order: explicit arg > env var > default."""
109
+ return (
110
+ explicit
111
+ or os.environ.get("ORBITRAGE_ENDPOINT")
112
+ or os.environ.get("TRACELOOP_BASE_URL") # for migrators
113
+ or _DEFAULT_ENDPOINT
114
+ )
115
+
116
+
117
+ def _resolve_api_key(explicit: Optional[str]) -> Optional[str]:
118
+ return (
119
+ explicit
120
+ or os.environ.get("ORBITRAGE_API_KEY")
121
+ or os.environ.get("TRACELOOP_API_KEY")
122
+ )
123
+
124
+
125
+ def _silence_otel_exporter_noise() -> None:
126
+ """Stop OTel from printing 'Failed to export span batch ...' on every retry.
127
+
128
+ Export failures (auth, quota, network) are recoverable: the BSP keeps
129
+ spans in its in-memory queue and retries on the next interval. Users
130
+ shouldn't see scary stack traces in their normal console output —
131
+ that's the equivalent of Sentry printing red errors every time the
132
+ Sentry backend is down. Errors are still logged at DEBUG level so
133
+ operators can opt in with ORBITRAGE_DEBUG=1.
134
+ """
135
+ level = logging.WARNING if os.environ.get("ORBITRAGE_DEBUG") else logging.CRITICAL
136
+ for name in (
137
+ "opentelemetry.exporter.otlp.proto.http.trace_exporter",
138
+ "opentelemetry.exporter.otlp.proto.http._log_exporter",
139
+ "opentelemetry.exporter.otlp.proto.http.metric_exporter",
140
+ "opentelemetry.sdk.trace.export",
141
+ ):
142
+ logging.getLogger(name).setLevel(level)
143
+
144
+
145
+ def _set_bsp_env_defaults() -> None:
146
+ """Tune OTel's BatchSpanProcessor for low-latency export.
147
+
148
+ Env vars are read by BatchSpanProcessor at instantiation time. We only
149
+ set them if the user didn't already — never override their intent.
150
+
151
+ - SCHEDULE_DELAY=500ms: flush twice per second instead of every 5s, so
152
+ short-lived scripts get data flushed without leaning on shutdown.
153
+ - MAX_QUEUE_SIZE=4096: roomy enough for chatty agents (each LLM call
154
+ can emit a dozen spans across http, langchain, openai, tool levels).
155
+ - MAX_EXPORT_BATCH_SIZE=512: bigger batches = fewer HTTP RTTs.
156
+ """
157
+ defaults = {
158
+ "OTEL_BSP_SCHEDULE_DELAY": "500",
159
+ "OTEL_BSP_MAX_QUEUE_SIZE": "4096",
160
+ "OTEL_BSP_MAX_EXPORT_BATCH_SIZE": "512",
161
+ "OTEL_BSP_EXPORT_TIMEOUT": "10000",
162
+ }
163
+ for k, v in defaults.items():
164
+ os.environ.setdefault(k, v)
165
+
166
+
167
+ _API_KEY_RE = re.compile(r"^orb_[A-Za-z0-9_\-]{10,200}$")
168
+
169
+
170
+ # ── init ──────────────────────────────────────────────────────────────────
171
+ def init(
172
+ api_key: Optional[str] = None,
173
+ *,
174
+ app_name: Optional[str] = None,
175
+ endpoint: Optional[str] = None,
176
+ enabled: bool = True,
177
+ disable_batch: bool = False,
178
+ capture_content: bool = True,
179
+ instruments: Optional[Set[Any]] = None,
180
+ block_instruments: Optional[Set[Any]] = None,
181
+ resource_attributes: Optional[dict] = None,
182
+ flush_on_exit: bool = True,
183
+ quiet: bool = False,
184
+ ) -> Any:
185
+ """Initialize Orbitrage observability. Call once at program startup.
186
+
187
+ Args:
188
+ api_key: Your Orbitrage API key (orb_xxx_yyy). Falls back to
189
+ ORBITRAGE_API_KEY env var.
190
+ app_name: Project / workflow name (groups spans in the dashboard).
191
+ Defaults to your script name.
192
+ endpoint: Override the telemetry endpoint. Almost never needed.
193
+ enabled: Set False to no-op the SDK (tests, CI).
194
+ disable_batch: We force-warn on this — synchronous OTLP export adds
195
+ ~800ms per span. Don't use it outside Jupyter.
196
+ capture_content: When True (default), prompt + completion text is
197
+ exported on every span so they render in the Orbitrage
198
+ flow graph. Set False to redact content for compliance
199
+ (token counts + cost + routing are still captured).
200
+ flush_on_exit: Register an atexit hook that flushes spans before
201
+ process exit. Default True — turn off only if you manage
202
+ the flush yourself.
203
+ quiet: Suppress the one-line "telemetry on" log.
204
+
205
+ Returns:
206
+ The underlying Traceloop client (or None if disabled).
207
+ """
208
+ global _initialized, _client, _tracer_wrapper
209
+
210
+ if _initialized:
211
+ return _client
212
+
213
+ api_key = _resolve_api_key(api_key)
214
+ if not enabled:
215
+ if not quiet:
216
+ logger.info("orbitrage disabled")
217
+ _initialized = True
218
+ return None
219
+
220
+ if not api_key:
221
+ # Soft-fail rather than raising: same philosophy as Sentry. The
222
+ # user's app should not crash because telemetry was misconfigured.
223
+ if not quiet:
224
+ sys.stderr.write(
225
+ "orbitrage: no API key provided — telemetry off. "
226
+ "Set ORBITRAGE_API_KEY or call orbitrage.init('orb_...').\n"
227
+ )
228
+ _initialized = True
229
+ return None
230
+
231
+ # Cheap shape check — rejects typos that could exfiltrate prompts to a
232
+ # wrong endpoint. Matches the live key format (orb_<prefix>_<secret>).
233
+ if not _API_KEY_RE.match(api_key):
234
+ sys.stderr.write(
235
+ "orbitrage: API key format invalid (expected `orb_<prefix>_<secret>`) "
236
+ "— telemetry off.\n"
237
+ )
238
+ _initialized = True
239
+ return None
240
+
241
+ # Force batch mode — this is the single biggest perf win. If a user
242
+ # explicitly asks for disable_batch=True we'll honor it after warning.
243
+ if disable_batch:
244
+ sys.stderr.write(
245
+ "orbitrage: disable_batch=True forces synchronous OTLP export "
246
+ "(adds ~100-200ms per span). Strongly consider leaving it False.\n"
247
+ )
248
+
249
+ _set_bsp_env_defaults()
250
+ _silence_otel_exporter_noise()
251
+
252
+ # Privacy-first content default. Traceloop's own default is "true" —
253
+ # meaning every prompt + completion would be POSTed to the OTLP endpoint.
254
+ # We require an explicit opt-in via `capture_content=True`. The env var
255
+ # is the toggle Traceloop's instrumentors read at span time.
256
+ if capture_content:
257
+ os.environ["TRACELOOP_TRACE_CONTENT"] = "true"
258
+ else:
259
+ # setdefault so a deliberately-set "true" via env still works.
260
+ os.environ.setdefault("TRACELOOP_TRACE_CONTENT", "false")
261
+
262
+ # Resolve endpoint and app name.
263
+ endpoint = _resolve_endpoint(endpoint)
264
+ app_name = app_name or os.environ.get("ORBITRAGE_APP_NAME") or _default_app_name()
265
+
266
+ # Lazy import: importing traceloop ~150 ms on cold disk. We pay that once
267
+ # but only when init is actually called.
268
+ from traceloop.sdk import Traceloop
269
+
270
+ # Auto-detect instruments unless the caller pinned a set explicitly.
271
+ # Cuts init from ~2.6s (all 30+ instrumentors) to ~0.9s on a typical
272
+ # openai-only app, because Traceloop only imports what's installed.
273
+ if instruments is None:
274
+ instruments = _autodetect_instruments()
275
+
276
+ try:
277
+ client = Traceloop.init(
278
+ app_name=app_name,
279
+ api_endpoint=endpoint,
280
+ api_key=api_key,
281
+ disable_batch=disable_batch,
282
+ telemetry_enabled=False, # don't phone home to traceloop.com
283
+ traceloop_sync_enabled=False,
284
+ instruments=instruments,
285
+ block_instruments=block_instruments,
286
+ resource_attributes=resource_attributes or {},
287
+ )
288
+ except Exception as e:
289
+ sys.stderr.write(f"orbitrage: init failed ({e}) — telemetry off.\n")
290
+ _initialized = True
291
+ return None
292
+
293
+ _client = client
294
+ _initialized = True
295
+
296
+ if flush_on_exit:
297
+ atexit.register(_safe_shutdown)
298
+
299
+ if not quiet:
300
+ logger.info("orbitrage: telemetry → %s app=%s", endpoint, app_name)
301
+ return client
302
+
303
+
304
+ def _default_app_name() -> str:
305
+ """Best-effort app name — file basename, falling back to 'orbitrage'."""
306
+ try:
307
+ argv0 = sys.argv[0] if sys.argv and sys.argv[0] else ""
308
+ if argv0:
309
+ base = os.path.basename(argv0)
310
+ stem = os.path.splitext(base)[0]
311
+ if stem:
312
+ return stem
313
+ except Exception:
314
+ pass
315
+ return "orbitrage"
316
+
317
+
318
+ # ── lifecycle ─────────────────────────────────────────────────────────────
319
+ def flush(timeout_ms: int = 5000) -> bool:
320
+ """Force-flush pending spans. Returns True on success."""
321
+ if not _initialized:
322
+ return True
323
+ try:
324
+ from opentelemetry import trace
325
+ provider = trace.get_tracer_provider()
326
+ fn = getattr(provider, "force_flush", None)
327
+ if callable(fn):
328
+ return bool(fn(timeout_ms))
329
+ except Exception:
330
+ pass
331
+ return True
332
+
333
+
334
+ def shutdown() -> None:
335
+ """Shut down the SDK. Flushes pending spans then closes the exporter."""
336
+ if not _initialized:
337
+ return
338
+ _safe_shutdown()
339
+
340
+
341
+ def _safe_shutdown() -> None:
342
+ """atexit-friendly shutdown — never raises."""
343
+ try:
344
+ from opentelemetry import trace
345
+ provider = trace.get_tracer_provider()
346
+ fn = getattr(provider, "shutdown", None)
347
+ if callable(fn):
348
+ fn()
349
+ except Exception:
350
+ pass
351
+
352
+
353
+ # ── decorators (re-export from Traceloop) ─────────────────────────────────
354
+ def workflow(name: Optional[str] = None, **kwargs) -> Callable:
355
+ """Mark a function as a workflow (groups all nested LLM calls).
356
+
357
+ @orbitrage.workflow("checkout")
358
+ def run():
359
+ ...
360
+ """
361
+ from traceloop.sdk.decorators import workflow as _wf
362
+ return _wf(name=name, **kwargs)
363
+
364
+
365
+ def task(name: Optional[str] = None, **kwargs) -> Callable:
366
+ """Mark a function as a single task inside a workflow."""
367
+ from traceloop.sdk.decorators import task as _t
368
+ return _t(name=name, **kwargs)
369
+
370
+
371
+ def tool(name: Optional[str] = None, **kwargs) -> Callable:
372
+ """Mark a function as a tool call."""
373
+ from traceloop.sdk.decorators import tool as _t
374
+ return _t(name=name, **kwargs)
375
+
376
+
377
+ def agent(name: Optional[str] = None, **kwargs) -> Callable:
378
+ """Mark a function as an agent step."""
379
+ from traceloop.sdk.decorators import agent as _a
380
+ return _a(name=name, **kwargs)
381
+
382
+
383
+ def set_association_properties(properties: dict) -> None:
384
+ """Attach metadata (user_id, tenant_id, etc.) to every span in scope."""
385
+ if not _initialized:
386
+ return
387
+ try:
388
+ from traceloop.sdk import Traceloop
389
+ Traceloop.set_association_properties(properties)
390
+ except Exception:
391
+ pass
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbitrage
3
+ Version: 0.2.0
4
+ Summary: One-line observability + intelligent LLM routing for Python agents.
5
+ Author: Orbitrage
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://orbitrage.xyz
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Monitoring
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: traceloop-sdk<1.0,>=0.50.0
21
+ Requires-Dist: opentelemetry-sdk>=1.27.0
22
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
23
+ Provides-Extra: openai
24
+ Requires-Dist: openai>=1.0; extra == "openai"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.20; extra == "anthropic"
27
+ Provides-Extra: langchain
28
+ Requires-Dist: langchain-openai>=0.2; extra == "langchain"
29
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
30
+
31
+ # orbitrage
32
+
33
+ One-line observability + intelligent LLM routing for Python agents.
34
+
35
+ ```python
36
+ import orbitrage
37
+ orbitrage.init("orb_xxx_yyy") # one line. that's it.
38
+
39
+ # now any OpenAI / Anthropic / LangChain call you make is auto-traced
40
+ # AND can be auto-routed via:
41
+ from openai import OpenAI
42
+ client = OpenAI(base_url="https://orbitrage.xyz/api/v1", api_key="orb_xxx_yyy")
43
+ ```
44
+
45
+ ## What it does
46
+
47
+ - **Zero-latency observability**: non-blocking batch span export — your hot
48
+ path stays at LLM-call speed, not LLM-call + telemetry RTT.
49
+ - **Auto-instruments** OpenAI, Anthropic, LangChain, LlamaIndex, etc. via
50
+ OpenLLMetry under the hood.
51
+ - **One endpoint**: `https://orbitrage.xyz/api/telemetry` — auth via your
52
+ Orbitrage API key.
53
+ - **Decorators** for grouping multi-step workflows:
54
+
55
+ ```python
56
+ from orbitrage import workflow, task
57
+
58
+ @workflow("checkout_flow")
59
+ def checkout(user_id):
60
+ plan = planner.invoke(...)
61
+ out = executor.invoke(...)
62
+ return formatter.invoke(...)
63
+ ```
64
+
65
+ ## Install
66
+
67
+ ```
68
+ pip install orbitrage
69
+ ```
70
+
71
+ ## Redacting prompts (opt-out)
72
+
73
+ Prompt + completion text is captured by default so it renders in the
74
+ Orbitrage dashboard. To redact content (token counts, costs, and routing
75
+ decisions still flow):
76
+
77
+ ```python
78
+ orbitrage.init("orb_xxx", capture_content=False)
79
+ ```
80
+
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/orbitrage/__init__.py
4
+ src/orbitrage/_client.py
5
+ src/orbitrage.egg-info/PKG-INFO
6
+ src/orbitrage.egg-info/SOURCES.txt
7
+ src/orbitrage.egg-info/dependency_links.txt
8
+ src/orbitrage.egg-info/requires.txt
9
+ src/orbitrage.egg-info/top_level.txt
@@ -0,0 +1,13 @@
1
+ traceloop-sdk<1.0,>=0.50.0
2
+ opentelemetry-sdk>=1.27.0
3
+ opentelemetry-exporter-otlp-proto-http>=1.27.0
4
+
5
+ [anthropic]
6
+ anthropic>=0.20
7
+
8
+ [langchain]
9
+ langchain-openai>=0.2
10
+ langchain-core>=0.3
11
+
12
+ [openai]
13
+ openai>=1.0
@@ -0,0 +1 @@
1
+ orbitrage