openrouter-provider 0.0.1__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.
@@ -0,0 +1,134 @@
1
+ from .LLMs import LLMModel
2
+
3
+ from enum import Enum
4
+ from PIL import Image
5
+ import base64
6
+ from io import BytesIO
7
+ from dataclasses import dataclass
8
+
9
+
10
+ class Role(Enum):
11
+ system = "system"
12
+ user = "user"
13
+ ai = "assistant"
14
+ agent = "agent"
15
+ tool = "tool"
16
+
17
+
18
+ @dataclass
19
+ class ToolCall:
20
+ id: str
21
+ name: str
22
+ arguments: dict
23
+ result: any = ""
24
+
25
+
26
+ class Chat_message:
27
+ def __init__(self, text: str, images: list[Image.Image]=None, role: Role=Role.user, answerdBy: LLMModel=None, token :int=0, cost: float=0) -> None:
28
+ self.role = role
29
+ self.text = text
30
+ self.images = self._process_image(images=images)
31
+ self.token = token
32
+ self.cost = cost
33
+ self.answeredBy: LLMModel = answerdBy
34
+
35
+ self.tool_calls: list[ToolCall] = []
36
+
37
+ def __str__(self) -> str:
38
+ # ANSI color codes for blue, green, and reset (to default)
39
+ BLUE = "\033[34m"
40
+ GREEN = "\033[32m"
41
+ RESET = "\033[0m"
42
+
43
+ message = ""
44
+
45
+ if self.role == Role.system:
46
+ message = "---------------------- System ----------------------\n"
47
+ elif self.role == Role.user:
48
+ message = BLUE + "----------------------- User -----------------------\n" + RESET
49
+ elif self.role == Role.ai:
50
+ message = GREEN + "--------------------- Assistant --------------------\n" + RESET
51
+
52
+ # Append text and reset color formatting at the end
53
+ message += self.text + RESET + "\n"
54
+
55
+ return message
56
+
57
+ def _process_image(self, images: list):
58
+ """
59
+ Process a list of images by resizing them to maintain aspect ratio and then converting them to base64 format.
60
+
61
+ Args:
62
+ images (list): A list of image objects to be processed.
63
+
64
+ Returns:
65
+ list: A list of base64-encoded image strings if input is not None/empty, otherwise `None`.
66
+
67
+ Note:
68
+ - Images should be provided as a "list" even if there is only a single image to process.
69
+ """
70
+ if images == None:
71
+ return None
72
+
73
+ base64_images = []
74
+ for image in images:
75
+ if image.mode == "RGBA":
76
+ image = image.convert("RGB")
77
+
78
+ image = self._resize_image_aspect_ratio(image=image)
79
+ image = self._convert_to_base64(image=image)
80
+ base64_images.append(image)
81
+
82
+ return base64_images
83
+
84
+ def _convert_to_base64(self, image: Image) -> str:
85
+ """
86
+ Convert an image to a base64-encoded string.
87
+
88
+ Args:
89
+ image (Image): The image object to be converted to base64 format.
90
+
91
+ Returns:
92
+ str: The base64-encoded string representation of the image.
93
+
94
+ Note:
95
+ - The image format will default to 'JPEG' if the format is not specified.
96
+ """
97
+ buffered = BytesIO()
98
+ format = image.format if image.format else 'JPEG'
99
+ image.save(buffered, format=format)
100
+ img_bytes = buffered.getvalue()
101
+ img_base64 = base64.b64encode(img_bytes).decode('utf-8')
102
+
103
+ return img_base64
104
+
105
+ def _resize_image_aspect_ratio(self, image: Image, target_length=1024):
106
+ """
107
+ Resize an image to a target length while maintaining its aspect ratio.
108
+
109
+ Args:
110
+ image (Image): The image object to be resized.
111
+ target_length (int, optional): The target length for the larger dimension (default is 1024).
112
+
113
+ Returns:
114
+ Image: The resized image object with maintained aspect ratio.
115
+
116
+ Note:
117
+ - The smaller dimension is scaled proportionally based on the larger dimension to maintain aspect ratio.
118
+ - If the image's aspect ratio is non-square, the target_length is applied to the larger dimension.
119
+ """
120
+
121
+ width, height = image.size
122
+
123
+ if width > height:
124
+ new_width = target_length
125
+ new_height = int((target_length / width) * height)
126
+ else:
127
+ new_height = target_length
128
+ new_width = int((target_length / height) * width)
129
+
130
+ resized_image = image.resize((new_width, new_height))
131
+
132
+ return resized_image
133
+
134
+
@@ -0,0 +1,121 @@
1
+ from .Chat_message import *
2
+ from .OpenRouterProvider import *
3
+ from .LLMs import LLMModel
4
+
5
+ from dotenv import load_dotenv
6
+ import time
7
+ import json
8
+
9
+ _base_system_prompt = """
10
+ It's [TIME] today.
11
+ You are an intelligent AI. You must follow the system_instruction below, which is provided by the user.
12
+
13
+ <system_instruction>
14
+ [SYSTEM_INSTRUCTION]
15
+ </system_instruction>
16
+ """
17
+
18
+ class Chatbot_manager:
19
+ def __init__(self, system_prompt:str="", tools:list[tool_model]=[]) -> None:
20
+ load_dotenv()
21
+
22
+ self._memory: list[Chat_message] = []
23
+ self.tools: list[tool_model] = tools
24
+ self.set_system_prompt(prompt=system_prompt)
25
+
26
+ def set_system_prompt(self, prompt: str):
27
+ m, d, y = time.localtime()[:3]
28
+
29
+ system_prompt = _base_system_prompt
30
+ system_prompt = system_prompt.replace("[TIME]", f"{m}/{d}/{y}")
31
+ system_prompt = system_prompt.replace("[SYSTEM_INSTRUCTION]", prompt)
32
+
33
+ self._system_prompt = Chat_message(text=system_prompt, role=Role.system)
34
+
35
+ def clear_memory(self):
36
+ self._memory = []
37
+
38
+ def print_memory(self):
39
+ print("\n--------------------- Chatbot memory ---------------------")
40
+ print(f"system : {self._system_prompt.text}")
41
+
42
+ for message in self._memory:
43
+ role = message.role.value
44
+ text = message.text.strip()
45
+
46
+ reset_code = "\033[0m"
47
+ role_str = f"{role.ljust(9)}:"
48
+ indent = " " * len(role_str)
49
+ lines = text.splitlines()
50
+
51
+ if role == "user":
52
+ color_code = "\033[94m" # blue
53
+ if lines:
54
+ print(f"{color_code}{role_str}{reset_code} {lines[0]}")
55
+ for line in lines[1:]:
56
+ print(f"{color_code}{indent}{reset_code} {line}")
57
+ else:
58
+ print(f"{color_code}{role_str}{reset_code}")
59
+
60
+ elif role == "assistant":
61
+ color_code = "\033[92m" # green
62
+ if lines:
63
+ print(f"{color_code}{role_str}{reset_code} {lines[0]}")
64
+ for line in lines[1:]:
65
+ print(f"{color_code}{indent}{reset_code} {line}")
66
+ else:
67
+ print(f"{color_code}{role_str}{reset_code}")
68
+
69
+ elif role == "tool":
70
+ color_code = "\033[93m" # orange
71
+ print(f"{color_code}{role_str}{reset_code} ", end="")
72
+
73
+ for tool in message.tool_calls:
74
+ print(f"{tool.name}({json.loads(tool.arguments)}), ", end="")
75
+ print()
76
+
77
+ else:
78
+ color_code = "\033[0m" # default color
79
+ print("Print error: The role is invalid.")
80
+
81
+ print("----------------------------------------------------------\n")
82
+
83
+ def invoke(self, model: LLMModel, query: Chat_message, tools: list[tool_model]=[]) -> Chat_message:
84
+ self._memory.append(query)
85
+ client = OpenRouterProvider()
86
+ reply = client.invoke(
87
+ model=model,
88
+ system_prompt=self._system_prompt,
89
+ querys=self._memory,
90
+ tools=self.tools + tools
91
+ )
92
+ reply.answeredBy = model
93
+ self._memory.append(reply)
94
+
95
+ if reply.tool_calls:
96
+ for requested_tool in reply.tool_calls:
97
+ args = requested_tool.arguments
98
+ if isinstance(args, str):
99
+ args = json.loads(args)
100
+
101
+ for tool in (self.tools + tools):
102
+ if tool.name == requested_tool.name:
103
+ result = tool(**args)
104
+ requested_tool.result = result
105
+ break
106
+ else:
107
+ print("Tool Not found", requested_tool.name)
108
+ return reply
109
+
110
+ reply = client.invoke(
111
+ model=model,
112
+ system_prompt=self._system_prompt,
113
+ querys=self._memory,
114
+ tools=self.tools + tools
115
+ )
116
+
117
+ reply.answeredBy = model
118
+ self._memory.append(reply)
119
+
120
+ return reply
121
+
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class LLMModel:
5
+ name: str
6
+ input_cost: float = 0
7
+ output_cost: float = 0
8
+
9
+
10
+ # OpenAI
11
+ gpt_4o = LLMModel(name='openai/gpt-4o', input_cost=2.5, output_cost=10.0)
12
+ gpt_4o_mini = LLMModel(name='openai/gpt-4o-mini', input_cost=0.15, output_cost=0.60)
13
+ gpt_4_1 = LLMModel(name='openai/gpt-4.1', input_cost=2, output_cost=8.0)
14
+ gpt_4_1_mini = LLMModel(name='openai/gpt-4.1-mini', input_cost=0.4, output_cost=1.6)
15
+ gpt_4_1_nano = LLMModel(name='openai/gpt-4.1-nano', input_cost=0.1, output_cost=0.4)
16
+ o4_mini = LLMModel(name='openai/o4-mini', input_cost=1.1, output_cost=4.4)
17
+ o4_mini_high = LLMModel(name='openai/o4-mini-high', input_cost=1.1, output_cost=4.4)
18
+ o3 = LLMModel(name='openai/o3', input_cost=10, output_cost=40)
19
+
20
+ # Anthropic
21
+ claude_3_7_sonnet = LLMModel(name='anthropic/claude-3.7-sonnet', input_cost=3.0, output_cost=15.0)
22
+ claude_3_7_sonnet_thinking = LLMModel(name='anthropic/claude-3.7-sonnet:thinking', input_cost=3.0, output_cost=15.0)
23
+ claude_3_5_haiku = LLMModel(name='anthropic/claude-3.5-haiku', input_cost=0.8, output_cost=4.0)
24
+
25
+ # Google
26
+ gemini_2_0_flash = LLMModel(name='google/gemini-2.0-flash-001', input_cost=0.1, output_cost=0.4)
27
+ gemini_2_0_flash_free = LLMModel(name='google/gemini-2.0-flash-exp:free', input_cost=0.1, output_cost=0.4)
28
+ gemini_2_5_flash = LLMModel(name='google/gemini-2.5-flash-preview', input_cost=0.15, output_cost=0.60)
29
+ gemini_2_5_flash_thinking = LLMModel(name='google/gemini-2.5-flash-preview:thinking', input_cost=0.15, output_cost=3.5)
30
+ gemini_2_5_pro = LLMModel(name='google/gemini-2.5-pro-preview-03-25', input_cost=1.25, output_cost=10)
31
+
32
+ # Deepseek
33
+ deepseek_v3_free = LLMModel(name='deepseek/deepseek-chat-v3-0324:free', input_cost=0, output_cost=0)
34
+ deepseek_v3 = LLMModel(name='deepseek/deepseek-chat-v3-0324', input_cost=0.3, output_cost=1.2)
35
+ deepseek_r1_free = LLMModel(name='deepseek/deepseek-r1:free', input_cost=0, output_cost=0)
36
+ deepseek_r1 = LLMModel(name='deepseek/deepseek-r1', input_cost=0.5, output_cost=2.2)
37
+
38
+ # xAI
39
+ grok_3_mini = LLMModel(name='x-ai/grok-3-mini-beta', input_cost=0.3, output_cost=0.5)
40
+ grok_3 = LLMModel(name='x-ai/grok-3-beta', input_cost=3, output_cost=15)
41
+
42
+ # Microsoft
43
+ mai_ds_r1_free = LLMModel(name="microsoft/mai-ds-r1:free", input_cost=0, output_cost=0)
44
+
45
+ # Others
46
+ llama_4_maverick_free = LLMModel(name="meta-llama/llama-4-maverick:free", input_cost=0, output_cost=0)
47
+ llama_4_scout = LLMModel(name="meta-llama/llama-4-scout", input_cost=0.11, output_cost=0.34)
48
+ mistral_small_3_1_24B_free = LLMModel(name="mistralai/mistral-small-3.1-24b-instruct:free", input_cost=0, output_cost=0)
@@ -0,0 +1,84 @@
1
+ from .Chat_message import *
2
+ from .Tool import tool_model
3
+ from .LLMs import *
4
+
5
+ from openai import OpenAI
6
+ from dotenv import load_dotenv
7
+ import os
8
+
9
+
10
+ class OpenRouterProvider:
11
+ def __init__(self) -> None:
12
+ load_dotenv()
13
+ self.client = OpenAI(
14
+ base_url="https://openrouter.ai/api/v1",
15
+ api_key=os.getenv("OPENROUTER_API_KEY"),
16
+ )
17
+
18
+ def make_prompt(self, system_prompt: Chat_message,
19
+ querys: list[Chat_message]) -> list[dict]:
20
+ messages = [{"role": "system", "content": system_prompt.text}]
21
+
22
+ for query in querys:
23
+ # ----- USER -----
24
+ if query.role == Role.user:
25
+ if query.images is None:
26
+ messages.append({"role": "user", "content": query.text})
27
+ else:
28
+ content = [{"type": "text", "text": query.text}]
29
+ for img in query.images[:50]:
30
+ content.append(
31
+ {"type": "image_url",
32
+ "image_url": {"url": f"data:image/jpeg;base64,{img}"}})
33
+ messages.append({"role": "user", "content": content})
34
+
35
+ # ----- ASSISTANT -----
36
+ elif query.role == Role.ai or query.role == Role.tool:
37
+ assistant_msg = {"role": "assistant"}
38
+ assistant_msg["content"] = query.text or None # ← content は明示必須
39
+
40
+ # ① tool_calls を付与(あれば)
41
+ if query.tool_calls:
42
+ assistant_msg["tool_calls"] = [
43
+ {
44
+ "id": str(t.id),
45
+ "type": "function",
46
+ "function": {
47
+ "name": t.name,
48
+ "arguments": t.arguments # JSON 文字列
49
+ }
50
+ }
51
+ for t in query.tool_calls
52
+ ]
53
+ messages.append(assistant_msg)
54
+
55
+ # ② tool メッセージを assistant の直後に並べる
56
+ for t in query.tool_calls:
57
+ messages.append({
58
+ "role": "tool",
59
+ "tool_call_id": str(t.id),
60
+ "content": str(t.result) # 実行結果(文字列)
61
+ })
62
+ return messages
63
+
64
+
65
+ def invoke(self, model: LLMModel, system_prompt: Chat_message, querys: list[Chat_message], tools:list[tool_model]=[]) -> Chat_message:
66
+ response = self.client.chat.completions.create(
67
+ model=model.name,
68
+ messages=self.make_prompt(system_prompt, querys),
69
+ tools=[tool.tool_definition for tool in tools],
70
+ extra_body={
71
+ "provider": {
72
+ "order": ["Groq"]
73
+ }
74
+ }
75
+ )
76
+ reply = Chat_message(text=response.choices[0].message.content, role=Role.ai)
77
+
78
+ if response.choices[0].message.tool_calls:
79
+ reply.role = Role.tool
80
+ for tool in response.choices[0].message.tool_calls:
81
+ reply.tool_calls.append(ToolCall(id=tool.id, name=tool.function.name, arguments=tool.function.arguments))
82
+
83
+ return reply
84
+
@@ -0,0 +1,71 @@
1
+ import inspect
2
+ from functools import wraps
3
+ from typing import get_type_hints, get_origin, get_args
4
+
5
+
6
+ class tool_model:
7
+ def __init__(self, func):
8
+ self.func = func
9
+ self.name = func.__name__
10
+
11
+ sig = inspect.signature(func)
12
+ type_hints = get_type_hints(func)
13
+
14
+ type_map = {
15
+ int: "integer",
16
+ str: "string",
17
+ float: "number",
18
+ bool: "boolean",
19
+ }
20
+
21
+ properties = {}
22
+ required = []
23
+
24
+ # Build JSON schema properties
25
+ for name, param in sig.parameters.items():
26
+ anno = type_hints.get(name, None)
27
+ origin = get_origin(anno)
28
+
29
+ if origin is list:
30
+ # Handle list element types
31
+ (elem_type,) = get_args(anno)
32
+ json_item_type = type_map.get(elem_type, "string")
33
+ schema = {
34
+ "type": "array",
35
+ "items": {"type": json_item_type},
36
+ "description": name,
37
+ }
38
+ else:
39
+ # Primitive types
40
+ json_type = type_map.get(anno, "string")
41
+ schema = {
42
+ "type": json_type,
43
+ "description": name,
44
+ }
45
+
46
+ properties[name] = schema
47
+ if param.default is inspect._empty:
48
+ required.append(name)
49
+
50
+ # Attach tool definition metadata
51
+ self.tool_definition = {
52
+ "type": "function",
53
+ "function": {
54
+ "name": func.__name__,
55
+ "description": func.__doc__,
56
+ "parameters": {
57
+ "type": "object",
58
+ "properties": properties,
59
+ "required": required,
60
+ "additionalProperties": False,
61
+ },
62
+ "strict": True,
63
+ },
64
+ }
65
+
66
+ wraps(func)(self)
67
+
68
+ def __call__(self, *args, **kwargs):
69
+ return self.func(*args, **kwargs)
70
+
71
+
__init__.py ADDED
File without changes
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: openrouter-provider
3
+ Version: 0.0.1
4
+ Summary: This is an unofficial wrapper of OpenRouter.
5
+ Author-email: Keisuke Miyamto <aichiboyhighschool@gmail.com>
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: annotated-types
9
+ Requires-Dist: anyio
10
+ Requires-Dist: certifi
11
+ Requires-Dist: distro
12
+ Requires-Dist: h11
13
+ Requires-Dist: httpcore
14
+ Requires-Dist: httpx
15
+ Requires-Dist: idna
16
+ Requires-Dist: jiter
17
+ Requires-Dist: openai
18
+ Requires-Dist: pillow
19
+ Requires-Dist: pydantic
20
+ Requires-Dist: pydantic_core
21
+ Requires-Dist: python-dotenv
22
+ Requires-Dist: sniffio
23
+ Requires-Dist: tqdm
24
+ Requires-Dist: typing-inspection
25
+ Requires-Dist: typing_extensions
@@ -0,0 +1,10 @@
1
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ OpenRouterProvider/Chat_message.py,sha256=EG6CGfe3qLdczehA6wkX9FWTEFdKteAKWprQ5VSRbbU,4259
3
+ OpenRouterProvider/Chatbot_manager.py,sha256=NxxzbARiS6KcyCTwo26dR81Q4bv6HNueqlZF8YFOeTo,4261
4
+ OpenRouterProvider/LLMs.py,sha256=-0ELd6fqmdDvsdaPIElRsluiK85-Y6USwvQb2b4M8TA,2607
5
+ OpenRouterProvider/OpenRouterProvider.py,sha256=c6lFsr_4yEUNfc6UQvvp1RIfFUPD6iDd8qkaFgazxrM,3292
6
+ OpenRouterProvider/Tool.py,sha256=QeeWOD2oaYjB9tjF-Jvcjd_G_qSUIuKwFgyh20Ne06I,2010
7
+ openrouter_provider-0.0.1.dist-info/METADATA,sha256=B4NslzpmN4eqZaCV7YRsTt6PgwbBTffkbpizLeLR9Xc,678
8
+ openrouter_provider-0.0.1.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
9
+ openrouter_provider-0.0.1.dist-info/top_level.txt,sha256=I5BMEzkQFEnEYTqOY1Ktmnp7r1rrZQyeWdclKyyyHKs,28
10
+ openrouter_provider-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ OpenRouterProvider
2
+ __init__