LlmService
Dual-provider LLM client (OpenAI + Anthropic chat-completions), translating each provider's tool-call wire format to and from a single OpenAI-shaped message array.
LlmService::__construct()
public function __construct(string $apiKey, string $model = 'gpt-4o', string $provider = 'openai', ?Psr\Log\LoggerInterface $logger = null, ?Psr\Http\Client\ClientInterface $httpClient = null, ?Psr\Http\Message\RequestFactoryInterface $requestFactory = null, ?Psr\Http\Message\StreamFactoryInterface $streamFactory = null, ?string $baseUrl = null, array $extraHeaders = [], ?Milpa\AiGateway\ChannelObserver $channelObserver = null, ?Closure $onStreamChunk = null):Parameters
| Name | Type | Description |
|---|---|---|
| $apiKey | string | |
| $model | string | |
| $provider | string | |
| $logger | ?Psr\Log\LoggerInterface | |
| $httpClient | ?Psr\Http\Client\ClientInterface | |
| $requestFactory | ?Psr\Http\Message\RequestFactoryInterface | |
| $streamFactory | ?Psr\Http\Message\StreamFactoryInterface | |
| $baseUrl | (string | null) | Dónde vive el modelo. `null` es el proveedor público — `api.openai.com` o `api.anthropic.com`— y cualquier otra cosa es un endpoint compatible: un Ollama en la LAN, un vLLM, un proxy corporativo. Sin esto, la única forma de probar el bucle del agente era gastarle tokens a un proveedor público, y la única forma de correrlo con datos que no pueden salir de la casa era no correrlo. |
| $extraHeaders | array<string, string> | Encabezados adicionales para cada llamada — un `Authorization: Basic …` cuando el endpoint local está detrás de auth básica, por ejemplo. Se aplican DESPUÉS de los propios, así que pueden reemplazar el `Authorization` del proveedor a propósito. |
| $channelObserver | ?Milpa\AiGateway\ChannelObserver | |
| $onStreamChunk | ?Closure |
LlmService::withStructuredOutput()
public function withStructuredOutput(Milpa\AiGateway\StructuredOutput $output): selfClone the client with an explicit format while retaining its transport and observers. The provider must support this wire option; an HTTP refusal is never retried as plain text. Responses remain unmodified and must still be judged by the caller.
Parameters
| Name | Type | Description |
|---|---|---|
| $output | Milpa\AiGateway\StructuredOutput |
LlmService::withMiniMaxThinking()
public function withMiniMaxThinking(string $mode): selfExplicit MiniMax-M3 thinking mode; omission preserves the provider's default. This controls generation, not reasoning output formatting. Unsupported models/providers fail before transport rather than silently applying a different model's option.
Parameters
| Name | Type | Description |
|---|---|---|
| $mode | string |
LlmService::withOllamaReasoningEffort()
public function withOllamaReasoningEffort(string $effort): selfExplicit reasoning effort for an OpenAI-compatible endpoint. Omission preserves the model's default. This extension remains opt-in: callers that know their endpoint accepts `reasoning_effort` may request it without the gateway guessing from a hostname. That includes Ollama Cloud and local llama.cpp servers with a compatible chat template.
Parameters
| Name | Type | Description |
|---|---|---|
| $effort | string |
LlmService::withOpenAiThinking()
public function withOpenAiThinking(bool $enabled): selfExplicitly enable or disable thinking in a compatible OpenAI chat template. The gateway does not infer support: omission preserves the endpoint's default, while an explicit choice sends the llama.cpp-compatible `chat_template_kwargs.enable_thinking` value unchanged.
Parameters
| Name | Type | Description |
|---|---|---|
| $enabled | bool |
LlmService::generateResponse()
public function generateResponse(string $prompt, array $tools = [], array $messages = [], int $maxTokens = 4096): arraySend a prompt (or a full message history) to the configured provider and return a single OpenAI-shaped assistant message, translating request and response tool-call formats to and from Anthropic's shape when needed.
Parameters
| Name | Type | Description |
|---|---|---|
| $prompt | string | |
| $tools | list<array<string, mixed>> | Tool summaries in MCP/OpenAI shape (`name`, `description`, `inputSchema`) |
| $messages | list<array<string, mixed>> | Full conversation so far; when empty, a single `user` message is built from `$prompt` |
| $maxTokens | int | Positive output-token limit sent to either provider. |
Returns
An OpenAI-shaped assistant message (`role`, `content`, and optionally `tool_calls`)
Throws
OutputTruncatedException When the provider declares an incomplete response.
ContextExceededException When the provider rejects an oversized input context.
\RuntimeException On provider or transport failures.
\InvalidArgumentException When the requested limit is not positive.
LlmService::toolsForRequest()
public function toolsForRequest(array $tools): arrayProject native summaries to the exact tool array sent to the selected provider. Budgeting and transport share this projection: registry metadata such as outputSchema or version does not consume input, while provider wrappers and schema defaults do. This is a pure projection; it neither sends a request nor changes the summaries.
Parameters
| Name | Type | Description |
|---|---|---|
| $tools | list<array<string, mixed>> | Native registry summaries. |
Returns
The provider's tool input.
LlmService::lastUsage()
public function lastUsage(): ?arrayThe usage the provider reported for the most recent call — its own token count — or null when that call reported none (or failed before a response). Reset at the start of every {@see generateResponse()}, so a stale count never describes a newer request.