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)
|
||||
if not ok:
|
||||
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")
|
||||
if not content:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""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
|
||||
|
||||
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:
|
||||
raise SystemExit(f"{name}: no {KEEP_CONTENT_TYPE} request body in the spec")
|
||||
body["content"] = kept
|
||||
|
||||
Reference in New Issue
Block a user