fix(logging): capture generations that bypass the SDK HTTP transporter - #757
fix(logging): capture generations that bypass the SDK HTTP transporter#757i-anubhav-anand wants to merge 2 commits into
Conversation
AI Request Logging only saw requests routed through the SDK's HTTP transporter, so providers with a custom transport (e.g. a localhost sidecar) were never logged even though they generate via wp_ai_client_prompt(). Add a provider-agnostic fallback (Logging_Event_Listener) that taps the core generation lifecycle hooks bridged from the SDK in wp-settings.php: wp_ai_client_before_generate_result / wp_ai_client_after_generate_result. The after-event records provider, model, token usage and duration for any provider regardless of transport. A per-generation flag, reset on the before-event and set by Logging_Http_Transporter::send(), prevents double-logging transporter-based providers. The SDK after-event fires on success only, so failed custom-transport generations remain uncaptured; this is documented and would need an SDK-level error event to address.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #757 +/- ##
=============================================
+ Coverage 74.45% 78.49% +4.04%
- Complexity 1740 2464 +724
=============================================
Files 85 105 +20
Lines 7521 9958 +2437
=============================================
+ Hits 5600 7817 +2217
- Misses 1921 2141 +220
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| $log_data = array( | ||
| 'type' => 'ai_client', | ||
| 'operation' => '' !== $capability ? $provider . ':' . $capability : $provider, |
There was a problem hiding this comment.
I think this is different than what we log in our transporter. Ideally this matches for filtering.
|
@henryperkins I know you reported the original issue, do you by chance have some time to verify if this PR fixes things for your use case? Thanks! |
|
@i-anubhav-anand are you able to make updates based on code review? |
|
@dkotter Sorry for the delayed response here, and thanks for the ping — I tested this against ai-provider-for-codex v2.1 at 2827bb5, using WordPress 7.0.1, PHP 8.3.31, AI Client 1.3.1, and this PR at bb6a7a7. I invoked the actual wp_ai_client_prompt()->using_model(...)->generate_text_result() path with a seeded user connection/model catalog. I intercepted only the loopback /v1/responses/text response to keep the test deterministic, so this did not use a live authenticated Codex account. Results:
The duplicate occurs because the provider already works around #732 by writing directly to the request log. This PR’s suppression flag only detects Logging_Http_Transporter, so it cannot tell that a custom provider has already logged the request. I also tested a failed runtime response. The after-generation event did not fire, so this PR added no failure row. The current provider’s existing bridge did record one error row, consistent with the known limitation described in the PR. So the event fallback does fix the original zero-log gap once the provider-side workaround is disabled or removed. With the current provider unchanged, however, successful requests are logged twice. The two paths also produce different operation values, which appears relevant to the existing filtering review comment. |
|
Thanks both — and thanks especially for the thorough live test @henryperkins, that's really useful. To summarise where this leaves the PR:
Plan for the follow-up commit:
On the duplicate specifically: this PR alone can't fully de-dupe a provider that logs through its own path, so item 4 is the hook that lets it. @henryperkins — since the codex bridge already gates on (Separately, minor: the bridge writes Will push the follow-up shortly. |
Rename Logging_Event_Listener::mark_transporter_logged() to mark_generation_logged() so any logger -- not only the SDK HTTP transporter -- can mark a generation as already recorded and suppress the event-based duplicate row. This lets a custom-transport provider that logs through its own bridge de-duplicate against this fallback. Set the new file's @SInCE tags to x.x.x per the contributor guide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed a follow-up commit addressing the review. Summary of what changed and one open question for @dkotter: 1. 2. Possible
So 3. De-duplication across custom loggers (re: @henryperkins's double-log finding) — the suppression signal was transporter-specific ( Open question on the I'll also expand the "known limitation" note in the description to cover the success-case duplicate for providers that don't adopt the marker. |
|
Thanks — and one clarification for the record: this was a deterministic WordPress/provider integration-path test with the loopback sidecar response mocked, not a live authenticated Codex generation. My preference is (b), with a backward-compatible feature check:
That avoids coupling the provider’s normal success path to another logging marker and gives the request log consistent type, operation, and filtering behavior. A transport-neutral marker could still be useful for custom providers that intentionally retain their own success logger. One caveat if the marker-based approach is retained: it should be called only after RequestLogWriter::record() returns true, not before the write. record() is best-effort and can fail; marking first could suppress the event listener even though the provider’s write failed, returning the request to zero log rows. Once the follow-up commit is available, I’m happy to rerun the same matrix against it and then update the provider around the final API. |
We definitely want to standardize and if this new approach of logging only has access to the capability, that seems like a pretty easy answer to make that the standard for filtering. But let me know if I'm missing something there. |
What?
Closes #732
Adds a provider-agnostic logging fallback so the AI Request Logging experiment captures successful generations from providers that don't route through the SDK's HTTP transporter (e.g. a provider that proxies to a localhost sidecar).
Why?
Today logging is installed purely by decorating the SDK HTTP transporter (
Logging_Http_Transporter), andLogging_Http_Transporter::send()is the only place that callsAI_Request_Log_Manager::log(). A request is logged iff it flows through that transporter. Any first-class provider reachable viawp_ai_client_prompt()that uses a custom transport is therefore silently absent from Tools → AI Request Log, even though the log presents itself as a record of every AI request. The concrete case in #732 is acodexprovider that brokers requests through a localhost sidecar.How?
WordPress core registers a PSR-14 dispatcher in
wp-settings.php(WordPress\AiClient\AiClient::setEventDispatcher( new WP_AI_Client_Event_Dispatcher() )) that bridges the SDK'sBeforeGenerateResultEvent/AfterGenerateResultEventto the action hookswp_ai_client_before_generate_result/wp_ai_client_after_generate_result. These fire fromPromptBuilder::generateResult(), through which allgenerate*()calls funnel — for every provider, regardless of transport.This PR adds
Logging_Event_Listener, wired up inLogging_Integration::init(), which:wp_ai_client_after_generate_result, writes a log row using the event:getModel()->providerMetadata()->getId()getModel()->metadata()->getId()getResult()->getTokenUsage()(prompt/completion)Logging_Http_Transporter::send(). When the transporter already logged the current generation, the after-event listener skips it.Known limitation (documented in the code): the SDK's after-event fires on success only — there is no error event — so failed custom-transport generations remain uncaptured. Transporter-based providers still log errors as before. Closing the failure case for custom transports would require an SDK-level error event and is out of scope here.
The diff is surgical: +8 lines across the two existing files, plus one new listener class and one new test file.
Use of AI Tools
AI assistance: Yes
Tool(s): AI coding assistant (agentic CLI)
Used for: investigating the SDK event/dispatcher flow in
wp-includes/php-ai-clientandwp-includes/ai-client, drafting the listener + tests, and writing this description. All code was written test-first, executed, and reviewed by me; I take responsibility for it.Testing Instructions
Automated (PHPUnit, via
wp-env):tests/Integration/Includes/Logging/Logging_Event_ListenerTest.phptest_logs_generation_that_bypasses_the_transporter— a generation that never touchesLogging_Http_Transporterproduces one row with the right provider/model/tokens.test_does_not_double_log_transporter_based_generations— a transporter-logged generation yields exactly one row (no event-based duplicate).--filter Logging) is green (120 tests); the full integration suite is green (838 tests, 34 pre-existing skips).Fail-before / pass-after (source stashed, test kept):
Manual: enable AI Request Logging, run a generation through a provider with a custom transport (localhost sidecar), and confirm a row now appears in Tools → AI Request Log.
Changelog Entry