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
6 changes: 6 additions & 0 deletions config/prism.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,11 @@
'x_title' => env('OPENROUTER_SITE_X_TITLE', null),
],
],
'azure' => [
'url' => env('AZURE_AI_URL', ''),
'api_key' => env('AZURE_AI_API_KEY', ''),
'api_version' => env('AZURE_AI_API_VERSION', '2024-10-21'),
'deployment_name' => env('AZURE_AI_DEPLOYMENT', ''),
],
],
];
1 change: 1 addition & 0 deletions src/Enums/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ enum Provider: string
case Gemini = 'gemini';
case VoyageAI = 'voyageai';
case ElevenLabs = 'elevenlabs';
case Azure = 'azure';
}
14 changes: 14 additions & 0 deletions src/PrismManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use InvalidArgumentException;
use Prism\Prism\Enums\Provider as ProviderEnum;
use Prism\Prism\Providers\Anthropic\Anthropic;
use Prism\Prism\Providers\Azure\Azure;
use Prism\Prism\Providers\DeepSeek\DeepSeek;
use Prism\Prism\Providers\ElevenLabs\ElevenLabs;
use Prism\Prism\Providers\Gemini\Gemini;
Expand Down Expand Up @@ -215,6 +216,19 @@ protected function createOpenrouterProvider(array $config): OpenRouter
);
}

/**
* @param array<string, string> $config
*/
protected function createAzureProvider(array $config): Azure
{
return new Azure(
url: $config['url'] ?? '',
apiKey: $config['api_key'] ?? '',
apiVersion: $config['api_version'] ?? '2024-10-21',
deploymentName: $config['deployment_name'] ?? null,
);
}

/**
* @param array<string, string> $config
*/
Expand Down
145 changes: 145 additions & 0 deletions src/Providers/Azure/Azure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Providers\Azure;

use Generator;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Prism\Prism\Concerns\InitializesClient;
use Prism\Prism\Embeddings\Request as EmbeddingsRequest;
use Prism\Prism\Embeddings\Response as EmbeddingsResponse;
use Prism\Prism\Enums\Provider as ProviderName;
use Prism\Prism\Exceptions\PrismException;
use Prism\Prism\Exceptions\PrismRateLimitedException;
use Prism\Prism\Exceptions\PrismRequestTooLargeException;
use Prism\Prism\Providers\Azure\Handlers\Embeddings;
use Prism\Prism\Providers\Azure\Handlers\Stream;
use Prism\Prism\Providers\Azure\Handlers\Structured;
use Prism\Prism\Providers\Azure\Handlers\Text;
use Prism\Prism\Providers\Provider;
use Prism\Prism\Structured\Request as StructuredRequest;
use Prism\Prism\Structured\Response as StructuredResponse;
use Prism\Prism\Text\Request as TextRequest;
use Prism\Prism\Text\Response as TextResponse;

