fix(tools): translate OpenAPI 3.0 nullable when inlining schemas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,7 @@ def responses_for(spec: dict[str, Any], method: str, path: str) -> dict[str, Any
|
|||||||
ok = sorted(c for c in (op.get("responses") or {}) if c.isdigit() and int(c) < 400)
|
ok = sorted(c for c in (op.get("responses") or {}) if c.isdigit() and int(c) < 400)
|
||||||
if not ok:
|
if not ok:
|
||||||
return None
|
return None
|
||||||
body = resolve_refs(op["responses"][ok[0]], spec)
|
body = as_json_schema(resolve_refs(op["responses"][ok[0]], spec))
|
||||||
content = body.get("content")
|
content = body.get("content")
|
||||||
if not content:
|
if not content:
|
||||||
return None
|
return None
|
||||||
@@ -105,6 +105,32 @@ def responses_for(spec: dict[str, Any], method: str, path: str) -> dict[str, Any
|
|||||||
return {ok[0]: {"content": kept}} if kept else None
|
return {ok[0]: {"content": kept}} if kept else None
|
||||||
|
|
||||||
|
|
||||||
|
def as_json_schema(node: Any) -> Any:
|
||||||
|
"""Translate OpenAPI 3.0's `nullable: true` into JSON Schema's null type.
|
||||||
|
|
||||||
|
The plugin's fields are documented as "OpenAPI format", and it validates them
|
||||||
|
with a JSON Schema validator (`jsonschema_rs`). `nullable` is not a JSON
|
||||||
|
Schema keyword, so it is ignored and `type: string` is enforced — meaning any
|
||||||
|
3.0 spec with a nullable field produces a tool that rejects the backend's own
|
||||||
|
valid response.
|
||||||
|
|
||||||
|
Seen for real: `list-products` returns `previous: null` on the first page, and
|
||||||
|
the call failed with *null is not of type "string", /previous* — after the
|
||||||
|
backend had already done the work, and only visible in the agent's log.
|
||||||
|
|
||||||
|
A 3.1 spec expresses this as `type: [x, "null"]` and needs none of this.
|
||||||
|
"""
|
||||||
|
if isinstance(node, dict):
|
||||||
|
out = {k: as_json_schema(v) for k, v in node.items() if k != "nullable"}
|
||||||
|
if node.get("nullable") is True and "type" in out:
|
||||||
|
t = out["type"]
|
||||||
|
out["type"] = [*t, "null"] if isinstance(t, list) else [t, "null"]
|
||||||
|
return out
|
||||||
|
if isinstance(node, list):
|
||||||
|
return [as_json_schema(v) for v in node]
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
def resolve_refs(node: Any, spec: dict[str, Any], seen: frozenset[str] = frozenset()) -> Any:
|
def resolve_refs(node: Any, spec: dict[str, Any], seen: frozenset[str] = frozenset()) -> Any:
|
||||||
"""Inline `$ref`s. The plugin's schema states it does not resolve them.
|
"""Inline `$ref`s. The plugin's schema states it does not resolve them.
|
||||||
|
|
||||||
@@ -167,7 +193,11 @@ def build(domain: Path) -> str:
|
|||||||
tool["host"] = UPSTREAM_HOST
|
tool["host"] = UPSTREAM_HOST
|
||||||
|
|
||||||
if body := tool.get("request_body"):
|
if body := tool.get("request_body"):
|
||||||
kept = {k: v for k, v in body["content"].items() if k == KEEP_CONTENT_TYPE}
|
kept = {
|
||||||
|
k: as_json_schema(v)
|
||||||
|
for k, v in body["content"].items()
|
||||||
|
if k == KEEP_CONTENT_TYPE
|
||||||
|
}
|
||||||
if not kept:
|
if not kept:
|
||||||
raise SystemExit(f"{name}: no {KEEP_CONTENT_TYPE} request body in the spec")
|
raise SystemExit(f"{name}: no {KEEP_CONTENT_TYPE} request body in the spec")
|
||||||
body["content"] = kept
|
body["content"] = kept
|
||||||
|
|||||||
Reference in New Issue
Block a user