This commit is contained in:
Wendong Li 2026-10-21 03:36:46 +00:00 • committed by GitHub
commit 66aaa95643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 807 additions and 3 deletions

View file

@ -0,0 +1,264 @@
from collections.abc import Iterator, Mapping, Sequence
from types import MappingProxyType
from typing import Final
from pydantic import TypeAdapter, ValidationError
_CLIENT_NAMESPACE: Final = "collaboration"
_UPSTREAM_NAMESPACE: Final = "codex_collaboration_plaintext"
_MESSAGE_ACTIONS: Final = frozenset({"spawn_agent", "send_message", "followup_task"})
_INVENTORY_ITEMS: Final = ("additional_tools", "tool_search_output")
_CALL_ITEMS: Final = ("function_call", "custom_tool_call")
_IDENTITY_KEYS: Final = ("type", "namespace", "name", "call_id")
_RECORD: Final = TypeAdapter(Mapping[str, object])
_SEQUENCE: Final = TypeAdapter(Sequence[object])
def _sequence(value: object) -> Sequence[object]:
return _SEQUENCE.validate_python(value, strict=True) if isinstance(value, (list, tuple)) else ()
def _record(value: object) -> Mapping[str, object] | None:
if not isinstance(value, Mapping):
return None
try:
return _RECORD.validate_python(value, strict=True)
except ValidationError:
return None
def _mapping(value: object) -> Mapping[str, object]:
return _record(value) or MappingProxyType({})
def _inventory(request: Mapping[str, object]) -> Iterator[Mapping[str, object]]:
for tool in _sequence(request.get("tools")):
if (tool_record := _record(tool)) is not None:
yield tool_record
for item in _sequence(request.get("input")):
if (item_record := _record(item)) is not None and item_record.get("type") in _INVENTORY_ITEMS:
for tool in _sequence(item_record.get("tools")):
if (nested_tool := _record(tool)) is not None:
yield nested_tool
def _encrypted_action(tool: object) -> str | None:
record: Final = _record(tool)
if record is None or record.get("type") != "function":
return None
name: Final = record.get("name")
parameters: Final = _mapping(record.get("parameters"))
properties: Final = _mapping(parameters.get("properties"))
message: Final = _mapping(properties.get("message"))
if isinstance(name, str) and name in _MESSAGE_ACTIONS:
return name if message.get("type") == "string" and message.get("encrypted") is True else None
return None
def _actions(request: Mapping[str, object]) -> Iterator[str]:
for tool in _inventory(request):
if tool.get("type") == "namespace" and tool.get("name") == _CLIENT_NAMESPACE:
for child in _sequence(tool.get("tools")):
if (action := _encrypted_action(child)) is not None:
yield action
def _identity(value: Mapping[str, object], source: str, target: str) -> Mapping[str, object]:
namespace: Final = value.get("namespace")
name: Final = value.get("name")
if isinstance(name, str):
for separator in (".", "__"):
if name.startswith(source + separator):
if namespace is not None and namespace != source:
raise ValueError("Conflicting Copilot collaboration namespace and function name")
return {**value, "namespace": target, "name": name[len(source) + len(separator) :]}
return {**value, "namespace": target} if namespace == source else value
def _plaintext_tool(tool: object) -> object:
record: Final = _record(tool)
if record is None or _encrypted_action(record) is None:
return tool
parameters: Final = _mapping(record.get("parameters"))
properties: Final = _mapping(parameters.get("properties"))
message: Final = _mapping(properties.get("message"))
return {
**record,
"parameters": {
**parameters,
"properties": {
**properties,
"message": {key: value for key, value in message.items() if key != "encrypted"},
},
},
}
def _tool(tool: object, source: str, target: str) -> object:
record: Final = _record(tool)
if record is None or record.get("type") != "namespace" or record.get("name") != source:
return tool
return {
**record,
"name": target,
**(
{
"tools": [
_plaintext_tool(child) if source == _CLIENT_NAMESPACE else child
for child in _sequence(record.get("tools"))
]
}
if isinstance(record.get("tools"), (list, tuple))
else {}
),
}
def _tools(value: object, source: str, target: str) -> object:
sequence: Final = _sequence(value)
return [_tool(tool, source, target) for tool in sequence] if sequence else value
def _choice(value: object, source: str, target: str) -> object:
record: Final = _record(value)
if record is None:
return value
if record.get("type") == "namespace" and record.get("name") == source:
return {**record, "name": target}
if record.get("type") == "allowed_tools":
return {**record, "tools": [_choice(tool, source, target) for tool in _sequence(record.get("tools"))]}
rewritten: Final = _identity(record, source, target)
name: Final = record.get("name")
if rewritten is not record and "namespace" not in record and isinstance(name, str):
separator: Final = "__" if name.startswith(source + "__") else "."
return {**record, "name": target + separator + str(rewritten["name"])}
return rewritten
def _project_item(value: object) -> object:
record: Final = _record(value)
if record is None:
return value
if record.get("type") in _INVENTORY_ITEMS and "tools" in record:
return {**record, "tools": _tools(record.get("tools"), _CLIENT_NAMESPACE, _UPSTREAM_NAMESPACE)}
return _identity(record, _CLIENT_NAMESPACE, _UPSTREAM_NAMESPACE) if record.get("type") in _CALL_ITEMS else value
def _project_input(value: object) -> object:
sequence: Final = _sequence(value)
return [_project_item(item) for item in sequence] if sequence else value
class CopilotPlaintextCollaboration:
def __init__(self) -> None:
self._actions: frozenset[str] = frozenset()
self._identities: Mapping[int, Mapping[str, object]] = MappingProxyType({})
@property
def active(self) -> bool:
return bool(self._actions)
def prepare_request(self, request: Mapping[str, object]) -> Mapping[str, object]:
self._actions = frozenset(_actions(request))
self._identities = MappingProxyType({})
if not self.active:
return request
if (
request.get("store") is True
or request.get("previous_response_id") is not None
or request.get("conversation") is not None
):
raise ValueError("Copilot plaintext collaboration requires stateless Responses requests")
if any(tool.get("name") == _UPSTREAM_NAMESPACE for tool in _inventory(request)):
raise ValueError(f"The namespace {_UPSTREAM_NAMESPACE!r} is reserved by the Copilot adapter")
projected: Final = {
key: (
_tools(value, _CLIENT_NAMESPACE, _UPSTREAM_NAMESPACE)
if key == "tools"
else _project_input(value)
if key == "input"
else _choice(value, _CLIENT_NAMESPACE, _UPSTREAM_NAMESPACE)
if key == "tool_choice"
else value
)
for key, value in request.items()
}
return {**projected, "store": False}
def _remember(self, value: Mapping[str, object], index: int | None, *, item: bool) -> Mapping[str, object]:
canonical: Final = _identity(value, _UPSTREAM_NAMESPACE, _UPSTREAM_NAMESPACE)
if index is None:
return canonical
known: Final = self._identities.get(index, MappingProxyType({}))
keys: Final = _IDENTITY_KEYS if item else _IDENTITY_KEYS[1:]
identity: Final = {
key: canonical[key] for key in keys if isinstance(canonical.get(key), str) and canonical.get(key) != ""
}
if any(key in known and known[key] != field for key, field in identity.items()):
raise ValueError("Conflicting Copilot collaboration identity for one output_index")
merged: Final = {**known, **identity}
self._identities = MappingProxyType({**self._identities, index: merged})
return {**canonical, **{key: merged[key] for key in keys if key in merged}}
def _restore_identity(self, value: Mapping[str, object]) -> Mapping[str, object]:
restored: Final = _identity(value, _UPSTREAM_NAMESPACE, _CLIENT_NAMESPACE)
name: Final = restored.get("name")
if restored is value or not isinstance(name, str) or name not in self._actions:
return restored
if "encrypted_function_args" in value and value["encrypted_function_args"] != []:
raise ValueError("Copilot returned encrypted arguments for a plaintext collaboration tool")
return {**restored, "encrypted_function_args": []}
def _restore_item(self, value: object, index: int | None) -> object:
record: Final = _record(value)
if record is None:
return value
if record.get("type") not in _CALL_ITEMS and index not in self._identities:
return value
remembered: Final = self._remember(record, index, item=True)
return (
self._restore_identity(remembered)
if remembered.get("type") == "function_call"
else _identity(remembered, _UPSTREAM_NAMESPACE, _CLIENT_NAMESPACE)
)
def restore_response(self, response: Mapping[str, object]) -> Mapping[str, object]:
if not self.active:
return response
return {
key: (
self._restore_output(value)
if key == "output"
else _tools(value, _UPSTREAM_NAMESPACE, _CLIENT_NAMESPACE)
if key == "tools"
else _choice(value, _UPSTREAM_NAMESPACE, _CLIENT_NAMESPACE)
if key == "tool_choice"
else value
)
for key, value in response.items()
}
def _restore_output(self, value: object) -> object:
sequence: Final = _sequence(value)
return [self._restore_item(item, index) for index, item in enumerate(sequence)] if sequence else value
def restore_event(self, event: Mapping[str, object]) -> Mapping[str, object]:
if not self.active:
return event
output_index: Final = event.get("output_index")
index: Final = output_index if isinstance(output_index, int) and not isinstance(output_index, bool) else None
restored: Final = {
key: (
self._restore_item(value, index)
if key == "item"
else self.restore_response(response)
if key == "response" and (response := _record(value)) is not None
else value
)
for key, value in event.items()
}
if event.get("type") == "response.function_call_arguments.done":
return self._restore_identity(self._remember(restored, index, item=False))
if event.get("type") == "response.function_call_arguments.delta":
return _identity(restored, _UPSTREAM_NAMESPACE, _CLIENT_NAMESPACE)
return restored

