main: 引入 AgentProvider 流式事件与 OpenAI 兼容适配
- 增加流式事件流支持,Provider 输出 `message.delta` 等事件 - 实现 OpenAI 兼容适配器,包括 RequestBuilder、ApiClient 等模块 - 更新 Agent Run 逻辑,支持流式增量写入与模型完成状态管理 - 扩展配置项 `agent.openai.*`,支持模型、密钥等配置 - 优化文档,完善流式事件与消息类型说明 - 增加单元测试,覆盖 Provider 和 OpenAI 适配相关逻辑 - 更新环境变量与配置示例,支持新功能
This commit is contained in:
87
app/Services/Agent/OpenAi/OpenAiApiClient.php
Normal file
87
app/Services/Agent/OpenAi/OpenAiApiClient.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent\OpenAi;
|
||||
|
||||
use App\Services\Agent\ProviderException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class OpenAiApiClient
|
||||
{
|
||||
public function __construct(
|
||||
private ?string $baseUrl = null,
|
||||
private ?string $apiKey = null,
|
||||
private ?string $organization = null,
|
||||
private ?string $project = null,
|
||||
private ?int $timeoutSeconds = null,
|
||||
private ?int $connectTimeoutSeconds = null,
|
||||
) {
|
||||
$this->baseUrl = $this->baseUrl ?? (string) config('agent.openai.base_url', '');
|
||||
$this->apiKey = $this->apiKey ?? (string) config('agent.openai.api_key', '');
|
||||
$this->organization = $this->organization ?? (string) config('agent.openai.organization', '');
|
||||
$this->project = $this->project ?? (string) config('agent.openai.project', '');
|
||||
$this->timeoutSeconds = $this->timeoutSeconds ?? (int) config('agent.provider.timeout_seconds', 30);
|
||||
$this->connectTimeoutSeconds = $this->connectTimeoutSeconds ?? (int) config('agent.provider.connect_timeout_seconds', 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a streaming response for the Chat Completions endpoint.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function openStream(array $payload): ResponseInterface
|
||||
{
|
||||
$baseUrl = trim((string) $this->baseUrl);
|
||||
$apiKey = trim((string) $this->apiKey);
|
||||
|
||||
if ($baseUrl === '' || $apiKey === '') {
|
||||
throw new ProviderException('CONFIG_MISSING', 'Agent provider configuration missing', false);
|
||||
}
|
||||
|
||||
$endpoint = rtrim($baseUrl, '/').'/chat/completions';
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer '.$apiKey,
|
||||
'Accept' => 'text/event-stream',
|
||||
];
|
||||
|
||||
if (trim((string) $this->organization) !== '') {
|
||||
$headers['OpenAI-Organization'] = (string) $this->organization;
|
||||
}
|
||||
|
||||
if (trim((string) $this->project) !== '') {
|
||||
$headers['OpenAI-Project'] = (string) $this->project;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders($headers)
|
||||
->connectTimeout($this->connectTimeoutSeconds)
|
||||
->timeout($this->timeoutSeconds)
|
||||
->withOptions(['stream' => true])
|
||||
->post($endpoint, $payload);
|
||||
} catch (ConnectionException $exception) {
|
||||
throw new ProviderException(
|
||||
'CONNECTION_FAILED',
|
||||
'Agent provider connection failed',
|
||||
true,
|
||||
null,
|
||||
$exception->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$status = $response->status();
|
||||
|
||||
if ($status < 200 || $status >= 300) {
|
||||
$retryable = $status === 429 || $status >= 500;
|
||||
throw new ProviderException(
|
||||
'HTTP_ERROR',
|
||||
'Agent provider failed',
|
||||
$retryable,
|
||||
$status,
|
||||
$response->body()
|
||||
);
|
||||
}
|
||||
|
||||
return $response->toPsrResponse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user