Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/advanced/provider-interoperability.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,49 @@ This approach can be especially helpful when you have complex or reusable provid
> [!TIP]
> The `whenProvider` method works with all request types in Prism including text, structured output, and embeddings requests.

## Using `ProviderOption`

The `ProviderOption` class provides a convenient way to define provider-specific options. It allows you to specify which provider an option applies to, ensuring options are only used with their intended provider.
```php
Prism::text()
->using(Provider::OpenAI, 'gpt-4o')
->withPrompt('Who are you?')
->withProviderOptions([
new \Prism\Prism\ValueObjects\ProviderOption(
'cacheType',
'ephemeral',
Provider::Anthropic,
),
])
->asText();
```

In this example, since OpenAI is used as the provider, the `cacheType` option is automatically skipped.

### Custom Provider Options

You can extend the `ProviderOption` class to create custom options with predefined keys and additional logic.

```php
class AnthropicCacheType extends ProviderOption
{
public function __construct(string $value)
{
parent::__construct('cacheType', $value, Provider::Anthropic);
}
}

Prism::text()
->using(Provider::OpenAI, 'gpt-4o')
->withPrompt('Who are you?')
->withProviderOptions([
new AnthropicCacheType('ephemeral'),
])
->asText();
```

Like the previous example, the `cacheType` option is skipped since OpenAI is the active provider.

## Best Practices

### Avoiding SystemMessages with Multiple Providers
Expand Down
42 changes: 37 additions & 5 deletions src/Concerns/HasProviderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,59 @@

namespace Prism\Prism\Concerns;

use Prism\Prism\ValueObjects\ProviderOption;

trait HasProviderOptions
{
/** @var array<string, mixed> */
/** @var array<string, ProviderOption> */
protected array $providerOptions = [];

/**
* @param array<string, mixed> $options
* @param array<int|string, mixed> $options
*/
public function withProviderOptions(array $options = []): self
{
$this->providerOptions = $options;
$options = collect($options)->mapWithKeys(function ($value, $key): array {
$value = $value instanceof ProviderOption
? $value
: new ProviderOption($key, $value);

return [$value->key => $value];
});

$this->providerOptions = $options->toArray();

return $this;
}

public function providerOptions(?string $valuePath = null): mixed
{
if ($valuePath === null) {
return $this->providerOptions;
return array_map(
fn (ProviderOption $option): mixed => $option->getValue(),
$this->getProviderOptions(),
);
}

return data_get($this->providerOptions, $valuePath);
$paths = explode('.', $valuePath, 2);
$option = data_get($this->getProviderOptions(), array_first($paths));

if (is_null($option)) {
return null;
}

return count($paths) > 1
? $option->getValue(array_last($paths))
: $option->getValue();
}

private function getProviderOptions(): array
{
return array_filter(
$this->providerOptions,
fn (ProviderOption $option): bool => $option->acceptsProvider(
$this->providerKey ?? null,
),
);
}
}
10 changes: 1 addition & 9 deletions src/Providers/OpenAI/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,11 @@ protected function sendRequest(Request $request): ClientResponse
'input' => (new MessageMap($request->messages(), $request->systemPrompts()))(),
'max_output_tokens' => $request->maxTokens(),
], Arr::whereNotNull([
...$request->providerOptions(),
'temperature' => $request->temperature(),
'top_p' => $request->topP(),
'metadata' => $request->providerOptions('metadata'),
'tools' => $this->buildTools($request),
'tool_choice' => ToolChoiceMap::map($request->toolChoice()),
'parallel_tool_calls' => $request->providerOptions('parallel_tool_calls'),
'previous_response_id' => $request->providerOptions('previous_response_id'),
'service_tier' => $request->providerOptions('service_tier'),
'text' => $request->providerOptions('text_verbosity') ? [
'verbosity' => $request->providerOptions('text_verbosity'),
] : null,
'truncation' => $request->providerOptions('truncation'),
'reasoning' => $request->providerOptions('reasoning'),
]))
);
}
Expand Down
37 changes: 37 additions & 0 deletions src/ValueObjects/ProviderOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Prism\Prism\ValueObjects;

use Illuminate\Support\Arr;
use Prism\Prism\Enums\Provider;

