Skip to content

OpenAI constructor options api_key / base_url are ignored and env vars are used instead #5

Description

@KlimTodrik

Summary

llm-php-ext documents OpenAI constructor options such as api_key, base_url, and timeout, but in practice they are not applied at runtime.

For openai:* models, the extension appears to ignore constructor transport options and instead relies on environment variables from the underlying provider layer.

This makes per-instance OpenAI configuration impossible and causes surprising behavior:

  • a fake api_key still works if OPENAI_API_KEY is present in the environment
  • a broken constructor base_url is ignored
  • OPENAI_API_URL from the environment takes effect instead

Versions

  • llm-php-ext: 0.1.0
  • observed from: Manticore Buddy
  • underlying crate from Cargo.toml: octolib = 0.5.1

Minimal Reproducible Example

<?php declare(strict_types=1);

if (!extension_loaded('llm')) {
    fwrite(STDERR, "llm extension is not loaded\n");
    exit(1);
}

$messages = [Message::user('Reply with exactly OK')];

function runCase(string $name, array $clearEnv, array $setEnv, array $options, array $messages): void {
    foreach ($clearEnv as $key) {
        putenv($key);
        unset($_ENV[$key], $_SERVER[$key]);
    }

    foreach ($setEnv as $key => $value) {
        putenv($key . '=' . $value);
        $_ENV[$key] = $value;
        $_SERVER[$key] = $value;
    }

    echo "CASE: {$name}\n";
    echo "ENV OPENAI_API_KEY=" . (getenv('OPENAI_API_KEY') ?: '<unset>') . "\n";
    echo "ENV OPENAI_API_URL=" . (getenv('OPENAI_API_URL') ?: '<unset>') . "\n";
    echo "OPT api_key={$options['api_key']}\n";
    echo "OPT base_url={$options['base_url']}\n";

    try {
        $response = (new Llm('openai:gpt-4o-mini', $options))->complete($messages);
        echo "RESULT: SUCCESS\n";
        echo "CONTENT: " . trim($response->getContent()) . "\n";
    } catch (Throwable $e) {
        echo "RESULT: ERROR\n";
        echo "ERROR: " . $e->getMessage() . "\n";
    }

    echo str_repeat('-', 60) . "\n";
}

runCase(
    'env key present, fake constructor options',
    ['OPENAI_API_URL'],
    ['OPENAI_API_KEY' => getenv('OPENAI_API_KEY') ?: ''],
    [
        'api_key' => 'dummy-key',
        'base_url' => 'http://host.docker.internal:9999/v1',
        'timeout' => 2,
    ],
    $messages
);

runCase(
    'no env, same fake constructor options',
    ['OPENAI_API_KEY', 'OPENAI_API_URL'],
    [],
    [
        'api_key' => 'dummy-key',
        'base_url' => 'http://host.docker.internal:9999/v1',
        'timeout' => 2,
    ],
    $messages
);

runCase(
    'env API URL present, constructor base_url different',
    [],
    [
        'OPENAI_API_KEY' => getenv('OPENAI_API_KEY') ?: '',
        'OPENAI_API_URL' => 'http://host.docker.internal:9999/v1/responses',
    ],
    [
        'api_key' => 'dummy-key',
        'base_url' => 'http://host.docker.internal:8787/v1',
        'timeout' => 2,
    ],
    $messages
);

Expected Behavior

  • new Llm('openai:gpt-4o-mini', ['api_key' => ..., 'base_url' => ...]) should use the provided constructor options
  • if constructor api_key is invalid and constructor base_url points to a dead endpoint, the request should fail regardless of OPENAI_API_KEY in the environment
  • if constructor base_url is provided, it should take precedence for that instance

Actual Behavior

  • when OPENAI_API_KEY is present in the environment, the request succeeds even if constructor api_key is fake and constructor base_url is invalid
  • when OPENAI_API_KEY is removed, the same code fails with an error about missing OPENAI_API_KEY
  • when OPENAI_API_URL is set in the environment, it is used instead of constructor base_url

Why This Happens

From the current source:

  • the constructor accepts _options but does not use them
  • withOptions() only applies generation parameters like temperature and max_tokens
  • runtime provider resolution is done only from the model string

Relevant code:

  • src/llm_class.rs
    • __construct(model, _options) ignores _options
    • complete() calls ProviderFactory::get_provider_for_model(&self.model)

Impact

  • per-model or per-instance OpenAI credentials are not possible
  • per-model or per-instance OpenAI proxy routing is not possible
  • libraries integrating llm-php-ext can silently use process-level env configuration instead of user-provided runtime settings

Suggested Fix

One of these should happen:

  1. implement actual support for constructor transport options:

    • api_key
    • base_url
    • timeout
  2. or explicitly remove/document these options as unsupported for now

At minimum, the current behavior should not silently ignore runtime transport options.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions