Expose API response metadata (like usageMetadata) in ApiClient service for token tracking
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3549099. -->
Reported by: [maxilein](https://www.drupal.org/user/1020354)
Related to !23
>>>
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
<p>Currently, the ApiClient service is designed to be very simple: the generateText() method makes a call to the Gemini API, extracts only the text portion of the response, and returns it as a string.</p>
<p>All other valuable information from the API response, most importantly the usageMetadata object which contains the totalTokenCount, is discarded.</p>
<p>This makes it impossible to perform essential tasks like:</p>
<p>Tracking token usage for cost analysis and budgeting.</p>
<p>Logging API consumption per user or per action.</p>
<p>Building custom ECA (Events - Conditions - Actions) workflows that react to the number of tokens used.</p>
<p>My specific use case is to create a custom ECA Action Plugin that calls the ApiClient service and then saves the totalTokenCount to a field on a Drupal entity. This is currently not possible because the data is not exposed by the service.</p>
<h4 id="summary-steps-reproduce">Steps to reproduce</h4>
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<p>The proposed solution is to make a small, non-breaking addition to the ApiClient service to make it more extensible.</p>
<p>Add a private property, $lastResponse, to the ApiClient class to store the full, decoded JSON response from the most recent API call.</p>
<p>In the generateText() method, populate this property with the response data before returning the text string.</p>
<p>Add a new public getter method, getLastResponse(): ?array, that allows other services and plugins to retrieve the full response data after a call has been made.</p>
<p>This change maintains perfect backward compatibility for existing integrations while enabling advanced use cases for developers who need access to the full API response.</p>
<p>Here is the suggested code for the gemini_provider/src/ApiClient.php file, incorporating the changes needed to expose the API usage data.<br>
The new additions are clearly marked with // <-- ADDED comments for clarity.</p>
<pre>namespace Drupal\gemini_provider;<br><br>use Drupal\Core\Config\ConfigFactoryInterface;<br>use Drupal\Core\Logger\LoggerChannelFactoryInterface;<br>use GuzzleHttp\ClientInterface;<br>use GuzzleHttp\Exception\RequestException;<br><br>/**<br> * Service to interact with the Gemini API.<br> */<br>class ApiClient {<br><br> /**<br> * The HTTP client.<br> *<br> * @var \GuzzleHttp\ClientInterface<br> */<br> protected ClientInterface $httpClient;<br><br> /**<br> * The config factory.<br> *<br> * @var \Drupal\Core\Config\ConfigFactoryInterface<br> */<br> protected ConfigFactoryInterface $configFactory;<br><br> /**<br> * The logger.<br> *<br> * @var \Drupal\Core\Logger\LoggerChannelInterface<br> */<br> protected $logger;<br><br> /**<br> * Stores the full decoded JSON response from the last API call.<br> *<br> * @var ?array<br> */<br> protected ?array $lastResponse = NULL; // <-- ADDED<br><br> /**<br> * Constructs an ApiClient object.<br> *<br> * @param \GuzzleHttp\ClientInterface $http_client<br> * The HTTP client.<br> * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory<br> * The config factory.<br> * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory<br> * The logger factory.<br> */<br> public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {<br> $this->httpClient = $http_client;<br> $this->configFactory = $config_factory;<br> $this->logger = $logger_factory->get('gemini_provider');<br> }<br><br> /**<br> * Generates text from a prompt.<br> *<br> * @param string $prompt<br> * The prompt to generate text from.<br> *<br> * @return string|null<br> * The generated text, or null on error.<br> */<br> public function generateText(string $prompt): ?string {<br> $config = $this->configFactory->get('gemini_provider.settings');<br> $apiKey = $config->get('api_key');<br> $model = $config->get('model');<br> $url = "https://generativelanguage.googleapis.com/v1beta/models/$model:generateContent";<br><br> $options = [<br> 'headers' => [<br> 'Content-Type' => 'application/json',<br> 'x-goog-api-key' => $apiKey,<br> ],<br> 'json' => [<br> 'contents' => [<br> [<br> 'parts' => [<br> [<br> 'text' => $prompt,<br> ],<br> ],<br> ],<br> ],<br> ],<br> ];<br><br> try {<br> $response = $this->httpClient->post($url, $options);<br> $data = json_decode($response->getBody()->getContents(), TRUE);<br> $this->lastResponse = $data; // <-- ADDED<br><br> return $data['candidates'][0]['content']['parts'][0]['text'] ?? NULL;<br> }<br> catch (RequestException $e) {<br> $this->lastResponse = NULL; // <-- ADDED<br> $this->logger->error('Error generating text: @message', ['@message' => $e->getMessage()]);<br> }<br><br> return NULL;<br> }<br><br> /**<br> * Returns the full decoded response from the most recent API call.<br> *<br> * @return ?array<br> * The last response, or null if no call has been made or an error occurred.<br> */<br> public function getLastResponse(): ?array { // <-- ADDED<br> return $this->lastResponse;<br> }<br><br>}</pre><h3 id="summary-remaining-tasks">Remaining tasks</h3>
<p>Review the proposed approach.</p>
<p>Create a patch with the changes.</p>
<p>Commit the patch.</p>
<h3 id="summary-ui-changes">User interface changes</h3>
<h3 id="summary-api-changes">API changes</h3>
<p>A new, non-breaking public method will be added to the gemini_provider.api_client service:</p>
<p>public function getLastResponse(): ?array</p>
<h3 id="summary-data-model-changes">Data model changes</h3>
<p>None.</p>
issue