class ProviderOption
{
public function __construct(
public string $key,
public mixed $value,
public ?Provider $provider = null,
) {}

public function getValue(?string $valuePath = null): mixed
{
if (is_null($valuePath) || ! Arr::accessible($this->value)) {
return $this->value;
}

return Arr::get($this->value, $valuePath);
}

public function acceptsProvider(Provider|string|null $provider): bool
{
if (is_null($provider) || is_null($this->provider)) {
return true;
}

$providerKey = $provider instanceof Provider
? $provider->value
: $provider;

return $this->provider->value === $providerKey;
}
}
73 changes: 73 additions & 0 deletions tests/Concerns/HasProviderOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Tests\Http;

use Prism\Prism\Enums\Provider;
use Prism\Prism\Text\PendingRequest;
use Prism\Prism\ValueObjects\ProviderOption;

test('providerOptions returns an array with all providerOptions if no valuePath is provided.', function (): void {
$class = new PendingRequest;
Expand All @@ -27,3 +29,74 @@

expect($class->providerOptions('foo'))->toBeNull();
});

test('providerOptions can be set without key using ProviderOption', function (): void {
$class = new PendingRequest;

$class->withProviderOptions([new ProviderOption('key', 'value')]);

expect($class->providerOptions('key'))->toBe('value');
});

test('providerOptions can be set using ProviderOption and class key takes precedence', function (): void {
$class = new PendingRequest;

$class->withProviderOptions(['key1' => new ProviderOption('key2', 'value')]);

expect($class->providerOptions('key2'))->toBe('value');
expect($class->providerOptions('key'))->toBeNull();
});

test('providerOptions can be set without key when using extended ProviderOption class', function (): void {
$class = new PendingRequest;

$reasoningOption = new class('value') extends ProviderOption
{
public function __construct(string $value)
{
parent::__construct('reasoning', $value);
}
};

$class->withProviderOptions([$reasoningOption]);

expect($class->providerOptions('reasoning'))->toBe('value');
});

test('providerOptions wont return ProviderOption for incorrect provider', function (): void {
$class = new PendingRequest;
$class->using(Provider::Anthropic);

$class->withProviderOptions([new ProviderOption(
'reasoning',
'value',
Provider::OpenAI,
)]);

expect($class->providerOptions('reasoning'))->toBeNull();
});

test('providerOptions returns ProviderOption for correct provider', function (): void {
$class = new PendingRequest;
$class->using(Provider::OpenAI);

$class->withProviderOptions([new ProviderOption(
'reasoning',
'value',
Provider::OpenAI,
)]);

expect($class->providerOptions('reasoning'))->toBe('value');
});

test('providerOptions will always return ProviderOption when option does not specify provider', function (): void {
$class = new PendingRequest;
$class->using(Provider::Anthropic);

$class->withProviderOptions([new ProviderOption(
'reasoning',
'value',
)]);

expect($class->providerOptions('reasoning'))->toBe('value');
});
38 changes: 36 additions & 2 deletions tests/Providers/OpenAI/TextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Prism\Prism\ValueObjects\Media\Document;
use Prism\Prism\ValueObjects\MessagePartWithCitations;
use Prism\Prism\ValueObjects\Messages\UserMessage;
use Prism\Prism\ValueObjects\ProviderOption;
use Prism\Prism\ValueObjects\ProviderTool;
use Prism\Prism\ValueObjects\ProviderToolCall;
use Tests\Fixtures\FixtureResponse;
Expand Down Expand Up @@ -442,13 +443,21 @@
'openai/generate-text-with-a-prompt'
);

$textVerbosity = 'medium';
$option = new class($textVerbosity = 'medium') extends ProviderOption
{
public function __construct(string $value)
{
parent::__construct('text', [
'verbosity' => $value,
]);
}
};

Prism::text()
->using(Provider::OpenAI, 'gpt-4o')
->withPrompt('Who are you?')
->withProviderOptions([
'text_verbosity' => $textVerbosity,
$option,
])
->asText();

Expand Down Expand Up @@ -741,3 +750,28 @@
expect($responseTwo->text)->toContain('Metcheck');
});
});

it('passes store parameter when specified', function (): void {
FixtureResponse::fakeResponseSequence(
'v1/responses',
'openai/generate-text-with-a-prompt'
);

$store = false;

Prism::text()
->using(Provider::OpenAI, 'gpt-4o')
->withPrompt('Give me TLDR of this legal document')
->withProviderOptions([
'store' => $store,
])
->asText();

Http::assertSent(function (Request $request) use ($store): true {
$body = json_decode($request->body(), true);

expect(data_get($body, 'store'))->toBe($store);

return true;
});
});