Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/xai/llms.txt

Use this file to discover all available pages before exploring further.

The aisdk/xai package is designed to be fully testable without making real API calls. Because the provider resolves its HTTP transport through a standard PSR-18 ClientInterface, you can swap in any compliant fake client at test time. Inject it through the Sdk value object passed to Generate::configure(), point XAI::create() at a dummy API key, and your tests run entirely in-process — fast, deterministic, and free of network dependencies.

The FakeHttpClient

The package ships a FakeHttpClient helper inside tests/Fakes/ that records the last outgoing request and returns a hand-crafted response. Its constructor accepts an HTTP status code, a raw response body string, and an optional Content-Type header (defaulting to application/json).
MemberDescription
__construct(int $status, string $body, string $contentType = 'application/json')Builds the fake with a fixed status and body to return for every request.
public ?RequestInterface $lastRequestPopulated with the most recent RequestInterface after each sendRequest() call. Inspect it to assert headers, URI path, and HTTP method.
sentBody(): array<string, mixed>JSON-decodes $lastRequest->getBody() and returns it as an associative array, making it easy to assert the exact payload your code sent.
final class FakeHttpClient implements ClientInterface
{
    public ?RequestInterface $lastRequest = null;

    public function __construct(
        private readonly int $status,
        private readonly string $body,
        private readonly string $contentType = 'application/json',
    ) {}

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        $this->lastRequest = $request;
        return new Response($this->status, ['Content-Type' => $this->contentType], $this->body);
    }

    public function sentBody(): array
    {
        $decoded = json_decode((string) $this->lastRequest?->getBody(), true);
        return is_array($decoded) ? $decoded : [];
    }
}

Setting Up a Test

The typical pattern is:
  1. Instantiate FakeHttpClient with the JSON payload you want the provider to “receive”.
  2. Wrap a Psr17Factory and the fake client together in an Sdk object and pass it to Generate::configure().
  3. Call XAI::create() with a dummy API key.
  4. Exercise your application code normally and assert on the returned result.
use AiSdk\Generate;
use AiSdk\Support\Sdk;
use AiSdk\XAI;
use Nyholm\Psr7\Factory\Psr17Factory;

afterEach(function () {
    Generate::reset();
    XAI::reset();
});

it('generates text end to end through the XAI vertical', function () {
    $client = new FakeHttpClient(200, json_encode([
        'id'      => 'chatcmpl_xai',
        'object'  => 'chat.completion',
        'created' => 1710000000,
        'model'   => 'grok-4',
        'choices' => [['index' => 0, 'message' => ['content' => 'Hello from xAI'], 'finish_reason' => 'stop']],
        'usage'   => ['prompt_tokens' => 8, 'completion_tokens' => 4],
    ]));

    $factory = new Psr17Factory;
    Generate::configure(new Sdk(
        httpClient: $client,
        requestFactory: $factory,
        streamFactory: $factory,
    ));

    XAI::create(['apiKey' => 'xai-test']);

    $result = Generate::text('Hi')->model(XAI::model('grok-4'))->run();

    expect($result->text)->toBe('Hello from xAI');
});
The helper function configureXAIWith() used throughout the package’s own test suite extracts the Sdk setup into a single reusable call, which can be useful if you write many tests against the same provider:
function configureXAIWith(FakeHttpClient $client): void
{
    $factory = new Psr17Factory;
    Generate::configure(new Sdk(
        httpClient: $client,
        requestFactory: $factory,
        streamFactory: $factory,
    ));
}

Asserting Request Shape

After calling ->run(), inspect what was actually sent over the wire using $client->sentBody() for the JSON body and $client->lastRequest for transport-level details like URI path and headers.
$body = $client->sentBody();
expect($body['model'])->toBe('grok-4')
    ->and($body['stream'])->toBeFalse();

expect($client->lastRequest->getUri()->getPath())->toBe('/v1/chat/completions')
    ->and($client->lastRequest->getHeaderLine('Authorization'))->toBe('Bearer xai-test');
You can also assert on nested message structure and any provider-specific fields in the same fluent chain:
expect($body['messages'][0]['role'])->toBe('user');

Testing Image Generation

Image generation follows the same injection pattern. Provide a fake response that mimics the xAI images endpoint, then assert on both the decoded output and the outgoing request body.
it('generates images through the XAI vertical', function () {
    $client = new FakeHttpClient(200, json_encode([
        'created' => 1710000000,
        'data'    => [['b64_json' => base64_encode('jpeg-bytes')]],
        'usage'   => ['prompt_tokens' => 6, 'completion_tokens' => 8, 'total_tokens' => 14],
    ]));
    configureXAIWith($client);

    XAI::create(['apiKey' => 'xai-test']);

    $result = Generate::image()
        ->model(XAI::image('grok-imagine-image-quality'))
        ->prompt('A futuristic skyline')
        ->count(2)
        ->aspectRatio('16:9')
        ->providerOptions('xai', ['raw' => ['resolution' => '2k']])
        ->run();

    expect($result->output->base64)->toBe(base64_encode('jpeg-bytes'))
        ->and($result->usage->totalTokens)->toBe(14);

    $body = $client->sentBody();
    expect($body)->toMatchArray([
        'model'           => 'grok-imagine-image-quality',
        'prompt'          => 'A futuristic skyline',
        'n'               => 2,
        'response_format' => 'b64_json',
        'aspect_ratio'    => '16:9',
        'resolution'      => '2k',
    ]);

    expect($client->lastRequest->getUri()->getPath())->toBe('/v1/images/generations')
        ->and($client->lastRequest->getHeaderLine('Authorization'))->toBe('Bearer xai-test');
});
Note that providerOptions('xai', ['raw' => [...]]) merges extra fields directly into the request body, so you can assert on them through $client->sentBody() just like any other key.

XAI::reset() and Generate::reset()

Both XAI and Generate hold static singleton state. If one test leaves a provider or Sdk instance registered, the next test will inherit it — leading to false positives or confusing failures. Call both reset methods in an afterEach hook to wipe all state between tests:
afterEach(function () {
    Generate::reset();
    XAI::reset();
});
  • XAI::reset() — sets the static $default XAIProvider instance back to null. The next call to XAI::model(), XAI::image(), or XAI::default() will lazy-create a fresh provider.
  • Generate::reset() — clears the Sdk registered with Generate::configure(). Without this, the fake httpClient from one test would be reused by the next.

Running the Test Suite

The composer.json for aisdk/xai ships two convenience scripts:
# Run the Pest test suite only
composer test

# Run lint (Pint), static analysis (PHPStan), and tests in sequence
composer test:all
The full test:all pipeline mirrors CI — it runs Laravel Pint in --test mode (no writes), PHPStan at its configured strictness level, and then Pest.
FakeHttpClient lives in the tests/ directory under the AiSdk\XAI\Tests\Fakes namespace and is not included in the production Composer autoload. It is therefore not available in your application’s vendor tree. If you want to use the same helper in your own project’s test suite, copy the class directly — it has no dependencies beyond nyholm/psr7 (a common PSR-7 implementation) and the PSR interfaces already required by aisdk/xai.

Build docs developers (and LLMs) love