Skip to content

Conversation

qbejs
Copy link

@qbejs qbejs commented Sep 3, 2025

Q A
Bug fix? no
New feature? yes
Docs? yes
Issues Fix #402
License MIT

This PR implements RFC #402 by introducing a ProviderFactory and ProviderConfigFactory
to centralize the creation of AI provider platforms (OpenAI, Azure OpenAI, Azure Meta) from DSNs.

What’s new

  • ProviderConfigFactory::fromDsn()
    Parses DSNs into a strongly typed ProviderConfig (provider, baseUri, apiKey, options, headers).
    Supports Azure family selection via engine (e.g. openai, meta) and common options like deployment, version, timeout, proxy, verify_peer, as well as headers[...].

  • ProviderFactory::fromDsn()
    Instantiates the correct bridge PlatformFactory (OpenAi, Azure\OpenAi, Azure\Meta) using the parsed config.
    Sets proper authentication headers per provider (Authorization: Bearer … or api-key for Azure), and propagates base_uri, options, and headers to the bridge.

  • Unit tests (positive & negative)
    Test coverage for both factories, including edge-cases for DSN parsing (e.g. no host → defaults, Azure validation).
    A dedicated PHPUnit configuration phpunit.provider-factory.xml runs tests that depend on fake bridge factories (see rationale below).
    A standard suite continues to run with the default phpunit.xml(.dist) without interference.

  • Changelog
    Entry added in src/platform/CHANGELOG.md.

Why

This provides a unified, extensible mechanism to construct provider platforms from DSNs.
It makes configuration easier, allows future providers to be added consistently,
and aligns with the design goals outlined in RFC #402.


Why a dedicated PHPUnit config for some tests?

The existing bridges expose static factory methods (e.g. Bridge\*\PlatformFactory::create(...)).
Static factories are not mock-friendly: traditional PHPUnit doubles cannot intercept static calls and signatures differ per bridge.
To test ProviderFactory::fromDsn() deterministically without coupling the suite to real providers, we load fake bridge factories under the same FQCN via a bootstrap that prepends a PSR-4 mapping for the Bridge\ namespace.

  • The special suite (phpunit.provider-factory.xml) uses tests/bootstrap_fake_bridges.php to ensure fake classes are loaded before the real ones.
  • The main suite keeps using the default bootstrap, so real bridge tests remain untouched.
  • These special tests are grouped (e.g. @group pf) and excluded from the main suite to avoid accidental mixing.

This split lets us:

  • keep real bridge tests pristine,
  • test the orchestration logic in ProviderFactory in isolation,
  • avoid reflective heuristics around static method signatures.

Proposed follow-up Improvement (separate PR/RFC)

To remove the need for a separate PHPUnit config and fakes in the future, I propose introducing a thin, optional abstraction layer:

  1. BridgePlatformFactoryInterface (small contract)
    interface BridgePlatformFactoryInterface {
        public function create(string $apiKey, HttpClientInterface $http, array $contract): object;
        public static function providerKey(): string;   // e.g. "openai", "azure"
        public static function engine(): ?string;       // e.g. "openai" | "meta" for Azure, null otherwise
    }

Adapters per existing bridge would simply delegate to the current static PlatformFactory::create(...).

  1. BridgeFactoryResolverInterface + default implementation
    A registry that maps (providerKey, engine) → the corresponding adapter (injected via DI).
    ProviderFactory::fromDsn() would ask the resolver for the correct adapter, then call ->create(...).

@carsonbot carsonbot added Feature New feature Platform Issues & PRs about the AI Platform component Status: Needs Review labels Sep 3, 2025
@qbejs qbejs force-pushed the feat/platform-dsn-provider-config-402 branch from bf08cff to 3055064 Compare September 3, 2025 16:07

use Symfony\AI\Platform\Exception\InvalidArgumentException;

final class Dsn
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does this code com from? We have several DSN classes in symfony/symfony which should be used. I propose to copy it from symonfy/notifer including tests.

public function getProvider(): string
{
$scheme = strtolower($this->scheme);
if (str_starts_with($scheme, 'ai+')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

no need for a prefix here


use Symfony\Contracts\HttpClient\HttpClientInterface;

final class PlatformFactory
Copy link
Contributor

Choose a reason for hiding this comment

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

these classes look weird

@OskarStark
Copy link
Contributor

Glad to have you here @qbejs 👋 😄

Comment on lines +64 to +67

## [Unreleased]

- Introduced `ProviderFactory` and `ProviderConfigFactory` to create AI provider platforms from DSNs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
## [Unreleased]
- Introduced `ProviderFactory` and `ProviderConfigFactory` to create AI provider platforms from DSNs.
- `ProviderFactory` and `ProviderConfigFactory` to create AI provider platforms from DSNs

Comment on lines +125 to +132
return match ($provider) {
'openai' => 'api.openai.com',
'anthropic' => 'api.anthropic.com',
'gemini' => 'generativelanguage.googleapis.com',
'vertex' => 'us-central1-aiplatform.googleapis.com',
'ollama' => 'localhost',
'azure' => throw new InvalidArgumentException('Azure DSN must specify host (e.g. "<resource>.openai.azure.com").'),
default => throw new InvalidArgumentException(\sprintf('Unknown AI provider "%s".', $provider)),
Copy link
Contributor

Choose a reason for hiding this comment

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

Those should not be defined in the DSN class, but rather (like symfony/notfier does) in the AIBundle.php, ofc SfNotfier does this in FrameworkBundle


if ('azure' === $providerKey) {
$engine = strtolower($config->options['engine'] ?? 'openai');
$factoryFqcn = match ($engine) {
Copy link
Contributor

@OskarStark OskarStark Sep 4, 2025

Choose a reason for hiding this comment

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

Lets go with dedicated PlatformFactory classes in each bridge (like the bridges in symfony/symfony)

@chr-hertel
Copy link
Member

I'd prefer not to introduce the new term Provider here, but stick to Platform

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 Status: Needs Work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Platform DSN for AI Provider Configuration
4 participants