googleapis / googleapis/python-genai

FinishReason enum is missing MALFORMED_RESPONSE, which the API actually returns

Open
#2,937 1 comment 0 reactions 1 assignee Claimed by @Venkaiahbabuneelam View on GitHub
priority: p2 type: bug
Dominant language
Python
Stars
4k
Forks
1k
Avg merge
2d 11h
Merged PRs (30d)
40

Description

### Description

The Gemini API returns `MALFORMED_RESPONSE` as a `finishReason`, but this value is not a member of the `FinishReason` enum in this SDK. As a result every response carrying it triggers a `UserWarning` and is coerced into a synthetic pseudo-member.

```
.../google/genai/_common.py:651: UserWarning: MALFORMED_RESPONSE is not a valid FinishReason
warnings.warn(f'{value} is not a valid {cls.__name__}')
```

### Current enum members

`google/genai/types.py` `class FinishReason` currently defines:

`FINISH_REASON_UNSPECIFIED`, `STOP`, `MAX_TOKENS`, `SAFETY`, `RECITATION`, `LANGUAGE`, `OTHER`, `BLOCKLIST`, `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`, `IMAGE_SAFETY`, `UNEXPECTED_TOOL_CALL`, `TOO_MANY_TOOL_CALLS`, `IMAGE_PROHIBITED_CONTENT`, `NO_IMAGE`, `IMAGE_RECITATION`, `IMAGE_OTHER`

`grep MALFORMED_RESPONSE` over the package returns nothing. The same is true for `js-genai` (`src/types.ts`), where the only `MALFORMED*` member is `MALFORMED_FUNCTION_CALL`.

`MALFORMED_RESPONSE` also does not appear in the public docs — it is absent from both [API errors](https://ai.google.dev/gemini-api/docs/api-errors) (which documents `malformed_function_call` and `malformed_tool_call`) and the [troubleshooting guide](https://ai.google.dev/gemini-api/docs/troubleshooting).

### What happens today

`CaseInSensitiveEnum._missing_` (`google/genai/_common.py`) does not return `None` for an unknown value — it warns and then fabricates a member:

```python
class CaseInSensitiveEnum(str, enum.Enum):
@classmethod
def _missing_(cls, value):
try: return cls[value.upper()]
except KeyError:
try: return cls[value.lower()]
except KeyError:
warnings.warn(f'{value} is not a valid {cls.__name__}')
try:
unknown_enum_val = super().__new__(cls, value)
unknown_enum_val._name_ = str(value)
unknown_enum_val._value_ = value
return unknown_enum_val
except:
return None
```

The resulting object behaves inconsistently, which is the practical problem:

```python
x = types.FinishReason("MALFORMED_RESPONSE") # warns

repr(x) #
isinstance(x, types.FinishReason) # True
isinstance(x, str) # True
x == "MALFORMED_RESPONSE" # True
x.name # 'MALFORMED_RESPONSE'

str(x) # 'FinishReason.MALFORMED_RESPONSE' <-- surprising
"MALFORMED_RESPONSE" in types.FinishReason.__members__ # False <-- surprising
types.FinishReason.MALFORMED_RESPONSE # AttributeError <-- surprising
```

So `== "..."` works, but `__members__` lookups, attribute access, and `str()` comparisons all fail. Code written against the enum cannot reference this value at all.

### Impact on downstream libraries

Consumers that exhaustively match on `FinishReason` crash rather than degrade:

- badlogic/pi-mono#2028 — `mapStopReason` throws on the unknown value, killing the agent run
- openclaw/openclaw#42149 — same root cause

A related symptom from the same "value outside the enum" family is #2024 (indefinite hang when accessing `finish_reason` for `IMAGE_SAFETY` / `NO_IMAGE`).

### Reproduction

`MALFORMED_RESPONSE` is returned intermittently by Gemini 3 family models. The enum gap itself, however, reproduces without any API call:

```python
from google.genai import types

print("MALFORMED_RESPONSE" in types.FinishReason.__members__) # False
x = types.FinishReason("MALFORMED_RESPONSE") # UserWarning
print(repr(x), x.name, str(x), x == "MALFORMED_RESPONSE")
print(types.FinishReason.MALFORMED_RESPONSE) # AttributeError
```

### Environment

- `google-genai` 2.7.0 (also verified absent on `main`)
- Python 3.13
- Observed with `gemini-3.5-flash-lite` via the Gemini API; other reports cite `gemini-3-flash-preview` and `gemini-3.1-pro-preview`

### Request

1. Add `MALFORMED_RESPONSE` to the `FinishReason` enum (and to `js-genai` / `go-genai` for parity).
2. Document its semantics. Right now there is no authoritative definition, so downstream code has to guess — third parties variously treat it as "the model's own generation broke, server-side and transient" or map it to a generic error. Without a documented meaning, any retry or error-handling logic keyed on this value is guesswork.
3. Consider whether `_missing_` should produce something more predictable for unknown values. The current pseudo-member satisfies `== "STRING"` but fails `__members__`, attribute access, and `str()`, which is a confusing middle ground. Even a documented note that unknown values are coerced this way would help.

Happy to supply more detail if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.