View file

@ -9,6 +9,7 @@ https://github.com/caozhiyuan/copilot-api
"""
import os
from collections.abc import Mapping
from typing import (
TYPE_CHECKING,
Any,
@ -16,6 +17,9 @@ from typing import (
cast, # noqa: TID251 # narrows untyped request dicts at the session boundary
)
import httpx
from pydantic import TypeAdapter
import litellm
from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
@ -24,6 +28,7 @@ from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfi
from litellm.types.llms.openai import (
ResponseInputParam,
ResponsesAPIOptionalRequestParams,
ResponsesAPIResponse,
ResponsesAPIStreamingResponse,
)
from litellm.types.router import GenericLiteLLMParams
@ -38,6 +43,9 @@ from ..common_utils import (
pin_session_authorization,
)
from ..per_user_auth import require_github_copilot_user_session
from .collaboration import CopilotPlaintextCollaboration
_RESPONSE_OBJECT: Final = TypeAdapter(Mapping[str, object])
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@ -97,13 +105,24 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
- Supports vision requests with special header
- Requires X-Initiator header based on input analysis
GITHUB_COPILOT_PLAINTEXT_COLLABORATION=true opts Codex agent messages into
plaintext dispatch over HTTPS. It requires stateless requests carrying the
encrypted collaboration tool inventory and does not recover old ciphertext.
Reference: https://api.githubcopilot.com/
"""
def __init__(self) -> None:
def __init__(self, plaintext_collaboration: bool | None = None) -> None:
super().__init__()
self.authenticator = Authenticator()
self._stream_item_ids_by_output_index: dict[int, str] = {}
enabled: Final = (
os.getenv("GITHUB_COPILOT_PLAINTEXT_COLLABORATION", "").lower() == "true"
if plaintext_collaboration is None
else plaintext_collaboration
)
self._collaboration = CopilotPlaintextCollaboration() if enabled else None
self._unprojected_request: Mapping[str, object] = {}
@property
def custom_llm_provider(self) -> LlmProviders:
@ -138,13 +157,66 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
parsed_chunk: dict,
logging_obj: LiteLLMLoggingObj,
) -> ResponsesAPIStreamingResponse:
parsed_chunk = self._normalize_stream_item_id(parsed_chunk)
normalized: Final = self._normalize_stream_item_id(parsed_chunk)
restored: Final = (
dict(self._collaboration.restore_event(_RESPONSE_OBJECT.validate_python(normalized)))
if self._collaboration is not None
else normalized
)
return super().transform_streaming_response(
model=model,
parsed_chunk=parsed_chunk,
parsed_chunk=restored,
logging_obj=logging_obj,
)
def transform_responses_api_request(
self,
model: str,
input: str | ResponseInputParam,
response_api_optional_request_params: Mapping[str, object],
litellm_params: GenericLiteLLMParams,
headers: Mapping[str, str],
) -> dict[str, object]: # mutable-ok: the HTTP handler expects a mutable provider request
request: Final = super().transform_responses_api_request(
model=model,
input=input,
response_api_optional_request_params=dict(response_api_optional_request_params),
litellm_params=litellm_params,
headers=dict(headers),
)
if self._collaboration is None:
return request
self._unprojected_request = _RESPONSE_OBJECT.validate_python(request)
return dict(self._collaboration.prepare_request(self._unprojected_request))
def transform_extra_body(
self,
extra_body: Mapping[str, object],
request: Mapping[str, object],
model: str,
litellm_params: GenericLiteLLMParams,
) -> Mapping[str, object]:
if self._collaboration is None:
return super().transform_extra_body(extra_body, request, model, litellm_params)
merged: Final = self.normalize_responses_api_request_dict({**self._unprojected_request, **extra_body})
return self._collaboration.prepare_request(_RESPONSE_OBJECT.validate_python(merged))
def transform_response_api_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ResponsesAPIResponse:
response: Final = super().transform_response_api_response(model, raw_response, logging_obj)
if self._collaboration is None or not self._collaboration.active:
return response
restored: Final = ResponsesAPIResponse.model_validate(
self._collaboration.restore_response(_RESPONSE_OBJECT.validate_python(response.model_dump()))
)
return response.model_copy(
update={"output": restored.output, "tools": restored.tools, "tool_choice": restored.tool_choice}
)
def _normalize_stream_item_id(self, parsed_chunk: dict) -> dict:
"""Rewrite streamed item ids to one stable id per output_index.

View file

@ -0,0 +1,468 @@
import copy
import datetime
from typing import Final
import httpx
import pytest
from pydantic import TypeAdapter
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.github_copilot.responses.collaboration import CopilotPlaintextCollaboration
from litellm.llms.github_copilot.responses.transformation import GithubCopilotResponsesAPIConfig
from litellm.types.router import GenericLiteLLMParams
CLIENT_NAMESPACE: Final = "collaboration"
UPSTREAM_NAMESPACE: Final = "codex_collaboration_plaintext"
JSON_OBJECT: Final = TypeAdapter(dict[str, object])
MESSAGE: Final = 'Keep all content, including unicode λ and "quotes".\nSecond line.'
ARGUMENTS: Final = '{"message":"hello","target":"child"}'
def _tools(namespace: str = CLIENT_NAMESPACE, encrypted: bool = True) -> list[dict[str, object]]:
return [
{
"type": "namespace",
"name": namespace,
"description": "Agent communication",
"tools": [
{
"type": "function",
"name": action,
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Full message",
**({"encrypted": True} if encrypted else {}),
},
"target": {"type": "string", "encrypted": True},
},
"required": ["message"],
},
}
for action in ("spawn_agent", "send_message", "followup_task")
]
+ [{"type": "function", "name": "wait_agent", "parameters": {"type": "object"}}],
}
]
def _call(namespace: str = UPSTREAM_NAMESPACE, name: str = "send_message") -> dict[str, object]:
return {
"type": "function_call",
"id": "fc_call",
"call_id": "call_1",
"name": name,
"namespace": namespace,
"arguments": ARGUMENTS,
"status": "completed",
"provider_extension": {"keep": True},
}
def _response(output: list[dict[str, object]]) -> dict[str, object]:
return {
"id": "resp_1",
"object": "response",
"created_at": 1,
"status": "completed",
"model": "gpt-6-astra",
"output": output,
"usage": {"input_tokens": 9, "output_tokens": 4, "total_tokens": 13},
"provider_extension": {"keep": True},
}
def _logging(stream: bool = False) -> Logging:
return Logging(
model="gpt-6-astra",
messages="hello",
stream=stream,
call_type="responses",
start_time=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
litellm_call_id="call_logging",
function_id="function_logging",
supports_correlation_logging=False,
)
def _activate(config: GithubCopilotResponsesAPIConfig) -> dict[str, object]:
return config.transform_responses_api_request(
model="gpt-6-astra",
input="hello",
response_api_optional_request_params={"tools": _tools(), "store": False},
litellm_params=GenericLiteLLMParams(),
headers={},
)
def test_request_projection_preserves_message_text_ciphertext_and_nonmessage_schema() -> None:
adapter: Final = CopilotPlaintextCollaboration()
history: Final = [
{"role": "user", "content": MESSAGE},
{**_call(CLIENT_NAMESPACE), "encrypted_function_args": [{"name": "message", "content": "old-ciphertext"}]},
{"type": "function_call_output", "call_id": "call_1", "output": "unchanged result"},
{"type": "agent_message", "encrypted_content": "historical-ciphertext"},
{"type": "reasoning", "encrypted_content": "reasoning-ciphertext", "summary": []},
{"type": "additional_tools", "tools": _tools()},
{"type": "tool_search_output", "call_id": "search_1", "tools": _tools()},
]
request: Final = {"model": "gpt-6-astra", "tools": _tools(), "input": history, "store": False}
original: Final = copy.deepcopy(request)
projected: Final = adapter.prepare_request(request)
assert projected == {
**request,
"tools": _tools(UPSTREAM_NAMESPACE, encrypted=False),
"input": [
history[0],
{**history[1], "namespace": UPSTREAM_NAMESPACE},
*history[2:5],
{"type": "additional_tools", "tools": _tools(UPSTREAM_NAMESPACE, encrypted=False)},
{"type": "tool_search_output", "call_id": "search_1", "tools": _tools(UPSTREAM_NAMESPACE, encrypted=False)},
],
}
assert request == original
@pytest.mark.parametrize("inventory_type", ["additional_tools", "tool_search_output"])
def test_historical_tool_inventory_can_activate_projection(inventory_type: str) -> None:
adapter: Final = CopilotPlaintextCollaboration()
request: Final = {"input": [{"type": inventory_type, "tools": _tools()}]}
assert adapter.prepare_request(request) == {
"input": [{"type": inventory_type, "tools": _tools(UPSTREAM_NAMESPACE, encrypted=False)}],
"store": False,
}
assert adapter.restore_response({"output": [_call()]}) == {
"output": [{**_call(CLIENT_NAMESPACE), "encrypted_function_args": []}]
}
@pytest.mark.parametrize(
"tool",
[
{"type": "namespace", "name": "collaboration", "tools": [{"type": "function", "name": "spawn_agent"}]},
*_tools(encrypted=False),
*_tools(namespace="ordinary_tools"),
{
"type": "namespace",
"name": "collaboration",
"tools": [
{
"type": "function",
"name": "unrelated",
"parameters": {"properties": {"message": {"type": "string", "encrypted": True}}},
}
],
},
],
)
def test_unrelated_collaboration_schemas_and_unactivated_responses_are_identity_passthrough(
tool: dict[str, object],
) -> None:
adapter: Final = CopilotPlaintextCollaboration()
request: Final = {"tools": [tool], "input": [{"role": "user", "content": MESSAGE}]}
response: Final = {"output": [_call()]}
event: Final = {"type": "response.output_item.added", "output_index": 0, "item": _call()}
assert adapter.prepare_request(request) is request
assert adapter.restore_response(response) is response
assert adapter.restore_event(event) is event
@pytest.mark.parametrize(
("choice", "expected"),
[
({"type": "namespace", "name": CLIENT_NAMESPACE}, {"type": "namespace", "name": UPSTREAM_NAMESPACE}),
(
{"type": "function", "namespace": CLIENT_NAMESPACE, "name": "send_message"},
{"type": "function", "namespace": UPSTREAM_NAMESPACE, "name": "send_message"},
),
(
{"type": "function", "name": "collaboration.spawn_agent"},
{"type": "function", "name": "codex_collaboration_plaintext.spawn_agent"},
),
(
{"type": "function", "name": "collaboration__followup_task"},
{"type": "function", "name": "codex_collaboration_plaintext__followup_task"},
),
],
)
def test_forced_and_allowed_tool_choices_follow_the_projected_inventory(
choice: dict[str, object], expected: dict[str, object]
) -> None:
adapter: Final = CopilotPlaintextCollaboration()
request: Final = {"tools": _tools(), "tool_choice": choice}
allowed: Final = {
"type": "allowed_tools",
"mode": "required",
"tools": [choice, {"type": "function", "name": "other"}],
}
assert adapter.prepare_request(request) == {
"tools": _tools(UPSTREAM_NAMESPACE, encrypted=False),
"tool_choice": expected,
"store": False,
}
assert adapter.prepare_request({"tools": _tools(), "tool_choice": allowed}) == {
"tools": _tools(UPSTREAM_NAMESPACE, encrypted=False),
"tool_choice": {**allowed, "tools": [expected, {"type": "function", "name": "other"}]},
"store": False,
}
@pytest.mark.parametrize(
"extra",
[
{"store": True},
{"previous_response_id": "resp_previous"},
{"conversation": "conv_previous"},
{"input": [{"type": "additional_tools", "tools": _tools(UPSTREAM_NAMESPACE)}]},
],
)
def test_active_projection_rejects_stateful_requests_and_alias_collisions(extra: dict[str, object]) -> None:
adapter: Final = CopilotPlaintextCollaboration()
with pytest.raises(ValueError, match=r"stateless Responses|reserved by the Copilot adapter"):
adapter.prepare_request({"tools": _tools(), **extra})
@pytest.mark.parametrize("separator", [".", "__"])
def test_qualified_history_and_returned_names_keep_call_identifiers(separator: str) -> None:
adapter: Final = CopilotPlaintextCollaboration()
history: Final = {
"type": "function_call",
"id": "fc_previous",
"call_id": "call_previous",
"name": CLIENT_NAMESPACE + separator + "spawn_agent",
"arguments": ARGUMENTS,
}
assert adapter.prepare_request({"tools": _tools(), "input": [history]}) == {
"tools": _tools(UPSTREAM_NAMESPACE, encrypted=False),
"input": [{**history, "namespace": UPSTREAM_NAMESPACE, "name": "spawn_agent"}],
"store": False,
}
assert adapter.restore_response(
{"output": [{**history, "name": UPSTREAM_NAMESPACE + separator + "spawn_agent"}]}
) == {"output": [{**history, "namespace": CLIENT_NAMESPACE, "name": "spawn_agent", "encrypted_function_args": []}]}
def test_extra_body_tools_and_history_activate_projection_and_ordinary_replacement_disables_it() -> None:
config: Final = GithubCopilotResponsesAPIConfig(plaintext_collaboration=True)
primary: Final = config.transform_responses_api_request(
model="gpt-6-astra",
input="hello",
response_api_optional_request_params={"store": False},
litellm_params=GenericLiteLLMParams(),
headers={},
)
overlay: Final = {
"tools": _tools(),
"tool_choice": {"type": "function", "namespace": CLIENT_NAMESPACE, "name": "send_message"},
"input": [_call(CLIENT_NAMESPACE)],
}
merged: Final = config.transform_extra_body(overlay, primary, "gpt-6-astra", GenericLiteLLMParams())
assert merged == {
**primary,
"tools": _tools(UPSTREAM_NAMESPACE, encrypted=False),
"tool_choice": {"type": "function", "namespace": UPSTREAM_NAMESPACE, "name": "send_message"},
"input": [_call()],
}
restored: Final = config.transform_response_api_response(
"gpt-6-astra", httpx.Response(200, json=_response([_call()])), _logging()
)
assert JSON_OBJECT.validate_json(restored.model_dump_json(exclude_none=True))["output"] == [
{**_call(CLIENT_NAMESPACE), "encrypted_function_args": []}
]
replacement: Final = {"tools": _tools(encrypted=False)}
assert config.transform_extra_body(replacement, primary, "gpt-6-astra", GenericLiteLLMParams()) == {
**primary,
**replacement,
}
untouched: Final = config.transform_response_api_response(
"gpt-6-astra", httpx.Response(200, json=_response([_call(CLIENT_NAMESPACE)])), _logging()
)
assert JSON_OBJECT.validate_json(untouched.model_dump_json(exclude_none=True))["output"] == [
_call(CLIENT_NAMESPACE)
]
@pytest.mark.parametrize(
"overlay",
[
{"store": True},
{"previous_response_id": "resp_previous"},
{"conversation": {"id": "conv_previous"}},
{"input": [{"type": "tool_search_output", "tools": _tools(UPSTREAM_NAMESPACE)}]},
],
)
def test_extra_body_cannot_bypass_stateless_and_namespace_guards(overlay: dict[str, object]) -> None:
config: Final = GithubCopilotResponsesAPIConfig(plaintext_collaboration=True)
primary: Final = _activate(config)
with pytest.raises(ValueError, match=r"stateless Responses|reserved by the Copilot adapter"):
config.transform_extra_body(overlay, primary, "gpt-6-astra", GenericLiteLLMParams())
def test_explicitly_disabled_provider_preserves_encryption_protocol() -> None:
config: Final = GithubCopilotResponsesAPIConfig(plaintext_collaboration=False)
raw: Final = httpx.Response(200, json=_response([_call(CLIENT_NAMESPACE)]))
assert _activate(config)["tools"] == _tools()
parsed: Final = config.transform_response_api_response("gpt-6-astra", raw, _logging())
assert JSON_OBJECT.validate_json(parsed.model_dump_json(exclude_none=True))["output"] == [_call(CLIENT_NAMESPACE)]
def test_provider_nonstream_restoration_survives_serialization_and_keeps_headers_and_usage() -> None:
config: Final = GithubCopilotResponsesAPIConfig(plaintext_collaboration=True)
logging: Final = _logging()
raw: Final = httpx.Response(
200,
json={**_response([_call(), _call(name="wait_agent")]), "tools": _tools(UPSTREAM_NAMESPACE, encrypted=False)},
headers={"x-request-id": "request_1", "x-ratelimit-remaining-requests": "10"},
)
assert _activate(config)["tools"] == _tools(UPSTREAM_NAMESPACE, encrypted=False)
response: Final = config.transform_response_api_response("gpt-6-astra", raw, logging)
serialized: Final = JSON_OBJECT.validate_json(response.model_dump_json(exclude_none=True))
assert serialized["output"] == [
{**_call(CLIENT_NAMESPACE), "encrypted_function_args": []},
_call(CLIENT_NAMESPACE, name="wait_agent"),
]
assert serialized["tools"] == _tools(encrypted=False)
assert serialized["provider_extension"] == {"keep": True}
assert response.usage is not None
assert (response.usage.input_tokens, response.usage.output_tokens, response.usage.total_tokens) == (9, 4, 13)
assert response.hidden_params["headers"] == dict(raw.headers)
assert response.hidden_params["additional_headers"]
assert logging.model_call_details["original_response"] == raw.text
def _stream(config: GithubCopilotResponsesAPIConfig, event: dict[str, object]) -> dict[str, object]:
return JSON_OBJECT.validate_json(
config.transform_streaming_response("gpt-6-astra", event, _logging(stream=True)).model_dump_json(
exclude_none=True
)
)
@pytest.mark.parametrize("identity", [{}, {"name": None, "namespace": None}, {"name": "", "namespace": ""}])
def test_stream_restores_sparse_events_and_completed_snapshot_through_real_event_models(
identity: dict[str, object],
) -> None:
config: Final = GithubCopilotResponsesAPIConfig(plaintext_collaboration=True)
_activate(config)
added: Final = _stream(config, {"type": "response.output_item.added", "output_index": 0, "item": _call()})
delta: Final = _stream(
config,
{
"type": "response.function_call_arguments.delta",
"output_index": 0,
"item_id": "fc_changed",
"namespace": UPSTREAM_NAMESPACE,
"name": "send_message",
"delta": ARGUMENTS[:12],
},
)
arguments: Final = _stream(
config,
{
"type": "response.function_call_arguments.done",
"output_index": 0,
"item_id": "fc_changed",
"arguments": ARGUMENTS,
**identity,
},
)
done: Final = _stream(
config,
{
"type": "response.output_item.done",
"output_index": 0,
"item": {"type": "function_call", "id": "fc_changed", "arguments": ARGUMENTS, **identity},
},
)
completed: Final = _stream(config, {"type": "response.completed", "response": _response([_call()])})
assert added["item"] == {**_call(CLIENT_NAMESPACE), "encrypted_function_args": []}
assert delta == {
"type": "response.function_call_arguments.delta",
"output_index": 0,
"item_id": "fc_call",
"namespace": CLIENT_NAMESPACE,
"name": "send_message",
"delta": ARGUMENTS[:12],
}
assert arguments == {
"type": "response.function_call_arguments.done",
"output_index": 0,
"item_id": "fc_call",
"call_id": "call_1",
"arguments": ARGUMENTS,
"namespace": CLIENT_NAMESPACE,
"name": "send_message",
"encrypted_function_args": [],
}
assert done["item"] == {
"type": "function_call",
"id": "fc_call",
"call_id": "call_1",
"arguments": ARGUMENTS,
"name": "send_message",
"namespace": CLIENT_NAMESPACE,
"encrypted_function_args": [],
}
snapshot: Final = JSON_OBJECT.validate_python(completed["response"])
assert snapshot["output"] == [{**_call(CLIENT_NAMESPACE), "encrypted_function_args": []}]
def test_custom_calls_keep_their_own_encryption_metadata_without_a_plaintext_dispatch_marker() -> None:
adapter: Final = CopilotPlaintextCollaboration()
adapter.prepare_request({"tools": _tools()})
call: Final = {
"type": "custom_tool_call",
"call_id": "custom_1",
"namespace": UPSTREAM_NAMESPACE,
"name": "send_message",
"input": "opaque custom payload",
"encrypted_function_args": ["provider_metadata"],
}
restored: Final = {**call, "namespace": CLIENT_NAMESPACE}
assert adapter.restore_response({"output": [call]}) == {"output": [restored]}
assert adapter.restore_event({"type": "response.output_item.done", "output_index": 0, "item": call}) == {
"type": "response.output_item.done",
"output_index": 0,
"item": restored,
}
@pytest.mark.parametrize("namespace", ["different_namespace", CLIENT_NAMESPACE])
def test_stream_rejects_conflicting_tool_identity(namespace: str) -> None:
adapter: Final = CopilotPlaintextCollaboration()
adapter.prepare_request({"tools": _tools()})
adapter.restore_event({"type": "response.output_item.added", "output_index": 0, "item": _call()})
with pytest.raises(ValueError, match="Conflicting Copilot collaboration identity"):
adapter.restore_event({"type": "response.output_item.done", "output_index": 0, "item": _call(namespace)})
@pytest.mark.parametrize("encrypted_args", [["message"], [{"name": "message", "content": "ciphertext"}], None])
def test_provider_never_labels_encrypted_arguments_as_plaintext(encrypted_args: object) -> None:
adapter: Final = CopilotPlaintextCollaboration()
adapter.prepare_request({"tools": _tools()})
output: Final = {**_call(), "encrypted_function_args": encrypted_args}
with pytest.raises(ValueError, match="encrypted arguments for a plaintext collaboration tool"):
adapter.restore_response({"output": [output]})
with pytest.raises(ValueError, match="encrypted arguments for a plaintext collaboration tool"):
adapter.restore_event({"type": "response.output_item.added", "output_index": 0, "item": output})