Skip to content

Capture AI usage from HTTP responses - #780

Open
iacobdaniel wants to merge 13 commits into
mainfrom
feat/ai-network-capture
Open

iacobdaniel wants to merge 13 commits into
mainfrom
feat/ai-network-capture

Conversation

@iacobdaniel

Copy link
Copy Markdown
Contributor

Count AI API calls from the responses httpx, httpx2, requests and urllib3 receive, when AIKIDO_FEATURE_AI_NETWORK_CAPTURE is on (off by default). A POST whose path ends like an AI API counts on any host and is reported with its endpoint URL as provider, with its tokens, and with cached tokens as cacheRead/cacheWrite when the provider reports them. The SDK sinks are unchanged, so with the flag on an SDK call shows up under both its URL and the SDK name. Without the flag nothing is patched; with it, an HTTP call costs about 1 µs more (httpx 23.1 → 24.3 µs, requests no measurable change).

Comment thread aikido_zen/sinks/httpx.py
def patch_client(m):
"""Patches the sync and async clients of httpx, or of httpx2 (same API)"""
patch_function(m, "Client.send", _send)
patch_function(m, "AsyncClient.send", _async_send)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth adding a test to ensure HTTP client instrumentation doesn't break anything? just like some basic HTTP client usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests that plain GET/POST calls through requests, httpx, httpx2 (sync and async) and urllib3 return the same status, headers and body with the hooks installed, and that connection errors still raise the client's own exception.

@on_import("requests")
def patch(m):
if is_ai_network_capture_enabled():
patch_function(m, "Session.send", _send)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth adding a test to ensure HTTP client instrumentation doesn't break anything? just like some basic HTTP client usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered by the same new tests.

@on_import("urllib3.connectionpool")
def patch(m):
if is_ai_network_capture_enabled():
patch_function(m, "HTTPConnectionPool.urlopen", _urlopen)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth adding a test to ensure HTTP client instrumentation doesn't break anything? just like some basic HTTP client usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered by the same tests.

Comment thread aikido_zen/storage/ai_statistics.py Outdated
if cache_read:
tokens["cacheRead"] = tokens.get("cacheRead", 0) + cache_read
if cache_write:
tokens["cacheWrite"] = tokens.get("cacheWrite", 0) + cache_write

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's safe to report these as 0? * is still 0?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so do we need add_cache_tokens?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's safe. After the fix every entry reports cacheRead and cacheWrite, starting at 0, so the code no longer needs a special case to leave them out.

DEFAULT_PORTS = {"http": 80, "https": 443}

# A redirect makes requests and urllib3 pass the final response through twice.
SEEN_ATTR = "_aikido_ai_seen"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're sure that we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, both are needed.
DEFAULT_PORTS exists because the HTTP clients describe the same address differently. urllib3 always gives the port (443), httpx never does. We leave out the scheme's standard port (80 for http, 443 for https), as the URL standard.
And without SEEN_ATTR, a redirected call through requests or an urllib3 pool is counted twice, because the same final response comes back. Added a test for it also.
There are alternative solutions, we can write them inline in the code if we do not want the constants, but would make the code a bit more complicated and I think a constant with a name keeps each one clear.

"import aikido_zen.sinks.urllib3\n"
"targets = [httpx.Client.send, httpx.AsyncClient.send, requests.Session.send,\n"
" urllib3.connectionpool.HTTPConnectionPool.urlopen]\n"
"print([hasattr(t, '__wrapped__') for t in targets])\n"

@hansott hansott Nov 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we need to test whether these methods are wrapped

if the flag is enabled, call the HTTP client like you normally would and see if nothing breaks, that's fine! (and then indeed call the get_ai_stats method to see what we've captured)

Also best to move the client specific bits into separate files per sink?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The tests call each client normally with the flag on and check get_ai_stats, and each client has its own file. I kept the wrapped check for the flag-off case, since it's the only test that proves the clients stay untouched without the flag.

Comment thread aikido_zen/sinks/httpx.py Outdated
@@ -0,0 +1,49 @@
"""
patching module httpx, when AIKIDO_FEATURE_AI_NETWORK_CAPTURE is on

@hansott hansott Nov 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revisit whether these comments are actually needed, if we remove the feature flag, it might not be updated? only add info that the code doesn't tell?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed them, the remaining comments only explain why.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants