Skip to content

Commit c2cd2ce

Browse files
committed
llms.txt + llm_api example + modernize examples to the module-level API
1 parent dca4143 commit c2cd2ce

18 files changed

Lines changed: 293 additions & 35 deletions

.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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ under Unreleased and move into a version section at release time.
77

88
### Added
99

10+
- `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)
11+
- 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.
1012
- `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.
1113
- `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`.
1214
- `register_model()` without a matching extractor defaults the model name to the explicit `model_id` instead of the placeholder object's type name.

README.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,23 @@ Useful flags:
5555
```python
5656
import wildedge
5757

58-
client = wildedge.init(
59-
dsn="...", # or WILDEDGE_DSN env var
60-
integrations=["transformers"],
61-
hubs=["huggingface"],
62-
)
58+
wildedge.init(integrations=["transformers"]) # optional under `wildedge run`
6359

64-
# models loaded after this point are tracked automatically
65-
```
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+
...
6665

67-
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+
```
6869

69-
`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.
7075
## Supported integrations
7176

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

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+
93102
**Hub tracking**
94103

95104
Pass `hubs=` to track model download provenance. Hubs are framework-agnostic and can be combined with any integration.
@@ -135,6 +144,12 @@ Report security and privacy issues to: support@wildedge.dev
135144

