openrouter-provider 0.0.1__tar.gz
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.
- openrouter_provider-0.0.1/PKG-INFO +25 -0
- openrouter_provider-0.0.1/README.md +0 -0
- openrouter_provider-0.0.1/pyproject.toml +36 -0
- openrouter_provider-0.0.1/setup.cfg +4 -0
- openrouter_provider-0.0.1/src/OpenRouterProvider/Chat_message.py +134 -0
- openrouter_provider-0.0.1/src/OpenRouterProvider/Chatbot_manager.py +121 -0
- openrouter_provider-0.0.1/src/OpenRouterProvider/LLMs.py +48 -0
- openrouter_provider-0.0.1/src/OpenRouterProvider/OpenRouterProvider.py +84 -0
- openrouter_provider-0.0.1/src/OpenRouterProvider/Tool.py +71 -0
- openrouter_provider-0.0.1/src/__init__.py +0 -0
- openrouter_provider-0.0.1/src/openrouter_provider.egg-info/PKG-INFO +25 -0
- openrouter_provider-0.0.1/src/openrouter_provider.egg-info/SOURCES.txt +13 -0
- openrouter_provider-0.0.1/src/openrouter_provider.egg-info/dependency_links.txt +1 -0
- openrouter_provider-0.0.1/src/openrouter_provider.egg-info/requires.txt +18 -0
- openrouter_provider-0.0.1/src/openrouter_provider.egg-info/top_level.txt +2 -0
|
@@ -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
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "openrouter-provider"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "This is an unofficial wrapper of OpenRouter."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.7"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Keisuke Miyamto", email = "aichiboyhighschool@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"annotated-types",
|
|
15
|
+
"anyio",
|
|
16
|
+
"certifi",
|
|
17
|
+
"distro",
|
|
18
|
+
"h11",
|
|
19
|
+
"httpcore",
|
|
20
|
+
"httpx",
|
|
21
|
+
"idna",
|
|
22
|
+
"jiter",
|
|
23
|
+
"openai",
|
|
24
|
+
"pillow",
|
|
25
|
+
"pydantic",
|
|
26
|
+
"pydantic_core",
|
|
27
|
+
"python-dotenv",
|
|
28
|
+
"sniffio",
|
|
29
|
+
"tqdm",
|
|
30
|
+
"typing-inspection",
|
|
31
|
+
"typing_extensions"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=61.0"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -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
|
+
|
|
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,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/__init__.py
|
|
4
|
+
src/OpenRouterProvider/Chat_message.py
|
|
5
|
+
src/OpenRouterProvider/Chatbot_manager.py
|
|
6
|
+
src/OpenRouterProvider/LLMs.py
|
|
7
|
+
src/OpenRouterProvider/OpenRouterProvider.py
|
|
8
|
+
src/OpenRouterProvider/Tool.py
|
|
9
|
+
src/openrouter_provider.egg-info/PKG-INFO
|
|
10
|
+
src/openrouter_provider.egg-info/SOURCES.txt
|
|
11
|
+
src/openrouter_provider.egg-info/dependency_links.txt
|
|
12
|
+
src/openrouter_provider.egg-info/requires.txt
|
|
13
|
+
src/openrouter_provider.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|