Skip to content

Commit c98ca2f

Browse files
authored
Merge pull request #48 from wild-edge/release/0.2.0
Release/0.2.0
2 parents 24330e2 + 86e6706 commit c98ca2f

48 files changed

Lines changed: 2279 additions & 239 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,19 @@ jobs:
4747
OUTPUT: /tmp/release-notes.md
4848
run: python3 scripts/build_changelog_comment.py
4949

50+
- name: Build llms.txt artifacts
51+
if: github.ref_type == 'tag'
52+
env:
53+
OUTPUT_DIR: llms-dist
54+
run: python3 scripts/build_llms_txt.py
55+
5056
- name: Create GitHub release
5157
if: github.ref_type == 'tag'
5258
uses: softprops/action-gh-release@v2
5359
with:
54-
files: dist/*
60+
files: |
61+
dist/*
62+
llms-dist/*
5563
body_path: /tmp/release-notes.md
5664

5765
- name: Publish package to PyPI

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Changelog
2+
3+
Notable changes to wildedge-sdk. Every behavior change lands here; entries go
4+
under Unreleased and move into a version section at release time.
5+
6+
## Unreleased
7+
8+
## 0.2.0 - 2026-07-14
9+
10+
### Added
11+
12+
- `register_model()` accepts a `model_format` override for models without a matching extractor; `llm_api()` registers its models as format `"api"`, matching the openai/anthropic integrations instead of `"unknown"`.
13+
- `source_from_base_url()` recognizes more provider hosts (anthropic, mistral, groq, together, deepseek, xai, google, fireworks, cerebras, perplexity, nvidia, huggingface, baseten) instead of falling back to raw hostnames, including suffix matching for per-resource subdomains (Azure OpenAI, Baseten).
14+
- `examples/llm_api_example.py`: raw-HTTP LLM tracking with `wildedge.llm_api()`; existing examples updated to the module-level API (`wildedge.span` / `wildedge.flush` / `wildedge.register_model` instead of threading a client variable)
15+
- Releases ship `llms.txt` and `llms-full.txt` as GitHub release assets: the full documentation for that exact version in one file, generated by `scripts/build_llms_txt.py`. README quickstart rewritten around the module-level API.
16+
- `wildedge doctor --send-test-event`: sends one real span event through the full pipeline and reports the ingest response, proving DSN auth and connectivity end to end. The report gains an `environment` section (`WILDEDGE_*` variables, autoload PYTHONPATH status) plus `config_status` / `connectivity_status` fields.
17+
- `wildedge.llm_api()`: provider-agnostic tracking for LLM calls made with any HTTP client (OpenRouter, vLLM, Ollama, OpenAI/Anthropic-compatible endpoints). Times the block, normalizes usage payloads from either provider shape via `call.usage()` / `call.response()`, supports TTFT marks and async use, and records exceptions as error events. See `docs/llm_api.md`.
18+
- `register_model()` without a matching extractor defaults the model name to the explicit `model_id` instead of the placeholder object's type name.
19+
- Process-wide default client: `wildedge.get_client()`, `wildedge.set_default_client()`, and module-level `trace`, `span`, `track_span`, `register_model`, `flush` delegating to it (#41)
20+
- `init()` reuses the default client installed by `wildedge run` unless `dsn` is passed, so CLI and in-process init share one client (#41)
21+
- `SpanContextManager.set_attributes()` and `fail()` for recording outcomes on an open span (#41)
22+
- `wildedge run --strict` (env `WILDEDGE_STRICT`): exit with a reserved code instead of running untracked when bootstrap fails (120 config error, 122 internal error)
23+
- Lazy default-client creation honors `WILDEDGE_INTEGRATIONS` and `WILDEDGE_HUBS` from the environment (#41)
24+
- `docs/deployment.md`: the deployment contract (no-DSN behavior, strict mode, fork/exec servers, one client per process)
25+
26+
### Changed
27+
28+
- `wildedge doctor` exit codes are now differentiated: 0 pass, 1 configuration or dependency failure, 2 connectivity failure (`--network-check` or `--send-test-event`). A failing network check previously exited 1.
29+
- The test suite isolates all default SDK state paths under tmp; tests no longer write to the machine-global state directory.
30+
31+
- `--strict-integrations` now takes effect: a failed required integration exits the process with code 121. Previously the enforcing code path was never invoked, so the flag was silently ignored.
32+
- `--print-startup-report` and `--no-propagate` now work under `wildedge run`; both were only wired to the unused runner path before.
33+
- Constructing a client without a DSN logs once per process at INFO. Previously every construction logged a WARNING.
34+
35+
### Removed
36+
37+
- `wildedge.runtime.runner`: an alternative bootstrap entry point that `wildedge run` never invoked. `sitecustomize` is the single bootstrap path; the runner's exit codes moved to `wildedge.runtime.bootstrap` and now apply under `--strict`.
38+
39+
## 0.1.5 - 2026-06-23
40+
41+
- Opt-in inference attachments (raw input/output upload)
42+
- Fixed accelerator detection wiring; macOS CPU frequency and thermal sampling
43+
44+
## 0.1.4 and earlier
45+
46+
Predate this changelog; see the git history.

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
1. Fork the repository and create a feature branch off `devel`.
3939
2. Make your changes and ensure tests pass.
4040
3. Update documentation if needed.
41-
4. Submit a pull request targeting `devel` with a clear description of the changes.
41+
4. Add a `CHANGELOG.md` entry under Unreleased for any user-visible or behavioral change.
42+
5. Submit a pull request targeting `devel` with a clear description of the changes.
4243

4344
## Release process
4445

README.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ WILDEDGE_DSN="https://<secret>@ingest.wildedge.dev/<key>" \
3030
wildedge run --integrations timm -- python app.py
3131
```
3232

33-
Validate your environment before deploying:
33+
Validate your environment before deploying. `--send-test-event` proves the
34+
whole pipeline end to end by sending one span event and reporting the ingest
35+
response (exit codes: 0 pass, 1 config failure, 2 connectivity failure):
3436

3537
```bash
36-
wildedge doctor --integrations all --network-check
38+
wildedge doctor --integrations all --send-test-event
3739
```
3840

3941
Useful flags:
@@ -43,7 +45,8 @@ Useful flags:
4345
| `--integrations` | Comma-separated list of integrations to activate (or `all`) |
4446
| `--hubs` | Hub trackers to activate: `huggingface`, `torchhub` |
4547
| `--print-startup-report` | Print per-integration status at startup |
46-
| `--strict-integrations` | Fail if a requested integration can't be loaded |
48+
| `--strict-integrations` | Exit (code 121) if a requested integration can't be instrumented |
49+
| `--strict` | Exit (120 config, 122 internal) instead of running untracked when bootstrap fails |
4750
| `--attachments` | Enable opt-in raw input/output attachment upload |
4851
| `--no-propagate` | Don't pass WildEdge env vars to child processes |
4952

@@ -52,18 +55,23 @@ Useful flags:
5255
```python
5356
import wildedge
5457

55-
client = wildedge.init(
56-
dsn="...", # or WILDEDGE_DSN env var
57-
integrations=["transformers"],
58-
hubs=["huggingface"],
59-
)
58+
wildedge.init(integrations=["transformers"]) # optional under `wildedge run`
6059

61-
# models loaded after this point are tracked automatically
62-
```
60+
# models loaded after this point are tracked automatically; add traces,
61+
# spans and LLM API calls anywhere, no client instance to pass around:
62+
with wildedge.trace(run_id="run-1"):
63+
with wildedge.span(kind="agent_step", name="plan"):
64+
...
6365

64-
If no DSN is configured, the client becomes a no-op and logs a warning.
66+
with wildedge.llm_api(model="openai/gpt-4o-mini", provider="openrouter") as call:
67+
call.response(data) # LLM calls made with plain HTTP clients
68+
```
6569

66-
`init(...)` is a convenience wrapper for `WildEdge(...)` + `instrument(...)`.
70+
One client per process: `wildedge run`, `init()`, and the module-level calls
71+
all share it, and `init()` without `dsn` reuses whatever already exists.
72+
Without a DSN everything is a silent no-op, so dev and CI need no
73+
configuration. See [Deployment](https://github.com/wild-edge/wildedge-python/blob/main/docs/deployment.md)
74+
for the full contract.
6775
## Supported integrations
6876

6977
**On-device**
@@ -87,6 +95,10 @@ If no DSN is configured, the client becomes a no-op and logs a warning.
8795
| `anthropic` | [anthropic_example.py](https://github.com/wild-edge/wildedge-python/blob/main/examples/anthropic_example.py) |
8896
| `openai` | [openai_example.py](https://github.com/wild-edge/wildedge-python/blob/main/examples/openai_example.py) |
8997

98+
Calling an LLM API with a plain HTTP client instead of these libraries? Use
99+
[`wildedge.llm_api()`](https://github.com/wild-edge/wildedge-python/blob/main/docs/llm_api.md):
100+
[llm_api_example.py](https://github.com/wild-edge/wildedge-python/blob/main/examples/llm_api_example.py).
101+
90102
**Hub tracking**
91103

92104
Pass `hubs=` to track model download provenance. Hubs are framework-agnostic and can be combined with any integration.
@@ -115,6 +127,7 @@ For advanced options (batching, queue tuning, dead-letter storage, attachments),
115127

116128
| Name | Link |
117129
|---|---|
130+
| outfitstudio.app | https://outfitstudio.app/ |
118131
| agntr | [github.com/pmaciolek/agntr](https://github.com/pmaciolek/agntr) |
119132
| demo-app | [github.com/wild-edge/demo-app](https://github.com/wild-edge/demo-app) |
120133
| *(your project here)* | - |
@@ -131,6 +144,12 @@ Report security and privacy issues to: support@wildedge.dev
131144

132145
## Links
133146

147+
- [Deployment guide](https://github.com/wild-edge/wildedge-python/blob/main/docs/deployment.md)
148+
- [Manual tracking](https://github.com/wild-edge/wildedge-python/blob/main/docs/manual-tracking.md)
149+
- [LLM API tracking](https://github.com/wild-edge/wildedge-python/blob/main/docs/llm_api.md)
134150
- [Compatibility Matrix](https://github.com/wild-edge/wildedge-python/blob/main/docs/compatibility.md)
135-
- [Changelog](https://github.com/wild-edge/wildedge-python/releases)
151+
- [Changelog](https://github.com/wild-edge/wildedge-python/blob/main/CHANGELOG.md)
136152
- [License](https://github.com/wild-edge/wildedge-python/blob/main/LICENSE)
153+
154+
Each GitHub release ships `llms.txt` and `llms-full.txt`: the full
155+
documentation for that exact version in one file, built for AI assistants.

docs/configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Configuration
22

3-
Full reference for all `WildEdge` client parameters.
3+
Full reference for all `WildEdge` client parameters. For runtime behavior
4+
(no-DSN mode, strict mode and exit codes, forking servers), see
5+
[Deployment](deployment.md).
46

57
## Core
68

docs/deployment.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Deployment
2+
3+
How the SDK behaves in real process models: servers that fork, workers that
4+
exec, build steps, and processes with no DSN configured. Everything on this
5+
page is a contract the SDK keeps, not an implementation detail.
6+
7+
## How initialization happens
8+
9+
There are three ways a process gets its client, in order of coverage:
10+
11+
| Mode | Instrumentation coverage | Use when |
12+
|---|---|---|
13+
| `wildedge run -- <cmd>` | Guaranteed: patches before any user code runs | You control the start command |
14+
| `wildedge.init(...)` at startup | Everything created after the call | You control application code |
15+
| Lazy, from the first module-level call | Best effort: only what is created afterwards | Manual tracking only |
16+
17+
All three produce the same thing: one process-wide default client that
18+
`wildedge.span()`, `wildedge.register_model()` and the rest delegate to.
19+
`init()` without `dsn` reuses whatever client already exists, so code written
20+
for in-process init runs unchanged under `wildedge run`.
21+
22+
The lazy mode configures itself from the environment: `WILDEDGE_DSN`,
23+
`WILDEDGE_INTEGRATIONS`, `WILDEDGE_HUBS`. Unset integration variables mean
24+
nothing is patched; auto-instrumentation is always opt-in.
25+
26+
## The no-DSN contract
27+
28+
Without `WILDEDGE_DSN`, the client is a no-op: no background threads, no
29+
network, no patched frameworks, every event dropped. This is a supported mode
30+
for development and CI, not an error; the SDK logs one INFO line per process
31+
and stays quiet. Leave the SDK integrated and unset the variable wherever you
32+
do not want telemetry (local dev, test runs, build and migration steps).
33+
34+
Under `wildedge run`, a missing DSN warns on stderr and your program runs
35+
untracked. Telemetry failing must not take production down; that is the
36+
default policy.
37+
38+
## Strict mode
39+
40+
`wildedge run --strict` inverts the failure policy for deployments where
41+
running unobserved is worse than not running. Bootstrap failures then
42+
terminate the process before your program starts:
43+
44+
| Exit code | Meaning |
45+
|---|---|
46+
| 120 | Configuration error (missing or invalid DSN) |
47+
| 121 | A requested integration could not be instrumented (requires `--strict-integrations`) |
48+
| 122 | Internal bootstrap error |
49+
50+
`--strict-integrations` is its own opt-in and exits with 121 on failure even
51+
without `--strict`; asking for it is the request to fail.
52+
53+
## Forking and multi-process servers
54+
55+
`wildedge run` prepends `wildedge/autoload/` to `PYTHONPATH` and replaces
56+
itself with your command. Every Python interpreter that starts under it runs
57+
the bundled `sitecustomize.py`, which bootstraps the runtime before any user
58+
code. Two mechanisms make this safe across worker models:
59+
60+
- Fresh interpreters (exec or spawn) bootstrap themselves via sitecustomize;
61+
a `sys.modules` marker prevents double initialization within one
62+
interpreter.
63+
- Forked children inherit the parent's initialized runtime, and
64+
`os.register_at_fork` hooks stop the SDK's background threads before each
65+
fork and start fresh ones in parent and child afterward, so forked workers
66+
transmit normally instead of inheriting dead threads.
67+
68+
How that plays out on common servers:
69+
70+
| Server | Worker model | Behavior under `wildedge run` |
71+
|---|---|---|
72+
| gunicorn (sync/gthread, with or without `preload_app`) | fork from master | Master bootstraps once; each worker gets fresh SDK threads via the at-fork hooks |
73+
| uvicorn (`--workers`, `--reload`) | spawn / exec | Every worker is a fresh interpreter and bootstraps itself |
74+
| granian | spawned worker processes | Each worker bootstraps itself |
75+
| daphne | single process | One bootstrap, nothing special |
76+
| waitress | single process, thread pool | One bootstrap; the client is thread-safe |
77+
| celery (prefork pool) | fork from master | Same as gunicorn: at-fork hooks restart threads per worker |
78+
79+
For managed platforms, wrap only the serving command. Build steps and
80+
migrations run in separate processes that gain nothing from telemetry; leave
81+
them unwrapped or unset `WILDEDGE_DSN` there.
82+
83+
## One client per process
84+
85+
- Framework patches are installed at most once per process; the first client
86+
to instrument owns the patch.
87+
- `init()` reuses the existing default client unless you pass `dsn`
88+
explicitly, so the CLI-installed client and application code share one
89+
client instead of racing.
90+
- Trace and span correlation lives in contextvars at module level, not on a
91+
client. Auto-instrumented events emitted inside `wildedge.trace(...)` /
92+
`wildedge.span(...)` blocks correlate into the same trace no matter which
93+
client emits them.
94+
95+
## Verifying a deployment
96+
97+
`wildedge doctor` answers "will events actually flow from this machine"
98+
before you rely on it:
99+
100+
```bash
101+
wildedge doctor --integrations all --send-test-event --format json
102+
```
103+
104+
- `--send-test-event` sends one real span event through the full pipeline
105+
(DSN auth, ingest endpoint, batch protocol) and reports the server's
106+
response, which a TCP-level `--network-check` cannot do.
107+
- The report includes an `environment` section with every `WILDEDGE_*`
108+
variable the runtime reads, plus whether the autoload dir is on
109+
`PYTHONPATH`.
110+
- Exit codes: 0 all pass, 1 configuration or dependency failure, 2 the
111+
config is fine but the ingest endpoint is unreachable or rejecting.
112+
113+
`--format json` makes the output machine-readable; pasting it into an issue
114+
or an AI assistant is the intended debugging flow.
115+
116+
## Environment propagation
117+
118+
By default, `wildedge run` leaves its `WILDEDGE_*` variables in the
119+
environment so that exec'd children (reload workers) can bootstrap. Pass
120+
`--no-propagate` to have each bootstrapped process scrub the run-scoped
121+
variables after initialization, keeping them away from nested subprocesses
122+
you spawn yourself.

docs/llm_api.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Tracking LLM calls without a client library
2+
3+
`wildedge.llm_api()` records LLM API calls made with any HTTP client: httpx or
4+
requests against OpenRouter, vLLM, Ollama, or any OpenAI-compatible or
5+
Anthropic-compatible endpoint. Use it when auto-instrumentation does not
6+
apply because your app does not use the `openai` or `anthropic` packages. If
7+
it does use them, prefer the integrations; they capture the same events with
8+
zero code.
9+
10+
The name is deliberate: this tracks calls to an LLM behind an *API boundary*.
11+
An LLM running inside your own process (llama.cpp, transformers, MLX) is
12+
covered by the [framework integrations](manual-tracking.md), which also
13+
capture what no API boundary can expose: quantization from the model
14+
artifact, memory footprint, load/unload timing, and hardware linkage.
15+
16+
The block is timed automatically, events correlate with surrounding
17+
`wildedge.trace()` / `wildedge.span()` blocks, and everything is a silent
18+
no-op without a DSN.
19+
20+
## Quickstart
21+
22+
```python
23+
import httpx
24+
import wildedge
25+
26+
async def generate(prompt: str, model: str) -> dict:
27+
with wildedge.llm_api(model=model, provider="openrouter", prompt=prompt) as call:
28+
async with httpx.AsyncClient(timeout=300) as http:
29+
response = await http.post(
30+
"https://openrouter.ai/api/v1/chat/completions",
31+
headers={"Authorization": f"Bearer {API_KEY}"},
32+
json={"model": model, "messages": [{"role": "user", "content": prompt}]},
33+
)
34+
data = response.json()
35+
call.response(data)
36+
return data
37+
```
38+
39+
`call.response()` pulls token usage, stop reason, and API metadata from the
40+
full response payload, dict or SDK object, OpenAI shape
41+
(`usage.prompt_tokens`, `choices[0].finish_reason`) or Anthropic shape
42+
(`usage.input_tokens`, top-level `stop_reason`).
43+
44+
Runnable version: [examples/llm_api_example.py](../examples/llm_api_example.py),
45+
stdlib urllib against OpenRouter, no client library at all.
46+
47+
## Recording pieces individually
48+
49+
When you do not have a full response payload, set what you know:
50+
51+
```python
52+
with wildedge.llm_api(model="gemma-7b", base_url="http://localhost:11434") as call:
53+
result = post_to_ollama(...)
54+
call.usage(result["usage"]) # payload in either provider shape
55+
call.usage(tokens_in=52, tokens_out=209) # or explicit fields; these win
56+
call.stop_reason = "stop"
57+
call.first_token() # TTFT mark for streaming
58+
call.success = False # delivered but unusable output
59+
```
60+
61+
An exception escaping the block records an error event with the exception
62+
class as the error code, and no inference event.
63+
64+
## Model identity
65+
66+
Events register under `model` as the model id. `provider` names the source
67+
directly; alternatively `base_url` derives it (`openrouter.ai` becomes
68+
`openrouter`, unknown hosts record the hostname). Pass `prompt` or `messages`
69+
(chat format) to attach input metadata such as prompt length.
70+
71+
## Agentic pipelines
72+
73+
Combine with traces for multi-step visibility:
74+
75+
```python
76+
with wildedge.trace(run_id=run_id, agent_id="skin-generator"):
77+
for attempt in range(2):
78+
with wildedge.span(kind="agent_step", name="generate", step_index=attempt):
79+
with wildedge.llm_api(model=model, provider="openrouter", prompt=prompt) as call:
80+
data = await post_chat_completion(prompt)
81+
call.response(data)
82+
```

0 commit comments

Comments
 (0)