Files
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

124 lines
4.2 KiB
Python

"""Request-body policy for OpenAI-compatible chat providers."""
from collections.abc import Callable, Iterable
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Literal
from loguru import logger
from free_claude_code.application.errors import InvalidRequestError
from free_claude_code.core.anthropic import ReasoningReplayMode, build_base_request_body
from free_claude_code.core.anthropic.conversion import OpenAIConversionError
from free_claude_code.core.anthropic.models import MessagesRequest
MaxTokensField = Literal["max_tokens", "max_completion_tokens"]
OpenAIChatPostprocessor = Callable[[dict[str, Any], MessagesRequest, bool], None]
ExtraBodyValidator = Callable[[dict[str, Any]], None]
@dataclass(frozen=True, slots=True)
class OpenAIChatRequestPolicy:
"""Provider policy for Anthropic-to-OpenAI chat request conversion."""
provider_name: str
include_extra_body: bool = False
extra_body_validator: ExtraBodyValidator | None = None
reject_extra_body_message: str | None = None
default_max_tokens: int | None = None
max_tokens_field: MaxTokensField = "max_tokens"
reasoning_replay: ReasoningReplayMode | None = None
strip_message_names: bool = False
unsupported_body_keys: frozenset[str] = field(default_factory=frozenset)
normalize_n_to_one: bool = False
def build_openai_chat_request_body(
request_data: MessagesRequest,
*,
thinking_enabled: bool,
policy: OpenAIChatRequestPolicy,
postprocessors: Iterable[OpenAIChatPostprocessor] = (),
) -> dict[str, Any]:
"""Build an OpenAI-compatible chat request body from an Anthropic request."""
logger.debug(
"{}_REQUEST: conversion start model={} msgs={}",
policy.provider_name,
request_data.model,
len(request_data.messages),
)
try:
reasoning_replay = policy.reasoning_replay
if reasoning_replay is None:
reasoning_replay = (
ReasoningReplayMode.REASONING_CONTENT
if thinking_enabled
else ReasoningReplayMode.DISABLED
)
body = build_base_request_body(
request_data,
default_max_tokens=policy.default_max_tokens,
reasoning_replay=reasoning_replay,
)
except OpenAIConversionError as exc:
raise InvalidRequestError(str(exc)) from exc
request_extra = request_data.extra_body
if isinstance(request_extra, dict) and request_extra:
if policy.reject_extra_body_message:
raise InvalidRequestError(policy.reject_extra_body_message)
if policy.include_extra_body:
extra_body = deepcopy(request_extra)
if policy.extra_body_validator is not None:
try:
policy.extra_body_validator(extra_body)
except ValueError as exc:
raise InvalidRequestError(str(exc)) from exc
body["extra_body"] = extra_body
_apply_common_openai_chat_policy(body, policy)
for postprocess in postprocessors:
postprocess(body, request_data, thinking_enabled)
logger.debug(
"{}_REQUEST: conversion done model={} msgs={} tools={}",
policy.provider_name,
body.get("model"),
len(body.get("messages", [])),
len(body.get("tools", [])),
)
return body
def _apply_common_openai_chat_policy(
body: dict[str, Any], policy: OpenAIChatRequestPolicy
) -> None:
if policy.strip_message_names:
_strip_message_names(body.get("messages"))
for key in policy.unsupported_body_keys:
body.pop(key, None)
if policy.max_tokens_field == "max_completion_tokens":
_normalize_max_completion_tokens(body)
if policy.normalize_n_to_one and body.get("n") is not None:
body["n"] = 1
def _strip_message_names(messages: Any) -> None:
if not isinstance(messages, list):
return
for message in messages:
if isinstance(message, dict):
message.pop("name", None)
def _normalize_max_completion_tokens(body: dict[str, Any]) -> None:
if "max_completion_tokens" in body:
body.pop("max_tokens", None)
return
if "max_tokens" in body and body["max_tokens"] is not None:
body["max_completion_tokens"] = body.pop("max_tokens")