koog icon indicating copy to clipboard operation
koog copied to clipboard

Unexpected 'null' value instead of string literal at path: $.choices[0].finishReason

Open adamglin0 opened this issue 2 months ago • 1 comments

When using some models in OpenRouter, the following error may occur. It is certain that https://openrouter.ai/qwen/qwen3-235b-a22b-2507

[DefaultDispatcher-worker-2] ERROR ai.koog.agents.core.agent.AIAgent - [agent id: 882f9190-b9b2-4c17-887d-7037fef422f3, run id: c8b11a22-e87f-439e-a544-40daec91138a] Reporting problem: Unexpected JSON token at offset 339: Unexpected 'null' value instead of string literal at path: $.choices[0].finishReason
JSON input: .....obs":null,"finish_reason":null,"native_finish_reason":null,".....
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 339: Unexpected 'null' value instead of string literal at path: $.choices[0].finishReason
JSON input: .....obs":null,"finish_reason":null,"native_finish_reason":null,".....
	at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
	at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
	at kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:587)
	at kotlinx.serialization.json.internal.AbstractJsonLexer.fail$default(AbstractJsonLexer.kt:585)
	at kotlinx.serialization.json.internal.AbstractJsonLexer.consumeStringLenientNotNull(AbstractJsonLexer.kt:430)
	at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeString(StreamingJsonDecoder.kt:338)
	at kotlinx.serialization.encoding.AbstractDecoder.decodeStringElement(AbstractDecoder.kt:58)
	at ai.koog.prompt.executor.clients.openai.base.models.OpenAIChoice$$serializer.deserialize(OpenAIDataModels.kt:738)

adamglin0 avatar Sep 08 '25 00:09 adamglin0

In Koog source code, the nullability of finishReason is inconsistent. Below is the current data class.

@Serializable
public class OpenAIStreamChoice(
    public val delta: OpenAIStreamDelta,
    public val finishReason: String? = null,
    public val index: Int,
    public val logprobs: OpenAIChoiceLogProbs? = null,
)
@Serializable
public class OpenAIChoice(
    public val finishReason: String,
    public val index: Int,
    public val logprobs: OpenAIChoiceLogProbs? = null,
    public val message: OpenAIMessage,
)

adamglin0 avatar Sep 08 '25 00:09 adamglin0

  • In OpenAI the finishReason is required field (proof)
  • In openRouter it is optional https://openrouter.ai/docs/api-reference/overview#completionsresponse-format.

Would it make sense to create OpenRouterChoice object for open router instead of using OpenAIChoice from base openai models?

kpavlov avatar Sep 10 '25 15:09 kpavlov

  • In OpenAI the finishReason is required field (proof)
  • In openRouter it is optional https://openrouter.ai/docs/api-reference/overview#completionsresponse-format.

Would it make sense to create OpenRouterChoice object for open router instead of using OpenAIChoice from base openai models?

Actually, many providers are not truly compatible with OpenAI's API, although they claim to be. To reduce the workload, I think creating a more lenient OpenAI-compatible client might also be a method.

adamglin0 avatar Sep 11 '25 04:09 adamglin0

Hello , I want to inform you about an issue I encountered during deserialization of the OpenAI API response. The field finish_reason was sometimes returned as null, but our model was expecting a non-nullable String. Resolution: I updated the data model to handle finish_reason as a nullable field with proper @SerialName mapping. This ensures the application no longer fails when the API returns null.

@Serializable data class OpenAIChoice( @SerialName("finish_reason") val finishReason: String? = null )

The issue has been resolved and everything is functioning as expected now.

Best regards, Mohit Anuragi

MohitAnuragi avatar Sep 18 '25 07:09 MohitAnuragi