Accept unknown Codex error variants in the app-server protocol (#49806)

## Why

New error variants should not prevent older clients from handling app-server errors.

## What changed

- Deserialize unknown `CodexErrorInfo` strings and objects as `Other` in Rust, while preserving known variants and the `"other"` serialization.
- Allow arbitrary strings and objects in the exported JSON schemas and TypeScript type.
- Update Python SDK generation and generated models to accept unknown error strings and object payloads.

## Testing

Add Rust tests for `"other"` round trips, unknown string and object fallbacks, known structured variants, and rejection of null, boolean, numeric, and array values.

GitOrigin-RevId: 40c17896019924b2ef7970ee2205ea0154d0d1b3
This commit is contained in:
Adam Perry @ OpenAI
2026-10-01 01:38:04 +00:00
committed by copyberry
parent e7798c9944
commit a5d56d8120
26 changed files with 303 additions and 69 deletions
+29 -11
View File
@@ -286,6 +286,18 @@ def _flatten_string_enum_one_of(definition: dict[str, Any]) -> bool:
return True
def _normalize_open_ended_enum(definition: dict[str, Any]) -> None:
variants = definition.pop("oneOf", definition.get("anyOf", []))
definition["anyOf"] = [
variant
for variant in variants
if "enum" in variant or variant.get("type") not in ("string", ["string", "object"])
]
object_fallback = {"type": "object", "additionalProperties": {}}
if object_fallback not in definition["anyOf"]:
definition["anyOf"].append(object_fallback)
DISCRIMINATOR_KEYS = ("type", "method", "mode", "state", "status", "role", "reason")
@@ -522,8 +534,10 @@ def _normalized_schema_bundle_text(schema_dir: Path) -> str:
_preserve_guardian_approval_path_wrappers(schema)
definitions = schema.get("definitions", {})
if isinstance(definitions, dict):
for definition in definitions.values():
for name, definition in definitions.items():
if isinstance(definition, dict):
if name == "CodexErrorInfo":
_normalize_open_ended_enum(definition)
_flatten_string_enum_one_of(definition)
# Normalize the schema into something datamodel-code-generator can map to
# stable class names instead of anonymous numbered helpers.
@@ -580,9 +594,10 @@ def generate_v2_all(schema_dir: Path) -> None:
_require_nullable_field(
out_path, "McpResourceReadTarget", r"link_id: Annotated\[\n(?: .*\n)+ \]"
)
_preserve_open_enum(out_path, "CodexErrorInfoValue", allow_empty=True)
_preserve_reasoning_effort_enum(out_path)
_preserve_thread_source_enum(out_path)
_preserve_plan_type_enum(out_path)
_preserve_open_enum(out_path, "PlanType")
_normalize_generated_timestamps(out_path)
@@ -687,27 +702,30 @@ def _preserve_thread_source_enum(out_path: Path) -> None:
out_path.write_text(source[:class_start] + open_enum + source[class_end:])
def _preserve_plan_type_enum(out_path: Path) -> None:
"""Keep the public plan constants while accepting values from newer runtimes."""
def _preserve_open_enum(out_path: Path, name: str, *, allow_empty: bool = False) -> None:
"""Keep generated enum constants while accepting values from newer runtimes."""
source = out_path.read_text()
class_start = source.find("class PlanType(Enum):")
class_start = source.find(f"class {name}(Enum):")
if class_start == -1:
raise RuntimeError("Generated SDK is missing PlanType")
raise RuntimeError(f"Generated SDK is missing {name}")
class_end = source.find("\n\nclass ", class_start)
if class_end == -1:
class_end = len(source)
class_source = source[class_start:class_end]
class_source = class_source.replace(
"class PlanType(Enum):",
"class PlanType(str, Enum):",
f"class {name}(Enum):",
f"class {name}(str, Enum):",
1,
).rstrip()
class_source += """
invalid_value = "not isinstance(value, str)"
if not allow_empty:
invalid_value += " or not value"
class_source += f"""
@classmethod
def _missing_(cls, value: object) -> PlanType | None:
if not isinstance(value, str) or not value:
def _missing_(cls, value: object) -> {name} | None:
if {invalid_value}:
return None
member = str.__new__(cls, value)
member._name_ = value
@@ -538,7 +538,7 @@ class ClientInfo(BaseModel):
version: str
class CodexErrorInfoValue(Enum):
class CodexErrorInfoValue(str, Enum):
context_window_exceeded = "contextWindowExceeded"
session_budget_exceeded = "sessionBudgetExceeded"
usage_limit_exceeded = "usageLimitExceeded"
@@ -555,6 +555,15 @@ class CodexErrorInfoValue(Enum):
sandbox_error = "sandboxError"
other = "other"
@classmethod
def _missing_(cls, value: object) -> CodexErrorInfoValue | None:
if not isinstance(value, str):
return None
member = str.__new__(cls, value)
member._name_ = value
member._value_ = value
return member
class HttpConnectionFailed(BaseModel):
model_config = ConfigDict(
@@ -7669,6 +7678,7 @@ class CodexErrorInfo(
| ResponseStreamDisconnectedCodexErrorInfo
| ResponseTooManyFailedAttemptsCodexErrorInfo
| ActiveTurnNotSteerableCodexErrorInfo
| dict[str, Any]
]
):
model_config = ConfigDict(
@@ -7680,7 +7690,8 @@ class CodexErrorInfo(
| ResponseStreamConnectionFailedCodexErrorInfo
| ResponseStreamDisconnectedCodexErrorInfo
| ResponseTooManyFailedAttemptsCodexErrorInfo
| ActiveTurnNotSteerableCodexErrorInfo,
| ActiveTurnNotSteerableCodexErrorInfo
| dict[str, Any],
Field(
description="This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant."
),