Skip to content

Regression of #371 — ResponsesModel.toString() leaks into Azure deployment URL path via PrepareRequest.modelNameOrNull() #967

Description

@lzandman

Regression of #371ResponsesModel.toString() leaks into Azure deployment URL path via PrepareRequest.modelNameOrNull()

Description

When using the Responses API (client.responses().create(...)) against an Azure OpenAI / Azure AI Foundry endpoint configured with a deployment-based (legacy-style) base URL, the generated request URL contains the toString() representation of the internal ResponsesModel wrapper instead of the plain model/deployment name.

This is the same symptom originally reported in #371 ("Malformed URL for Response API when using Azure OpenAI"), which was fixed in v0.44.2 via #387. It appears the fix did not account for the ResponsesModel union/sealed type that was introduced later (supporting a plain string, a ChatModel enum, or a "Responses-only" model), so the same failure mode has resurfaced through a different path.

Reproduction

AzureOpenAIClient client = OpenAIOkHttpClient.builder()
    .baseUrl("https://<resource>.example.azure-api.net/foundry/") // deployment-based / legacy Azure URL mode
    .credential(BearerTokenCredential.create(() -> "<token>"))
    .build();

ResponseCreateParams params = ResponseCreateParams.builder()
    .model("gpt-5.4")
    .input("hello")
    .build();

client.responses().create(params);

Expected URL

POST https://<resource>.example.azure-api.net/foundry/openai/deployments/gpt-5.4/responses?api-version=2024-10-21

Actual URL

POST https://<resource>.example.azure-api.net/foundry/openai/deployments/ResponsesModel%7Bstring=gpt-5.4%7D/responses?api-version=2024-10-21

URL-decoded, the deployment path segment is literally ResponsesModel{string=gpt-5.4} instead of gpt-5.4. The backend/gateway correctly rejects this with a 404 since no such deployment exists.

Root cause

com.openai.core.PrepareRequest.modelNameOrNull() uses Kotlin reflection to find a method named model on the request params, invokes it, unwraps the result if it's an Optional, and then unconditionally calls .toString() on whatever remains:

internal fun Params.modelNameOrNull(): String? {
    val modelName =
        try {
            this::class.declaredFunctions.find { it.name == "model" }?.call(this)
        } catch (_: Exception) {
            null
        }

    return when (modelName) {
        is Optional<*> -> modelName.orElse(null)?.toString()
        else -> modelName?.toString()
    }
}

For ResponseCreateParams, .model() returns Optional<ResponsesModel>. ResponsesModel is a union/sealed wrapper (it can hold a plain String, a ChatModel, or a ResponsesOnlyModel), and its toString() is implemented for debugging/logging purposes:

override fun toString(): String =
    if (this.string != null) "ResponsesModel{string=${this.string}}"
    else if (this.chat != null) "ResponsesModel{chat=${this.chat}}"
    else if (this.only != null) "ResponsesModel{only=${this.only}}"
    else "ResponsesModel{_unknown=${this._json}}"

Because modelNameOrNull() calls .toString() instead of resolving the wrapper via its accessors (e.g. checking isString() and calling asString(), or otherwise handling the chat/only variants), the debug-style string leaks directly into the URL path.

This is analogous to the originally-reported #371 symptom and to #492 (a similar "Optional[ ]" leak for the Azure Image Service), suggesting modelNameOrNull()'s generic reflection + toString() approach is fragile whenever a request type's model field is backed by a union/wrapper type rather than a plain String or simple enum.

Sample app

repro.zip

This Java console app reproduces a bug in the OpenAI Java SDK where the ResponsesModel.toString()
representation leaks into Azure deployment URLs instead of using the plain model name.

The app uses a local MockWebServer to capture the actual request URL generated by the SDK
when making a Responses API call against an Azure OpenAI / Azure AI Foundry endpoint configured
with a deployment-based (legacy-style) base URL. It requires no credentials and sends no
actual request to Azure.

This behavior is a regression of openai-java issue #371.

Build and Run

The reproduction is pinned to com.openai:openai-java:4.56.0 in pom.xml.
Pinning the dependency keeps the sample behavior reproducible if a future SDK version changes
the URL handling.

To build and run:

mvn clean compile exec:java

The output shows the SDK-generated request path, making the ResponsesModel leak
visible without manually constructing the URL.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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