Skip to content

[Platform] Add support for Eden AI gateway - #2417

Open
welcoMattic wants to merge 1 commit into
symfony:mainfrom
welcoMattic:platform/edenai
Open

[Platform] Add support for Eden AI gateway#2417
welcoMattic wants to merge 1 commit into
symfony:mainfrom
welcoMattic:platform/edenai

Conversation

@welcoMattic

@welcoMattic welcoMattic commented Aug 17, 2026

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? yes
Docs? yes
Issues -
License MIT

Eden AI is an AI gateway exposing hundreds of models from many
providers behind a single API, using provider/model identifiers. This adds a
symfony/ai-eden-ai-platform bridge covering both halves of its v3 API.

OpenAI-compatible endpoints

Chat completions (/v3/chat/completions, including streaming and tool calling) and
embeddings (/v3/embeddings) reuse the Generic bridge:

use Symfony\AI\Platform\Bridge\EdenAi\Factory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

$platform = Factory::createPlatform($apiKey);

$messages = new MessageBag(Message::ofUser('What is the Symfony framework?'));
echo $platform->invoke('openai/gpt-4o-mini', $messages)->asText();

$vectors = $platform->invoke('openai/text-embedding-3-small', 'Some text')->asVectors();

Expert models

The /v3/universal-ai endpoints expose non-LLM features, addressed as
feature/subfeature/provider[/model]: OCR, document parsing (invoices, resumes, identity
documents), text-to-speech, speech-to-text, image analysis (object detection, explicit
content) and image generation.

use Symfony\AI\Platform\Bridge\EdenAi\Ocr\Result\OcrResult;
use Symfony\AI\Platform\Message\Content\Audio;
use Symfony\AI\Platform\Message\Content\ImageUrl;

// OCR: raw text and bounding boxes
$result = $platform->invoke('ocr/ocr/google', new ImageUrl('https://example.com/scan.jpg'), [
    'language' => 'en',
]);

$ocr = $result->asObject();
\assert($ocr instanceof OcrResult);
echo $ocr->getText();

// Document parsing: structured data out of an invoice
$parsing = $platform->invoke('ocr/financial_parser/affinda', new DocumentUrl('https://example.com/invoice.pdf'), [
    'language' => 'en',
])->asObject();

// Text-to-speech
$platform->invoke('audio/tts/amazon/neural', 'Welcome to Symfony AI!')->asFile('welcome.mp3');

// Speech-to-text: the async job is polled transparently, and a local file is
// uploaded through /v3/upload first
echo $platform->invoke('audio/speech_to_text_async/deepgram', Audio::fromFile('./audio.mp3'))->asText();

// Image generation
$platform->invoke('image/generation/stabilityai', 'A red apple on a white table')->asFile('apple.png');

Expert models accept their input as a direct file URL, as a file ID, or as Audio,
Document and Image content, which is transparently uploaded through /v3/upload
beforehand. Speech-to-text runs on /v3/universal-ai/async, whose job is polled until it
reaches a terminal state. Options that are not root-level request fields (fallbacks,
provider_params, show_original_response, webhook_receiver,
user_webhook_parameters) are forwarded inside the input object, so per-feature
parameters like language, document_type or voice are passed as invocation options.

Notes

The catalog ships a curated subset of the available models; any other one can be
registered through the $additionalModels constructor argument. Capabilities are derived
from the metadata /v3/models exposes per entry: supports_response_schema maps to
OUTPUT_STRUCTURED, supports_function_calling to TOOL_CALLING, and the
input_modalities to the matching INPUT_* cases.

Gateways like Eden AI answer authentication failures with {"detail": "..."} rather than
an OpenAI-style error payload, which made the Generic completions converter emit PHP
warnings instead of throwing an AuthenticationException, so it now reads both shapes.

Endpoints, payload keys, the upload contract and every output shape were checked against
the live API, and the twelve examples under examples/edenai/ were all run against it.
One discrepancy is worth flagging for future readers: the output_schema advertised by
GET /v3/info for image/generation describes a single item without the enclosing
items list the endpoint really returns, which the converter documents.

Bridge documentation is in docs/components/platform.rst, and 115 tests cover the model
catalog, the contract normalizers, both model clients and every result converter.

cc @Guikingone

@carsonbot carsonbot added Feature New feature Platform Issues & PRs about the AI Platform component Status: Needs Review labels Aug 17, 2026
@welcoMattic
welcoMattic force-pushed the platform/edenai branch 4 times, most recently from 18398f3 to d329111 Compare August 20, 2026 13:46
Eden AI is an AI gateway exposing hundreds of models from many providers behind a
single API, using `provider/model` identifiers.

The bridge covers both halves of the v3 API:

 * the OpenAI-compatible endpoints, `/v3/chat/completions` and `/v3/embeddings`,
   reusing the Generic bridge for chat (including streaming and tool calling) and
   embeddings;
 * the expert models of the `/v3/universal-ai` endpoints: OCR, document parsing
   (invoices, resumes, identity documents), text-to-speech, speech-to-text, image
   analysis (object detection, explicit content, logo detection, face detection,
   AI detection, deepfake detection) and image generation.

Expert models take their input either as a direct file URL, as a file ID, or as
`Audio`, `Document` and `Image` content, which is transparently uploaded through
`/v3/upload` first. Speech-to-text runs on `/v3/universal-ai/async`, whose job is
polled until it reaches a terminal state, unless a `webhook_receiver` is given.
Options that are not root-level request fields are forwarded inside the `input`
object.