136145
## Links
137146

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)
138150
- [Compatibility Matrix](https://github.com/wild-edge/wildedge-python/blob/main/docs/compatibility.md)
139-
- [Changelog](https://github.com/wild-edge/wildedge-python/releases)
151+
- [Changelog](https://github.com/wild-edge/wildedge-python/blob/main/CHANGELOG.md)
140152
- [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/llm_api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ full response payload, dict or SDK object, OpenAI shape
4141
(`usage.prompt_tokens`, `choices[0].finish_reason`) or Anthropic shape
4242
(`usage.input_tokens`, top-level `stop_reason`).
4343

44+
Runnable version: [examples/llm_api_example.py](../examples/llm_api_example.py),
45+
stdlib urllib against OpenRouter, no client library at all.
46+
4447
## Recording pieces individually
4548

4649
When you do not have a full response payload, set what you know:

examples/agentic_example.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
import wildedge
2828

29-
we = wildedge.init(
29+
wildedge.init(
3030
app_version="1.0.0",
3131
integrations="openai",
3232
)
@@ -96,7 +96,7 @@ def calculator(expression: str) -> str:
9696

9797

9898
def call_tool(name: str, arguments: dict) -> str:
99-
with we.span(
99+
with wildedge.span(
100100
kind="tool",
101101
name=name,
102102
input_summary=json.dumps(arguments)[:200],
@@ -108,7 +108,7 @@ def call_tool(name: str, arguments: dict) -> str:
108108

109109
def retrieve_context(query: str) -> str:
110110
"""Fetch relevant context from the vector store (~120ms)."""
111-
with we.span(
111+
with wildedge.span(
112112
kind="retrieval",
113113
name="vector_search",
114114
input_summary=query[:200],
@@ -125,7 +125,7 @@ def run_agent(task: str, step_index: int, messages: list) -> str:
125125
messages.append({"role": "user", "content": f"{task}\n\nContext: {context}"})
126126

127127
while True:
128-
with we.span(
128+
with wildedge.span(
129129
kind="agent_step",
130130
name="reason",
131131
step_index=step_index,
@@ -172,10 +172,10 @@ def run_agent(task: str, step_index: int, messages: list) -> str:
172172
system_prompt = "You are a helpful assistant. Use tools when needed."
173173
messages = [{"role": "system", "content": system_prompt}]
174174

175-
with we.trace(agent_id="demo-agent", run_id=str(uuid.uuid4())):
175+
with wildedge.trace(agent_id="demo-agent", run_id=str(uuid.uuid4())):
176176
for i, task in enumerate(TASKS, start=1):
177177
print(f"\nTask {i}: {task}")
178178
reply = run_agent(task, step_index=i, messages=messages)
179179
print(f"Reply: {reply}")
180180

181-
we.flush()
181+
wildedge.flush()

examples/anthropic_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import wildedge
2020

21-
client = wildedge.init(
21+
wildedge.init(
2222
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
2323
integrations="anthropic",
2424
)
@@ -44,5 +44,5 @@
4444
print(event.delta.text, end="", flush=True)
4545
print("\n")
4646

47-
client.flush()
47+
wildedge.flush()
4848
print("Done. Events flushed to WildEdge.")

examples/attachments_example.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def redact(attachments: list[Attachment]) -> list[Attachment]:
2626
return [a for a in attachments if a.content_type != "application/secret"]
2727

2828

29-
client = wildedge.init(
29+
wildedge.init(
3030
app_version="1.0.0",
3131
attachments_enabled=True,
3232
max_attachments_per_inference=5,
@@ -35,7 +35,7 @@ def redact(attachments: list[Attachment]) -> list[Attachment]:
3535
attachment_filter=redact,
3636
)
3737

38-
handle = client.register_model(
38+
handle = wildedge.register_model(
3939
object(),
4040
model_id="doc-classifier-v1",
4141
source="local",
@@ -59,4 +59,4 @@ def redact(attachments: list[Attachment]) -> list[Attachment]:
5959
print(f"tracked inference {inference_id[:8]}… with 2 attachments")
6060

6161
# Bytes upload in the background; flush/close lets buffered events drain.
62-
client.close()
62+
wildedge.flush()

examples/feedback_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121

2222
CONFIDENCE_THRESHOLD = 0.6
2323

24-
client = wildedge.init(
24+
wildedge.init(
2525
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
2626
integrations="timm",
2727
)
2828

2929
model = timm.create_model("resnet18", pretrained=True)
3030
model.eval()
31-
handle = client.register_model(
31+
handle = wildedge.register_model(
3232
model
3333
) # auto-instrumented already; returns existing handle
3434

examples/gguf_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import wildedge
1414

15-
client = wildedge.init(
15+
wildedge.init(
1616
app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op
1717
integrations="gguf",
1818
hubs=["huggingface"],

examples/llm_api_example.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = ["wildedge-sdk"]
4+
#
5+
# [tool.uv.sources]
6+
# wildedge-sdk = { path = "..", editable = true }
7+
# ///
8+
"""Track LLM calls made over plain HTTP with wildedge.llm_api().
9+
10+
No openai or anthropic client library involved: requests go through stdlib
11+
urllib against OpenRouter's OpenAI-compatible endpoint, and llm_api() records
12+
the same inference events the integrations emit (tokens, TTFT, stop reason),
13+
correlated into the surrounding trace and spans.
14+
15+
Run with: uv run llm_api_example.py
16+
Requires: OPENROUTER_API_KEY environment variable. Set WILDEDGE_DSN to send events.
17+
"""
18+
19+
import json
20+
import os
21+
import urllib.request
22+
import uuid
23+
24+
import wildedge
25+
26+
wildedge.init(app_version="1.0.0") # uses WILDEDGE_DSN if set; otherwise no-op
27+
28+
URL = "https://openrouter.ai/api/v1/chat/completions"
29+
MODEL = "openai/gpt-4o-mini"
30+
31+
32+
def chat(prompt: str) -> dict:
33+
request = urllib.request.Request(
34+
URL,
35+
data=json.dumps(
36+
{"model": MODEL, "messages": [{"role": "user", "content": prompt}]}
37+
).encode(),
38+
headers={
39+
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
40+
"Content-Type": "application/json",
41+
},
42+
)
43+
with urllib.request.urlopen(request, timeout=120) as response:
44+
return json.load(response)
45+
46+
47+
PROMPTS = [
48+
"What is on-device AI in one sentence?",
49+
"Name three edge inference runtimes.",
50+
]
51+
52+
with wildedge.trace(agent_id="llm-api-example", run_id=str(uuid.uuid4())):
53+
for step, prompt in enumerate(PROMPTS):
54+
with wildedge.span(kind="agent_step", name="ask", step_index=step):
55+
with wildedge.llm_api(
56+
model=MODEL, provider="openrouter", prompt=prompt
57+
) as call:
58+
data = chat(prompt)
59+
call.response(data)
60+
print(f"Q: {prompt}\nA: {data['choices'][0]['message']['content']}\n")
61+
62+
wildedge.flush()
63+
print("Done. Events flushed to WildEdge.")

0 commit comments

Comments
 (0)