Skip to content

upstage platform support #2153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions camel/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .sglang_config import SGLANG_API_PARAMS, SGLangConfig
from .siliconflow_config import SILICONFLOW_API_PARAMS, SiliconFlowConfig
from .togetherai_config import TOGETHERAI_API_PARAMS, TogetherAIConfig
from .upstage_config import UPSTAGE_API_PARAMS, UpstageConfig
from .vllm_config import VLLM_API_PARAMS, VLLMConfig
from .watsonx_config import WATSONX_API_PARAMS, WatsonXConfig
from .yi_config import YI_API_PARAMS, YiConfig
Expand Down Expand Up @@ -108,6 +109,8 @@
'AIML_API_PARAMS',
'OpenRouterConfig',
'OPENROUTER_API_PARAMS',
'UpstageConfig',
'UPSTAGE_API_PARAMS',
'LMSTUDIO_API_PARAMS',
'LMStudioConfig',
'WatsonXConfig',
Expand Down
103 changes: 103 additions & 0 deletions camel/configs/upstage_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
from __future__ import annotations

from typing import Optional, Sequence, Union

from camel.configs.base_config import BaseConfig


class UpstageConfig(BaseConfig):
r"""Defines the parameters for generating chat completions using OpenAI
compatibility.

Reference: https://console.upstage.ai/api/chat

Args:
temperature (float, optional): Sampling temperature to use, between
:obj:`0` and :obj:`2`. Higher values make the output more random,
while lower values make it more focused and deterministic.
(default: :obj:`None`)
top_p (float, optional): An alternative to sampling with temperature,
called nucleus sampling, where the model considers the results of
the tokens with top_p probability mass. So :obj:`0.1` means only
the tokens comprising the top 10% probability mass are considered.
(default: :obj:`None`)
n (int, optional): How many chat completion choices to generate for
each input message. (default: :obj:`None`)
response_format (object, optional): An object specifying the format
that the model must output. Compatible with GPT-4 Turbo and all
GPT-3.5 Turbo models newer than gpt-3.5-turbo-1106. Setting to
{"type": "json_object"} enables JSON mode, which guarantees the
message the model generates is valid JSON. Important: when using
JSON mode, you must also instruct the model to produce JSON
yourself via a system or user message. Without this, the model
may generate an unending stream of whitespace until the generation
reaches the token limit, resulting in a long-running and seemingly
"stuck" request. Also note that the message content may be
partially cut off if finish_reason="length", which indicates the
generation exceeded max_tokens or the conversation exceeded the
max context length.
stream (bool, optional): If True, partial message deltas will be sent
as data-only server-sent events as they become available.
(default: :obj:`None`)
stop (str or list, optional): Up to :obj:`4` sequences where the API
will stop generating further tokens. (default: :obj:`None`)
max_tokens (int, optional): The maximum number of tokens to generate
in the chat completion. The total length of input tokens and
generated tokens is limited by the model's context length.
(default: :obj:`None`)
presence_penalty (float, optional): Number between :obj:`-2.0` and
:obj:`2.0`. Positive values penalize new tokens based on whether
they appear in the text so far, increasing the model's likelihood
to talk about new topics. See more information about frequency and
presence penalties. (default: :obj:`None`)
frequency_penalty (float, optional): Number between :obj:`-2.0` and
:obj:`2.0`. Positive values penalize new tokens based on their
existing frequency in the text so far, decreasing the model's
likelihood to repeat the same line verbatim. See more information
about frequency and presence penalties. (default: :obj:`None`)
user (str, optional): A unique identifier representing your end-user,
which can help OpenAI to monitor and detect abuse.
(default: :obj:`None`)
tools (list[FunctionTool], optional): A list of tools the model may
call. Currently, only functions are supported as a tool. Use this
to provide a list of functions the model may generate JSON inputs
for. A max of 128 functions are supported.
tool_choice (Union[dict[str, str], str], optional): Controls which (if
any) tool is called by the model. :obj:`"none"` means the model
will not call any tool and instead generates a message.
:obj:`"auto"` means the model can pick between generating a
message or calling one or more tools. :obj:`"required"` means the
model must call one or more tools. Specifying a particular tool
via {"type": "function", "function": {"name": "my_function"}}
forces the model to call that tool. :obj:`"none"` is the default
when no tools are present. :obj:`"auto"` is the default if tools
are present.
"""

