OpenAI Compatibility API does not support message.refusal field set to null

When using the Gemini OpenAI Compatibility API, if a message object contains a refusal field with the value null, Gemini returns a 400 Value is not a string: null error.

Example request:

{
  "messages" : [
    {
      "content" : "hi",
      "role" : "user"
    },
    {
      "role" : "assistant",
      "content" : "Hi there! How can I help you today?",
      "refusal" : null,
      "tool_calls" : []
    },
    {
      "content" : "how are you",
      "role" : "user"
    }
  ],
  "model" : "gemini-2.5-flash",
  "stream" : true
}

According to the OpenAI API specification, the refusal field is allowed to be null.

I have tested both the Java and Python OpenAI SDKs. When calling toParam, the generated request includes the refusal field with a null value. If you return it directly causes a BadRequestError while using Gemini OpenAI Compatibility.

As a temporary workaround, I can either manually remove the refusal field or set it to an empty string. However, the preferred solution is for Gemini’s compatibility layer to correctly handle refusal: null, since this is the default behavior in OpenAI’s SDKs.

1 Like

Hello,

Welcome to the Forum!

From your input, I understand that you are struggling with multi-turn conversations. I am assuming you are using response = client.chat.completions.create() as the endpoint.

For handling multi-turn conversations, you can try using response.choices[0].message.to_dict() and then append this to your conversation. Doing so should remove the refusal field.

Thank you for the suggested workaround. In my case, I’m primarily using the Java client:

public Flux<ChatCompletionChunk> sendMessageStream(ChatSession chat, OpenAIRequest request) {
    ChatCompletionAccumulator chatCompletionAccumulator = ChatCompletionAccumulator.create();
    return generateContentStream(chat, request).doOnNext(chatCompletionAccumulator::accumulate).doOnComplete(() -> {
        ChatCompletion chatCompletion = chatCompletionAccumulator.chatCompletion();
        chatCompletion.choices().forEach(choice -> {
            ChatCompletionMessage.Builder assistant = choice.message().toBuilder();
            // set null refusal json ignored
            if (choice.message().refusal().isEmpty()) {
                assistant.refusal(JsonMissing.of());
            }
            chat.addHistory(ChatCompletionMessageParam.ofAssistant(assistant.build().toParam()));
        });
    });
}

As shown above, I currently have to manually check and handle the refusal field. While it’s true that this can be resolved relatively easily on the client side, the expected behavior should not require client-side handling. I’d prefer this to be fixed at the Compatibility level so that clients don’t have to implement exception handling logic for a spec compliance issue.

1 Like

Hello,

Could you try align your code with code provided in documentation. This should return an object with two keys: [ 'content', 'role' ].

Code is as:

import OpenAI from "openai";

const openai = new OpenAI({
    apiKey: "GEMINI_API_KEY",
    baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
});

const response = await openai.chat.completions.create({
    model: "gemini-2.5-flash",   
    messages: [
        { role: "system", content: "You are a helpful assistant." },
        {
            role: "user",
            content: "Explain to me how AI works",
        },
    ],
});

console.log(response.choices[0].message);

Hi, Lalit_Kumar

I understand that the format you provided works, and it is valid according to the Google documentation. However, in practice, the Java OpenAI SDK generates messages in the format I demonstrated in my example above. This format is consistent with OpenAI’s official specification and is widely used in client implementations.

Given this, I believe the OpenAI compatibility layer in Gemini should ideally handle the refusal field automatically, regardless of whether it is explicitly null or omitted, to align with the OpenAI standard. Requiring manual adjustments on the client side can lead to inconsistencies and additional overhead for developers using standard OpenAI SDKs.

Hello,

Could you please share the reproduction steps along with your full API request payload? We will try to reproduce the issue on our side to analyze it further.

Sure, with openai-java 3.1.2 sdk:

    public void runOpenAITest() {
        OpenAIClient client = OpenAIOkHttpClient.builder().baseUrl("https://generativelanguage.googleapis.com/v1beta/openai/").apiKey("KEY").build();

        ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder();
        builder.model("gemini-2.5-flash");
        builder.addMessage(ChatCompletionMessageParam.ofUser(ChatCompletionUserMessageParam.builder().content("hello").build()));

        Mono.fromCallable(() -> {
                System.out.println("First request...");
                return builder.build();
            })
            .flatMapMany(params -> generateContentStream(client, params))
            .collect(ChatCompletionAccumulator::create, ChatCompletionAccumulator::accumulate)
            .flatMap(accumulator -> {
                ChatCompletion chatCompletion = accumulator.chatCompletion();
                chatCompletion.choices().forEach(choice -> {
                    System.out.println("First reply: " + choice.message().content());
                    ChatCompletionMessage.Builder assistant = choice.message().toBuilder();
                    // ***** should set null refusal to json ignored manully
                    //if (choice.message().refusal().isEmpty()) {
                    //    assistant.refusal(JsonMissing.of());
                    //}
                    builder.addMessage(assistant.build());
                });
                builder.addMessage(ChatCompletionMessageParam.ofUser(ChatCompletionUserMessageParam.builder().content("how are you").build()));
                return Mono.just(builder.build());
            })
            .flatMapMany(params -> {
                System.out.println("Second request...");
                return generateContentStream(client, params);
            })
            .collect(ChatCompletionAccumulator::create, ChatCompletionAccumulator::accumulate)
            .doOnNext(accumulator -> {
                ChatCompletion chatCompletion = accumulator.chatCompletion();
                chatCompletion.choices().forEach(choice -> {
                    System.out.println("Second reply: " + choice.message().content());
                });
            })
            .doOnTerminate(() -> {
                System.out.println("All requests processed");
            })
            .doOnError(error -> {
                System.err.println("Error processing: " + error.getMessage());
                error.printStackTrace();
            })
            .subscribe();
    }

    private static Flux<ChatCompletionChunk> generateContentStream(OpenAIClient client, ChatCompletionCreateParams request) {
        System.out.println(request.toString());
        return Flux.create(emitter -> {
            AsyncStreamResponse<ChatCompletionChunk> streamResponse = client.async().chat().completions().createStreaming(request);
            streamResponse.subscribe(new AsyncStreamResponse.Handler<>() {
                @Override
                public void onNext(ChatCompletionChunk event) {
                    emitter.next(event);
                }

                @Override
                public void onComplete(@NotNull Optional<Throwable> error) {
                    if (error.isPresent()) {
                        emitter.error(error.get());
                    }
                    else {
                        emitter.complete();
                    }
                }
            });
        });
    }

