orbitrage 0.2.0__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.
- orbitrage/__init__.py +31 -0
- orbitrage/_client.py +391 -0
- orbitrage-0.2.0.dist-info/METADATA +80 -0
- orbitrage-0.2.0.dist-info/RECORD +6 -0
- orbitrage-0.2.0.dist-info/WHEEL +5 -0
- orbitrage-0.2.0.dist-info/top_level.txt +1 -0
orbitrage/__init__.py
ADDED
|
@@ -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"
|
orbitrage/_client.py
ADDED
|
@@ -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,6 @@
|
|
|
1
|
+
orbitrage/__init__.py,sha256=cej4X_qvPgjlipUQKEVphLgWIQGL1JLC1A6dhIEK2wA,487
|
|
2
|
+
orbitrage/_client.py,sha256=mf84uXSdYuFNcSLEGIc4T16I2O1v7eHoD9SJS23Y4pk,15581
|
|
3
|
+
orbitrage-0.2.0.dist-info/METADATA,sha256=5rU_wTgvMIE3keFgtkiWDgiG-0Zx4wSaIcL-T5-8_NU,2608
|
|
4
|
+
orbitrage-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
orbitrage-0.2.0.dist-info/top_level.txt,sha256=x-k_0Wk9P1meLDIOfc10DSjgyFwAkWLwe8G0xjknSOc,10
|
|
6
|
+
orbitrage-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orbitrage
|