Skip to content

PromptBuilder::isSupported() throws instead of returning false when the output modality has no matching capability #270

Description

@jigneshbhavani

PromptBuilder::isSupported() is declared : bool, but it throws a RuntimeException when the configured output modality does not map to a capability.

inferCapabilityFromOutputModalities() handles text, image, audio and video, and throws for anything else:

if ($outputModality->isText()) {
    return CapabilityEnum::textGeneration();
} elseif ($outputModality->isImage()) {
    return CapabilityEnum::imageGeneration();
} elseif ($outputModality->isAudio()) {
    return CapabilityEnum::speechGeneration();
} elseif ($outputModality->isVideo()) {
    return CapabilityEnum::videoGeneration();
} else {
    // For unsupported modalities, provide a clear error message
    throw new RuntimeException(
        sprintf('Output modality "%s" is not yet supported.', $outputModality->value)
    );
}

ModalityEnum has a DOCUMENT case, with document() and isDocument() both declared in the class docblock, and CapabilityEnum has no document capability. So document output reaches the else.

isSupported() calls it with nothing in between:

// If still no capability, infer from output modalities
if ($capability === null) {
    $capability = $this->inferCapabilityFromOutputModalities();
}

Steps to reproduce

$builder = new PromptBuilder($registry, 'Test prompt');
$builder->asOutputModalities(ModalityEnum::document());

$builder->isSupported(); // RuntimeException: Output modality "document" is not yet supported.

Expected

false. "Is this prompt supported" has a correct answer for an unsupported modality, and it is no. A caller checking support before generating has to wrap the call in a try/catch today, which is not what the signature suggests.

Notes

generateResult() throwing for the same modality is correct, and the suite already pins that in testGenerateResultThrowsExceptionForUnsupportedOutputModality(). There is no equivalent coverage going through isSupported(), which is how this stayed open.

Looking at where it came from, the throw was written for the generation path, and isSupported() inherited it later. Before d5d782e ("refactor: adjusts isSupported to work more like generateResults") the method was private, took an explicit capability, and never inferred anything from output modalities, so it had no way to throw. That refactor gave it the shared inference, and the throw came along with it. Making the method public in 0.3.0 then turned it into public API. Issue #128 asked for that change so callers could check support the same way they generate, and avoid awkward conditionals, which is the use that a thrown exception gets in the way of.

Reproduced on trunk at a31b0ec, after the 1.4.0 release. Also present in 1.3.1, which is the version currently vendored by WordPress 7.0.

There is a downstream effect worth mentioning. WordPress core wraps this class in WP_AI_Client_Prompt_Builder, whose __call() catches the exception but then returns the wrapper object from its support check methods rather than a boolean, so a check that failed reads as truthy. That is being fixed on the core side in https://core.trac.wordpress.org/ticket/65781, but the throw starts here.

I have a patch and a test for this, happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions