outlaya 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. outlaya-0.1.0/.github/workflows/ci.yml +37 -0
  2. outlaya-0.1.0/.github/workflows/publish.yml +37 -0
  3. outlaya-0.1.0/CHANGELOG.md +24 -0
  4. outlaya-0.1.0/LICENSE +21 -0
  5. outlaya-0.1.0/MANIFEST.in +7 -0
  6. outlaya-0.1.0/NOTICE +6 -0
  7. outlaya-0.1.0/PKG-INFO +381 -0
  8. outlaya-0.1.0/README.md +335 -0
  9. outlaya-0.1.0/examples/cursor_sync.py +28 -0
  10. outlaya-0.1.0/examples/manual_reporting.py +36 -0
  11. outlaya-0.1.0/examples/openai_async.py +35 -0
  12. outlaya-0.1.0/examples/openai_sync.py +30 -0
  13. outlaya-0.1.0/examples/openrouter_sync.py +41 -0
  14. outlaya-0.1.0/pyproject.toml +88 -0
  15. outlaya-0.1.0/setup.cfg +4 -0
  16. outlaya-0.1.0/src/outlaya/__init__.py +90 -0
  17. outlaya-0.1.0/src/outlaya/_batching.py +96 -0
  18. outlaya-0.1.0/src/outlaya/_client.py +728 -0
  19. outlaya-0.1.0/src/outlaya/_context.py +72 -0
  20. outlaya-0.1.0/src/outlaya/_events.py +402 -0
  21. outlaya-0.1.0/src/outlaya/_metadata.py +138 -0
  22. outlaya-0.1.0/src/outlaya/_providers/__init__.py +31 -0
  23. outlaya-0.1.0/src/outlaya/_providers/_base.py +248 -0
  24. outlaya-0.1.0/src/outlaya/_providers/anthropic.py +532 -0
  25. outlaya-0.1.0/src/outlaya/_providers/cursor.py +517 -0
  26. outlaya-0.1.0/src/outlaya/_providers/gemini.py +315 -0
  27. outlaya-0.1.0/src/outlaya/_providers/openai.py +515 -0
  28. outlaya-0.1.0/src/outlaya/_providers/openrouter.py +11 -0
  29. outlaya-0.1.0/src/outlaya/_types.py +182 -0
  30. outlaya-0.1.0/src/outlaya/_usage.py +86 -0
  31. outlaya-0.1.0/src/outlaya/_util.py +283 -0
  32. outlaya-0.1.0/src/outlaya/_version.py +11 -0
  33. outlaya-0.1.0/src/outlaya/_wrap.py +109 -0
  34. outlaya-0.1.0/src/outlaya/py.typed +0 -0
  35. outlaya-0.1.0/src/outlaya.egg-info/PKG-INFO +381 -0
  36. outlaya-0.1.0/src/outlaya.egg-info/SOURCES.txt +51 -0
  37. outlaya-0.1.0/src/outlaya.egg-info/dependency_links.txt +1 -0
  38. outlaya-0.1.0/src/outlaya.egg-info/requires.txt +23 -0
  39. outlaya-0.1.0/src/outlaya.egg-info/top_level.txt +1 -0
  40. outlaya-0.1.0/tests/__init__.py +1 -0
  41. outlaya-0.1.0/tests/conftest.py +25 -0
  42. outlaya-0.1.0/tests/helpers.py +68 -0
  43. outlaya-0.1.0/tests/test_anthropic.py +199 -0
  44. outlaya-0.1.0/tests/test_batching.py +34 -0
  45. outlaya-0.1.0/tests/test_client.py +189 -0
  46. outlaya-0.1.0/tests/test_context.py +47 -0
  47. outlaya-0.1.0/tests/test_cursor.py +156 -0
  48. outlaya-0.1.0/tests/test_gemini.py +144 -0
  49. outlaya-0.1.0/tests/test_integration_sdks.py +210 -0
  50. outlaya-0.1.0/tests/test_metadata.py +75 -0
  51. outlaya-0.1.0/tests/test_openai.py +252 -0
  52. outlaya-0.1.0/tests/test_usage.py +68 -0
  53. outlaya-0.1.0/tests/test_wrap.py +99 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, development]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - run: python -m pip install --upgrade pip