Two catalogs are provided. `ModelCatalog` curates a static subset and accepts extra
entries through `$additionalModels`. `ModelApiCatalog` discovers everything the
gateway currently serves - 1083 models against the 65 curated ones - from the public
`/v3/models`, `/v3/embeddings/models` and `/v3/info` endpoints; expert subfeatures
the bridge has no converter for stay hidden, so an unsupported model fails at lookup
instead of at conversion time. Capabilities are derived from the metadata
`/v3/models` exposes per entry: `supports_response_schema` maps to
`OUTPUT_STRUCTURED`, `supports_function_calling` to `TOOL_CALLING`, and the
`input_modalities` to the matching `INPUT_*` cases.

Errors get a dedicated `ErrorHandlingTrait` because Eden AI is a FastAPI application
and reports failures through `detail`, in shapes the shared
`Result\HttpStatusErrorHandlingTrait` cannot read - so an unmapped status used to
surface as a misleading "Response does not contain ..." message. All the shapes below
were captured from the live API, including a 422 body that differs from what the
published OpenAPI schema documents, and a 403 that nothing mapped at all:

    403 {"detail": "Not authenticated"}
    401 {"detail": "Invalid token"}
    404 {"detail": {"error": "Provider not found", "message": "..."}}
    400 {"detail": {"error": "Invalid provider format", "message": "..."}}
    422 {"detail": "Validation error", "errors": [{"field": "language", ...}]}

A missing required option now reports `Validation error: language: Field required`
rather than `Response does not contain audio_resource_url.`.

Image generation returns every generated image instead of only the first: since
`num_images` accepts up to 10 and each one is billed, several are exposed as a
`MultiPartResult`, mirroring the OpenAI image bridge. The gateway `cost` and
`provider` - which vary per request once `fallbacks` is used - are exposed as result
metadata on the binary and text results too, not only on the object ones. The
text-to-speech download, the asynchronous job decoding and the file upload all map
their own failures onto platform exceptions rather than leaking HttpClient ones, and
an unusable binary input is rejected before it can be uploaded empty and billed.

The synthesized audio is reported with its real format: the CDN serving it answers
`binary/octet-stream` whatever `audio_format` was requested, so the extension Eden AI
puts in the resource URL decides, which was verified against the audio magic bytes for
mp3 and wav.

Endpoints, payload keys, the upload contract and every output shape were checked
against the live API, and the thirteen examples were all run against it. Note that
the `output_schema` that `GET /v3/info` advertises for `image/generation` describes
a single item without the enclosing `items` list the endpoint really returns, which
is documented in the converter so the discrepancy does not read as a bug.

@wachterjohannes wachterjohannes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structurally this holds up: scaffolding, deptrac, splitsh and the bundle wiring all match the sibling bridges, and the two things that looked like omissions (no base_url in the bundle config, two catalogs) are what the siblings do. Details inline.

  • Record and replay is the direction we want to take bridges, so that a converter is pinned against the provider's real shapes rather than hand-written fixtures. This bridge is the strongest case for it so far, eight converters at once, and you already ran every example against the live API, so examples/runner --record edenai would come almost for free. ExamplesReplayTest iterates over the cassettes, so without one the bridge stays silently uncovered.

On scope I am deliberately not deciding: a gateway with eight expert-feature families and its own result types is a maintenance call, so I would like @chr-hertel to weigh in before this moves.

Verified locally, all green.

Comment on lines +91 to +92
for ($poll = 0; $this->isPending($data); ++$poll) {
if ($poll >= $this->maxPolls) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decode() forces the response, so the whole poll loop runs inside Platform::invoke(), not when the result is read. Measured with a counting clock:

invoke() returned after 3 sleeps, 4 http calls
reading the result caused 0 further sleeps

With the defaults that is up to 120 × 1s inside invoke(), and the returned DeferredResult is not deferred for this model type.

#2408 is open and adds exactly this as a shared concern (Job\JobClientInterface, JobRunner, JobTimeoutException), with resumability this loop gives up. Not to be solved here, but worth ordering: adopting it once #2408 lands would drop the loop entirely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess that's also the reason for failing on my end:

Image

so let me pivot reviewing to 2408 to get that in :)

Comment on lines +51 to +52
$error = json_decode($response->getContent(false), true);
$errorMessage = $error['error']['message'] ?? $error['detail'] ?? 'Authentication failed.';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change and its test are right, but I could not reproduce the reason given in the description ("PHP warnings instead of throwing an AuthenticationException"). With a detail-only body, error as a string, a list, null, invalid JSON or an empty body, the old expression warns in none of them, ?? suppresses the offset notices, and it threw AuthenticationException every time, just with the generic message.

So this improves the message rather than fixing a warning. Worth rewording, since the description lands verbatim in the merge commit.

Comment thread examples/composer.json
"symfony/ai-deepgram-platform": "^0.12",
"symfony/ai-docker-model-runner-platform": "^0.12",
"symfony/ai-doctrine-message-store": "^0.12",
"symfony/ai-eden-ai-platform": "^0.12",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need dev-main plus a path repo here for making it work

Suggested change
"symfony/ai-eden-ai-platform": "^0.12",
"symfony/ai-eden-ai-platform": "dev-main",

The result exposes every ``Page`` with its markdown, dimensions, extracted layout images
(with bounding boxes) and optional annotations.

Eden AI exposes OCR and document parsing (invoices, resumes, identity documents) from multiple

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move the changes from here and below to a provider specific rst file like docs/components/platform/edenai.rst

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature New feature Platform Issues & PRs about the AI Platform component Status: Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants