This makes per-instance OpenAI configuration impossible and causes surprising behavior:
<?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
);
At minimum, the current behavior should not silently ignore runtime transport options.
Summary
llm-php-extdocuments OpenAI constructor options such asapi_key,base_url, andtimeout, 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:
api_keystill works ifOPENAI_API_KEYis present in the environmentbase_urlis ignoredOPENAI_API_URLfrom the environment takes effect insteadVersions
llm-php-ext:0.1.0Manticore BuddyCargo.toml:octolib = 0.5.1Minimal Reproducible Example
Expected Behavior
new Llm('openai:gpt-4o-mini', ['api_key' => ..., 'base_url' => ...])should use the provided constructor optionsapi_keyis invalid and constructorbase_urlpoints to a dead endpoint, the request should fail regardless ofOPENAI_API_KEYin the environmentbase_urlis provided, it should take precedence for that instanceActual Behavior
OPENAI_API_KEYis present in the environment, the request succeeds even if constructorapi_keyis fake and constructorbase_urlis invalidOPENAI_API_KEYis removed, the same code fails with an error about missingOPENAI_API_KEYOPENAI_API_URLis set in the environment, it is used instead of constructorbase_urlWhy This Happens
From the current source:
_optionsbut does not use themwithOptions()only applies generation parameters liketemperatureandmax_tokensRelevant code:
src/llm_class.rs__construct(model, _options)ignores_optionscomplete()callsProviderFactory::get_provider_for_model(&self.model)Impact
llm-php-extcan silently use process-level env configuration instead of user-provided runtime settingsSuggested Fix
One of these should happen:
implement actual support for constructor transport options:
api_keybase_urltimeoutor explicitly remove/document these options as unsupported for now
At minimum, the current behavior should not silently ignore runtime transport options.