main: 引入 AgentProvider 流式事件与 OpenAI 兼容适配

- 增加流式事件流支持,Provider 输出 `message.delta` 等事件
- 实现 OpenAI 兼容适配器,包括 RequestBuilder、ApiClient 等模块
- 更新 Agent Run 逻辑,支持流式增量写入与模型完成状态管理
- 扩展配置项 `agent.openai.*`,支持模型、密钥等配置
- 优化文档,完善流式事件与消息类型说明
- 增加单元测试,覆盖 Provider 和 OpenAI 适配相关逻辑
- 更新环境变量与配置示例,支持新功能
This commit is contained in:
2025-12-19 02:35:37 +08:00
parent 56523c1f0a
commit 8c4ad80dab
27 changed files with 1006 additions and 166 deletions

View 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();
}
}