temperature: Optional[float] = None
top_p: Optional[float] = None
n: Optional[int] = None
stream: Optional[bool] = None
stop: Optional[Union[str, Sequence[str]]] = None
max_tokens: Optional[int] = None
presence_penalty: Optional[float] = None
response_format: Optional[dict] = None
frequency_penalty: Optional[float] = None
user: Optional[str] = None
tool_choice: Optional[Union[dict[str, str], str]] = None


UPSTAGE_API_PARAMS = {param for param in UpstageConfig.model_fields.keys()}
2 changes: 2 additions & 0 deletions camel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from .siliconflow_model import SiliconFlowModel
from .stub_model import StubModel
from .togetherai_model import TogetherAIModel
from .upstage_model import UpstageModel
from .vllm_model import VLLMModel
from .volcano_model import VolcanoModel
from .watsonx_model import WatsonXModel
Expand Down Expand Up @@ -94,6 +95,7 @@
'BaseAudioModel',
'SiliconFlowModel',
'VolcanoModel',
'UpstageModel',
'LMStudioModel',
'WatsonXModel',
]
3 changes: 3 additions & 0 deletions camel/models/model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from camel.models.siliconflow_model import SiliconFlowModel
from camel.models.stub_model import StubModel
from camel.models.togetherai_model import TogetherAIModel
from camel.models.upstage_model import UpstageModel
from camel.models.vllm_model import VLLMModel
from camel.models.volcano_model import VolcanoModel
from camel.models.watsonx_model import WatsonXModel
Expand Down Expand Up @@ -163,6 +164,8 @@ def create(
model_class = LMStudioModel
elif model_platform.is_openrouter and model_type.is_openrouter:
model_class = OpenRouterModel
elif model_platform.is_upstage and model_type.is_upstage:
model_class = UpstageModel
elif model_platform.is_zhipuai and model_type.is_zhipuai:
model_class = ZhipuAIModel
elif model_platform.is_gemini and model_type.is_gemini:
Expand Down
210 changes: 210 additions & 0 deletions camel/models/upstage_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
import os
from typing import Any, Dict, List, Optional, Type, Union

from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
from pydantic import BaseModel

from camel.configs import UPSTAGE_API_PARAMS, UpstageConfig
from camel.messages import OpenAIMessage
from camel.models import BaseModelBackend
from camel.models._utils import try_modify_message_with_format
from camel.types import (
ChatCompletion,
ChatCompletionChunk,
ModelType,
)
from camel.utils import (
BaseTokenCounter,
OpenAITokenCounter,
api_keys_required,
)


class UpstageModel(BaseModelBackend):
r"""LLM API served by Upstage in a unified BaseModelBackend interface.

Args:
model_type (Union[ModelType, str]): Model for which a backend is
created.
model_config_dict (Optional[Dict[str, Any]], optional): A dictionary
that will be fed into:obj:`openai.ChatCompletion.create()`.
If:obj:`None`, :obj:`UpstageConfig().as_dict()` will be used.
(default: :obj:`None`)
api_key (Optional[str], optional): The API key for authenticating
with the Upstage service. (default: :obj:`None`).
url (Optional[str], optional): The url to the Upstage service.
(default: :obj:`None`)
token_counter (Optional[BaseTokenCounter], optional): Token counter to
use for the model. If not provided, :obj:`OpenAITokenCounter(
ModelType.GPT_4O_MINI)` will be used.
(default: :obj:`None`)
timeout (Optional[float], optional): The timeout value in seconds for
API calls. If not provided, will fall back to the MODEL_TIMEOUT
environment variable or default to 180 seconds.
(default: :obj:`None`)
"""

@api_keys_required([("api_key", "UPSTAGE_API_KEY")])
def __init__(
self,
model_type: Union[ModelType, str],
model_config_dict: Optional[Dict[str, Any]] = None,
api_key: Optional[str] = None,
url: Optional[str] = None,
token_counter: Optional[BaseTokenCounter] = None,
timeout: Optional[float] = None,
) -> None:
if model_config_dict is None:
model_config_dict = UpstageConfig().as_dict()
api_key = api_key or os.environ.get("UPSTAGE_API_KEY")
url = url or os.environ.get(
"UPSTAGE_API_BASE_URL", "https://api.upstage.ai/v1"
)
timeout = timeout or float(os.environ.get("MODEL_TIMEOUT", 180))
super().__init__(
model_type, model_config_dict, api_key, url, token_counter, timeout
)
self._client = OpenAI(
timeout=self._timeout,
max_retries=3,
api_key=self._api_key,
base_url=self._url,
)
self._async_client = AsyncOpenAI(
timeout=self._timeout,
max_retries=3,
api_key=self._api_key,
base_url=self._url,
)

@property
def token_counter(self) -> BaseTokenCounter:
r"""Initialize the token counter for the model backend.

Returns:
BaseTokenCounter: The token counter following the model's
tokenization style.
"""
if not self._token_counter:
self._token_counter = OpenAITokenCounter(ModelType.GPT_4O_MINI)
return self._token_counter

def _prepare_request(
self,
messages: List[OpenAIMessage],
response_format: Optional[Type[BaseModel]] = None,
tools: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
request_config = self.model_config_dict.copy()
if tools:
request_config["tools"] = tools
elif response_format:
try_modify_message_with_format(messages[-1], response_format)
request_config["response_format"] = {"type": "json_object"}

return request_config

def _run(
self,
messages: List[OpenAIMessage],
response_format: Optional[type[BaseModel]] = None,
tools: Optional[List[Dict[str, Any]]] = None,
) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
r"""Runs inference of Upstage chat completion.

Args:
messages (List[OpenAIMessage]): Message list with the chat history
in OpenAI API format.
response_format (Optional[Type[BaseModel]]): The format of the
response.
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
use for the request.

Returns:
Union[ChatCompletion, Stream[ChatCompletionChunk]]:
`ChatCompletion` in the non-stream mode, or
`Stream[ChatCompletionChunk]` in the stream mode.
"""
request_config = self._prepare_request(
messages, response_format, tools
)

response = self._client.chat.completions.create(
messages=messages,
model=self.model_type,
**request_config,
)

return response

async def _arun(
self,
messages: List[OpenAIMessage],
response_format: Optional[type[BaseModel]] = None,
tools: Optional[List[Dict[str, Any]]] = None,
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
r"""Runs inference of Upstage chat completion asynchronously.

Args:
messages (List[OpenAIMessage]): Message list with the chat history
in OpenAI API format.
response_format (Optional[Type[BaseModel]]): The format of the
response.
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
use for the request.

Returns:
Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
`ChatCompletion` in the non-stream mode, or
`AsyncStream[ChatCompletionChunk]` in the stream mode.
"""
request_config = self._prepare_request(
messages, response_format, tools
)

response = await self._async_client.chat.completions.create(
messages=messages,
model=self.model_type,
**request_config,
)

return response

def check_model_config(self):
r"""Check whether the model configuration contains any unexpected
arguments to Upstage API. But Upstage API does not have any additional
arguments to check.

Raises:
ValueError: If the model configuration dictionary contains any
unexpected arguments to Upstage API.
"""
for param in self.model_config_dict:
if param not in UPSTAGE_API_PARAMS:
raise ValueError(
f"Unexpected argument `{param}` is "
"input into Upstage model backend."
)

@property
def stream(self) -> bool:
r"""Returns whether the model is in stream mode, which sends partial
results each time.

Returns:
bool: Whether the model is in stream mode.
"""
return self.model_config_dict.get("stream", False)
Loading
Loading