diff --git a/docs/suts-how-to.md b/docs/suts-how-to.md index 02df7ff4..e62c8751 100644 --- a/docs/suts-how-to.md +++ b/docs/suts-how-to.md @@ -57,48 +57,6 @@ Llama-4-Maverick-17B-128E-Instruct on sambanova via Huggingface: `meta-llama/Llama-4-Maverick-17B-128E-Instruct:sambanova:hfrelay` -### OpenAI-Compatible Dynamic SUTs - -If your SUT has an OpenAI-compatible API, you can add it with minimal code. VLLM and models hosted by OpenAI -(like the chatgpt family) support the OpenAI API. Other providers offer that option too. This is a good option -if you self-host a model using VLLM. - -The UID for an OpenAI-compatible SUT works the same way as above, with "openai" as the `driver` section and a string of your choice as the `provider` section of the UID, e.g.: - -`my/big_model:my_host:openai` - -Because these SUTs need a base URL for the API, you do need to write a little code as follows: - -1. Create a subclass of `OpenAIGenericSUTFactory` in [openai_sut_factory.py](../src/modelgauge/suts/openai_sut_factory.py): - * `base_url` is the base URL of your API server. - * `provider` is a string of your choice. It must be a valid TOML section identifier. We strongly recommend lowercase ASCII letters. -2. Add your new class to the `OPENAI_SUT_FACTORIES` dict in [openai_sut_factory.py](../src/modelgauge/suts/openai_sut_factory.py). The dict key must be the same as the value set for `provider`. - -```python -class MySUTFactory(OpenAIGenericSUTFactory): - def __init__(self, raw_secrets, **kwargs): - super().__init__(raw_secrets) - self.provider = "my_host" - self.base_url = "https://example.net/v1/" - -OPENAI_SUT_FACTORIES: dict = {"my_host": MySUTFactory} -``` - -3. Add a scope containing the `api_key` secret to your API in [config/secrets.toml](../config/secrets.toml). The scope must be named the same as the `provider` in your SUT factory class. - -```toml -[my_host] -api_key= -``` - -Your SUT UID will look like `my/big_model:my_host:openai`, and you can use it with modelgauge and modelbench like this: - -```bash -uv run modelgauge run-sut --sut my/big_model:my_host:openai --prompt "Why did the chicken cross the road?" - -uv run modelbench benchmark general --sut my/big_model:my_host:openai --prompt-set practice --evaluator default -m 10 -``` - ### Dynamic SUTs With New Drivers If your SUT provider requires custom client code that isn't available in this repo, you will need to write some driver code. Details are in [add-a-new-sut-driver.md](./add-a-new-sut-driver.md). diff --git a/src/modelgauge/sut_factory.py b/src/modelgauge/sut_factory.py index 653111cb..ae5c6059 100644 --- a/src/modelgauge/sut_factory.py +++ b/src/modelgauge/sut_factory.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional +from typing import Optional, Any from modelgauge.config import load_secrets_from_config from modelgauge.dynamic_sut_factory import DynamicDriverSUTFactory, UnknownSUTMakerError @@ -212,4 +212,26 @@ def get_missing_dependencies(self, uid: str, *, secrets: RawSecrets): return self.sut_registry.get_missing_dependencies(uid, secrets=secrets) -SUT_FACTORY = SUTFactory(SUTS) +# Stop SUTFactory from initializing on startup. Yes, this is kooky; the right solution +# is to make this not be a global variable + +_GLOBAL_VARIABLES: dict[str, Any] = {} + + +def __getattr__(name: str): + if name == "SUT_FACTORY": + _GLOBAL_VARIABLES["SUT_FACTORY"] = SUTFactory(SUTS) + + if name in _GLOBAL_VARIABLES: + return _GLOBAL_VARIABLES[name] + if name in globals(): + return globals()[name] + if name in vars(): + return vars()[name] + if name == "__path__": + return None # this works, but I'm not sure it's correct in all cases + raise (Exception(f"unknown: {name}")) + + +def __setattr__(name: str, value: str): + _GLOBAL_VARIABLES[name] = value diff --git a/src/modelgauge/suts/aws_bedrock_client.py b/src/modelgauge/suts/aws_bedrock_client.py deleted file mode 100644 index 060468ee..00000000 --- a/src/modelgauge/suts/aws_bedrock_client.py +++ /dev/null @@ -1,178 +0,0 @@ -# as defined here: -# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html - -import os -from typing import Any, Dict, List, Optional - -import boto3 - -from modelgauge.general import APIException -from modelgauge.prompt import TextPrompt -from modelgauge.retry_decorator import retry -from modelgauge.secret_values import InjectSecret, RequiredSecret, SecretDescription -from modelgauge.sut import PromptResponseSUT, SUTResponse -from modelgauge.model_options import ModelOptions -from modelgauge.sut_capabilities import AcceptsTextPrompt -from modelgauge.sut_decorator import modelgauge_sut -from modelgauge.sut_registry import SUTS -from pydantic import BaseModel - - -class AwsAccessKeyId(RequiredSecret): - @classmethod - def description(cls) -> SecretDescription: - return SecretDescription( - scope="aws", - key="access_key_id", - instructions="See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html", - ) - - -class AwsSecretAccessKey(RequiredSecret): - @classmethod - def description(cls) -> SecretDescription: - return SecretDescription( - scope="aws", - key="secret_access_key", - instructions="See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html", - ) - - -class BedrockRequest(BaseModel): - class BedrockMessage(BaseModel): - role: str = "user" - content: List[Dict[str, str]] = [] - - class InferenceConfig(BaseModel): - maxTokens: Optional[int] = None - temperature: Optional[float] = None - topP: Optional[float] = None - stopSequences: Optional[list[str]] = None - - class GuardrailConfig(BaseModel): - guardrailIdentifier: str = None - guardrailVersion: str = None - trace: Optional[str] = None - - modelId: str = None - messages: List[BedrockMessage] = [] - system: Optional[List[Dict]] = None - inferenceConfig: Optional[InferenceConfig] = None - toolConfig: Optional[Dict] = None - guardrailConfig: Optional[GuardrailConfig] = None - additionalModelRequestFields: Optional[Any] = None - promptVariables: Optional[Dict] = None - additionalModelResponseFieldPaths: Optional[List[str]] = None - requestMetadata: Optional[Dict] = None - performanceConfig: Optional[Dict] = None - - -class BedrockResponse(BaseModel): - class BedrockResponseMetadata(BaseModel): - RequestId: str = None - HTTPStatusCode: Optional[int] = None - HTTPHeaders: Dict[str, str] = None - RetryAttempts: int = None - - class BedrockResponseOutput(BaseModel): - class BedrockResponseMessage(BaseModel): - class BedrockResponseContent(BaseModel): - text: Optional[str] = None - image: Optional[dict] = None - document: Optional[dict] = None - video: Optional[dict] = None - toolUse: Optional[dict] = None - toolResult: Optional[dict] = None - guardContent: Optional[dict] = None - - role: str = None - content: List[BedrockResponseContent] = [] - - message: BedrockResponseMessage = None - HTTPStatusCode: Optional[int] = None - - class BedrockResponseUsage(BaseModel): - inputTokens: Optional[int] = None - outputTokens: Optional[int] = None - totalTokens: Optional[int] = None - - ResponseMetadata: BedrockResponseMetadata = None - output: BedrockResponseOutput = None - stopReason: str = None - usage: BedrockResponseUsage = None - metrics: dict = None - additionalModelResponseFields: Optional[Any] = None - trace: Optional[dict] = None - performanceConfig: Optional[dict] = None - - -@modelgauge_sut(capabilities=[AcceptsTextPrompt]) -class AmazonBedrockSut(PromptResponseSUT): - - def __init__(self, uid: str, model_id: str, access_key_id: AwsAccessKeyId, secret_access_key: AwsSecretAccessKey): - super().__init__(uid) - self.model_id = model_id - self.access_key_id = access_key_id.value - self.secret_access_key = secret_access_key.value - self.client = None - - def _load_client(self): - return boto3.client( - service_name="bedrock-runtime", - region_name=os.getenv("AWS_REGION", "us-east-1"), - aws_access_key_id=self.access_key_id, - aws_secret_access_key=self.secret_access_key, - ) - - def translate_text_prompt(self, prompt: TextPrompt, options: ModelOptions) -> BedrockRequest: - inference_config = BedrockRequest.InferenceConfig( - maxTokens=options.max_tokens, - temperature=options.temperature, - topP=options.top_p, - stopSequences=options.stop_sequences, - ) - - return BedrockRequest( - modelId=self.model_id, - messages=[ - BedrockRequest.BedrockMessage(content=[{"text": prompt.text}]), - ], - inferenceConfig=inference_config, - ) - - @retry() - def evaluate(self, request: BedrockRequest) -> BedrockResponse: - if self.client is None: - # Lazy load the client. - self.client = self._load_client() - response = self.client.converse(**request.model_dump(exclude_none=True)) - return BedrockResponse(**response) - - def translate_response(self, request: BedrockRequest, response: BedrockResponse) -> SUTResponse: - if response.output is None: - raise APIException(f"No output in response: {response}") - content = response.output.message.content - assert len(content) == 1, f"Expected a single response message, got {len(content)}." - return SUTResponse(text=content[0].text) - - -BEDROCK_MODELS = ["micro", "lite", "pro"] - -for model in BEDROCK_MODELS: - SUTS.register( - AmazonBedrockSut, - f"amazon-nova-1.0-{model}", - f"amazon.nova-{model}-v1:0", - InjectSecret(AwsAccessKeyId), - InjectSecret(AwsSecretAccessKey), - ) - -BEDROCK_INFERENCE_PROFILES = ["premier"] -for model in BEDROCK_INFERENCE_PROFILES: - SUTS.register( - AmazonBedrockSut, - f"amazon-nova-1.0-{model}", - f"us.amazon.nova-{model}-v1:0", - InjectSecret(AwsAccessKeyId), - InjectSecret(AwsSecretAccessKey), - ) diff --git a/src/modelgauge/suts/aws_bedrock_sut_factory.py b/src/modelgauge/suts/aws_bedrock_sut_factory.py deleted file mode 100644 index a2cfb535..00000000 --- a/src/modelgauge/suts/aws_bedrock_sut_factory.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -import boto3 - -from modelgauge.dynamic_sut_factory import DynamicDriverSUTFactory, ModelNotSupportedError -from modelgauge.secret_values import InjectSecret, RawSecrets -from modelgauge.sut import SUT -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.aws_bedrock_client import AmazonBedrockSut, AwsAccessKeyId, AwsSecretAccessKey - - -class AWSBedrockSUTFactory(DynamicDriverSUTFactory): - DRIVER_NAME = "aws" - - def __init__(self, raw_secrets: RawSecrets): - super().__init__(raw_secrets) - self._client = None # Lazy load. - - @property - def client(self): - if self._client is None: - self._client = boto3.client( - service_name="bedrock", - region_name=os.getenv("AWS_REGION", "us-east-1"), - aws_access_key_id=self.injected_secrets()[0].value, - aws_secret_access_key=self.injected_secrets()[1].value, - ) - return self._client - - def list_suts(self) -> list[SUTDefinition]: - return list(self._get_available_models().values()) - - def _get_available_models(self) -> dict[str, SUTDefinition]: - response = self.client.list_foundation_models() - models = {} - for m in response["modelSummaries"]: - if m.get("modelLifecycle", {}).get("status") != "ACTIVE": - continue - models[m["modelId"]] = self._convert_model_id(m["modelId"]) - return models - - def _convert_model_id(self, model_id: str) -> SUTDefinition: - """Convert AWS model IDs (maker.model[:version?]) to our standard format.""" - maker, model_name = model_id.split(".", maxsplit=1) - model_name = model_name.replace(":", ".") - return SUTDefinition({"maker": maker, "model": model_name, "driver": self.DRIVER_NAME}) - - def _get_model_id(self, sut_definition: SUTDefinition): - models = self._get_available_models() - for model_id, model_definition in models.items(): - if str(model_definition.to_dynamic_sut_metadata()) == str(sut_definition.to_dynamic_sut_metadata()): - return model_id - supported_models = [model_def.to_dynamic_sut_metadata().external_model_name() for model_def in models.values()] - raise ModelNotSupportedError( - f"Model {sut_definition.external_model_name()} not found among AWS Bedrock models. AWS carries the following models from maker {sut_definition.get("maker")}: {supported_models} " - ) - - def get_secrets(self) -> list[InjectSecret]: - return [InjectSecret(AwsAccessKeyId), InjectSecret(AwsSecretAccessKey)] - - def make_sut(self, sut_definition: SUTDefinition) -> SUT: - model_id = self._get_model_id(sut_definition) - return AmazonBedrockSut(sut_definition.dynamic_uid, model_id, *self.injected_secrets()) diff --git a/src/modelgauge/suts/mistral_client.py b/src/modelgauge/suts/mistral_client.py deleted file mode 100644 index f8c2e5a5..00000000 --- a/src/modelgauge/suts/mistral_client.py +++ /dev/null @@ -1,80 +0,0 @@ -from mistralai.client import Mistral -from mistralai.client.errors import HTTPValidationError, SDKError -from mistralai.client.utils import BackoffStrategy, RetryConfig - -from modelgauge.secret_values import RequiredSecret, SecretDescription - -BACKOFF_INITIAL_MILLIS = 1000 -BACKOFF_MAX_INTERVAL_MILLIS = 100_000 -BACKOFF_EXPONENT = 1.9 -BACKOFF_MAX_ELAPSED_MILLIS = 86_400_000 # 1 day - - -class MistralAIAPIKey(RequiredSecret): - @classmethod - def description(cls) -> SecretDescription: - return SecretDescription( - scope="mistralai", - key="api_key", - instructions="MistralAI API key. See https://docs.mistral.ai/getting-started/quickstart/", - ) - - -class MistralAIClient: - def __init__(self, api_key: MistralAIAPIKey): - self.api_key = api_key.value - self._client = None - - @property - def client(self) -> Mistral: - if not self._client: - self._client = Mistral( - api_key=self.api_key, - timeout_ms=BACKOFF_MAX_ELAPSED_MILLIS * 3, - retry_config=RetryConfig( - "backoff", - BackoffStrategy( - BACKOFF_INITIAL_MILLIS, - BACKOFF_MAX_INTERVAL_MILLIS, - BACKOFF_EXPONENT, - BACKOFF_MAX_INTERVAL_MILLIS, - ), - True, - ), - ) - return self._client - - @staticmethod - def _make_request(endpoint, kwargs: dict): - try: - response = endpoint(**kwargs) - return response - # TODO check if this actually happens - except HTTPValidationError as exc: - raise (exc) - # TODO check if the retry strategy takes care of this - except SDKError as exc: - raise (exc) - # TODO what else can happen? - except Exception as exc: - raise (exc) - - def model_info(self, model): - return self._make_request(self.client.models.retrieve, {"model_id": model}) - - def request(self, req: dict): - if self.client.chat.sdk_configuration._hooks.before_request_hooks: - # work around bug in client - self.client.chat.sdk_configuration._hooks.before_request_hooks = [] - return self._make_request(self.client.chat.complete, req) - - def score_conversation(self, model, prompt, response): - """Returns moderation object for a conversation.""" - req = { - "model": model, - "inputs": [ - {"role": "user", "content": prompt}, - {"role": "assistant", "content": response}, - ], - } - return self._make_request(self.client.classifiers.moderate_chat, req) diff --git a/src/modelgauge/suts/mistral_sut.py b/src/modelgauge/suts/mistral_sut.py deleted file mode 100644 index 0450c76b..00000000 --- a/src/modelgauge/suts/mistral_sut.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Optional - -from mistralai.client.errors import SDKError -from mistralai.client.models import ChatCompletionResponse -from modelgauge.model_options import ModelOptions -from modelgauge.prompt import TextPrompt -from modelgauge.retry_decorator import retry -from modelgauge.secret_values import InjectSecret -from modelgauge.sut import PromptResponseSUT, SUTResponse -from modelgauge.sut_capabilities import AcceptsTextPrompt -from modelgauge.sut_decorator import modelgauge_sut -from modelgauge.sut_registry import SUTS -from modelgauge.suts.mistral_client import MistralAIAPIKey, MistralAIClient -from pydantic import BaseModel - -_USER_ROLE = "user" - - -class MistralAIRequest(BaseModel): - model: str - messages: list[dict] - temperature: Optional[float] = None - max_tokens: Optional[int] - n: int = 1 # Number of completions to generate. - - -class MistralAIResponse(ChatCompletionResponse): - """The ChatCompletionResponse class from Mistral matches our Response - objects now, but we subclass it for consistency and so we can adjust it - in case the upstream object changes.""" - - pass - - -@modelgauge_sut(capabilities=[AcceptsTextPrompt]) -class MistralAISut(PromptResponseSUT): - """A MistralAI SUT hosted on MistralAI.""" - - def __init__( - self, - uid: str, - model_name: str, - api_key: MistralAIAPIKey, - ): - super().__init__(uid) - self.model_name = model_name - self._api_key = api_key - self._client = None - - @property - def client(self): - if not self._client: - self._client = MistralAIClient(self._api_key) - return self._client - - def translate_text_prompt(self, prompt: TextPrompt, options: ModelOptions) -> MistralAIRequest: - args = {"model": self.model_name, "messages": [{"role": _USER_ROLE, "content": prompt.text}]} - if options.temperature is not None: - args["temperature"] = options.temperature - if options.max_tokens is not None: - args["max_tokens"] = options.max_tokens - return MistralAIRequest(**args) - - @retry(transient_exceptions=[SDKError]) - def evaluate(self, request: MistralAIRequest) -> ChatCompletionResponse: - response = self.client.request(request.model_dump(exclude_none=True)) # type: ignore - return response - - def translate_response(self, request: MistralAIRequest, response: MistralAIResponse) -> SUTResponse: - assert len(response.choices) == 1, f"Expected 1 completion, got {len(response.choices)}." - text = response.choices[0].message.content - assert text is not None - return SUTResponse(text=str(text)) - - -def register_suts_for_model(model_name): - MISTRAL_API_KEY = InjectSecret(MistralAIAPIKey) - # Register standard SUT. - model_uid = f"mistralai-{model_name}" - SUTS.register(MistralAISut, model_uid, model_name, MISTRAL_API_KEY) - - -register_suts_for_model("ministral-8b-2410") -register_suts_for_model("mistral-large-2411") -register_suts_for_model("mistral-large-2402") diff --git a/src/modelgauge/suts/mistral_sut_factory.py b/src/modelgauge/suts/mistral_sut_factory.py deleted file mode 100644 index 9f9936f6..00000000 --- a/src/modelgauge/suts/mistral_sut_factory.py +++ /dev/null @@ -1,41 +0,0 @@ -from mistralai.client.models import ModelList - -from modelgauge.dynamic_sut_factory import DynamicDriverSUTFactory, ModelNotSupportedError -from modelgauge.secret_values import InjectSecret, RawSecrets -from modelgauge.sut import SUT -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.mistral_client import MistralAIAPIKey, MistralAIClient -from modelgauge.suts.mistral_sut import MistralAISut - - -class MistralSUTFactory(DynamicDriverSUTFactory): - DRIVER_NAME = "mistral" - - def __init__(self, raw_secrets: RawSecrets): - super().__init__(raw_secrets) - self._client = None # Lazy load. - - @property - def client(self) -> MistralAIClient: - if self._client is None: - api_key = self.injected_secrets()[0] - self._client = MistralAIClient(api_key) - return self._client - - def get_secrets(self) -> list[InjectSecret]: - api_key = InjectSecret(MistralAIAPIKey) - return [api_key] - - def list_suts(self) -> list[SUTDefinition]: - model_list: ModelList = self.client.client.models.list() - return [SUTDefinition(maker=self.DRIVER_NAME, model=m.id, driver=self.DRIVER_NAME) for m in model_list.data] - - def make_sut(self, sut_definition: SUTDefinition) -> SUT: - model_name = sut_definition.to_dynamic_sut_metadata().external_model_name() - - try: - self.client.model_info(model_name) - except Exception as e: - raise ModelNotSupportedError(f"Model {model_name} not found or not available on mistral: {e}") - - return MistralAISut(sut_definition.dynamic_uid, model_name, *self.injected_secrets()) diff --git a/src/modelgauge/suts/nvidia_nim_api_client.py b/src/modelgauge/suts/nvidia_nim_api_client.py deleted file mode 100644 index 78a66eaa..00000000 --- a/src/modelgauge/suts/nvidia_nim_api_client.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import Optional - -from modelgauge.secret_values import ( - InjectSecret, - RequiredSecret, - SecretDescription, -) -from modelgauge.suts.openai_client import OpenAIChatSUT, OpenAIChatRequest -from modelgauge.model_options import ModelOptions -from modelgauge.sut_capabilities import ( - AcceptsChatPrompt, - AcceptsTextPrompt, -) -from modelgauge.sut_decorator import modelgauge_sut -from modelgauge.sut_registry import SUTS - -BASE_URL = "https://integrate.api.nvidia.com/v1" - - -class NvidiaNIMApiKey(RequiredSecret): - @classmethod - def description(cls) -> SecretDescription: - return SecretDescription( - scope="nvidia-nim-api", - key="api_key", - instructions="See https://build.nvidia.com/", - ) - - -class NIMOpenAIChatRequest(OpenAIChatRequest): - max_tokens: Optional[int] = ( - 256 # NVIDIA NIM uses the deprecated "max_tokens" param name instead of "max_completion_tokens" - ) - - -@modelgauge_sut( - capabilities=[ - AcceptsTextPrompt, - AcceptsChatPrompt, - ] -) -class NvidiaNIMApiClient(OpenAIChatSUT): - """ - Documented at https://https://docs.api.nvidia.com/ - """ - - def __init__(self, uid: str, model: str, api_key: NvidiaNIMApiKey): - super().__init__(uid, model, api_key=api_key, base_url=BASE_URL) - - def _translate_request_with_temperature( - self, messages, options: ModelOptions, temperature: float | None - ) -> NIMOpenAIChatRequest: - request = super()._translate_request_with_temperature(messages, options, temperature) - request_json = request.model_dump(exclude_none=True) - del request_json["max_completion_tokens"] # NIM API doesn't allow extra inputs - return NIMOpenAIChatRequest( - max_tokens=options.max_tokens, - **request_json, - ) - - -SUTS.register( - NvidiaNIMApiClient, - "nvidia-nemotron-mini-4b-instruct", - "nvidia/nemotron-mini-4b-instruct", - InjectSecret(NvidiaNIMApiKey), -) diff --git a/src/modelgauge/suts/nvidia_nim_sut_factory.py b/src/modelgauge/suts/nvidia_nim_sut_factory.py deleted file mode 100644 index 1e89358a..00000000 --- a/src/modelgauge/suts/nvidia_nim_sut_factory.py +++ /dev/null @@ -1,40 +0,0 @@ -from openai import OpenAI - -from modelgauge.dynamic_sut_factory import DynamicDriverSUTFactory, ModelNotSupportedError -from modelgauge.secret_values import InjectSecret -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.nvidia_nim_api_client import BASE_URL, NvidiaNIMApiKey, NvidiaNIMApiClient - - -class NvidiaNIMSUTFactory(DynamicDriverSUTFactory): - DRIVER_NAME = "nvidia-nim" - - def __init__(self, raw_secrets): - super().__init__(raw_secrets) - self._client = None - - @property - def client(self) -> OpenAI: - if self._client is None: - self._client = OpenAI(api_key=self.injected_secrets()[0].value, base_url=BASE_URL) - return self._client - - def list_suts(self) -> list[SUTDefinition]: - model_list = self.client.models.list().data - return [self._definition_for(m.id, self.DRIVER_NAME) for m in model_list] - - def get_secrets(self) -> list[InjectSecret]: - return [InjectSecret(NvidiaNIMApiKey)] - - def _model_exists(self, model_name: str) -> bool: - try: - self.client.models.retrieve(model_name) # type: ignore - return True - except: - return False - - def make_sut(self, sut_definition: SUTDefinition) -> NvidiaNIMApiClient: - model_name = sut_definition.external_model_name() - if not self._model_exists(model_name): - raise ModelNotSupportedError(f"Model {model_name} not found or not available on nvidia NIM.") - return NvidiaNIMApiClient(sut_definition.uid, model_name, *self.injected_secrets()) diff --git a/src/modelgauge/suts/openai_sut_factory.py b/src/modelgauge/suts/openai_sut_factory.py index b88bb97f..55db9450 100644 --- a/src/modelgauge/suts/openai_sut_factory.py +++ b/src/modelgauge/suts/openai_sut_factory.py @@ -1,11 +1,9 @@ -from openai import OpenAI, NotFoundError +from openai import OpenAI from modelgauge.auth.openai_compatible_secrets import OpenAICompatibleApiKey from modelgauge.dynamic_sut_factory import ( DynamicSUTFactory, - DynamicDriverSUTFactory, ModelNotSupportedError, - ProviderNotFoundError, ) from modelgauge.secret_values import InjectSecret, RawSecrets from modelgauge.sut_definition import SUTDefinition @@ -36,31 +34,6 @@ def _make_client(self) -> OpenAI: return _client -class OpenAICompatibleSUTFactory(BaseOpenAISUTFactory, DynamicDriverSUTFactory): - DRIVER_NAME = "openai" - - def make_sut(self, sut_definition: SUTDefinition) -> OpenAIResponsesSUT: - factory = factory_class = None - self.provider = sut_definition.get("provider") # type: ignore - - if not self.provider or self.provider == "openai": - factory = OpenAISUTFactory(self.raw_secrets) - else: - factory_class = OPENAI_SUT_FACTORIES.get(self.provider, None) - # we don't have a prebuilt factory... - if not factory_class: - # ... but maybe we have credentials and a base url, and we can try to make a SUT - base_url = sut_definition.get("base_url", None) - has_secret = self.provider in self.raw_secrets - if base_url and has_secret: - factory_class = OpenAIGenericSUTFactory - if factory_class: - factory = factory_class(self.raw_secrets) - else: - raise ProviderNotFoundError(f"I don't know how to make a {self.provider} SUT with the OpenAI client") - return factory.make_sut(sut_definition) - - class OpenAISUTFactory(BaseOpenAISUTFactory): """OpenAI SUT hosted by OpenAI""" @@ -81,42 +54,3 @@ def make_sut(self, sut_definition: SUTDefinition) -> OpenAIResponsesSUT: f"Model {sut_definition.external_model_name()} not found or not available on openai." ) return OpenAIResponsesSUT(sut_definition.uid, sut_definition.get("model"), client=self.client) # type: ignore - - -class OpenAIGenericSUTFactory(BaseOpenAISUTFactory): - """A SUT that uses the OpenAI client, not hosted by OpenAI""" - - def __init__(self, raw_secrets: RawSecrets, base_url: str | None = None): - super().__init__(raw_secrets) - self.base_url = base_url - - def _make_client(self): - assert self.base_url - [api_key] = self.injected_secrets() - _client = OpenAI(api_key=api_key.value, base_url=self.base_url, max_retries=NUM_RETRIES) - return _client - - def make_sut(self, sut_definition: SUTDefinition, base_url: str | None = None) -> OpenAIResponsesSUT: - the_base_url = sut_definition.get("base_url", None) - if base_url: - the_base_url = base_url - self.provider = sut_definition.get("provider") # type: ignore - if the_base_url: - self.base_url = the_base_url - return OpenAIResponsesSUT(sut_definition.uid, sut_definition.get("model"), client=self.client) # type: ignore - - -# this is how you add a new OpenAI-compatible SUT -class DemoOpenAICompatibleSUTFactory(OpenAIGenericSUTFactory): - def __init__(self, raw_secrets, **kwargs): - super().__init__(raw_secrets) - self.provider = "demo" - self.base_url = "https://example.net/v1/" - - # the SUT UID is maker/model:demo:openai - # the credentials in secrets.toml must be: - # [demo] - # api_key = "abcd" - - -OPENAI_SUT_FACTORIES: dict = {"demo": DemoOpenAICompatibleSUTFactory} diff --git a/tests/modelgauge_tests/sut_tests/test_aws_bedrock_client.py b/tests/modelgauge_tests/sut_tests/test_aws_bedrock_client.py deleted file mode 100644 index 61a05336..00000000 --- a/tests/modelgauge_tests/sut_tests/test_aws_bedrock_client.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest -from unittest.mock import patch - -from modelgauge.prompt import TextPrompt -from modelgauge.sut import SUTResponse -from modelgauge.model_options import ModelOptions -from modelgauge.typed_data import is_typeable - -from modelgauge.suts.aws_bedrock_client import ( - AmazonBedrockSut, - AwsAccessKeyId, - AwsSecretAccessKey, - BedrockRequest, - BedrockResponse, -) - -FAKE_MODEL_ID = "fake-model" - - -@pytest.fixture -def fake_sut(): - return AmazonBedrockSut( - "fake-sut", FAKE_MODEL_ID, AwsAccessKeyId("fake-api-key"), AwsSecretAccessKey("fake-secret-key") - ) - - -def _make_request(model_id, prompt_text, **inference_params): - inference_config = BedrockRequest.InferenceConfig(**inference_params) - return BedrockRequest( - modelId=model_id, - messages=[ - BedrockRequest.BedrockMessage(content=[{"text": prompt_text}]), - ], - inferenceConfig=inference_config, - ) - - -def _make_response(response_text): - return BedrockResponse( - output=BedrockResponse.BedrockResponseOutput( - message=BedrockResponse.BedrockResponseOutput.BedrockResponseMessage(content=[{"text": response_text}]) - ) - ) - - -def test_translate_text_prompt(fake_sut): - default_options = ModelOptions() - prompt = TextPrompt(text="some-text") - request = fake_sut.translate_text_prompt(prompt, default_options) - - assert isinstance(request, BedrockRequest) - assert request.modelId == FAKE_MODEL_ID - assert len(request.messages) == 1 - message = request.messages[0] - assert message.content == [{"text": "some-text"}] - assert request.inferenceConfig.maxTokens == default_options.max_tokens # Default ModelOptions value - - -def test_can_cache_request(): - request = _make_request(FAKE_MODEL_ID, "some-text", maxTokens=100) - assert is_typeable(request) - - -def test_can_cache_response(): - response = _make_response("response") - assert is_typeable(response) - - -@patch("modelgauge.suts.aws_bedrock_client.boto3.client") -def test_evaluate_sends_correct_params(mock_client, fake_sut): - fake_sut.client = mock_client - request = _make_request(FAKE_MODEL_ID, "some-text", maxTokens=100, topP=0.5) - fake_sut.evaluate(request) - - mock_client.converse.assert_called_with( - modelId=FAKE_MODEL_ID, - messages=[{"content": [{"text": "some-text"}], "role": "user"}], - inferenceConfig={"maxTokens": 100, "topP": 0.5}, - ) - - -def test_translate_response(fake_sut): - request = _make_request(FAKE_MODEL_ID, "some-text") - response = _make_response("response") - - translated_response = fake_sut.translate_response(request, response) - - assert translated_response == SUTResponse(text="response") diff --git a/tests/modelgauge_tests/sut_tests/test_aws_bedrock_sut_factory.py b/tests/modelgauge_tests/sut_tests/test_aws_bedrock_sut_factory.py deleted file mode 100644 index ff4f790c..00000000 --- a/tests/modelgauge_tests/sut_tests/test_aws_bedrock_sut_factory.py +++ /dev/null @@ -1,74 +0,0 @@ -from unittest.mock import patch - -import pytest - -from modelgauge.dynamic_sut_factory import ModelNotSupportedError -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.aws_bedrock_client import AmazonBedrockSut -from modelgauge.suts.aws_bedrock_sut_factory import AWSBedrockSUTFactory - - -@pytest.fixture -def factory(): - return AWSBedrockSUTFactory({"aws": {"access_key_id": "value", "secret_access_key": "value"}}) - - -@pytest.fixture -def mock_list_foundation_models(): - models = { - "modelSummaries": [ - {"modelId": "amazon.nova-1.0-micro-v1:0", "modelLifecycle": {"status": "ACTIVE"}}, - {"modelId": "old_model", "modelLifecycle": {"status": "LEGACY"}}, - ] - } - - with patch("boto3.client") as mock_client: - - mock_client.return_value.list_foundation_models.return_value = models - - yield mock_client - - -def test_convert_model_id(factory): - definition = factory._convert_model_id("amazon.nova-v1") - assert definition.get("maker") == "amazon" - assert definition.get("model") == "nova-v1" - assert definition.get("driver") == "aws" - - # Sometimes they have colons - definition = factory._convert_model_id("amazon.nova-v1:0") - assert definition.get("maker") == "amazon" - assert definition.get("model") == "nova-v1.0" - assert definition.get("driver") == "aws" - - # "." in the model name - definition = factory._convert_model_id("moonshotai.kimi-k2.5") - assert definition.get("maker") == "moonshotai" - assert definition.get("model") == "kimi-k2.5" - assert definition.get("driver") == "aws" - - -def test_make_sut(factory, mock_list_foundation_models): - sut_definition = SUTDefinition(model="nova-1.0-micro-v1.0", maker="amazon", driver="aws") - sut = factory.make_sut(sut_definition) - - assert isinstance(sut, AmazonBedrockSut) - assert sut.uid == "amazon/nova-1.0-micro-v1.0:aws" - assert sut.model_id == "amazon.nova-1.0-micro-v1:0" - - -def test_make_sut_no_model(factory, mock_list_foundation_models): - sut_definition = SUTDefinition(model="unknown", maker="amazon", driver="aws") - with pytest.raises(ModelNotSupportedError): - factory.make_sut(sut_definition) - - -def test_make_sut_legacy_model(factory, mock_list_foundation_models): - sut_definition = SUTDefinition(model="old_model", maker="amazon", driver="aws") - with pytest.raises(ModelNotSupportedError): - factory.make_sut(sut_definition) - - -def test_list_suts(factory, mock_list_foundation_models): - suts = factory.list_suts() - assert "amazon/nova-1.0-micro-v1.0:aws" in [s.uid for s in suts] diff --git a/tests/modelgauge_tests/sut_tests/test_mistral_sut.py b/tests/modelgauge_tests/sut_tests/test_mistral_sut.py deleted file mode 100644 index 6d01a442..00000000 --- a/tests/modelgauge_tests/sut_tests/test_mistral_sut.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest -from mistralai.client.models import ( - AssistantMessage, - ChatCompletionChoice, - UsageInfo, -) -from modelgauge.model_options import ModelOptions -from modelgauge.prompt import TextPrompt -from modelgauge.sut import SUTResponse -from modelgauge.suts.mistral_client import MistralAIAPIKey -from modelgauge.suts.mistral_sut import ( - MistralAIResponse, - MistralAISut, -) - - -@pytest.fixture -def req(): - return { - "model": "ministral-8b-latest", - "messages": [{"role": "user", "content": "Why did the chicken cross the road?"}], - "max_tokens": 91, - "temperature": 0.3, - "n": 1, - } - - -@pytest.fixture -def response(): - return MistralAIResponse( - id="ed6c8eccd53e4b319a7bc566f6a53357", - object="chat.completion", - model="ministral-8b-latest", - created=1731977771, - usage=UsageInfo(prompt_tokens=11, completion_tokens=22, total_tokens=33), - choices=[ - ChatCompletionChoice( - index=0, - message=AssistantMessage( - content="The classic joke has several variations", - tool_calls=None, - prefix=False, - role="assistant", - ), - finish_reason="stop", - ) - ], - ) - - -@pytest.fixture -def sut(): - return MistralAISut("ministral-8b-latest", "ministral-8b-latest", MistralAIAPIKey("fake")) - - -class TestMistralAISut: - - def test_request(self, sut, req): - translated_req = sut.translate_text_prompt( - TextPrompt(text="Why did the chicken cross the road?"), ModelOptions(temperature=0.3, max_tokens=91) - ) - assert translated_req.model_dump(exclude_none=True) == req - - def test_response(self, sut, req, response): - resp = sut.translate_response(request=req, response=response) - assert resp == SUTResponse(text="The classic joke has several variations") diff --git a/tests/modelgauge_tests/sut_tests/test_mistral_sut_factory.py b/tests/modelgauge_tests/sut_tests/test_mistral_sut_factory.py deleted file mode 100644 index ceca94f2..00000000 --- a/tests/modelgauge_tests/sut_tests/test_mistral_sut_factory.py +++ /dev/null @@ -1,40 +0,0 @@ -from unittest.mock import patch, MagicMock - -import pytest - -from modelgauge.dynamic_sut_factory import ModelNotSupportedError -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.mistral_sut import MistralAISut -from modelgauge.suts.mistral_sut_factory import MistralSUTFactory -from modelgauge_tests.utilities import FakeObject - - -@pytest.fixture -def factory(): - return MistralSUTFactory({"mistralai": {"api_key": "value"}}) - - -def test_make_sut(factory): - with patch("modelgauge.suts.mistral_client.MistralAIClient.model_info", return_value="model exists"): - sut_definition = SUTDefinition(model="bar", maker="foo", driver="mistral") - sut = factory.make_sut(sut_definition) - - assert isinstance(sut, MistralAISut) - assert sut.uid == "foo/bar:mistral" - assert sut.model_name == "foo/bar" - assert sut._api_key.value == "value" - - -def test_make_sut_bad_model(factory): - sut_definition = SUTDefinition(model="bogus", maker="fake", driver="mistral") - with patch("modelgauge.suts.mistral_client.MistralAIClient.model_info", side_effect=Exception()): - with pytest.raises(ModelNotSupportedError): - factory.make_sut(sut_definition) - - -def test_list_suts(factory): - model_list = MagicMock() - model_list.data = [FakeObject(id="thingy-1.0")] - factory._client = MagicMock() - factory._client.client.models.list.return_value = model_list - assert "mistral/thingy-1.0:mistral" in [s.uid for s in factory.list_suts()] diff --git a/tests/modelgauge_tests/sut_tests/test_nvidia_nim_api_client.py b/tests/modelgauge_tests/sut_tests/test_nvidia_nim_api_client.py deleted file mode 100644 index 2da3b7bd..00000000 --- a/tests/modelgauge_tests/sut_tests/test_nvidia_nim_api_client.py +++ /dev/null @@ -1,57 +0,0 @@ -from openai.types.chat import ChatCompletion - -from modelgauge.prompt import TextPrompt -from modelgauge.sut import SUTResponse -from modelgauge.suts.nvidia_nim_api_client import NIMOpenAIChatRequest, NvidiaNIMApiKey, NvidiaNIMApiClient -from modelgauge.suts.openai_client import OpenAIChatMessage -from modelgauge.model_options import ModelOptions - - -def _make_client(): - return NvidiaNIMApiClient(uid="test-model", model="some-model", api_key=NvidiaNIMApiKey("some-value")) - - -def test_openai_chat_translate_request(): - client = _make_client() - prompt = TextPrompt(text="some-text") - request = client.translate_text_prompt(prompt, ModelOptions(max_tokens=100)) - assert request == NIMOpenAIChatRequest( - model="some-model", - messages=[OpenAIChatMessage(content="some-text", role="user")], - max_tokens=100, - n=1, - ) - - -def test_openai_chat_translate_response(): - client = _make_client() - request = NIMOpenAIChatRequest( - model="some-model", - messages=[], - ) - # response is base on openai request: https://platform.openai.com/docs/api-reference/chat/create - response = ChatCompletion.model_validate_json("""\ -{ - "id": "chatcmpl-123", - "object": "chat.completion", - "created": 1677652288, - "model": "nvidia/nemotron-mini-4b-instruct", - "system_fingerprint": "fp_44709d6fcb", - "choices": [{ - "index": 0, - "message": { - "role": "assistant", - "content": "Hello there, how may I assist you today?" - }, - "logprobs": null, - "finish_reason": "stop" - }], - "usage": { - "prompt_tokens": 9, - "completion_tokens": 12, - "total_tokens": 21 - } -} -""") - result = client.translate_response(request, response) - assert result == SUTResponse(text="Hello there, how may I assist you today?", top_logprobs=None) diff --git a/tests/modelgauge_tests/sut_tests/test_nvidia_nim_sut_factory.py b/tests/modelgauge_tests/sut_tests/test_nvidia_nim_sut_factory.py deleted file mode 100644 index 665f344d..00000000 --- a/tests/modelgauge_tests/sut_tests/test_nvidia_nim_sut_factory.py +++ /dev/null @@ -1,43 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from modelgauge.dynamic_sut_factory import ModelNotSupportedError -from modelgauge.sut_definition import SUTDefinition -from modelgauge.suts.nvidia_nim_api_client import NvidiaNIMApiClient -from modelgauge.suts.nvidia_nim_sut_factory import NvidiaNIMSUTFactory -from modelgauge_tests.utilities import FakeObject - - -@pytest.fixture -def factory(): - return NvidiaNIMSUTFactory({"nvidia-nim-api": {"api_key": "value"}}) - - -def test_make_sut(factory): - factory._client = MagicMock() - factory._client.models.retrieve.return_value = "model exists" - - sut_definition = SUTDefinition(model="bar", maker="foo", driver="nvidia-nim") - sut = factory.make_sut(sut_definition) - - assert isinstance(sut, NvidiaNIMApiClient) - assert sut.uid == "foo/bar:nvidia-nim" - assert sut.model == "foo/bar" - - -def test_make_sut_bad_model(factory): - sut_definition = SUTDefinition(model="bogus", maker="fake", driver="nvidia-nim") - factory._client = MagicMock() - factory._client.models.retrieve.side_effect = Exception() - with pytest.raises(ModelNotSupportedError): - factory.make_sut(sut_definition) - - -def test_list_suts(factory): - m = FakeObject(id="fnord/thingy-1.0") - model_list = MagicMock() - model_list.data = [m] - factory._client = MagicMock() - factory._client.models.list.return_value = model_list - assert "fnord/thingy-1.0:nvidia-nim" in [s.uid for s in factory.list_suts()] diff --git a/tests/modelgauge_tests/sut_tests/test_openai_sut_factory.py b/tests/modelgauge_tests/sut_tests/test_openai_sut_factory.py index dcfd8987..f8067937 100644 --- a/tests/modelgauge_tests/sut_tests/test_openai_sut_factory.py +++ b/tests/modelgauge_tests/sut_tests/test_openai_sut_factory.py @@ -4,10 +4,10 @@ from openai import OpenAI from modelgauge.config import load_secrets_from_config -from modelgauge.dynamic_sut_factory import ModelNotSupportedError, ProviderNotFoundError +from modelgauge.dynamic_sut_factory import ModelNotSupportedError from modelgauge.sut_definition import SUTDefinition from modelgauge.suts.openai_client import OpenAIResponsesSUT -from modelgauge.suts.openai_sut_factory import OpenAICompatibleSUTFactory, OpenAIGenericSUTFactory, OpenAISUTFactory +from modelgauge.suts.openai_sut_factory import OpenAISUTFactory from modelgauge_tests.utilities import expensive_tests @@ -16,21 +16,6 @@ def openai_factory(): return OpenAISUTFactory(raw_secrets={"openai": {"api_key": "some_key"}}) -@pytest.fixture -def openai_generic_factory(): - return OpenAIGenericSUTFactory(raw_secrets={"demo": {"api_key": "some_key"}}, base_url="some_url") - - -@pytest.fixture -def factory(): - return OpenAICompatibleSUTFactory( - raw_secrets={ - "openai": {"api_key": "some_key", "organization": "some_org"}, - "demo": {"api_key": "some_key"}, - } - ) - - @pytest.fixture def sut_definition(): return SUTDefinition(model="some_model", maker="some_maker", driver="openai", provider="demo") @@ -72,61 +57,6 @@ def test_make_unknown_sut_raises_error(openai_factory): openai_factory.make_sut(sut_definition) -### SUTs using the OpenAI client running anywhere -def test_make_generic_sut(openai_generic_factory, sut_definition): - openai_generic_factory.base_url = "https://example.com" - sut = openai_generic_factory.make_sut(sut_definition) - assert isinstance(sut, OpenAIResponsesSUT) - assert sut.uid == "some_maker/some_model:demo:openai" - assert sut.model == "some_model" - assert isinstance(sut.client, OpenAI) - - -### SUTs using the OpenAI client running anywhere -def test_make_generic_sut_with_late_base_url(openai_generic_factory, sut_definition): - sut = openai_generic_factory.make_sut(sut_definition, base_url="https://example.com") - assert isinstance(sut, OpenAIResponsesSUT) - assert sut.uid == "some_maker/some_model:demo:openai" - assert sut.model == "some_model" - assert isinstance(sut.client, OpenAI) - - -### Factory that decides which kind of OpenAI-compatible SUT you want -def test_factory_makes_the_right_generic_sut(factory): - sut_definition = SUTDefinition( - model="some_model", maker="some_maker", driver="openai", provider="demo", base_url="https://example.org" - ) - sut = factory.make_sut(sut_definition) - assert isinstance(sut, OpenAIResponsesSUT) - assert sut.uid == "some_maker/some_model:demo:openai;url=https://example.org" - assert sut.model == "some_model" - assert isinstance(sut.client, OpenAI) - - -### Factory that tries to create a generic OpenAI-compatible SUT if you pass in the URL and secrets exist -def test_factory_tries_to_make_a_generic_sut(factory, sut_definition): - # there's a base URL but no secret for that provider, so we give up - sut_definition = SUTDefinition( - model="some_model", maker="some_maker", driver="openai", provider="unknown", base_url="https://www.example.net" - ) - with pytest.raises(ProviderNotFoundError): - _ = factory.make_sut(sut_definition) - - # there is a secret for the "unknown" provider, so we try to make a SUT - new_factory = OpenAICompatibleSUTFactory(raw_secrets={"unknown": {"api_key": "some_key"}}) - sut = new_factory.make_sut(sut_definition) - assert isinstance(sut, OpenAIResponsesSUT) - assert isinstance(sut.client, OpenAI) - - -def test_factory_makes_the_right_openai_sut(factory): - with patch("modelgauge.suts.openai_sut_factory.BaseOpenAISUTFactory._make_client"): - sut_definition = SUTDefinition(model="gpt-5", maker="openai", driver="openai") - sut = factory.make_sut(sut_definition) - assert sut.uid == "openai/gpt-5:openai" - assert sut.model == "gpt-5" - - @expensive_tests def test_connection(): factory = OpenAISUTFactory(load_secrets_from_config(path="."))