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.
- outlaya-0.1.0/.github/workflows/ci.yml +37 -0
- outlaya-0.1.0/.github/workflows/publish.yml +37 -0
- outlaya-0.1.0/CHANGELOG.md +24 -0
- outlaya-0.1.0/LICENSE +21 -0
- outlaya-0.1.0/MANIFEST.in +7 -0
- outlaya-0.1.0/NOTICE +6 -0
- outlaya-0.1.0/PKG-INFO +381 -0
- outlaya-0.1.0/README.md +335 -0
- outlaya-0.1.0/examples/cursor_sync.py +28 -0
- outlaya-0.1.0/examples/manual_reporting.py +36 -0
- outlaya-0.1.0/examples/openai_async.py +35 -0
- outlaya-0.1.0/examples/openai_sync.py +30 -0
- outlaya-0.1.0/examples/openrouter_sync.py +41 -0
- outlaya-0.1.0/pyproject.toml +88 -0
- outlaya-0.1.0/setup.cfg +4 -0
- outlaya-0.1.0/src/outlaya/__init__.py +90 -0
- outlaya-0.1.0/src/outlaya/_batching.py +96 -0
- outlaya-0.1.0/src/outlaya/_client.py +728 -0
- outlaya-0.1.0/src/outlaya/_context.py +72 -0
- outlaya-0.1.0/src/outlaya/_events.py +402 -0
- outlaya-0.1.0/src/outlaya/_metadata.py +138 -0
- outlaya-0.1.0/src/outlaya/_providers/__init__.py +31 -0
- outlaya-0.1.0/src/outlaya/_providers/_base.py +248 -0
- outlaya-0.1.0/src/outlaya/_providers/anthropic.py +532 -0
- outlaya-0.1.0/src/outlaya/_providers/cursor.py +517 -0
- outlaya-0.1.0/src/outlaya/_providers/gemini.py +315 -0
- outlaya-0.1.0/src/outlaya/_providers/openai.py +515 -0
- outlaya-0.1.0/src/outlaya/_providers/openrouter.py +11 -0
- outlaya-0.1.0/src/outlaya/_types.py +182 -0
- outlaya-0.1.0/src/outlaya/_usage.py +86 -0
- outlaya-0.1.0/src/outlaya/_util.py +283 -0
- outlaya-0.1.0/src/outlaya/_version.py +11 -0
- outlaya-0.1.0/src/outlaya/_wrap.py +109 -0
- outlaya-0.1.0/src/outlaya/py.typed +0 -0
- outlaya-0.1.0/src/outlaya.egg-info/PKG-INFO +381 -0
- outlaya-0.1.0/src/outlaya.egg-info/SOURCES.txt +51 -0
- outlaya-0.1.0/src/outlaya.egg-info/dependency_links.txt +1 -0
- outlaya-0.1.0/src/outlaya.egg-info/requires.txt +23 -0
- outlaya-0.1.0/src/outlaya.egg-info/top_level.txt +1 -0
- outlaya-0.1.0/tests/__init__.py +1 -0
- outlaya-0.1.0/tests/conftest.py +25 -0
- outlaya-0.1.0/tests/helpers.py +68 -0
- outlaya-0.1.0/tests/test_anthropic.py +199 -0
- outlaya-0.1.0/tests/test_batching.py +34 -0
- outlaya-0.1.0/tests/test_client.py +189 -0
- outlaya-0.1.0/tests/test_context.py +47 -0
- outlaya-0.1.0/tests/test_cursor.py +156 -0
- outlaya-0.1.0/tests/test_gemini.py +144 -0
- outlaya-0.1.0/tests/test_integration_sdks.py +210 -0
- outlaya-0.1.0/tests/test_metadata.py +75 -0
- outlaya-0.1.0/tests/test_openai.py +252 -0
- outlaya-0.1.0/tests/test_usage.py +68 -0
- 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.
|
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.
|