Summary
json_schema_to_pydantic_model() applies extra="forbid" only to the outermost generated model, and returns a bare dict for free-form type: object properties. The rendered JSON Schema therefore comes out with nested $defs carrying no additionalProperties key at all, and free-form object properties carrying additionalProperties: true. Strict structured-output providers reject both.
Still reproducible on 0.11.0, so #116 does not cover this.
Environment
|
|
agent-lifecycle-toolkit |
0.11.0 (tag v0.11.0, PyPI 2026-08-10) |
litellm |
1.96.0 |
pydantic |
2.13.4 |
| Python |
3.12.13 |
| Package root |
/usr/local/lib/python3.12/site-packages/altk/ |
Reproduction
Using ALTK's own SPARC metric schema, no network needed:
import json
from altk.core.llm.output_parser import json_schema_to_pydantic_model
P = "<site-packages>/altk/pre_tool/sparc/function_calling/metrics"
d = json.load(open(f"{P}/function_call/general_metrics_runtime.json"))
m = [x for x in d if x["name"] == "general_hallucination_check"][0]
out = json_schema_to_pydantic_model(m["jsonschema"]).model_json_schema()
def walk(o, path="$"):
if isinstance(o, dict):
if o.get("additionalProperties") is True:
print("additionalProperties: true ", path)
if o.get("type") == "object" and "additionalProperties" not in o:
print("additionalProperties MISSING ", path)
for k, v in o.items():
walk(v, f"{path}.{k}")
elif isinstance(o, list):
for i, v in enumerate(o):
walk(v, f"{path}[{i}]")
walk(out)
Output on 0.11.0:
additionalProperties MISSING $.$defs.AutoModel_1
additionalProperties MISSING $.$defs.AutoModel_1_1
additionalProperties: true $.$defs.AutoModel_1_1.properties.corrected_value
additionalProperties MISSING $.$defs.AutoModel_1_2
additionalProperties: true $.$defs.AutoModel_1_2.properties.arguments
Three nested $defs with no additionalProperties key, two properties with additionalProperties: true. The top-level object does get additionalProperties: false.
The same sweep across all seven runtime metrics shows every metric affected — for example parameter_hallucination_check yields additionalProperties: true at $.$defs.AutoModel_1.properties.parameter, and function_selection_appropriateness yields one $defs with the key missing.
Note that the source schema (m["jsonschema"]) does declare "additionalProperties": false at its top level, so this is the conversion losing information the input carried, not the input being underspecified.
Expected vs actual
Expected: the rendered schema declares additionalProperties: false on every object schema, including nested $defs, unless the source schema explicitly asks for true.
Actual: only the outermost object gets it; nested $defs get nothing, and free-form objects get true.
Cause
Two places, both in altk/core/llm/output_parser.py:
1. output_parser.py:156-162 — extra="forbid" is set on the model that create_model just returned, so it applies to that model only. Nested models are built by a recursive json_schema_to_pydantic_model call at :71-76 on the sub-schema, and those sub-schemas typically do not repeat additionalProperties: false, so the if at :160 is false for them:
model = create_model(model_name, **fields)
# Mirror ``additionalProperties: false`` — providers with strict structured
# output need it, and without it the model may invent extra keys that the
# original schema then rejects.
if schema.get("additionalProperties") is False:
model.model_config["extra"] = "forbid"
return model
2. output_parser.py:62-79 — _map_object_for_prop returns a bare dict for a free-form object (one with no properties sub-schema) when free_form_object_as_str is false, and Pydantic renders dict as {"type": "object", "additionalProperties": true}:
if "properties" in prop_schema:
return json_schema_to_pydantic_model(...)
if free_form_object_as_str:
return str
return dict
free_form_object_as_str=True does drop the additionalProperties: true count to zero, but it is off by default (output_parser.py:32, :242) and changes the emitted type from object to string, which is a different trade-off rather than a fix for the strict case.
The free-form objects in the SPARC schemas come from CorrectionField (altk/pre_tool/sparc/metrics/field.py:220-236), which matches a correction property of type: object with no sub-properties.
Provider errors this produces
Recorded against ALTK 0.10.x, same schema shape:
BedrockException: output_config.format.schema: For 'object' type, 'additionalProperties: true' is not supported. Please set 'additionalProperties' to false
Azure: Invalid schema for response_format 'AutoModel': In context=('properties', 'correction'), 'additionalProperties' is required to be supplied and to be false.
In fairness, on our current setup (openai/aws/claude-haiku-4-5 through an OpenAI-compatible LiteLLM proxy) the additionalProperties rejection is not what we hit today — we hit #118 instead, and litellm's own strict-schema post-processing appears to inject additionalProperties: false into generated $defs before the request leaves, masking cause 1 on that path. So the schema defect above is reproducible with certainty; how visible it is depends on whether a litellm layer sits in front of the provider. It would still bite a strict client that talks to the provider directly.
Possible direction
Offered as a suggestion:
- Default nested generated models to
extra="forbid", opting out only where the source sub-schema explicitly declares additionalProperties: true. Threading the outer decision down through the recursive call at :71-76 would be one way.
- For free-form objects under a strict-mode target, emit an object schema with
additionalProperties: false rather than a bare dict.
A regression test asserting zero additionalProperties: true and no missing additionalProperties in $defs for a nested-object schema would pin this down. Happy to open a PR along those lines if the direction looks right.
Summary
json_schema_to_pydantic_model()appliesextra="forbid"only to the outermost generated model, and returns a baredictfor free-formtype: objectproperties. The rendered JSON Schema therefore comes out with nested$defscarrying noadditionalPropertieskey at all, and free-form object properties carryingadditionalProperties: true. Strict structured-output providers reject both.Still reproducible on
0.11.0, so #116 does not cover this.Environment
agent-lifecycle-toolkit0.11.0(tagv0.11.0, PyPI 2026-08-10)litellm1.96.0pydantic2.13.43.12.13/usr/local/lib/python3.12/site-packages/altk/Reproduction
Using ALTK's own SPARC metric schema, no network needed:
Output on
0.11.0:Three nested
$defswith noadditionalPropertieskey, two properties withadditionalProperties: true. The top-level object does getadditionalProperties: false.The same sweep across all seven runtime metrics shows every metric affected — for example
parameter_hallucination_checkyieldsadditionalProperties: trueat$.$defs.AutoModel_1.properties.parameter, andfunction_selection_appropriatenessyields one$defswith the key missing.Note that the source schema (
m["jsonschema"]) does declare"additionalProperties": falseat its top level, so this is the conversion losing information the input carried, not the input being underspecified.Expected vs actual
Expected: the rendered schema declares
additionalProperties: falseon every object schema, including nested$defs, unless the source schema explicitly asks fortrue.Actual: only the outermost object gets it; nested
$defsget nothing, and free-form objects gettrue.Cause
Two places, both in
altk/core/llm/output_parser.py:1.
output_parser.py:156-162—extra="forbid"is set on the model thatcreate_modeljust returned, so it applies to that model only. Nested models are built by a recursivejson_schema_to_pydantic_modelcall at:71-76on the sub-schema, and those sub-schemas typically do not repeatadditionalProperties: false, so theifat:160is false for them:2.
output_parser.py:62-79—_map_object_for_propreturns a baredictfor a free-form object (one with nopropertiessub-schema) whenfree_form_object_as_stris false, and Pydantic rendersdictas{"type": "object", "additionalProperties": true}:free_form_object_as_str=Truedoes drop theadditionalProperties: truecount to zero, but it is off by default (output_parser.py:32,:242) and changes the emitted type from object to string, which is a different trade-off rather than a fix for the strict case.The free-form objects in the SPARC schemas come from
CorrectionField(altk/pre_tool/sparc/metrics/field.py:220-236), which matches acorrectionproperty oftype: objectwith no sub-properties.Provider errors this produces
Recorded against ALTK 0.10.x, same schema shape:
In fairness, on our current setup (
openai/aws/claude-haiku-4-5through an OpenAI-compatible LiteLLM proxy) theadditionalPropertiesrejection is not what we hit today — we hit #118 instead, and litellm's own strict-schema post-processing appears to injectadditionalProperties: falseinto generated$defsbefore the request leaves, masking cause 1 on that path. So the schema defect above is reproducible with certainty; how visible it is depends on whether a litellm layer sits in front of the provider. It would still bite a strict client that talks to the provider directly.Possible direction
Offered as a suggestion:
extra="forbid", opting out only where the source sub-schema explicitly declaresadditionalProperties: true. Threading the outer decision down through the recursive call at:71-76would be one way.additionalProperties: falserather than a baredict.A regression test asserting zero
additionalProperties: trueand no missingadditionalPropertiesin$defsfor a nested-object schema would pin this down. Happy to open a PR along those lines if the direction looks right.