Files
azure-gpt-cursor/app/azure/request_adapter.py
T
dat-nguyen96 e371af9cd2 Add content conversion helper methods for Azure request handling
- Introduced `_content_to_text` method in `request_adapter.py` to convert various content types into a string format suitable for Azure.
- Updated existing logic to utilize this new method for processing message content.
- Added `_content_to_string` method in `logging.py` for similar content conversion, enhancing display capabilities for message content.
2026-03-12 13:18:15 +01:00

221 lines
8.2 KiB
Python

"""Request adaptation helpers for Azure Responses API.
This module defines RequestAdapter, which transforms incoming OpenAI-style
requests into Azure Responses API request parameters.
"""
from __future__ import annotations
from typing import Any, Dict, List
from flask import Request, current_app
from ..exceptions import CursorConfigurationError, ServiceConfigurationError
class RequestAdapter:
"""Handle pre-request adaptation for the Azure Responses API.
Transforms OpenAI Completions/Chat-style inputs into Azure Responses API
request parameters suitable for streaming completions in this codebase.
Returns request_kwargs for requests.request(**kwargs). Also sets
per-request state on the adapter (model).
"""
def __init__(self, adapter: Any) -> None:
"""Initialize the adapter with a reference to the AzureAdapter."""
self.adapter = adapter # AzureAdapter instance for shared config/env
# ---- Helpers (kept local to minimize cross-module coupling) ----
def _content_to_text(self, content: Any) -> str:
"""Convert message content (string or list of parts) to a string for Azure."""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for part in content:
if isinstance(part, dict):
if part.get("type") == "text":
parts.append(part.get("text", ""))
elif part.get("type") == "image_url":
parts.append("[image]")
else:
parts.append(f"[{part.get('type', 'unknown')}]")
else:
parts.append(str(part))
return "\n".join(parts) if parts else ""
return str(content)
def _copy_request_headers_for_azure(
self, src: Request, *, api_key: str
) -> Dict[str, str]:
headers: Dict[str, str] = {k: v for k, v in src.headers.items()}
headers.pop("Host", None)
# Azure prefers api-key header
headers.pop("Authorization", None)
headers["api-key"] = api_key
return headers
def _messages_to_responses_input_and_instructions(
self, messages: List[Dict[str, Any]]
) -> Dict[str, Any]:
instructions_parts: List[str] = []
input_items: List[Dict[str, Any]] = []
for m in messages:
role = m.get("role")
content = m.get("content")
if role in {"system", "developer"}:
instructions_parts.append(self._content_to_text(content))
continue
# For user/assistant/tools as inputs
if role == "tool":
call_id = m.get("tool_call_id")
item = {
"type": "function_call_output",
"output": self._content_to_text(content),
"status": "completed",
"call_id": call_id,
}
input_items.append(item)
else:
text_content = self._content_to_text(content)
item = {
"role": role or "user",
"content": [
{
"type": "input_text" if role == "user" else "output_text",
"text": text_content,
},
],
}
input_items.append(item)
if tool_calls := m.get("tool_calls"):
for tool_call in tool_calls:
function = tool_call.get("function", {})
call_id = tool_call.get("id")
item = {
"type": "function_call",
"name": function.get("name"),
"arguments": function.get("arguments"),
"call_id": call_id,
}
input_items.append(item)
instructions = "\n\n".join(instructions_parts) if instructions_parts else None
return {
"instructions": instructions,
"input": input_items if input_items else None,
}
def _transform_tools_for_responses(self, tools: Any) -> Any:
out: List[Dict[str, Any]] = []
if not isinstance(tools, list):
current_app.logger.debug(
"Skipping tool transformation because tools payload is not a list: %r",
tools,
)
return out
for tool in tools:
function = tool.get("function")
transformed: Dict[str, Any] = {
"type": "function",
"name": function.get("name"),
"description": function.get("description"),
"parameters": function.get("parameters"),
"strict": False,
}
out.append(transformed)
return out
# ---- Main adaptation (always streaming completions-like) ----
def adapt(self, req: Request) -> Dict[str, Any]:
"""Build requests.request kwargs for the Azure Responses API call.
Maps inputs to the Responses schema and returns a dict suitable for
requests.request(**kwargs).
"""
# Reset per-request state
self.adapter.inbound_model = None
# Parse request body
payload = req.get_json(silent=True, force=False)
# Determine target model: prefer env AZURE_MODEL/AZURE_DEPLOYMENT
inbound_model = payload.get("model") if isinstance(payload, dict) else None
self.adapter.inbound_model = inbound_model
settings = current_app.config
upstream_headers = self._copy_request_headers_for_azure(
req, api_key=settings["AZURE_API_KEY"]
)
# Map Chat/Completions to Responses (always streaming)
messages = payload.get("messages") or []
responses_body = (
self._messages_to_responses_input_and_instructions(messages)
if isinstance(messages, list)
else {"input": None, "instructions": None}
)
responses_body["model"] = settings["AZURE_DEPLOYMENT"]
# Transform tools and tool choice
responses_body["tools"] = self._transform_tools_for_responses(
payload.get("tools", [])
)
responses_body["tool_choice"] = payload.get("tool_choice")
responses_body["prompt_cache_key"] = payload.get("user")
# Always streaming
responses_body["stream"] = True
reasoning_effort = inbound_model.replace("gpt-", "").lower()
if reasoning_effort not in {"high", "medium", "low", "minimal"}:
raise CursorConfigurationError(
"Model name must be either gpt-high, gpt-medium, gpt-low, or gpt-minimal."
f"\n\nGot: {inbound_model}"
)
responses_body["reasoning"] = {
"effort": reasoning_effort,
}
# Concise is not supported by GPT-5,
# but allowing it for now to be able to test it on other models
if settings["AZURE_SUMMARY_LEVEL"] in {"auto", "detailed", "concise"}:
responses_body["reasoning"]["summary"] = settings["AZURE_SUMMARY_LEVEL"]
else:
raise ServiceConfigurationError(
"AZURE_SUMMARY_LEVEL must be either auto, detailed, or concise."
f"\n\nGot: {settings['AZURE_SUMMARY_LEVEL']}"
)
# No need to pass verbosity if it's set to medium, as it's the model's default
if settings["AZURE_VERBOSITY_LEVEL"] in {"low", "high"}:
responses_body["text"] = {"verbosity": settings["AZURE_VERBOSITY_LEVEL"]}
responses_body["store"] = False
responses_body["stream_options"] = {"include_obfuscation": False}
if settings["AZURE_TRUNCATION"] == "auto":
responses_body["truncation"] = settings["AZURE_TRUNCATION"]
request_kwargs: Dict[str, Any] = {
"method": "POST",
"url": settings["AZURE_RESPONSES_API_URL"],
"headers": upstream_headers,
"json": responses_body,
"data": None,
"stream": True,
"timeout": (60, None),
}
return request_kwargs