Python: Fix broken samples for GitHub Copilot, declarative, and Responses API (#4915)

* Python: Fix broken samples for GitHub Copilot, declarative, and Responses API

- Add missing on_permission_request handler to github_copilot_basic and
  github_copilot_with_session samples (required by copilot SDK)
- Increase timeout for remote MCP query in github_copilot_with_mcp sample
- Soften session isolation claim in github_copilot_with_session sample
- Fix inline_yaml sample: pass project_endpoint via client_kwargs instead
  of relying on YAML connection block (AzureAIClient expects
  project_endpoint, not endpoint)
- Handle raw JSON schemas in Responses client _convert_response_format
  so declarative outputSchema works with the Responses API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve raw JSON schema detection heuristic and add tests

- Broaden raw schema detection to handle anyOf, oneOf, allOf, $ref, $defs
  keywords and JSON Schema primitive types, not just 'properties'
- Apply same raw schema handling to azure-ai _shared.py for consistency
- Add unit tests for both openai and azure-ai response_format conversion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-03-27 09:27:19 -07:00
committed by GitHub
Unverified
parent cc0cfaaac8
commit 6b47cdbf52
8 changed files with 181 additions and 8 deletions
@@ -571,4 +571,25 @@ def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, An
if format_type in {"json_object", "text"}:
return {"type": format_type}
# Handle raw JSON schemas (e.g. {"type": "object", "properties": {...}})
# by wrapping them in the expected json_schema envelope.
# Detect by checking for JSON Schema primitive types or known schema keywords.
json_schema_keywords = {"properties", "anyOf", "oneOf", "allOf", "$ref", "$defs"}
json_schema_primitive_types = {"object", "array", "string", "number", "integer", "boolean", "null"}
if format_type in json_schema_primitive_types or (
format_type is None and any(k in response_format for k in json_schema_keywords)
):
schema = dict(response_format)
if schema.get("type") == "object" and "additionalProperties" not in schema:
schema["additionalProperties"] = False
# Pop title from schema since OpenAI strict mode rejects unknown keys;
# use it as the schema name in the envelope instead.
name = str(schema.pop("title", None) or "response")
return {
"type": "json_schema",
"name": name,
"schema": schema,
"strict": True,
}
raise IntegrationInvalidRequestException("Unsupported response_format provided for Azure AI client.")
@@ -404,6 +404,32 @@ def test_convert_response_format_json_schema_missing_schema_raises() -> None:
_convert_response_format({"type": "json_schema", "json_schema": {}})
def test_convert_response_format_raw_json_schema_with_properties() -> None:
"""Test raw JSON schema with properties is wrapped in json_schema envelope."""
result = _convert_response_format({"type": "object", "properties": {"x": {"type": "string"}}, "title": "MyOutput"})
assert result["type"] == "json_schema"
assert result["name"] == "MyOutput"
assert result["strict"] is True
assert result["schema"]["additionalProperties"] is False
assert "title" not in result["schema"]
def test_convert_response_format_raw_json_schema_no_title() -> None:
"""Test raw JSON schema without title defaults name to 'response'."""
result = _convert_response_format({"type": "object", "properties": {"x": {"type": "string"}}})
assert result["name"] == "response"
def test_convert_response_format_raw_json_schema_with_anyof() -> None:
"""Test raw JSON schema with anyOf keyword is detected."""
result = _convert_response_format({"anyOf": [{"type": "string"}, {"type": "number"}]})
assert result["type"] == "json_schema"
assert result["strict"] is True
def test_from_azure_ai_tools_mcp_approval_mode_always() -> None:
"""Test from_azure_ai_tools converts MCP require_approval='always' to dict."""
tools = [
@@ -636,6 +636,27 @@ class RawOpenAIChatClient( # type: ignore[misc]
if format_type in {"json_object", "text"}:
return {"type": format_type}
# Handle raw JSON schemas (e.g. {"type": "object", "properties": {...}})
# by wrapping them in the expected json_schema envelope.
# Detect by checking for JSON Schema primitive types or known schema keywords.
json_schema_keywords = {"properties", "anyOf", "oneOf", "allOf", "$ref", "$defs"}
json_schema_primitive_types = {"object", "array", "string", "number", "integer", "boolean", "null"}
if format_type in json_schema_primitive_types or (
format_type is None and any(k in response_format for k in json_schema_keywords)
):
schema = dict(response_format)
if schema.get("type") == "object" and "additionalProperties" not in schema:
schema["additionalProperties"] = False
# Pop title from schema since OpenAI strict mode rejects unknown keys;
# use it as the schema name in the envelope instead.
name = str(schema.pop("title", None) or "response")
return {
"type": "json_schema",
"name": name,
"schema": schema,
"strict": True,
}
raise ChatClientInvalidRequestException("Unsupported response_format provided for Responses client.")
def _get_conversation_id(
@@ -1713,6 +1713,71 @@ def test_response_format_json_schema_missing_schema() -> None:
client._prepare_response_and_text_format(response_format=response_format, text_config=None)
def test_response_format_raw_json_schema_with_properties() -> None:
"""Test raw JSON schema with properties is wrapped in json_schema envelope."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
response_format = {"type": "object", "properties": {"x": {"type": "string"}}, "title": "MyOutput"}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
fmt = text_config["format"]
assert fmt["type"] == "json_schema"
assert fmt["name"] == "MyOutput"
assert fmt["strict"] is True
assert fmt["schema"]["additionalProperties"] is False
assert "title" not in fmt["schema"]
def test_response_format_raw_json_schema_no_title() -> None:
"""Test raw JSON schema without title defaults name to 'response'."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
response_format = {"type": "object", "properties": {"x": {"type": "string"}}}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["name"] == "response"
def test_response_format_raw_json_schema_preserves_additional_properties() -> None:
"""Test raw JSON schema preserves existing additionalProperties."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
response_format = {"type": "object", "properties": {"x": {"type": "string"}}, "additionalProperties": True}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["schema"]["additionalProperties"] is True
def test_response_format_raw_json_schema_non_object_type() -> None:
"""Test raw JSON schema with non-object type does not inject additionalProperties."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
response_format = {"type": "array", "items": {"type": "string"}}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert "additionalProperties" not in text_config["format"]["schema"]
def test_response_format_raw_json_schema_with_anyof() -> None:
"""Test raw JSON schema with anyOf keyword is detected."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
response_format = {"anyOf": [{"type": "string"}, {"type": "number"}]}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["type"] == "json_schema"
def test_response_format_unsupported_type() -> None:
"""Test unsupported response_format type raises error."""
client = OpenAIChatClient(model="test-model", api_key="test-key")