Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/openai/lib/_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import inspect
from typing import Any, TypeVar
from urllib.parse import unquote
from typing_extensions import TypeGuard

import pydantic
Expand Down Expand Up @@ -119,7 +120,7 @@ def resolve_ref(*, root: dict[str, object], ref: str) -> object:
if not ref.startswith("#/"):
raise ValueError(f"Unexpected $ref format {ref!r}; Does not start with #/")

path = ref[2:].split("/")
path = [key.replace("~1", "/").replace("~0", "~") for key in unquote(ref[2:]).split("/")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode the fragment before requiring its leading slash

When a URI encoder also percent-encodes the JSON Pointer's leading separator, such as #%2F$defs%2FGroup%2FItem, this valid fragment decodes to #/$defs/Group/Item but is rejected by the preceding startswith("#/") check before unquote() runs. Decode the entire substring after # before validating and splitting it so refs produced by encoders such as encodeURIComponent can be resolved.

Useful? React with 👍 / 👎.

resolved = root
for key in path:
value = resolved[key]
Expand Down
47 changes: 46 additions & 1 deletion tests/lib/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,56 @@

import openai
from openai._compat import PYDANTIC_V1
from openai.lib._pydantic import to_strict_json_schema
from openai.lib._pydantic import resolve_ref, to_strict_json_schema, _ensure_strict_json_schema

from .schema_types.query import Query


def test_resolve_ref_decodes_json_pointer_tokens() -> None:
schema: dict[str, object] = {
"$defs": {
"path/to model~v1": {"type": "string"},
}
}

assert resolve_ref(root=schema, ref="#/$defs/path~1to%20model~0v1") == {"type": "string"}


def test_resolve_ref_decodes_fragment_before_splitting_tokens() -> None:
schema: dict[str, object] = {
"$defs": {
"Group": {
"Item": {"type": "string"},
},
}
}

assert resolve_ref(root=schema, ref="#/$defs%2FGroup%2FItem") == {"type": "string"}


def test_strict_schema_inlines_escaped_ref() -> None:
schema: dict[str, object] = {
"$defs": {"path/to model~v1": {"type": "object", "properties": {"value": {"type": "string"}}}},
"type": "object",
"properties": {
"result": {
"$ref": "#/$defs/path~1to%20model~0v1",
"description": "A custom result",
}
},
}

strict_schema = _ensure_strict_json_schema(schema, path=(), root=schema)

assert strict_schema["properties"]["result"] == {
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
"description": "A custom result",
"additionalProperties": False,
}


def test_most_types() -> None:
if not PYDANTIC_V1:
assert openai.pydantic_function_tool(Query)["function"] == snapshot(
Expand Down