class Azure extends Provider
{
use InitializesClient;

public function __construct(
public readonly string $url,
#[\SensitiveParameter] public readonly string $apiKey,
public readonly string $apiVersion,
public readonly ?string $deploymentName = null,
) {}

#[\Override]
public function text(TextRequest $request): TextResponse
{
$handler = new Text($this->client(
$request->clientOptions(),
$request->clientRetry(),
$this->buildUrl($request->model())
));

return $handler->handle($request);
}

#[\Override]
public function structured(StructuredRequest $request): StructuredResponse
{
$handler = new Structured($this->client(
$request->clientOptions(),
$request->clientRetry(),
$this->buildUrl($request->model())
));

return $handler->handle($request);
}

#[\Override]
public function embeddings(EmbeddingsRequest $request): EmbeddingsResponse
{
$handler = new Embeddings($this->client(
$request->clientOptions(),
$request->clientRetry(),
$this->buildUrl($request->model())
));

return $handler->handle($request);
}

#[\Override]
public function stream(TextRequest $request): Generator
{
$handler = new Stream($this->client(
$request->clientOptions(),
$request->clientRetry(),
$this->buildUrl($request->model())
));

return $handler->handle($request);
}

public function handleRequestException(string $model, RequestException $e): never
{
$statusCode = $e->response->getStatusCode();
$responseData = $e->response->json();
$errorMessage = data_get($responseData, 'error.message', 'Unknown error');

match ($statusCode) {
429 => throw PrismRateLimitedException::make(
rateLimits: [],
retryAfter: (int) $e->response->header('retry-after')
),
413 => throw PrismRequestTooLargeException::make(ProviderName::Azure),
400, 404 => throw PrismException::providerResponseError(
sprintf('Azure Error: %s', $errorMessage)
),
default => throw PrismException::providerRequestError($model, $e),
};
}

/**
* Build the URL for the Azure deployment.
* Supports both Azure OpenAI and Azure AI Model Inference endpoints.
*/
protected function buildUrl(string $model): string
{
// If URL already contains the full path, use it directly
if (str_contains($this->url, '/chat/completions') || str_contains($this->url, '/embeddings')) {
return $this->url;
}

// Use deployment name from config, or model name as fallback
$deployment = $this->deploymentName ?: $model;

// If URL already contains 'deployments', append the deployment
if (str_contains($this->url, '/openai/deployments')) {
return rtrim($this->url, '/')."/{$deployment}";
}

// Build standard Azure OpenAI URL pattern
return rtrim($this->url, '/')."/openai/deployments/{$deployment}";
}

/**
* @param array<string, mixed> $options
* @param array<mixed> $retry
*/
protected function client(array $options = [], array $retry = [], ?string $baseUrl = null): PendingRequest
{
return $this->baseClient()
->withHeaders([
'api-key' => $this->apiKey,
])
->withQueryParameters([
'api-version' => $this->apiVersion,
])
->withOptions($options)
->when($retry !== [], fn ($client) => $client->retry(...$retry))
->baseUrl($baseUrl ?? $this->url);
}
}
19 changes: 19 additions & 0 deletions src/Providers/Azure/Concerns/MapsFinishReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Providers\Azure\Concerns;

use Prism\Prism\Enums\FinishReason;
use Prism\Prism\Providers\Azure\Maps\FinishReasonMap;

trait MapsFinishReason
{
/**
* @param array<string, mixed> $data
*/
protected function mapFinishReason(array $data): FinishReason
{
return FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', ''));
}
}
26 changes: 26 additions & 0 deletions src/Providers/Azure/Concerns/ValidatesResponses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Providers\Azure\Concerns;

use Prism\Prism\Exceptions\PrismException;

trait ValidatesResponses
{
/**
* @param array<string, mixed> $data
*/
protected function validateResponse(array $data): void
{
if ($data === []) {
throw PrismException::providerResponseError('Azure Error: Empty response');
}

if (data_get($data, 'error')) {
throw PrismException::providerResponseError(
sprintf('Azure Error: %s', data_get($data, 'error.message', 'Unknown error'))
);
}
}
}
64 changes: 64 additions & 0 deletions src/Providers/Azure/Handlers/Embeddings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Providers\Azure\Handlers;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Prism\Prism\Embeddings\Request;
use Prism\Prism\Embeddings\Response as EmbeddingsResponse;
use Prism\Prism\Exceptions\PrismException;
use Prism\Prism\ValueObjects\Embedding;
use Prism\Prism\ValueObjects\EmbeddingsUsage;
use Prism\Prism\ValueObjects\Meta;

class Embeddings
{
public function __construct(protected PendingRequest $client) {}

public function handle(Request $request): EmbeddingsResponse
{
$response = $this->sendRequest($request);

$this->validateResponse($response);

$data = $response->json();

return new EmbeddingsResponse(
embeddings: array_map(fn (array $item): Embedding => Embedding::fromArray($item['embedding']), data_get($data, 'data', [])),
usage: new EmbeddingsUsage(data_get($data, 'usage.total_tokens')),
meta: new Meta(
id: '',
model: data_get($data, 'model', ''),
),
);
}

protected function sendRequest(Request $request): Response
{
/** @var Response $response */
$response = $this->client->post(
'embeddings',
[
'input' => $request->inputs(),
...($request->providerOptions() ?? []),
]
);

return $response;
}

protected function validateResponse(Response $response): void
{
if ($response->json() === null) {
throw PrismException::providerResponseError('Azure Error: Empty embeddings response');
}

if ($response->json('error')) {
throw PrismException::providerResponseError(
sprintf('Azure Error: %s', data_get($response->json(), 'error.message', 'Unknown error'))
);
}
}
}
Loading