Result without the field handler, as refusal=null in the params:

First request...
ChatCompletionCreateParams{body=Body{messages=[ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=hello}, role=user, name=, additionalProperties={}}}], model=gemini-2.5-flash, audio=, frequencyPenalty=, functionCall=, functions=, logitBias=, logprobs=, maxCompletionTokens=, maxTokens=, metadata=, modalities=, n=, parallelToolCalls=, prediction=, presencePenalty=, promptCacheKey=, reasoningEffort=, responseFormat=, safetyIdentifier=, seed=, serviceTier=, stop=, store=, streamOptions=, temperature=, toolChoice=, tools=, topLogprobs=, topP=, user=, verbosity=, webSearchOptions=, additionalProperties={}}, additionalHeaders=Headers{map={}}, additionalQueryParams=QueryParams{map={}}}
First reply: Optional[Hello! How can I help you today?]
Second request...
ChatCompletionCreateParams{body=Body{messages=[ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=hello}, role=user, name=, additionalProperties={}}}, ChatCompletionMessageParam{assistant=ChatCompletionAssistantMessageParam{role=assistant, audio=, content=Content{text=Hello! How can I help you today?}, functionCall=, name=, refusal=null, toolCalls=[], additionalProperties={}}}, ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=how are you}, role=user, name=, additionalProperties={}}}], model=gemini-2.5-flash, audio=, frequencyPenalty=, functionCall=, functions=, logitBias=, logprobs=, maxCompletionTokens=, maxTokens=, metadata=, modalities=, n=, parallelToolCalls=, prediction=, presencePenalty=, promptCacheKey=, reasoningEffort=, responseFormat=, safetyIdentifier=, seed=, serviceTier=, stop=, store=, streamOptions=, temperature=, toolChoice=, tools=, topLogprobs=, topP=, user=, verbosity=, webSearchOptions=, additionalProperties={}}, additionalHeaders=Headers{map={}}, additionalQueryParams=QueryParams{map={}}}
Error processing: com.openai.errors.BadRequestException: 400: null

Result with the field hander, as refusal= and ignored in the params:

First request...
ChatCompletionCreateParams{body=Body{messages=[ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=hello}, role=user, name=, additionalProperties={}}}], model=gemini-2.5-flash, audio=, frequencyPenalty=, functionCall=, functions=, logitBias=, logprobs=, maxCompletionTokens=, maxTokens=, metadata=, modalities=, n=, parallelToolCalls=, prediction=, presencePenalty=, promptCacheKey=, reasoningEffort=, responseFormat=, safetyIdentifier=, seed=, serviceTier=, stop=, store=, streamOptions=, temperature=, toolChoice=, tools=, topLogprobs=, topP=, user=, verbosity=, webSearchOptions=, additionalProperties={}}, additionalHeaders=Headers{map={}}, additionalQueryParams=QueryParams{map={}}}
First reply: Optional[Hello! How can I help you today?]
Second request...
ChatCompletionCreateParams{body=Body{messages=[ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=hello}, role=user, name=, additionalProperties={}}}, ChatCompletionMessageParam{assistant=ChatCompletionAssistantMessageParam{role=assistant, audio=, content=Content{text=Hello! How can I help you today?}, functionCall=, name=, refusal=, toolCalls=[], additionalProperties={}}}, ChatCompletionMessageParam{user=ChatCompletionUserMessageParam{content=Content{text=how are you}, role=user, name=, additionalProperties={}}}], model=gemini-2.5-flash, audio=, frequencyPenalty=, functionCall=, functions=, logitBias=, logprobs=, maxCompletionTokens=, maxTokens=, metadata=, modalities=, n=, parallelToolCalls=, prediction=, presencePenalty=, promptCacheKey=, reasoningEffort=, responseFormat=, safetyIdentifier=, seed=, serviceTier=, stop=, store=, streamOptions=, temperature=, toolChoice=, tools=, topLogprobs=, topP=, user=, verbosity=, webSearchOptions=, additionalProperties={}}, additionalHeaders=Headers{map={}}, additionalQueryParams=QueryParams{map={}}}
Second reply: Optional[As an AI, I don't have feelings or a physical state like humans do, so I can't be "good" or "bad" in the traditional sense.

However, I'm functioning perfectly and ready to assist you! How are you doing today?]
All requests processed

Hello,

We have noted your feedback and shared it with the concerned team. Thank you for your patience.