Expose API response metadata (like usageMetadata) in ApiClient service for token tracking

Created on 27 September 2025, 8 days ago

Problem/Motivation

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.

All other valuable information from the API response, most importantly the usageMetadata object which contains the totalTokenCount, is discarded.

This makes it impossible to perform essential tasks like:

Tracking token usage for cost analysis and budgeting.

Logging API consumption per user or per action.

Building custom ECA (Events - Conditions - Actions) workflows that react to the number of tokens used.

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.

Steps to reproduce

Proposed resolution

The proposed solution is to make a small, non-breaking addition to the ApiClient service to make it more extensible.

Add a private property, $lastResponse, to the ApiClient class to store the full, decoded JSON response from the most recent API call.

In the generateText() method, populate this property with the response data before returning the text string.

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.

This change maintains perfect backward compatibility for existing integrations while enabling advanced use cases for developers who need access to the full API response.

Here is the suggested code for the gemini_provider/src/ApiClient.php file, incorporating the changes needed to expose the API usage data.
The new additions are clearly marked with // <-- ADDED comments for clarity.


namespace Drupal\gemini_provider;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;

/**
 * Service to interact with the Gemini API.
 */
class ApiClient {

  /**
   * The HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected ClientInterface $httpClient;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The logger.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Stores the full decoded JSON response from the last API call.
   *
   * @var ?array
   */
  protected ?array $lastResponse = NULL; // <-- ADDED

  /**
   * Constructs an ApiClient object.
   *
   * @param \GuzzleHttp\ClientInterface $http_client
   * The HTTP client.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * The config factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   * The logger factory.
   */
  public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
    $this->httpClient = $http_client;
    $this->configFactory = $config_factory;
    $this->logger = $logger_factory->get('gemini_provider');
  }

  /**
   * Generates text from a prompt.
   *
   * @param string $prompt
   * The prompt to generate text from.
   *
   * @return string|null
   * The generated text, or null on error.
   */
  public function generateText(string $prompt): ?string {
    $config = $this->configFactory->get('gemini_provider.settings');
    $apiKey = $config->get('api_key');
    $model = $config->get('model');
    $url = "https://generativelanguage.googleapis.com/v1beta/models/$model:generateContent";

    $options = [
      'headers' => [
        'Content-Type' => 'application/json',
        'x-goog-api-key' => $apiKey,
      ],
      'json' => [
        'contents' => [
          [
            'parts' => [
              [
                'text' => $prompt,
              ],
            ],
          ],
        ],
      ],
    ];

    try {
      $response = $this->httpClient->post($url, $options);
      $data = json_decode($response->getBody()->getContents(), TRUE);
      $this->lastResponse = $data; // <-- ADDED

      return $data['candidates'][0]['content']['parts'][0]['text'] ?? NULL;
    }
    catch (RequestException $e) {
      $this->lastResponse = NULL; // <-- ADDED
      $this->logger->error('Error generating text: @message', ['@message' => $e->getMessage()]);
    }

    return NULL;
  }

  /**
   * Returns the full decoded response from the most recent API call.
   *
   * @return ?array
   * The last response, or null if no call has been made or an error occurred.
   */
  public function getLastResponse(): ?array { // <-- ADDED
    return $this->lastResponse;
  }

}

Remaining tasks

Review the proposed approach.

Create a patch with the changes.

Commit the patch.

User interface changes

API changes

A new, non-breaking public method will be added to the gemini_provider.api_client service:

public function getLastResponse(): ?array

Data model changes

None.

Feature request
Status

Active

Version

1.0

Component

Code

Created by

🇦🇹Austria maxilein

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

No activities found.

Production build 0.71.5 2024