Skip to main content

Documentation Index

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

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

The aisdk/voyageai package is built for testability from the ground up. Every network call flows through a PSR-18 ClientInterface that is injected via AiSdk\Support\Sdk. By supplying a fake implementation of that interface you can intercept HTTP requests, return canned responses, and make assertions on the exact payload that would have been sent — all without touching the network or consuming API quota.

How It Works

1

Implement a fake PSR-18 client

Create a class that implements Psr\Http\Client\ClientInterface. Its sendRequest() method stores the incoming RequestInterface and immediately returns a pre-configured ResponseInterface — no real HTTP call is made.
2

Wrap the fake client in an Sdk instance

AiSdk\Support\Sdk accepts a PSR-18 client plus PSR-17 request and response factories. Pass your fake client together with a real factory implementation such as Nyholm\Psr7\Factory\Psr17Factory.
3

Configure Generate and VoyageAI

Call Generate::configure(new Sdk(...)) to register the fake HTTP stack, then VoyageAI::create(['apiKey' => 'test']) to register the provider. All subsequent SDK calls in the test will use the fake client.
4

Reset singletons after each test

Call Generate::reset() and VoyageAI::reset() in afterEach to clear the singletons and prevent state from leaking between tests.

The FakeHttpClient

The test suite ships with a ready-made FakeHttpClient that captures the last outbound request and returns a configurable response:
namespace AiSdk\VoyageAI\Tests\Fakes;

use Nyholm\Psr7\Response;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

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);
    }

    /** @return array<string, mixed> */
    public function sentBody(): array
    {
        $decoded = json_decode((string) $this->lastRequest?->getBody(), true);

        return is_array($decoded) ? $decoded : [];
    }
}
Construct it with the HTTP status code and a JSON string that represents the API response you want to simulate. The sentBody() helper decodes the request body so you can assert on the exact JSON that would have been sent to Voyage AI.

Full Embedding Test Example

The test below mirrors the integration test from the package’s own test suite. It verifies the full round-trip: building the request, serialising it to JSON, sending it through the fake client, and parsing the response back into a typed EmbeddingResponse.
use AiSdk\Generate;
use AiSdk\Support\Sdk;
use AiSdk\VoyageAI;
use AiSdk\VoyageAI\Tests\Fakes\FakeHttpClient;
use Nyholm\Psr7\Factory\Psr17Factory;

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

it('generates Voyage AI text embeddings', function () {
    $client = new FakeHttpClient(200, json_encode([
        'object' => 'list',
        'model' => 'voyage-4-large',
        'data' => [
            ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2]],
            ['object' => 'embedding', 'index' => 1, 'embedding' => [0.3, 0.4]],
        ],
        'usage' => ['total_tokens' => 11],
    ]));
    $factory = new Psr17Factory();
    Generate::configure(new Sdk($client, $factory, $factory));
    VoyageAI::create(['apiKey' => 'voyage-test']);

    $result = Generate::embedding(['First document', 'Second document'])
        ->model(VoyageAI::model('voyage-4-large'))
        ->dimensions(512)
        ->providerOptions('voyageai', [
            'input_type' => 'document',
            'truncation' => false,
        ])
        ->run();

    expect($result->output->vector)->toBe([0.1, 0.2])
        ->and($result->embeddings[1]->vector)->toBe([0.3, 0.4])
        ->and($result->usage->inputTokens)->toBe(11)
        ->and($result->usage->totalTokens)->toBe(11)
        ->and($result->providerMetadata['voyageai']['model'])->toBe('voyage-4-large')
        ->and((string) $client->lastRequest?->getUri())->toBe('https://api.voyageai.com/v1/embeddings')
        ->and($client->lastRequest?->getHeaderLine('Authorization'))->toBe('Bearer voyage-test')
        ->and($client->sentBody())->toBe([
            'model' => 'voyage-4-large',
            'input' => ['First document', 'Second document'],
            'output_dimension' => 512,
            'input_type' => 'document',
            'truncation' => false,
        ]);
});

Testing Error Cases

Some invalid inputs are rejected before the HTTP request is sent. You can test these guard clauses without a fake client at all — they throw immediately when ->run() is called.

Quantized output types

Voyage AI only supports output_dtype: float. Passing any other dtype throws InvalidArgumentException:
it('rejects quantized Voyage AI output before sending a request', function () {
    VoyageAI::create(['apiKey' => 'voyage-test']);

    Generate::embedding('A document')
        ->model(VoyageAI::model('voyage-4-large'))
        ->providerOptions('voyageai', ['output_dtype' => 'binary'])
        ->run();
})->throws(\AiSdk\Exceptions\InvalidArgumentException::class, 'only output_dtype: float');

Base64 encoding format

The portable float-vector contract does not support base64 encoding. Passing encoding_format: base64 also throws immediately:
it('rejects base64 Voyage AI output before sending a request', function () {
    VoyageAI::create(['apiKey' => 'voyage-test']);

    Generate::embedding('A document')
        ->model(VoyageAI::model('voyage-4-large'))
        ->providerOptions('voyageai', ['encoding_format' => 'base64'])
        ->run();
})->throws(\AiSdk\Exceptions\InvalidArgumentException::class, 'do not support base64');

Resetting Between Tests

Because VoyageAI and Generate both hold static singletons, failing to reset them means configuration from one test can bleed into another. Always add an afterEach block:
afterEach(function () {
    Generate::reset();
    VoyageAI::reset();
});
If you are using PHPUnit instead of Pest, put the same calls in protected function tearDown(): void.
use PHPUnit\Framework\TestCase;
use AiSdk\Generate;
use AiSdk\VoyageAI;

abstract class VoyageAITestCase extends TestCase
{
    protected function tearDown(): void
    {
        Generate::reset();
        VoyageAI::reset();

        parent::tearDown();
    }
}

Testing with a Custom Base URL

When routing requests through a local mock server or a proxy, pass baseUrl inside the VoyageAI::create() config. The fake client will still capture the request, and the URI assertion will reflect your custom host:
VoyageAI::create([
    'apiKey'  => 'voyage-test',
    'baseUrl' => 'https://mock.example.com/v1',
]);

// ... run embedding ...

expect((string) $client->lastRequest?->getUri())
    ->toBe('https://mock.example.com/v1/embeddings');
The test suite for aisdk/voyageai itself is written with Pest and serves as the canonical reference for all testing patterns. Run composer test in the package root to execute the full suite.

Build docs developers (and LLMs) love