unionllm 0.2.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
unionllm/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .main import UnionLLM, unionchat
unionllm/exceptions.py ADDED
@@ -0,0 +1,146 @@
1
+ class UnionLLMError(Exception):
2
+ """Base exception class for UnionLLM."""
3
+ pass
4
+
5
+ class ProviderError(UnionLLMError):
6
+ """Exception raised when there is an issue with the model provider."""
7
+ pass
8
+
9
+ class APICallError(UnionLLMError):
10
+ """Exception raised when an API call fails."""
11
+ pass
12
+
13
+ from openai import (
14
+ AuthenticationError,
15
+ BadRequestError,
16
+ RateLimitError,
17
+ APIStatusError,
18
+ OpenAIError,
19
+ APIError,
20
+ APITimeoutError,
21
+ APIConnectionError,
22
+ )
23
+ import httpx
24
+
25
+ class AuthenticationError(AuthenticationError): # type: ignore
26
+ def __init__(self, message, llm_provider, model, response: httpx.Response):
27
+ self.status_code = 401
28
+ self.message = message
29
+ self.llm_provider = llm_provider
30
+ self.model = model
31
+ super().__init__(
32
+ self.message,
33
+ response=response,
34
+ body=None
35
+ ) # Call the base class constructor with the parameters it needs
36
+
37
+ class BadRequestError(BadRequestError): # type: ignore
38
+ def __init__(self, message, model, llm_provider, response: httpx.Response):
39
+ self.status_code = 400
40
+ self.message = message
41
+ self.model = model
42
+ self.llm_provider = llm_provider
43
+ super().__init__(
44
+ self.message,
45
+ response=response,
46
+ body=None
47
+ ) # Call the base class constructor with the parameters it needs
48
+
49
+ class Timeout(APITimeoutError): # type: ignore
50
+ def __init__(self, message, model, llm_provider, request: httpx.Request):
51
+ self.status_code = 408
52
+ self.message = message
53
+ self.model = model
54
+ self.llm_provider = llm_provider
55
+ super().__init__(
56
+ request=request
57
+ ) # Call the base class constructor with the parameters it needs
58
+
59
+ class RateLimitError(RateLimitError): # type: ignore
60
+ def __init__(self, message, llm_provider, model, response: httpx.Response):
61
+ self.status_code = 429
62
+ self.message = message
63
+ self.llm_provider = llm_provider
64
+ self.modle = model
65
+ super().__init__(
66
+ self.message,
67
+ response=response,
68
+ body=None
69
+ ) # Call the base class constructor with the parameters it needs
70
+
71
+ # sub class of rate limit error - meant to give more granularity for error handling context window exceeded errors
72
+ class ContextWindowExceededError(BadRequestError): # type: ignore
73
+ def __init__(self, message, model, llm_provider, response: httpx.Response):
74
+ self.status_code = 400
75
+ self.message = message
76
+ self.model = model
77
+ self.llm_provider = llm_provider
78
+ super().__init__(
79
+ message=self.message,
80
+ model=self.model, # type: ignore
81
+ llm_provider=self.llm_provider, # type: ignore
82
+ response=response
83
+ ) # Call the base class constructor with the parameters it needs
84
+
85
+ class ServiceUnavailableError(APIStatusError): # type: ignore
86
+ def __init__(self, message, llm_provider, model):
87
+ self.status_code = 503
88
+ self.message = message
89
+ self.llm_provider = llm_provider
90
+ self.model = model
91
+ super().__init__(
92
+ self.message
93
+ ) # Call the base class constructor with the parameters it needs
94
+
95
+
96
+ # raise this when the API returns an invalid response object - https://github.com/openai/openai-python/blob/1be14ee34a0f8e42d3f9aa5451aa4cb161f1781f/openai/api_requestor.py#L401
97
+ class APIError(APIError): # type: ignore
98
+ def __init__(self, status_code, message, llm_provider, model, request: httpx.Request):
99
+ self.status_code = status_code
100
+ self.message = message
101
+ self.llm_provider = llm_provider
102
+ self.model = model
103
+ super().__init__(
104
+ self.message,
105
+ request=request # type: ignore
106
+ )
107
+
108
+ # raised if an invalid request (not get, delete, put, post) is made
109
+ class APIConnectionError(APIConnectionError): # type: ignore
110
+ def __init__(self, message, llm_provider, model):
111
+ self.message = message
112
+ self.llm_provider = llm_provider
113
+ self.model = model
114
+ super().__init__(
115
+ self.message
116
+ )
117
+
118
+ class OpenAIError(OpenAIError): # type: ignore
119
+ def __init__(self, original_exception):
120
+ self.status_code = original_exception.http_status
121
+ super().__init__(
122
+ http_body=original_exception.http_body,
123
+ http_status=original_exception.http_status,
124
+ json_body=original_exception.json_body,
125
+ headers=original_exception.headers,
126
+ code=original_exception.code,
127
+ )
128
+ self.llm_provider = "openai"
129
+
130
+ class BudgetExceededError(Exception):
131
+ def __init__(self, current_cost, max_budget):
132
+ self.current_cost = current_cost
133
+ self.max_budget = max_budget
134
+ message = f"Budget has been exceeded! Current cost: {current_cost}, Max budget: {max_budget}"
135
+ super().__init__(message)
136
+
137
+ ## DEPRECATED ##
138
+ class InvalidRequestError(BadRequestError): # type: ignore
139
+ def __init__(self, message, model, llm_provider):
140
+ self.status_code = 400
141
+ self.message = message
142
+ self.model = model
143
+ self.llm_provider = llm_provider
144
+ super().__init__(
145
+ self.message, f"{self.model}"
146
+ ) # Call the base class constructor with the parameters it needs
unionllm/main.py ADDED
@@ -0,0 +1,142 @@
1
+ import logging
2
+ import asyncio
3
+ import os
4
+ from functools import partial
5
+
6
+ from typing import Any, List, Optional
7
+ from .providers import zhipu, moonshot, xai, minimax, qwen, tiangong, baichuan, wenxin, xunfei, xunfei_http, dify, fastgpt, coze, litellm, lingyi, stepfun, doubao, deepseek, gemini
8
+ from .exceptions import ProviderError
9
+ # from litellm import completion as litellm_completion
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class UnionLLM:
14
+ def __init__(self, provider: Optional[str] = None, **kwargs):
15
+ self.provider = provider.lower() if provider else None
16
+ self.litellm_call_type = None
17
+
18
+ if self.provider == "zhipuai":
19
+ self.provider_instance = zhipu.ZhipuAIProvider(**kwargs)
20
+ elif self.provider == "moonshot":
21
+ self.provider_instance = moonshot.MoonshotAIProvider(**kwargs)
22
+ elif self.provider == "minimax":
23
+ self.provider_instance = minimax.MinimaxAIProvider(**kwargs)
24
+ elif self.provider == "qwen":
25
+ # 根据model判断是否需要使用dashscope的api, 否则使用openai的兼容api
26
+ model = kwargs.get("model")
27
+ # 预留特殊模型通过litellm调用
28
+ if model is not None and model not in ["qwen-special-model"]:
29
+ self.provider_instance = qwen.QwenAIProvider(**kwargs)
30
+ else:
31
+ # 使用DASHSCOPE_API_KEY作为OPENAI_API_KEY
32
+ os.environ["OPENAI_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY")
33
+ self.provider_instance = litellm.LiteLLMProvider(**kwargs)
34
+ self.litellm_call_type = 3
35
+ elif self.provider == "tiangong":
36
+ self.provider_instance = tiangong.TianGongAIProvider(**kwargs)
37
+ elif self.provider == "baichuan":
38
+ self.provider_instance = baichuan.BaiChuanAIProvider(**kwargs)
39
+ elif self.provider == "wenxin":
40
+ self.provider_instance = wenxin.WenXinAIProvider(**kwargs)
41
+ elif self.provider == "xunfei":
42
+ self.provider_instance = xunfei.XunfeiAIProvider(**kwargs)
43
+ elif self.provider == "xunfei_http":
44
+ self.provider_instance = xunfei_http.XunfeiHTTPProvider(**kwargs)
45
+ # elif self.provider == "xai":
46
+ # self.provider_instance = xai.XAIHTTPProvider(**kwargs)
47
+ elif self.provider == "dify":
48
+ self.provider_instance = dify.DifyAIProvider(**kwargs)
49
+ elif self.provider == "fastgpt":
50
+ self.provider_instance = fastgpt.FastGPTProvider(**kwargs)
51
+ elif self.provider == "coze":
52
+ self.provider_instance = coze.CozeAIProvider(**kwargs)
53
+ elif self.provider == "lingyi":
54
+ self.provider_instance = lingyi.LingyiAIProvider(**kwargs)
55
+ elif self.provider == "stepfun":
56
+ self.provider_instance = stepfun.StepfunAIProvider(**kwargs)
57
+ elif self.provider == "doubao":
58
+ self.provider_instance = doubao.DouBaoAIProvider(**kwargs)
59
+ elif self.provider == "deepseek":
60
+ self.provider_instance = deepseek.DeepSeekAIProvider(**kwargs)
61
+ # elif self.provider == "gemini":
62
+ # self.provider_instance = gemini.GeminiAIProvider(**kwargs)
63
+ elif self.provider == "gemini" and kwargs.get("multimodal") and kwargs['multimodal'] == True:
64
+ self.provider_instance = gemini.GeminiAIProvider(**kwargs)
65
+ elif self.provider == "gemini" and kwargs.get("fileinput") and kwargs['fileinput'] == True:
66
+ self.provider_instance = gemini.GeminiAIProvider(**kwargs)
67
+ elif self.provider == "gemini" and kwargs.get("videoinput") and kwargs['videoinput'] == True:
68
+ self.provider_instance = gemini.GeminiAIProvider(**kwargs)
69
+
70
+ elif self.provider:
71
+ if_litellm_support, support_type = self.check_litellm_providers(provider=self.provider)
72
+ if if_litellm_support:
73
+ self.provider_instance = litellm.LiteLLMProvider(**kwargs)
74
+ if support_type == 1:
75
+ self.litellm_call_type = 1
76
+ elif support_type == 2:
77
+ self.litellm_call_type = 2
78
+ elif support_type == 3:
79
+ self.litellm_call_type = 3
80
+ else:
81
+ raise ProviderError(f"Provider '{self.provider}' is not supported.")
82
+ else:
83
+ self.provider_instance = litellm.LiteLLMProvider(**kwargs)
84
+
85
+ def completion(self, model: str, messages: List[str], **kwargs) -> Any:
86
+ if not self.provider_instance:
87
+ raise ProviderError(f"Provider '{self.provider}' is not initialized.")
88
+ if self.litellm_call_type:
89
+ if self.litellm_call_type == 1:
90
+ # Jugde whether the model starts with self.provider, if not, add it
91
+ if not model.startswith(self.provider+"/"):
92
+ model = f"{self.provider}/{model}"
93
+ return self.provider_instance.completion(model, messages, **kwargs)
94
+ elif self.litellm_call_type == 2:
95
+ return self.provider_instance.completion(model, messages, **kwargs)
96
+ elif self.litellm_call_type == 3:
97
+ # append OpenAI as provider name
98
+ if self.provider == 'xai':
99
+ kwargs['api_base'] = "https://api.x.ai/v1"
100
+ if self.provider == 'qwen':
101
+ kwargs['api_base'] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
102
+ model = f"openai/{model}"
103
+ return self.provider_instance.completion(model, messages, **kwargs)
104
+ else:
105
+ return self.provider_instance.completion(model, messages, **kwargs)
106
+
107
+ async def acompletion(self, model: str, messages: List[str], **kwargs) -> Any:
108
+ loop = asyncio.get_event_loop()
109
+ if not self.provider_instance:
110
+ raise ProviderError(f"Provider '{self.provider}' is not initialized.")
111
+
112
+ if self.litellm_call_type:
113
+ if self.litellm_call_type == 1:
114
+ if not model.startswith(self.provider + "/"):
115
+ model = f"{self.provider}/{model}"
116
+ func = partial(self.provider_instance.completion, model, messages, **kwargs)
117
+ return await loop.run_in_executor(None, func)
118
+ elif self.litellm_call_type == 2:
119
+ func = partial(self.provider_instance.completion, model, messages, **kwargs)
120
+ return await loop.run_in_executor(None, func)
121
+ else:
122
+ func = partial(self.provider_instance.completion, model, messages, **kwargs)
123
+ return await loop.run_in_executor(None, func)
124
+
125
+ def check_litellm_providers(self, provider: str) -> bool:
126
+ # Judge whether the provider is supported by LiteLLM, and if provider name should be added to the model name
127
+ if provider in ['azure', 'azure_ai', 'anthropic', 'deepseek', 'sagemaker', 'bedrock', 'vertex_ai', 'vertex_ai_beta', 'palm', 'gemini', 'mistral', 'cloudflare', 'huggingface', 'replicate', 'together_ai', 'openrouter', 'baseten', 'nlp_cloud', 'petals', 'ollama', 'perplexity', 'groq', 'anyscale', 'watsonx', 'voyage', 'xinference']:
128
+ # provider name should be added to the model name
129
+ return True, 1
130
+ elif provider in ['openai', 'cohere', 'ai21', 'deepinfra', 'ai21', 'alpha_alpha']:
131
+ # provider name should not be added to the model name
132
+ return True, 2
133
+ elif provider in ['xai', 'qwen']:
134
+ # append OpenAI as procall as OpenAI campatible API
135
+ return True, 3
136
+ else:
137
+ return False, 0
138
+
139
+ # This is the new function to simplify calling
140
+ def unionchat(model: str, messages: List[dict], **kwargs) -> Any:
141
+ client = UnionLLM(**kwargs)
142
+ return client.completion(model=model, messages=messages, **kwargs)
unionllm/models.py ADDED
@@ -0,0 +1,28 @@
1
+ from typing import List, Optional
2
+
3
+ class ResponseModel:
4
+ def __init__(self, prompt_tokens: List[str], completion_tokens: List[str], total_tokens: int, total_attempts: int, raw_response: Optional[dict] = None, usage: Optional[dict] = None):
5
+ self.prompt_tokens = prompt_tokens
6
+ self.completion_tokens = completion_tokens
7
+ self.total_tokens = total_tokens
8
+ self.total_attempts = total_attempts
9
+ self.raw_response = raw_response
10
+ self.usage = usage
11
+
12
+ def to_dict(self) -> dict:
13
+ return {
14
+ "prompt_tokens": self.prompt_tokens,
15
+ "completion_tokens": self.completion_tokens,
16
+ "total_tokens": self.total_tokens,
17
+ "total_attempts": self.total_attempts,
18
+ "raw_response": self.raw_response,
19
+ "usage": self.usage
20
+ }
21
+
22
+ def get_completions(self) -> list[str]:
23
+ completions = []
24
+ if self.raw_response and "choices" in self.raw_response:
25
+ for choice in self.raw_response["choices"]:
26
+ if "message" in choice and "content" in choice["message"]:
27
+ completions.append(choice["message"]["content"])
28
+ return completions
File without changes
@@ -0,0 +1,143 @@
1
+ import json
2
+ from .base_provider import BaseProvider
3
+ from unionllm.utils import ModelResponse, Message, Choices, Usage, Delta, StreamingChoices
4
+ import requests
5
+ import logging, os
6
+
7
+ class BaiChuanOpenAIError(Exception):
8
+ def __init__(
9
+ self,
10
+ status_code,
11
+ message,
12
+ ):
13
+ self.status_code = status_code
14
+ self.message = message
15
+ super().__init__(self.message)
16
+
17
+ class BaiChuanAIProvider(BaseProvider):
18
+ def __init__(self, **model_kwargs):
19
+ # Get BAICHUAN_API_KEY from environment variables
20
+ _env_api_key = os.environ.get("BAICHUAN_API_KEY")
21
+ self.api_key = model_kwargs.get("api_key") if model_kwargs.get("api_key") else _env_api_key
22
+ if not self.api_key:
23
+ raise BaiChuanOpenAIError(
24
+ status_code=422, message=f"Missing API key"
25
+ )
26
+ self.endpoint_url = "https://api.baichuan-ai.com/v1/chat/completions"
27
+
28
+ def pre_processing(self, **kwargs):
29
+ supported_params = [
30
+ "model", "messages", "max_tokens", "temperature", "top_p", "n",
31
+ "logprobs", "stream", "stop", "presence_penalty", "frequency_penalty",
32
+ "best_of", "logit_bias", "tools", "tool_choice"
33
+ ]
34
+ for key in list(kwargs.keys()):
35
+ if key not in supported_params:
36
+ kwargs.pop(key)
37
+ return kwargs
38
+
39
+ def post_stream_processing_wrapper(self, model, messages, **new_kwargs):
40
+ payload = json.dumps({"model": model, "messages": messages, **new_kwargs})
41
+ headers = {
42
+ "Authorization": f"Bearer {self.api_key}",
43
+ "Content-Type": "application/json",
44
+ }
45
+ response = requests.post(self.endpoint_url, headers=headers, data=payload)
46
+ for line in response.iter_lines():
47
+ if line:
48
+ new_line = line.decode("utf-8").replace("data: ", "")
49
+ if new_line == "[DONE]":
50
+ break
51
+ data = json.loads(new_line)
52
+ chunk_choices = []
53
+ for choice in data["choices"]:
54
+ chunk_delta = Delta()
55
+ delta = choice.get("delta")
56
+ if delta:
57
+ if "role" in choice['delta']:
58
+ chunk_delta.role = choice['delta']["role"]
59
+ if "content" in choice['delta']:
60
+ chunk_delta.content = choice['delta']["content"]
61
+ chunk_choices.append(StreamingChoices(index=choice['index'], delta=chunk_delta))
62
+
63
+ if "usage" in data:
64
+ chunk_usage = Usage()
65
+ if "prompt_tokens" in data["usage"]:
66
+ chunk_usage.prompt_tokens = data["usage"]["prompt_tokens"]
67
+ if "completion_tokens" in data["usage"]:
68
+ chunk_usage.completion_tokens = data["usage"]["completion_tokens"]
69
+ if "total_tokens" in data["usage"]:
70
+ chunk_usage.total_tokens = data["usage"]["total_tokens"]
71
+
72
+ chunk_response = ModelResponse(
73
+ id=data["id"],
74
+ choices=chunk_choices,
75
+ created=data["created"],
76
+ model=model,
77
+ usage=chunk_usage if "usage" in data else None,
78
+ stream=True
79
+ )
80
+ yield chunk_response
81
+
82
+ def create_model_response_wrapper(self, result, model):
83
+ response_dict = result.json()
84
+ choices = []
85
+ for choice in response_dict["choices"]:
86
+ message = Message(
87
+ content=choice["message"]["content"], role=choice["message"]["role"]
88
+ )
89
+ choices.append(
90
+ Choices(
91
+ message=message,
92
+ index=choice["index"],
93
+ finish_reason=choice["finish_reason"],
94
+ )
95
+ )
96
+ usage = Usage(
97
+ prompt_tokens=response_dict["usage"]["prompt_tokens"],
98
+ completion_tokens=response_dict["usage"]["completion_tokens"],
99
+ total_tokens=response_dict["usage"]["total_tokens"],
100
+ )
101
+ response = ModelResponse(
102
+ id=response_dict["id"],
103
+ choices=choices,
104
+ created=response_dict["created"],
105
+ model=model,
106
+ usage=usage,
107
+ )
108
+ return response
109
+
110
+
111
+ def completion(self, model: str, messages: list, **kwargs):
112
+ try:
113
+ if model is None or messages is None:
114
+ raise BaiChuanOpenAIError(
115
+ status_code=422, message=f"Missing model or messages"
116
+ )
117
+
118
+ message_check_result = self.check_prompt("baichuan", model, messages)
119
+ if message_check_result['pass_check']:
120
+ messages = message_check_result['messages']
121
+ else:
122
+ raise BaiChuanOpenAIError(
123
+ status_code=422, message=message_check_result['reason']
124
+ )
125
+
126
+ new_kwargs = self.pre_processing(**kwargs)
127
+ stream = kwargs.get("stream", False)
128
+
129
+ if stream:
130
+ return self.post_stream_processing_wrapper(model, messages, **new_kwargs)
131
+ else:
132
+ payload = json.dumps({"model": model, "messages": messages, **new_kwargs})
133
+ headers = {
134
+ "Authorization": f"Bearer {self.api_key}",
135
+ "Content-Type": "application/json",
136
+ }
137
+ result = requests.post(self.endpoint_url, headers=headers, data=payload)
138
+ return self.create_model_response_wrapper(result, model=model)
139
+ except Exception as e:
140
+ if hasattr(e, "status_code"):
141
+ raise BaiChuanOpenAIError(status_code=e.status_code, message=str(e))
142
+ else:
143
+ raise BaiChuanOpenAIError(status_code=500, message=str(e))