Skip to content
Merged
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
8 changes: 8 additions & 0 deletions config/ai.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@
'url' => env('VOYAGEAI_URL', 'https://api.voyageai.com/v1'),
],

'workersai' => [
'driver' => 'workersai',
'key' => env('CLOUDFLARE_AI_API_KEY'),
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
'gateway' => env('WORKERSAI_GATEWAY'),
'url' => env('WORKERSAI_URL'),
],

'xai' => [
'driver' => 'xai',
'key' => env('XAI_API_KEY'),
Expand Down
12 changes: 12 additions & 0 deletions src/AiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Laravel\Ai\Providers\OpenRouterProvider;
use Laravel\Ai\Providers\Provider;
use Laravel\Ai\Providers\VoyageAiProvider;
use Laravel\Ai\Providers\WorkersAiProvider;
use Laravel\Ai\Providers\XaiProvider;
use LogicException;

Expand Down Expand Up @@ -424,6 +425,17 @@ public function createVoyageaiDriver(array $config): VoyageAiProvider
);
}

/**
* Create a Workers AI powered instance.
*/
public function createWorkersaiDriver(array $config): WorkersAiProvider
{
return new WorkersAiProvider(
$config,
$this->app->make(Dispatcher::class),
);
}

/**
* Create an xAI powered instance.
*/
Expand Down
77 changes: 77 additions & 0 deletions src/Gateway/WorkersAi/Concerns/BuildsTextRequests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Laravel\Ai\Gateway\WorkersAi\Concerns;

use Illuminate\Support\Arr;
use Laravel\Ai\Gateway\TextGenerationOptions;
use Laravel\Ai\ObjectSchema;
use Laravel\Ai\Providers\Provider;

trait BuildsTextRequests
{
/**
* Build the request body for the Chat Completions API.
*/
protected function buildTextRequestBody(
Provider $provider,
string $model,
?string $instructions,
array $messages,
array $tools,
?array $schema,
?TextGenerationOptions $options,
): array {
$body = [
'model' => $model,
'messages' => $this->mapMessagesToChat($messages, $instructions),
];

if (filled($tools)) {
$mappedTools = $this->mapTools($tools);

if (filled($mappedTools)) {
$body['tool_choice'] = 'auto';
$body['tools'] = $mappedTools;
}
}

if (filled($schema)) {
$body['response_format'] = $this->buildResponseFormat($schema);
}

if (! is_null($options?->maxTokens)) {
$body['max_completion_tokens'] = $options->maxTokens;
}

if (! is_null($options?->temperature)) {
$body['temperature'] = $options->temperature;
}

$providerOptions = $options?->providerOptions($provider->driver());

if (filled($providerOptions)) {
$body = array_merge($body, Arr::except($providerOptions, ['session_affinity']));
}

return $body;
}

/**
* Build the response format options for structured output.
*/
protected function buildResponseFormat(array $schema): array
{
$objectSchema = new ObjectSchema($schema);

$schemaArray = $objectSchema->toSchema();

return [
'type' => 'json_schema',
'json_schema' => [
'name' => $schemaArray['name'] ?? 'schema_definition',
'schema' => Arr::except($schemaArray, ['name']),
'strict' => true,
],
];
}
}
73 changes: 73 additions & 0 deletions src/Gateway/WorkersAi/Concerns/CreatesWorkersAiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Laravel\Ai\Gateway\WorkersAi\Concerns;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Exceptions\AiException;
use Laravel\Ai\Providers\Provider;

trait CreatesWorkersAiClient
{
/**
* Get an HTTP client for the Workers AI API.
*/
protected function client(Provider $provider, ?int $timeout = null): PendingRequest
{
$client = Http::baseUrl($this->baseUrl($provider))
->withToken($provider->providerCredentials()['key'])
->timeout($timeout ?? 60)
->throw();

$additionalConfig = $provider->additionalConfiguration();

if (! empty($additionalConfig['session_affinity'])) {
$client->withHeaders(['x-session-affinity' => $additionalConfig['session_affinity']]);
}

return $client;
}

/**
* Get the base URL for the Workers AI API.
*/
protected function baseUrl(Provider $provider): string
{
$config = $provider->additionalConfiguration();

if (! empty($config['url'])) {
return rtrim($config['url'], '/');
}

$accountId = $config['account_id'] ?? '';

if (empty($accountId)) {
throw new AiException(
"Workers AI requires an 'account_id' or explicit 'url' in your provider configuration. "
.'Set CLOUDFLARE_ACCOUNT_ID in your .env file or provide a WORKERSAI_URL.'
);
}

if (! empty($config['gateway'])) {
return "https://gateway.ai.cloudflare.com/v1/{$accountId}/{$config['gateway']}/workers-ai/v1";
}

return "https://api.cloudflare.com/client/v4/accounts/{$accountId}/ai/v1";
}

/**
* Validate model name compatibility with the configured endpoint.
*/
protected function validateModelName(Provider $provider, string $model): void
{
$url = $this->baseUrl($provider);

if (str_ends_with(rtrim($url, '/'), '/compat') && ! str_starts_with($model, 'workers-ai/')) {
throw new AiException(
"Workers AI model '{$model}' requires the 'workers-ai/' prefix when using the /compat endpoint. "
."Either prefix your model name (e.g., 'workers-ai/{$model}') or use the 'gateway' config option "
."instead of an explicit URL — this handles routing automatically with no model name changes."
);
}
}
}
Loading