googleapis / googleapis/python-genai
FinishReason enum is missing MALFORMED_RESPONSE, which the API actually returns
- 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
Assessment
This issue has not been assessed yet.