Skip to content

Commit 8378293

Browse files
[0.x] Add support for configuring provider options on agents (#166)
* Add support for configuring provider options on agents * refactor: pass provider to providerOptions() for fallback support - providerOptions() now receives Lab|string provider parameter - Supports both Lab enum (recommended) and string for flexibility - Enables per-provider options when using provider fallback - TextGenerationOptions resolves provider options lazily at call time - Framework passes Lab enum internally, falls back to string for custom providers Addresses feedback from Taylor Otwell on provider fallback support. * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent f10ec93 commit 8378293

File tree

5 files changed

+205
-3
lines changed

5 files changed

+205
-3
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Laravel\Ai\Contracts;
4+
5+
use Laravel\Ai\Enums\Lab;
6+
7+
interface HasProviderOptions
8+
{
9+
/**
10+
* Get the provider-specific options to be passed to the provider.
11+
*
12+
* @return array<string, mixed>
13+
*/
14+
public function providerOptions(Lab|string $provider): array;
15+
}

src/Gateway/Prism/Concerns/CreatesPrismTextRequests.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Laravel\Ai\Gateway\Prism\Concerns;
44

55
use Laravel\Ai\Contracts\Prompt;
6+
use Laravel\Ai\Enums\Lab;
67
use Laravel\Ai\Gateway\TextGenerationOptions;
78
use Laravel\Ai\ObjectSchema;
89
use Laravel\Ai\Providers\AnthropicProvider;
@@ -59,14 +60,30 @@ protected function withStructuredOutputOptions($request, Provider $provider, arr
5960
*/
6061
protected function withProviderOptions($request, Provider $provider, ?array $schema, ?TextGenerationOptions $options)
6162
{
63+
$agentProviderOptions = $options?->providerOptions(
64+
Lab::tryFrom($provider->driver()) ?? $provider->driver()
65+
);
66+
6267
if ($provider instanceof AnthropicProvider) {
68+
$providerOptions = array_filter([
69+
'use_tool_calling' => $schema ? true : null,
70+
]);
71+
72+
// Merge agent provider options (agent options can override defaults)...
73+
if (! is_null($agentProviderOptions)) {
74+
$providerOptions = array_merge($providerOptions, $agentProviderOptions);
75+
}
76+
6377
return $request
64-
->withProviderOptions(array_filter([
65-
'use_tool_calling' => $schema ? true : null,
66-
]))
78+
->withProviderOptions($providerOptions)
6779
->withMaxTokens($options?->maxTokens ?? 64_000);
6880
}
6981

82+
// For non-Anthropic providers, apply agent provider options if available...
83+
if (! is_null($agentProviderOptions)) {
84+
$request = $request->withProviderOptions($agentProviderOptions);
85+
}
86+
7087
if (! is_null($options?->maxTokens)) {
7188
$request = $request->withMaxTokens($options->maxTokens);
7289
}

src/Gateway/TextGenerationOptions.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Laravel\Ai\Attributes\MaxTokens;
77
use Laravel\Ai\Attributes\Temperature;
88
use Laravel\Ai\Contracts\Agent;
9+
use Laravel\Ai\Contracts\HasProviderOptions;
10+
use Laravel\Ai\Enums\Lab;
911
use ReflectionClass;
1012

1113
class TextGenerationOptions
@@ -14,10 +16,25 @@ public function __construct(
1416
public readonly ?int $maxSteps = null,
1517
public readonly ?int $maxTokens = null,
1618
public readonly ?float $temperature = null,
19+
public readonly ?Agent $agent = null,
1720
) {
1821
//
1922
}
2023

24+
/**
25+
* Get the provider-specific options for the given provider.
26+
*
27+
* @return array<string, mixed>|null
28+
*/
29+
public function providerOptions(Lab|string $provider): ?array
30+
{
31+
if ($this->agent instanceof HasProviderOptions) {
32+
return $this->agent->providerOptions($provider);
33+
}
34+
35+
return null;
36+
}
37+
2138
/**
2239
* Create a new TextGenerationOptions instance for the given agent.
2340
*/
@@ -33,6 +50,7 @@ public static function forAgent(Agent $agent): self
3350
maxSteps: ! empty($maxSteps) ? $maxSteps[0]->newInstance()->value : null,
3451
maxTokens: ! empty($maxTokens) ? $maxTokens[0]->newInstance()->value : null,
3552
temperature: ! empty($temperature) ? $temperature[0]->newInstance()->value : null,
53+
agent: $agent,
3654
);
3755
}
3856
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Laravel\Ai\Enums\Lab;
6+
use Laravel\Ai\Gateway\TextGenerationOptions;
7+
use Laravel\Ai\Prompts\AgentPrompt;
8+
use Tests\Feature\Agents\AssistantAgent;
9+
use Tests\Feature\Agents\ProviderOptionsAgent;
10+
use Tests\TestCase;
11+
12+
class AgentProviderOptionsTest extends TestCase
13+
{
14+
public function test_text_generation_options_can_extract_provider_options_for_openai(): void
15+
{
16+
$options = TextGenerationOptions::forAgent(new ProviderOptionsAgent);
17+
18+
$providerOptions = $options->providerOptions(Lab::OpenAI);
19+
20+
$this->assertNotNull($providerOptions);
21+
$this->assertIsArray($providerOptions);
22+
23+
$this->assertEquals([
24+
'reasoning' => [
25+
'effort' => 'high',
26+
],
27+
'frequency_penalty' => 0.5,
28+
'presence_penalty' => 0.3,
29+
], $providerOptions);
30+
}
31+
32+
public function test_text_generation_options_can_extract_provider_options_for_anthropic(): void
33+
{
34+
$options = TextGenerationOptions::forAgent(new ProviderOptionsAgent);
35+
36+
$providerOptions = $options->providerOptions(Lab::Anthropic);
37+
38+
$this->assertNotNull($providerOptions);
39+
40+
$this->assertEquals([
41+
'thinking' => [
42+
'type' => 'enabled',
43+
'budget_tokens' => 10000,
44+
],
45+
], $providerOptions);
46+
}
47+
48+
public function test_text_generation_options_accept_string_provider(): void
49+
{
50+
$options = TextGenerationOptions::forAgent(new ProviderOptionsAgent);
51+
52+
$providerOptions = $options->providerOptions('openai');
53+
54+
$this->assertNotNull($providerOptions);
55+
56+
$this->assertEquals([
57+
'reasoning' => [
58+
'effort' => 'high',
59+
],
60+
'frequency_penalty' => 0.5,
61+
'presence_penalty' => 0.3,
62+
], $providerOptions);
63+
}
64+
65+
public function test_text_generation_options_return_empty_array_for_unknown_provider(): void
66+
{
67+
$options = TextGenerationOptions::forAgent(new ProviderOptionsAgent);
68+
69+
$providerOptions = $options->providerOptions(Lab::Gemini);
70+
71+
$this->assertEquals([], $providerOptions);
72+
}
73+
74+
public function test_text_generation_options_have_null_provider_options_when_agent_does_not_implement_interface(): void
75+
{
76+
$options = TextGenerationOptions::forAgent(new AssistantAgent);
77+
78+
$this->assertNull($options->providerOptions(Lab::OpenAI));
79+
}
80+
81+
public function test_provider_options_are_passed_through_when_prompting(): void
82+
{
83+
ProviderOptionsAgent::fake();
84+
85+
(new ProviderOptionsAgent)->prompt('Hello');
86+
87+
ProviderOptionsAgent::assertPrompted(function (AgentPrompt $prompt) {
88+
$options = TextGenerationOptions::forAgent($prompt->agent);
89+
90+
return $options->providerOptions(Lab::OpenAI) === [
91+
'reasoning' => [
92+
'effort' => 'high',
93+
],
94+
'frequency_penalty' => 0.5,
95+
'presence_penalty' => 0.3,
96+
];
97+
});
98+
}
99+
100+
public function test_provider_options_default_to_null_when_not_provided(): void
101+
{
102+
AssistantAgent::fake();
103+
104+
(new AssistantAgent)->prompt('Hello');
105+
106+
AssistantAgent::assertPrompted(function (AgentPrompt $prompt) {
107+
$options = TextGenerationOptions::forAgent($prompt->agent);
108+
109+
return $options->providerOptions(Lab::OpenAI) === null;
110+
});
111+
}
112+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Tests\Feature\Agents;
4+
5+
use Laravel\Ai\Contracts\Agent;
6+
use Laravel\Ai\Contracts\HasProviderOptions;
7+
use Laravel\Ai\Enums\Lab;
8+
use Laravel\Ai\Promptable;
9+
10+
class ProviderOptionsAgent implements Agent, HasProviderOptions
11+
{
12+
use Promptable;
13+
14+
public function instructions(): string
15+
{
16+
return 'You are a helpful assistant.';
17+
}
18+
19+
public function providerOptions(Lab|string $provider): array
20+
{
21+
$provider = is_string($provider) ? Lab::tryFrom($provider) : $provider;
22+
23+
return match ($provider) {
24+
Lab::OpenAI => [
25+
'reasoning' => [
26+
'effort' => 'high',
27+
],
28+
'frequency_penalty' => 0.5,
29+
'presence_penalty' => 0.3,
30+
],
31+
Lab::Anthropic => [
32+
'thinking' => [
33+
'type' => 'enabled',
34+
'budget_tokens' => 10000,
35+
],
36+
],
37+
default => [],
38+
};
39+
}
40+
}

0 commit comments

Comments
 (0)