200 || preg_match('/[\x00-\x20\x7f]/', $model) || trim($label) === '' || strlen($label) > 200 || preg_match('/[\x00-\x1f\x7f]/', $label)) { throw new FollowupAudioException('CONFIG_INVALID'); } $base = rtrim($base, '/'); $path = rtrim((string) ($parts['path'] ?? ''), '/'); if ($driver === 'dify' && str_ends_with($path, '/chat/completions')) { throw new FollowupAudioException('DIFY_APPLICATION_REQUIRED'); } if ($driver === 'openai_audio' && str_ends_with($path, '/chat-messages')) { throw new FollowupAudioException('CONFIG_INVALID'); } $endpoint = $driver === 'dify' ? '/chat-messages' : '/chat/completions'; if (str_ends_with($base, $endpoint)) { $base = substr($base, 0, -strlen($endpoint)); } elseif (!str_ends_with($base, '/v1')) { $base .= '/v1'; } $fingerprint = hash('sha256', json_encode([$driver, $base, $model, $key], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); return ['driver' => $driver, 'base_url' => $base, 'api_key' => $key, 'model' => $model, 'label' => $label, 'fingerprint' => $fingerprint]; } /** No endpoint, model credential or plaintext provider data in capability responses. */ public static function status(string $profile, ?array $settings = null, ?array $legacy = null): array { try { $provider = self::resolve($profile, $settings, $legacy); return ['configured' => true, 'profile' => $profile, 'code' => 'OK', 'application_fingerprint' => $provider['fingerprint']]; } catch (FollowupAudioException $error) { return ['configured' => false, 'profile' => in_array($profile, ['qwen', 'openai'], true) ? $profile : 'invalid', 'code' => $error->errorCode]; } } public static function isVerified(string $profile): bool { return self::verified($profile, (array) config('followup_audio', []), (array) config('prescription_ai', [])); } /** Shared with injected-config transport tests; enabled remains an independent operational switch. */ public static function verified(string $profile, array $settings, array $legacy): bool { if (empty($settings['audio_verified']) || !in_array($profile, (array) ($settings['verified_profiles'] ?? []), true)) { return false; } try { $provider = self::resolve($profile, $settings, $legacy); } catch (FollowupAudioException $error) { return false; } $verified = (string) ($settings['providers'][$profile]['verified_fingerprint'] ?? ''); return str_starts_with($provider['base_url'], 'https://') && preg_match('/^[a-f0-9]{64}$/D', $verified) && hash_equals($provider['fingerprint'], $verified); } public static function publicModels(): array { $models = []; foreach (['qwen', 'openai'] as $profile) { if (self::isVerified($profile)) { $models[] = ['value' => $profile, 'label' => self::resolve($profile)['label']]; } } return $models; } }