22
+ - run: python -m pip install -e ".[dev]"
23
+ - run: pytest -q
24
+ - run: ruff check src tests examples
25
+ - run: mypy src/outlaya
26
+
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+ - run: python -m pip install build
35
+ - run: python -m build
36
+ - run: python -m pip install dist/*.whl
37
+ - run: python -c "import outlaya; print(outlaya.__version__)"
@@ -0,0 +1,37 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ - run: python -m pip install build
20
+ - run: python -m build
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment:
30
+ name: pypi
31
+ url: https://pypi.org/p/outlaya
32
+ steps:
33
+ - uses: actions/download-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ ## [0.1.0] - 2026-11-09
6
+
7
+ ### Added
8
+
9
+ - `outlaya.init()`, `get_client()`, and `flush()` for the process-level client.
10
+ - `OutlayaClient` with bounded background batching, retries, HTTP 413 splitting,
11
+ drop accounting, idempotency keys, `stats()`, `on_error`, `throw_on_error`,
12
+ and interpreter-exit flushing.
13
+ - Automatic attribution adapters for OpenAI, Anthropic, Gemini, and OpenRouter,
14
+ including sync/async clients, streaming, tool/vision flags, cache token legs,
15
+ and provider errors.
16
+ - `wrap()`, provider-specific wrappers, and `ol_metadata` / `extra_body`
17
+ metadata extraction.
18
+ - Contextual attribution with `metadata()`, `with_metadata()`, and
19
+ `current_metadata()`.
20
+ - `track_usage()` for manual reporting.
21
+ - `sync_cursor_usage()` Cursor Admin API pull adapter with injectable transport
22
+ and stable idempotency keys.
23
+ - Public type aliases and a `py.typed` marker for type checkers.
24
+ - Documentation, examples, tests, and CI/publish workflows.
outlaya-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Outlaya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include NOTICE
3
+ include README.md
4
+ include CHANGELOG.md
5
+ recursive-include examples *.py
6
+ recursive-include tests *.py
7
+ recursive-include .github *.yml
outlaya-0.1.0/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ Outlaya Python SDK
2
+
3
+ This package is an independent implementation for Outlaya. Its architecture
4
+ and event contract are derived from the MIT-licensed Outlaya JavaScript SDK
5
+ (@outlaya/js). The original project is Copyright (c) its authors and is used
6
+ here as a design reference under the MIT License.
outlaya-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,381 @@
1
+ Metadata-Version: 2.4
2
+ Name: outlaya
3
+ Version: 0.1.0
4
+ Summary: Outlaya Python SDK for AI cost attribution across OpenAI, Anthropic, Gemini, OpenRouter, and Cursor
5
+ Author: Outlaya
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nzwi263/outlaya-python-sdk
8
+ Project-URL: Repository, https://github.com/nzwi263/outlaya-python-sdk
9
+ Project-URL: Changelog, https://github.com/nzwi263/outlaya-python-sdk/blob/development/CHANGELOG.md
10
+ Keywords: ai,cost-management,attribution,openai,anthropic,gemini,openrouter,cursor,observability
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: NOTICE
27
+ Provides-Extra: openai
28
+ Requires-Dist: openai; extra == "openai"
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic; extra == "anthropic"
31
+ Provides-Extra: google
32
+ Requires-Dist: google-genai; extra == "google"
33
+ Provides-Extra: all
34
+ Requires-Dist: openai; extra == "all"
35
+ Requires-Dist: anthropic; extra == "all"
36
+ Requires-Dist: google-genai; extra == "all"
37
+ Provides-Extra: dev
38
+ Requires-Dist: anthropic; extra == "dev"
39
+ Requires-Dist: google-genai; extra == "dev"
40
+ Requires-Dist: mypy>=1.10; extra == "dev"
41
+ Requires-Dist: openai; extra == "dev"
42
+ Requires-Dist: pytest>=7.0; extra == "dev"
43
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
44
+ Requires-Dist: ruff>=0.5; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # outlaya
48
+
49
+ Outlaya's Python SDK for server-side AI cost attribution. Wrap a supported
50
+ provider client, read usage from its response, and send one normalized event to
51
+ Outlaya without putting the ingest call on your request path.
52
+
53
+ Supported providers:
54
+
55
+ | Provider | Integration |
56
+ | --- | --- |
57
+ | OpenAI | `wrap_openai` |
58
+ | Anthropic | `wrap_anthropic` |
59
+ | Gemini | `wrap_google_genai` (alias `wrap_gemini`) |
60
+ | OpenRouter | `wrap_openrouter` (or `wrap_openai` on an OpenRouter client) |
61
+ | Cursor | `sync_cursor_usage` (Admin API pull; no client to wrap) |
62
+
63
+ The SDK requires Python 3.9+ and has no runtime dependencies. Provider packages
64
+ are optional extras.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install outlaya
70
+ ```
71
+
72
+ Install the matching provider extra to pin a tested provider SDK:
73
+
74
+ ```bash
75
+ pip install "outlaya[openai]"
76
+ pip install "outlaya[anthropic]"
77
+ pip install "outlaya[google]"
78
+ pip install "outlaya[all]"
79
+ ```
80
+
81
+ ## Quickstart
82
+
83
+ ```python
84
+ import os
85
+
86
+ import outlaya
87
+ from openai import OpenAI
88
+
89
+ outlaya.init(api_key=os.environ["OL_API_KEY"])
90
+
91
+ client = outlaya.wrap_openai(
92
+ OpenAI(),
93
+ metadata={"environment": "production"},
94
+ )
95
+
96
+ response = client.chat.completions.create(
97
+ model="gpt-4o-mini",
98
+ messages=[{"role": "user", "content": "Hello"}],
99
+ ol_metadata={"customer_id": customer.id, "feature": "chat"},
100
+ )
101
+ ```
102
+
103
+ The model call returns the provider's response unchanged. The usage event is
104
+ queued and sent in the background. `ol_metadata` is stripped before the request
105
+ reaches the provider.
106
+
107
+ Environment variables:
108
+
109
+ | Variable | Required | Default | Purpose |
110
+ | --- | --- | --- | --- |
111
+ | `OL_API_KEY` | yes | — | Workspace API key with the `ingest` scope |
112
+ | `OL_BASE_URL` | no | `https://app.outlaya.com` | Ingest base URL |
113
+
114
+ You can also pass `api_key` and `base_url` to `outlaya.init()` or
115
+ `outlaya.OutlayaClient()`.
116
+
117
+ ## Attribution metadata
118
+
119
+ Precedence, lowest to highest:
120
+
121
+ 1. Wrapper defaults: `wrap_openai(client, metadata={...})`.
122
+ 2. Execution scope: `with outlaya.metadata({...}):`.
123
+ 3. Per-call: `ol_metadata=` / `extra_body={"ol_metadata": {...}}`.
124
+
125
+ ```python
126
+ import outlaya
127
+
128
+ with outlaya.metadata({"team": "support", "session_id": run_id}):
129
+ agent.run(input)
130
+ ```
131
+
132
+ `with_metadata(values, callback)` is available when a callback reads more
133
+ naturally. Coroutine callbacks are awaited inside the scope.
134
+
135
+ First-class keys: `customer_id`, `feature`, `team`, `environment`,
136
+ `prompt_key`, `prompt_version`, `session_id`, `turn_id`, `region`,
137
+ `deployment_type`, `service_tier`. Any other scalar becomes a custom tag.
138
+ `custom_tags` merge across layers; scalar fields override.
139
+
140
+ ## Providers
141
+
142
+ ### OpenAI
143
+
144
+ ```python
145
+ from openai import OpenAI
146
+
147
+ import outlaya
148
+
149
+ client = outlaya.wrap_openai(OpenAI())
150
+ ```
151
+
152
+ Async clients work too:
153
+
154
+ ```python
155
+ from openai import AsyncOpenAI
156
+
157
+ client = outlaya.wrap(AsyncOpenAI())
158
+ response = await client.chat.completions.create(
159
+ model="gpt-4o-mini",
160
+ messages=[{"role": "user", "content": "Hello"}],
161
+ )
162
+ ```
163
+
164
+ Sync and async streaming are supported. For OpenAI chat completions the SDK
165
+ injects `stream_options={"include_usage": True}` so the final chunk carries
166
+ usage. Events are submitted when the stream finishes.
167
+
168
+ ```python
169
+ for chunk in client.chat.completions.create(
170
+ model="gpt-4o-mini",
171
+ messages=[{"role": "user", "content": "Hello"}],
172
+ stream=True,
173
+ ):
174
+ ...
175
+
176
+ # Or as a context manager:
177
+ with client.chat.completions.create(..., stream=True) as stream:
178
+ for chunk in stream:
179
+ ...
180
+ ```
181
+
182
+ ### Anthropic
183
+
184
+ ```python
185
+ import anthropic
186
+
187
+ import outlaya
188
+
189
+ client = outlaya.wrap_anthropic(anthropic.Anthropic())
190
+ client.messages.create(
191
+ model="claude-sonnet-4",
192
+ max_tokens=256,
193
+ messages=[{"role": "user", "content": "Hello"}],
194
+ )
195
+
196
+ with client.messages.stream(
197
+ model="claude-sonnet-4",
198
+ max_tokens=256,
199
+ messages=[{"role": "user", "content": "Hello"}],
200
+ ) as stream:
201
+ message = stream.get_final_message()
202
+ ```
203
+
204
+ The adapter supports `messages.create` non-streaming, raw `stream=True`, and the
205
+ `messages.stream()` manager on sync and async clients. Cache reads are folded
206
+ into `input_tokens` and sent separately as `cached_tokens`; cache-write TTL legs
207
+ are preserved.
208
+
209
+ ### Gemini
210
+
211
+ ```python
212
+ from google import genai
213
+
214
+ import outlaya
215
+
216
+ client = outlaya.wrap_google_genai(genai.Client(api_key="..."))
217
+ client.models.generate_content(
218
+ model="gemini-2.0-flash",
219
+ contents="Hello",
220
+ )
221
+
222
+ for chunk in client.models.generate_content_stream(
223
+ model="gemini-2.0-flash",
224
+ contents="Hello",
225
+ ):
226
+ ...
227
+ ```
228
+
229
+ The `aio` client is supported. Gemini `thoughts_token_count` is sent as
230
+ `custom_tags.reasoning_tokens`.
231
+
232
+ ### OpenRouter
233
+
234
+ OpenRouter is OpenAI-compatible.
235
+
236
+ ```python
237
+ from openai import OpenAI
238
+
239
+ import outlaya
240
+
241
+ router = outlaya.wrap_openrouter(
242
+ OpenAI(
243
+ api_key=os.environ["OPENROUTER_API_KEY"],
244
+ base_url="https://openrouter.ai/api/v1",
245
+ )
246
+ )
247
+ ```
248
+
249
+ The adapter adds `extra_body={"usage": {"include": True}}` and forwards the
250
+ provider-reported cost as `reported_cost_usd_micros`, plus upstream provider,
251
+ upstream inference cost, and BYOK flags for reconciliation.
252
+
253
+ ### Generic dispatch
254
+
255
+ `wrap(client)` detects the client shape and dispatches to the right adapter. Use
256
+ the explicit wrapper when the provider is already known.
257
+
258
+ ```python
259
+ client = outlaya.wrap(OpenAI())
260
+ ```
261
+
262
+ ## Cursor
263
+
264
+ Cursor is an IDE/agent product, so it is a pull integration rather than a
265
+ wrapped client. Run the sync from a server-side job:
266
+
267
+ ```python
268
+ import os
269
+
270
+ import outlaya
271
+
272
+ outlaya.init(api_key=os.environ["OL_API_KEY"])
273
+
274
+ result = outlaya.sync_cursor_usage(
275
+ api_key=os.environ["CURSOR_ADMIN_API_KEY"],
276
+ since=last_synced_at,
277
+ until=now,
278
+ resolve_customer_id=lambda record: member_to_customer.get(record.get("email")),
279
+ flush=True,
280
+ )
281
+
282
+ print(result.fetched, result.submitted, result.skipped)
283
+ ```
284
+
285
+ Cursor records are sent as `provider: "cursor"`, `source: "pull"`,
286
+ `operation: "coding_agent"`, with a stable `cursor:<id>` idempotency key. The
287
+ default transport targets `POST {base_url}/teams/usage` on the Cursor Admin API;
288
+ pass `transport=` to override it if your Cursor contract differs.
289
+
290
+ ## Delivery
291
+
292
+ - `submit()` only enqueues. It never waits for ingest and never raises.
293
+ - The queue is bounded by JSON bytes (16 MiB by default) and optionally by
294
+ event count. When full, the newest event is dropped and counted.
295
+ - Requests carry up to 50 events by default, with a 4 MiB body cap, a
296
+ 500-event hard cap, a 15 second timeout, and two retries with exponential
297
+ backoff.
298
+ - HTTP 413 halves the batch until it fits.
299
+ - Failed retryable batches return to the front of the queue and retry in the
300
+ background.
301
+ - Each event gets an `idempotency_key` if missing; resends reuse it.
302
+ - Call `outlaya.flush()` before a script, Lambda, or job returns when you need
303
+ to confirm delivery. On CPython, pending sends also flush at interpreter
304
+ exit.
305
+
306
+ ```python
307
+ import outlaya
308
+
309
+ outlaya.flush()
310
+ ```
311
+
312
+ Inspect queue and drop counters:
313
+
314
+ ```python
315
+ client = outlaya.get_client()
316
+ print(client.stats())
317
+ ```
318
+
319
+ Set `throw_on_error=True` to raise collected delivery failures from `flush()`,
320
+ or pass `on_error=` to observe them without raising. On platforms that restrict
321
+ background threads, construct the client with `auto_start=False` and call
322
+ `flush()` synchronously before returning.
323
+
324
+ ## Manual reporting
325
+
326
+ For harnesses that cannot be wrapped:
327
+
328
+ ```python
329
+ import outlaya
330
+
331
+ outlaya.track_usage(
332
+ provider="openai",
333
+ model="gpt-4o-mini",
334
+ usage={"input_tokens": 120, "output_tokens": 40, "cached_tokens": 20},
335
+ latency_ms=830,
336
+ metadata={"customer_id": customer.id, "feature": "summarize"},
337
+ )
338
+ ```
339
+
340
+ Usage accepts both Outlaya names (`input_tokens`, `cached_input_tokens`) and
341
+ provider/JavaScript aliases (`prompt_tokens`, `completion_tokens`,
342
+ `cachedInputTokens`). Do not report calls that a wrapped client already tracks.
343
+
344
+ ## Async and threads
345
+
346
+ Provider adapters preserve the client's sync or async nature. Metadata is stored
347
+ in `contextvars`, so it propagates to asyncio tasks and to work submitted with
348
+ `asyncio.to_thread`. Plain `threading.Thread` instances start with an empty
349
+ context. Background delivery uses daemon threads;
350
+ call `outlaya.flush()` or `client.shutdown()` before a short-lived process
351
+ exits. In async code, run the blocking flush off the event loop:
352
+
353
+ ```python
354
+ import asyncio
355
+
356
+ import outlaya
357
+
358
+ await asyncio.to_thread(outlaya.flush)
359
+ ```
360
+
361
+ ## Development
362
+
363
+ ```bash
364
+ python -m venv .venv
365
+ . .venv/bin/activate
366
+ pip install -e ".[dev]"
367
+ pytest
368
+ ruff check src tests
369
+ mypy src/outlaya
370
+ ```
371
+
372
+ Build distribution artifacts with:
373
+
374
+ ```bash
375
+ python -m build
376
+ ```
377
+
378
+ ## Licensing
379
+
380
+ MIT. See `LICENSE` and `NOTICE`. The architecture and event contract are derived
381
+ from the MIT-licensed Outlaya JavaScript SDK.