PHPackages                             milpa/ai-gateway - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. milpa/ai-gateway

ActiveLibrary[API Development](/categories/api)

milpa/ai-gateway
================

Dual-provider LLM gateway for the Milpa PHP framework: OpenAI + Anthropic chat-completions client with tool-call format translation, an agentic tool-use loop, and a ToolRegistry facade for LLM/MCP exposure.

v0.2.1(2d ago)02Apache-2.0PHP &gt;=8.3

Since Jul 7Compare

[ Source](https://github.com/getmilpa/ai-gateway)[ Packagist](https://packagist.org/packages/milpa/ai-gateway)[ RSS](/packages/milpa-ai-gateway/feed)WikiDiscussions Synced today

READMEChangelog (4)Dependencies (12)Versions (5)Used By (0)

 [   ![Milpa](https://raw.githubusercontent.com/getmilpa/core/main/art/lockup/milpa-lockup-v-color-light.svg)  ](https://github.com/getmilpa)

Milpa AI Gateway
================

[](#milpa-ai-gateway)

> A **dual-provider LLM gateway** for the Milpa PHP framework — one client for OpenAI and Anthropic chat completions, translating each provider's tool-call wire format to and from a single shape, plus an **agentic tool-use loop** that drives a `milpa/tool-runtime``ToolRegistry` (resolve → validate → authorize → execute → audit) until the model is done.

[![CI](https://github.com/getmilpa/ai-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/getmilpa/ai-gateway/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/fa86564e9cc90cb67898cc8e403feb615dbc0cac72dcdd3531412c5165834ed7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d696c70612f61692d676174657761792e737667)](https://packagist.org/packages/milpa/ai-gateway)[![PHP](https://camo.githubusercontent.com/ca03f11ea27dac4dedc8ad56a7bdfc4a9ff5feb825055f9d2983616115076607/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135253230382e332d3737376262342e737667)](https://www.php.net/)[![License](https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667)](LICENSE)[![Docs](https://camo.githubusercontent.com/c6dc6a3411e15b0ac7cc4583e8e6a8144181caedb82f5d98753353decda06d77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d4150492532307265666572656e63652d626c75652e737667)](https://getmilpa.github.io/ai-gateway/)

`milpa/ai-gateway` is the LLM tier of Milpa: the piece that turns a `milpa/tool-runtime``ToolRegistry` into something a model can actually drive. `LlmService` implements `milpa/core`'s `LlmServiceInterface` seam against two concrete providers — OpenAI and Anthropic — so callers write one message shape and one tool-call shape regardless of which provider answers. `AgentOrchestrator` runs the loop every agent needs: ask the model, execute whatever tools it asks for through the registry pipeline, feed the results back, repeat until the model returns a final answer or a step budget runs out. **No product coupling, no Telegram/HTTP-specific code** — those live in your host application.

Install
-------

[](#install)

```
composer require milpa/ai-gateway
```

Quick example
-------------

[](#quick-example)

Register a tool on a `ToolRegistry` (from `milpa/tool-runtime`), wrap it in `McpClientService`, and hand both to `AgentOrchestrator` along with an `LlmService`:

```
use Milpa\AiGateway\AgentOrchestrator;
use Milpa\AiGateway\LlmService;
use Milpa\AiGateway\McpClientService;
use Milpa\ToolRuntime\ToolRegistry;
use Psr\Log\NullLogger;

$registry = new ToolRegistry(new NullLogger());
$registry->register(
    'get_time',
    'Get the current time',
    [],
    fn () => ['time' => '12:00 PM'],
);

$mcpClient = new McpClientService($registry);
$llm = new LlmService(apiKey: getenv('OPENAI_API_KEY'), model: 'gpt-4o', provider: 'openai');
// LlmService talks HTTP through PSR-18 (Psr\Http\Client\ClientInterface), defaulting to a
// Guzzle client when none is injected — see "Bringing your own HTTP client" below.

$orchestrator = new AgentOrchestrator($llm, $mcpClient);

echo $orchestrator->run('What time is it?');
// -> asks the model, the model requests `get_time`, AgentOrchestrator executes it through
//    the registry, feeds the result back, and returns the model's final answer.
```

Swap `provider: 'anthropic'` and a Claude model name (or let a `claude` model name in `$model` select it automatically) to point the same call at Anthropic instead — `LlmService`translates the tool list, the message history, and the tool-call response to and from Anthropic's shape internally, so `AgentOrchestrator` and `McpClientService` never see a provider-specific format.

The agent loop
--------------

[](#the-agent-loop)

`AgentOrchestrator::run()` alternates between two calls until the model is done or `$maxSteps` (default 20) is reached:

1. **Ask** — `LlmService::generateResponse()` sends the running message history plus the registry's tool summaries (`McpClientService::getToolSummaries()`) to the provider and returns a single OpenAI-shaped assistant message.
2. **Act** — if that message carries `tool_calls`, each one is executed via `McpClientService::callTool()`, which runs it through the full `ToolRegistry` pipeline (validate → authorize → execute → audit) under whatever `ToolContext` was set with `setToolContext()`. The result — rendered through a `RendererRegistry` when one is configured, JSON otherwise — is fed back into the message history as a `tool` message, and the loop repeats.

If a tool result requires confirmation or is blocked by policy, the loop stops immediately and returns that outcome instead of continuing — the caller (a chat handler, a CLI, a bot) is responsible for the confirm/cancel round trip on the next user turn.

Provider translation
--------------------

[](#provider-translation)

`LlmService` speaks one shape to its callers — OpenAI's `messages` / `tool_calls` — and translates both directions for Anthropic:

- **Outbound**: `system` messages become Anthropic's top-level `system` parameter; `tool`role messages become `user` messages carrying a `tool_result` content block; an assistant message with `tool_calls` becomes `tool_use` content blocks. Tool summaries are reshaped from `{name, description, inputSchema}` to Anthropic's `{name, description, input_schema}`, with an empty `properties` object substituted where a tool declares none (Anthropic requires a non-empty schema object, not an empty array).
- **Inbound**: Anthropic's `content: [{type: text, ...}, {type: tool_use, ...}]` array is flattened back into a single OpenAI-shaped assistant message (`content` + `tool_calls`), so `AgentOrchestrator` runs identical logic regardless of provider.

### Bringing your own HTTP client (PSR-18)

[](#bringing-your-own-http-client-psr-18)

`LlmService`'s constructor accepts a PSR-18 `ClientInterface` (plus PSR-17 request/stream factories) — inject your own for connection pooling, retry/circuit-breaker middleware, or tests that assert on the outgoing request without touching the network:

```
use Milpa\AiGateway\LlmService;

$llm = new LlmService(
    apiKey: getenv('ANTHROPIC_API_KEY'),
    model: 'claude-3-5-sonnet-20241022',
    provider: 'anthropic',
    logger: $logger,               // Psr\Log\LoggerInterface; optional
    httpClient: $yourPsr18Client,  // Psr\Http\Client\ClientInterface; omit for Guzzle
    requestFactory: $yourFactory,  // Psr\Http\Message\RequestFactoryInterface; optional
    streamFactory: $yourFactory,   // Psr\Http\Message\StreamFactoryInterface; optional
);
```

When `httpClient` is omitted, `LlmService` builds a Guzzle client with a **600s timeout**shared by both providers. That number used to be OpenAI-only-60s / Anthropic-only-600s (a per-request Guzzle option on the Anthropic call, since Claude tool-use responses can run long) — PSR-18's `sendRequest()` takes only a `RequestInterface`, with no per-call options bag, so a per-provider timeout has no seam to hang off anymore. The default now simply covers the slower case for both. Inject your own `ClientInterface` if you need the tighter OpenAI-side timeout back.

What lives where
----------------

[](#what-lives-where)

LayerPackageOwnsContracts`milpa/core``LlmServiceInterface` — the seam `LlmService` implements.Tool execution`milpa/tool-runtime``ToolRegistry`, `ToolContext`, `ToolResult`, channel rendering — the pipeline `McpClientService` and `AgentOrchestrator` drive.**Gateway****`milpa/ai-gateway`** (this package)The concrete `LlmService` (OpenAI + Anthropic, format translation both ways), `McpClientService` (registry facade), and `AgentOrchestrator` (the ask-act loop).Your appyour host / pluginsAPI keys and secrets management, the PSR-3 logger you wire in, and any channel-specific glue (Telegram, web chat, CLI) around `AgentOrchestrator::run()`.Requirements
------------

[](#requirements)

- PHP **≥ 8.3**
- [`milpa/core`](https://packagist.org/packages/milpa/core) **^0.3**
- [`milpa/tool-runtime`](https://packagist.org/packages/milpa/tool-runtime) **^0.2**
- [`guzzlehttp/guzzle`](https://packagist.org/packages/guzzlehttp/guzzle) **^7.10** — the default PSR-18 implementation `LlmService` falls back to when no `ClientInterface` is injected (also brings `guzzlehttp/psr7`, used as the default PSR-17 factory)
- `psr/http-client`, `psr/http-factory`, `psr/http-message` — the interfaces `LlmService`'s constructor is typed against
- [`psr/log`](https://packagist.org/packages/psr/log) **^3**

Security note
-------------

[](#security-note)

`LlmService` can log provider request/response detail at `debug` level, including a slice of the **raw** LLM response body — never enable that logging in production. See [SECURITY.md](SECURITY.md) for the specifics.

Documentation
-------------

[](#documentation)

**Full API reference: [getmilpa.github.io/ai-gateway](https://getmilpa.github.io/ai-gateway/)** — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing
------------

[](#contributing)

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues via [SECURITY.md](SECURITY.md), and note that this project follows a [Code of Conduct](CODE_OF_CONDUCT.md).

License
-------

[](#license)

[Apache-2.0](LICENSE) © TeamX Agency.

---

Milpa is designed, built, and maintained by **[TeamX Agency](https://teamx.agency/?utm_source=github&utm_medium=readme&utm_campaign=milpa&utm_content=ai-gateway)**.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance99

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

4

Last Release

2d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1993784?v=4)[rodrigomx](/maintainers/rodrigomx)[@rodrigomx](https://github.com/rodrigomx)

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/milpa-ai-gateway/health.svg)

```
[![Health](https://phpackages.com/badges/milpa-ai-gateway/health.svg)](https://phpackages.com/packages/milpa-ai-gateway)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[aws/aws-sdk-php

AWS SDK for PHP - Use Amazon Web Services in your PHP project

6.3k543.5M2.7k](/packages/aws-aws-sdk-php)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M750](/packages/sylius-sylius)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M423](/packages/drupal-core-recommended)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M582](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
