getMethod($name); return $method->invoke(null, ...$arguments); } $generic = callPrivate('buildRequestSpecs', [ 'https://ai.example.test/v1', 'model-name', ['prompt_version' => 'test'], 'clinical prompt', 'server-user', ]); expectSame(2, count($generic), 'ambiguous /v1 base should support both protocols'); expectSame('https://ai.example.test/v1/chat-messages', $generic[0]['url'], 'Dify endpoint'); expectSame('blocking', $generic[0]['payload']['response_mode'], 'Dify blocking request'); expectSame('https://ai.example.test/v1/chat/completions', $generic[1]['url'], 'OpenAI endpoint'); expectSame('model-name', $generic[1]['payload']['model'], 'profile model selection'); expectSame('clinical prompt', $generic[1]['payload']['messages'][0]['content'], 'OpenAI prompt'); $serializedSpecs = json_encode($generic, JSON_UNESCAPED_SLASHES) ?: ''; expectSame(false, str_contains($serializedSpecs, 'api_key'), 'credential field is absent from request bodies'); expectSame(false, str_contains($serializedSpecs, 'provider'), 'provider override is absent from request bodies'); expectSame(false, str_contains($serializedSpecs, 'base_url'), 'base URL override is absent from request bodies'); $openAi = callPrivate('buildRequestSpecs', [ 'https://ai.example.test/v1/chat/completions', 'model-name', [], 'prompt', 'server-user', ]); expectSame(1, count($openAi), 'explicit OpenAI endpoint should not probe Dify'); expectSame('openai', $openAi[0]['protocol'], 'explicit OpenAI protocol'); $dify = callPrivate('buildRequestSpecs', [ 'https://ai.example.test/v1/chat-messages', 'model-name', [], 'prompt', 'server-user', ]); expectSame(1, count($dify), 'explicit Dify endpoint should not probe OpenAI'); expectSame('dify', $dify[0]['protocol'], 'explicit Dify protocol'); expectSame('Dify answer', callPrivate('extractContent', [['answer' => ' Dify answer ']]), 'Dify response'); expectSame( 'OpenAI answer', callPrivate('extractContent', [['choices' => [['message' => ['content' => ' OpenAI answer ']]]]]), 'OpenAI response' ); expectSame( 'multipart answer', callPrivate('extractContent', [['choices' => [['message' => ['content' => [ ['type' => 'text', 'text' => 'multipart '], ['type' => 'text', 'text' => 'answer'], ]]]]]]), 'OpenAI multipart response' ); expectSame(true, callPrivate('isValidBaseUrl', ['https://ai.example.test/v1']), 'https URL'); expectSame(true, callPrivate('isValidBaseUrl', ['http://127.0.0.1:8080/v1']), 'internal http URL'); expectSame(false, callPrivate('isValidBaseUrl', ['file:///tmp/socket']), 'non-http URL'); expectSame(false, callPrivate('isValidBaseUrl', ['https://user@example.test/v1']), 'userinfo URL'); expectSame(false, callPrivate('isValidBaseUrl', ['https://ai.example.test/v1?unsafe=query']), 'query URL'); expectSame(true, callPrivate('isValidTimeout', [90]), 'normal timeout'); expectSame(false, callPrivate('isValidTimeout', [0]), 'zero timeout'); expectSame(false, callPrivate('isValidTimeout', [301]), 'excessive timeout'); echo "Prescription AI upstream contract: OK\n";