Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

251 lines
9.5 KiB
Python

import os
from typing import Any, AsyncIterator, Iterator
from cua_core.http import cua_version_headers
from litellm import acompletion, completion
from litellm.llms.custom_llm import CustomLLM
from litellm.types.utils import GenericStreamingChunk, ModelResponse
class CUAAdapter(CustomLLM):
def __init__(self, base_url: str | None = None, api_key: str | None = None, **_: Any):
super().__init__()
self.base_url = base_url or os.environ.get("CUA_BASE_URL") or "https://inference.cua.ai/v1"
self.api_key = (
api_key or os.environ.get("CUA_INFERENCE_API_KEY") or os.environ.get("CUA_API_KEY")
)
def _normalize_model(self, model: str) -> str:
"""Strip known prefixes to get the base model name."""
known_prefixes = ("cua/", "anthropic/", "gemini/", "google/", "openai/")
result = model
for prefix in known_prefixes:
if result.startswith(prefix):
result = result[len(prefix) :]
return result
def _resolve_route(self, model: str, api_base: str) -> tuple[str, str]:
"""Return (prefixed_model, api_base) for the CUA inference API."""
if "anthropic/" in model:
return f"anthropic/{self._normalize_model(model)}", api_base.removesuffix("/v1")
elif "gemini/" in model or "google/" in model:
return f"gemini/{self._normalize_model(model)}", api_base + "/gemini"
else:
return f"openai/{self._normalize_model(model)}", api_base
def _resolve_api_key(self, kwargs: dict | None = None) -> str:
"""Resolve the CUA API key, raising a clear error if missing.
Checks kwargs (from ComputerAgent api_key param) then falls back
to self.api_key (from CUA_API_KEY / CUA_INFERENCE_API_KEY env vars).
This validation must run before the inner litellm call because that
call uses an anthropic/ or openai/ model prefix, which would cause
litellm to fall back to ANTHROPIC_API_KEY from env — sending the
wrong key to the CUA inference endpoint.
"""
resolved = (kwargs.get("api_key") if kwargs else None) or self.api_key
if not resolved:
raise ValueError(
"No CUA API key provided for cua/ model inference. "
"Please either set the CUA_API_KEY environment variable "
"or pass api_key to ComputerAgent()."
)
return resolved
def completion(self, *args, **kwargs) -> ModelResponse:
model, api_base = self._resolve_route(
kwargs.get("model", ""), kwargs.get("api_base") or self.base_url
)
api_key = self._resolve_api_key(kwargs)
# Ensure the CUA inference API always receives Bearer auth;
# merge caller headers first, then force Authorization so it cannot be overridden.
extra_headers = {}
if "extra_headers" in kwargs:
extra_headers.update(kwargs.pop("extra_headers"))
extra_headers["Authorization"] = f"Bearer {api_key}"
params = {
"model": model,
"messages": kwargs.get("messages", []),
"api_base": api_base,
"api_key": api_key,
"extra_headers": extra_headers,
"stream": False,
}
# Forward tools if provided
if "tools" in kwargs:
params["tools"] = kwargs["tools"]
if "optional_params" in kwargs:
protected_keys = {"api_key", "extra_headers", "model", "api_base", "stream"}
filtered = {
k: v for k, v in kwargs["optional_params"].items() if k not in protected_keys
}
params.update(filtered)
del kwargs["optional_params"]
if "headers" in kwargs:
params["headers"] = kwargs["headers"]
del kwargs["headers"]
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
ignored_keys = {
"litellm_params",
"client",
"print_verbose",
"acompletion",
"timeout",
"logging_obj",
"encoding",
"custom_prompt_dict",
"model_response",
"logger_fn",
}
dropped_keys = original_keys - used_keys - ignored_keys
if dropped_keys:
dropped_keyvals = {k: kwargs[k] for k in dropped_keys}
# print(f"CUAAdapter.completion: Dropped parameters: {dropped_keyvals}")
return completion(**params) # type: ignore
async def acompletion(self, *args, **kwargs) -> ModelResponse:
model, api_base = self._resolve_route(
kwargs.get("model", ""), kwargs.get("api_base") or self.base_url
)
api_key = self._resolve_api_key(kwargs)
# Ensure the CUA inference API always receives Bearer auth;
# merge caller headers first, then force Authorization so it cannot be overridden.
extra_headers = {}
if "extra_headers" in kwargs:
extra_headers.update(kwargs.pop("extra_headers"))
extra_headers["Authorization"] = f"Bearer {api_key}"
params = {
"model": model,
"messages": kwargs.get("messages", []),
"api_base": api_base,
"api_key": api_key,
"extra_headers": extra_headers,
"stream": False,
}
# Forward tools if provided
if "tools" in kwargs:
params["tools"] = kwargs["tools"]
if "optional_params" in kwargs:
protected_keys = {"api_key", "extra_headers", "model", "api_base", "stream"}
filtered = {
k: v for k, v in kwargs["optional_params"].items() if k not in protected_keys
}
params.update(filtered)
del kwargs["optional_params"]
if "headers" in kwargs:
params["headers"] = kwargs["headers"]
del kwargs["headers"]
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
ignored_keys = {
"litellm_params",
"client",
"print_verbose",
"acompletion",
"timeout",
"logging_obj",
"encoding",
"custom_prompt_dict",
"model_response",
"logger_fn",
}
dropped_keys = original_keys - used_keys - ignored_keys
if dropped_keys:
dropped_keyvals = {k: kwargs[k] for k in dropped_keys}
# print(f"CUAAdapter.acompletion: Dropped parameters: {dropped_keyvals}")
response = await acompletion(**params) # type: ignore
return response
def streaming(self, *args, **kwargs) -> Iterator[GenericStreamingChunk]:
params = dict(kwargs)
model, api_base = self._resolve_route(
params.get("model", ""), params.get("api_base") or self.base_url
)
api_key = self._resolve_api_key(kwargs)
# Ensure the CUA inference API always receives Bearer auth;
# merge caller headers first, then force Authorization so it cannot be overridden.
extra_headers = {}
if "extra_headers" in params:
extra_headers.update(params.pop("extra_headers"))
extra_headers["Authorization"] = f"Bearer {api_key}"
params.update(
{
"model": model,
"api_base": api_base,
"api_key": api_key,
"extra_headers": extra_headers,
"stream": True,
}
)
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
# Yield chunks directly from LiteLLM's streaming generator
for chunk in completion(**params): # type: ignore
yield chunk # type: ignore
async def astreaming(self, *args, **kwargs) -> AsyncIterator[GenericStreamingChunk]:
params = dict(kwargs)
model, api_base = self._resolve_route(
params.get("model", ""), params.get("api_base") or self.base_url
)
api_key = self._resolve_api_key(kwargs)
# Ensure the CUA inference API always receives Bearer auth;
# merge caller headers first, then force Authorization so it cannot be overridden.
extra_headers = {}
if "extra_headers" in params:
extra_headers.update(params.pop("extra_headers"))
extra_headers["Authorization"] = f"Bearer {api_key}"
params.update(
{
"model": model,
"api_base": api_base,
"api_key": api_key,
"extra_headers": extra_headers,
"stream": True,
}
)
# Always include CUA version headers
version_hdrs = cua_version_headers()
if version_hdrs:
params["headers"] = {**version_hdrs, **params.get("headers", {})}
stream = await acompletion(**params) # type: ignore
async for chunk in stream: # type: ignore
yield chunk # type: ignore