Skip to content

Conversation

@SenreySong
Copy link

The extraBody field was being lost when merging OpenAiChatOptions into ChatCompletionRequest using ModelOptionsUtils.merge(). This fix ensures that extraBody parameters (like top_k, repetition_penalty, etc.) are properly preserved during the merge operation.

Added test ExtraBodyMergeTest to verify the fix.

The extraBody field was being lost when merging OpenAiChatOptions
into ChatCompletionRequest using ModelOptionsUtils.merge(). This fix
ensures that extraBody parameters (like top_k, repetition_penalty, etc.)
are properly preserved during the merge operation.

Added test ExtraBodyMergeTest to verify the fix.

Signed-off-by: SenreySong <25841017+SenreySong@users.noreply.github.com>
@SenreySong
Copy link
Author

@markpollack
This PR fixes an issue where the extraBody field was being lost during merge operations. The root cause was that extraBody lacks a @JsonProperty annotation, so the generic field filtering in the merge method was excluding it.
The fix explicitly includes extraBody in the preserved fields list during merge operations. Please let me know if you have any questions!

@markpollack
Copy link
Member

This is not the behavior in ser/deser that is expected for 'extra_body'. The field 'extra_body' does not go into the JSON, it get's flattened. You can see this in the openai java sdk here - and the assertion here.

Also this vllm issue clarifies it more

Note that the extra_body argument is not part of the server’s REST API and if you pass extra_body as an argument within an HTTP client request, the server will ignore it. extra_body is simply a “catch-all” argument supported by the Python SDK to handle “special” parameters. Internally, the SDK unpacks extra_body into REST API arguments. The server does not see the extra_body argument.

@SenreySong
Copy link
Author

This is not the behavior in ser/deser that is expected for 'extra_body'. The field 'extra_body' does not go into the JSON, it get's flattened. You can see this in the openai java sdk here - and the assertion here.

Also this vllm issue clarifies it more

Note that the extra_body argument is not part of the server’s REST API and if you pass extra_body as an argument within an HTTP client request, the server will ignore it. extra_body is simply a “catch-all” argument supported by the Python SDK to handle “special” parameters. Internally, the SDK unpacks extra_body into REST API arguments. The server does not see the extra_body argument.

Thank you for clarifying the intended behavior of extra_body in the REST API — I completely understand that extra_body is not a direct parameter sent to the server, but rather a convenience mechanism used by the Python SDK to collect optional or non-standard parameters, which are then flattened into the actual request body before transmission.

That said, in this Java implementation, the issue isn’t about misrepresenting extra_body in the payload — it’s that the extra_body object itself is being silently dropped during the merge step:

inOpenAiChatModel-> ChatCompletionRequest createRequest(Prompt prompt, boolean stream)

ChatCompletionRequest request = new ChatCompletionRequest(chatCompletionMessages, stream);

OpenAiChatOptions requestOptions = (OpenAiChatOptions) prompt.getOptions();
request = ModelOptionsUtils.merge(requestOptions, request, ChatCompletionRequest.class);

The current ModelOptionsUtils.merge logic only copies fields annotated with @JsonProperty, and since extra_body is not one of those fields, it’s entirely omitted from the final request. As a result, even though the SDK should later flatten its contents, those contents never make it into the request object to begin with — so the flattening step has nothing to work with.

I believe the fix should ensure that extra_body, when present, is preserved during the merge so that the subsequent deserialization/unpacking logic (as used in the Python SDK) can still function as intended. This isn’t about changing how extra_body works — it’s about making sure it isn’t lost before it even gets to that stage.

@SenreySong
Copy link
Author

@markpollack By default, this merge operation only processes fields in the target class that are annotated with @JsonProperty; the extra_body field, lacking this annotation, is discarded entirely.

public static <T> T merge(Object source, Object target, Class<T> clazz) {
    return ModelOptionsUtils.merge(source, target, clazz, null);
}

public static <T> T merge(Object source, Object target, Class<T> clazz, List<String> acceptedFieldNames) {

    if (source == null) {
        source = Map.of();
    }

    List<String> requestFieldNames = CollectionUtils.isEmpty(acceptedFieldNames)
            ? REQUEST_FIELD_NAMES_PER_CLASS.computeIfAbsent(clazz, ModelOptionsUtils::getJsonPropertyValues)
            : acceptedFieldNames;

    if (CollectionUtils.isEmpty(requestFieldNames)) {
        throw new IllegalArgumentException("No @JsonProperty fields found in the " + clazz.getName());
    }

    Map<String, Object> sourceMap = ModelOptionsUtils.objectToMap(source);
    Map<String, Object> targetMap = ModelOptionsUtils.objectToMap(target);

    targetMap.putAll(sourceMap.entrySet()
        .stream()
        .filter(e -> e.getValue() != null)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));

    targetMap = targetMap.entrySet()
        .stream()
        .filter(e -> requestFieldNames.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

    return ModelOptionsUtils.mapToClass(targetMap, clazz);
}```

@cytle
Copy link

cytle commented Nov 19, 2025

@markpollack I encountered the same issue as @SenreySong . For the qwen3 model, model-specific parameters like enable_thinking must be passed through the extraBody mechanism, but currently extraBody is being ruthlessly discarded.

Ref: https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2712576

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants