Spaces:
Sleeping
Sleeping
| """Message and tool format converters.""" | |
| import json | |
| from typing import Any | |
| def get_block_attr(block: Any, attr: str, default: Any = None) -> Any: | |
| """Get attribute from object or dict.""" | |
| if hasattr(block, attr): | |
| return getattr(block, attr) | |
| if isinstance(block, dict): | |
| return block.get(attr, default) | |
| return default | |
| def get_block_type(block: Any) -> str | None: | |
| """Get block type from object or dict.""" | |
| return get_block_attr(block, "type") | |
| class AnthropicToOpenAIConverter: | |
| """Converts Anthropic message format to OpenAI format.""" | |
| def convert_messages( | |
| messages: list[Any], | |
| *, | |
| include_reasoning_for_openrouter: bool = False, | |
| ) -> list[dict[str, Any]]: | |
| """Convert a list of Anthropic messages to OpenAI format. | |
| When include_reasoning_for_openrouter is True, assistant messages with | |
| thinking blocks get reasoning_content added for OpenRouter multi-turn | |
| reasoning continuation. | |
| """ | |
| result = [] | |
| for msg in messages: | |
| role = msg.role | |
| content = msg.content | |
| if role == "system": | |
| text = content if isinstance(content, str) else " ".join( | |
| getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text" | |
| ) | |
| result.append({"role": "system", "content": text}) | |
| elif isinstance(content, str): | |
| result.append({"role": role, "content": content}) | |
| elif isinstance(content, list): | |
| if role == "assistant": | |
| result.extend( | |
| AnthropicToOpenAIConverter._convert_assistant_message( | |
| content, | |
| include_reasoning_for_openrouter=include_reasoning_for_openrouter, | |
| ) | |
| ) | |
| elif role == "user": | |
| result.extend( | |
| AnthropicToOpenAIConverter._convert_user_message(content) | |
| ) | |
| else: | |
| result.append({"role": role, "content": str(content)}) | |
| return result | |
| def _convert_assistant_message( | |
| content: list[Any], | |
| *, | |
| include_reasoning_for_openrouter: bool = False, | |
| ) -> list[dict[str, Any]]: | |
| """Convert assistant message blocks, preserving interleaved thinking+text order.""" | |
| content_parts: list[str] = [] | |
| thinking_parts: list[str] = [] | |
| tool_calls: list[dict[str, Any]] = [] | |
| for block in content: | |
| block_type = get_block_type(block) | |
| if block_type == "text": | |
| content_parts.append(get_block_attr(block, "text", "")) | |
| elif block_type == "thinking": | |
| thinking = get_block_attr(block, "thinking", "") | |
| content_parts.append(f"<think>\n{thinking}\n</think>") | |
| if include_reasoning_for_openrouter: | |
| thinking_parts.append(thinking) | |
| elif block_type == "tool_use": | |
| tool_input = get_block_attr(block, "input", {}) | |
| tool_calls.append( | |
| { | |
| "id": get_block_attr(block, "id"), | |
| "type": "function", | |
| "function": { | |
| "name": get_block_attr(block, "name"), | |
| "arguments": json.dumps(tool_input) | |
| if isinstance(tool_input, dict) | |
| else str(tool_input), | |
| }, | |
| } | |
| ) | |
| content_str = "\n\n".join(content_parts) | |
| # Ensure content is never an empty string for assistant messages | |
| # NIM (especially Mistral models) requires non-empty content if there are no tool calls | |
| if not content_str and not tool_calls: | |
| content_str = " " | |
| msg: dict[str, Any] = { | |
| "role": "assistant", | |
| "content": content_str, | |
| } | |
| if tool_calls: | |
| msg["tool_calls"] = tool_calls | |
| if include_reasoning_for_openrouter and thinking_parts: | |
| msg["reasoning_content"] = "\n".join(thinking_parts) | |
| return [msg] | |
| def _convert_user_message(content: list[Any]) -> list[dict[str, Any]]: | |
| """Convert user message blocks (including tool results), preserving order.""" | |
| result: list[dict[str, Any]] = [] | |
| text_parts: list[str] = [] | |
| def flush_text() -> None: | |
| if text_parts: | |
| result.append({"role": "user", "content": "\n".join(text_parts)}) | |
| text_parts.clear() | |
| for block in content: | |
| block_type = get_block_type(block) | |
| if block_type == "text": | |
| text_parts.append(get_block_attr(block, "text", "")) | |
| elif block_type == "tool_result": | |
| flush_text() | |
| tool_content = get_block_attr(block, "content", "") | |
| if isinstance(tool_content, list): | |
| tool_content = "\n".join( | |
| item.get("text", str(item)) | |
| if isinstance(item, dict) | |
| else str(item) | |
| for item in tool_content | |
| ) | |
| result.append( | |
| { | |
| "role": "tool", | |
| "tool_call_id": get_block_attr(block, "tool_use_id"), | |
| "content": str(tool_content) if tool_content else "", | |
| } | |
| ) | |
| flush_text() | |
| return result | |
| def convert_tools(tools: list[Any]) -> list[dict[str, Any]]: | |
| """Convert Anthropic tools to OpenAI format.""" | |
| return [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": tool.name, | |
| "description": tool.description or "", | |
| "parameters": tool.input_schema, | |
| }, | |
| } | |
| for tool in tools | |
| ] | |
| def convert_system_prompt(system: Any) -> dict[str, str] | None: | |
| """Convert Anthropic system prompt to OpenAI format.""" | |
| if isinstance(system, str): | |
| return {"role": "system", "content": system} | |
| elif isinstance(system, list): | |
| text_parts = [ | |
| get_block_attr(block, "text", "") | |
| for block in system | |
| if get_block_type(block) == "text" | |
| ] | |
| if text_parts: | |
| return {"role": "system", "content": "\n\n".join(text_parts).strip()} | |
| return None | |
| def build_base_request_body( | |
| request_data: Any, | |
| *, | |
| default_max_tokens: int | None = None, | |
| include_reasoning_for_openrouter: bool = False, | |
| ) -> dict[str, Any]: | |
| """Build the common parts of an OpenAI-format request body. | |
| Handles message conversion, system prompt, max_tokens, temperature, | |
| top_p, stop sequences, tools, and tool_choice. Provider-specific | |
| parameters (extra_body, penalties, NIM settings) are added by callers. | |
| """ | |
| from providers.common.utils import set_if_not_none | |
| messages = AnthropicToOpenAIConverter.convert_messages( | |
| request_data.messages, | |
| include_reasoning_for_openrouter=include_reasoning_for_openrouter, | |
| ) | |
| system = getattr(request_data, "system", None) | |
| if system: | |
| system_msg = AnthropicToOpenAIConverter.convert_system_prompt(system) | |
| if system_msg: | |
| messages.insert(0, system_msg) | |
| body: dict[str, Any] = {"model": request_data.model, "messages": messages} | |
| max_tokens = getattr(request_data, "max_tokens", None) | |
| set_if_not_none(body, "max_tokens", max_tokens or default_max_tokens) | |
| set_if_not_none(body, "temperature", getattr(request_data, "temperature", None)) | |
| set_if_not_none(body, "top_p", getattr(request_data, "top_p", None)) | |
| stop_sequences = getattr(request_data, "stop_sequences", None) | |
| if stop_sequences: | |
| body["stop"] = stop_sequences | |
| tools = getattr(request_data, "tools", None) | |
| if tools: | |
| body["tools"] = AnthropicToOpenAIConverter.convert_tools(tools) | |
| tool_choice = getattr(request_data, "tool_choice", None) | |
| if tool_choice: | |
| body["tool_choice"] = tool_choice | |
| return body | |