openrouter-provider 1.0.1__py3-none-any.whl → 1.0.10__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.
openrouter/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .llms import *
2
+ from .message import *
3
+ from .openrouter import *
4
+ from .openrouter_provider import *
5
+ from .tool import *
@@ -34,9 +34,8 @@ claude_4_1_opus = LLMModel(name='anthropic/claude-opus-4.1', input_cost=15, outp
34
34
  # Google
35
35
  gemini_2_0_flash = LLMModel(name='google/gemini-2.0-flash-001', input_cost=0.1, output_cost=0.4)
36
36
  gemini_2_5_flash_lite = LLMModel(name='google/gemini-2.5-flash-lite', input_cost=0.1, output_cost=0.4)
37
- gemini_2_5_flash = LLMModel(name='google/gemini-2.5-flash-preview', input_cost=0.15, output_cost=0.60)
38
- gemini_2_5_flash_thinking = LLMModel(name='google/gemini-2.5-flash-preview:thinking', input_cost=0.15, output_cost=3.5)
39
- gemini_2_5_pro = LLMModel(name='google/gemini-2.5-pro-preview-03-25', input_cost=1.25, output_cost=10)
37
+ gemini_2_5_flash = LLMModel(name='google/gemini-2.5-flash', input_cost=0.3, output_cost=2.5)
38
+ gemini_2_5_pro = LLMModel(name='google/gemini-2.5-pro', input_cost=1.25, output_cost=10)
40
39
 
41
40
  # Deepseek
42
41
  deepseek_v3_free = LLMModel(name='deepseek/deepseek-chat-v3-0324:free', input_cost=0, output_cost=0)
@@ -3,12 +3,13 @@ from dataclasses import dataclass
3
3
  from enum import Enum
4
4
  from io import BytesIO
5
5
  import base64
6
+ import uuid
6
7
  from typing import Optional, Any
7
8
 
8
9
  from PIL import Image
9
10
  from openai.types.chat import ChatCompletion
10
11
 
11
- from .llms import LLMModel
12
+ from openrouter.llms import LLMModel
12
13
 
13
14
 
14
15
  class Role(Enum):
@@ -34,8 +35,10 @@ class Message:
34
35
  images: Optional[list[Image.Image]] = None,
35
36
  role: Role = Role.user,
36
37
  answered_by: Optional[LLMModel] = None,
37
- raw_response: Optional[ChatCompletion] = None
38
+ raw_response: Optional[ChatCompletion] = None,
39
+ id: Optional[str] = None
38
40
  ) -> None:
41
+ self.id = id if id is not None else str(uuid.uuid4())
39
42
  self.role = role
40
43
  self.text = text
41
44
  self.images = self._process_image(images)
@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
  import json
3
3
  import time
4
+ from copy import deepcopy
4
5
  from typing import Iterator, AsyncIterator
5
6
 
6
7
  from dotenv import load_dotenv
7
8
  from pydantic import BaseModel
8
9
 
9
- from .llms import *
10
- from .message import *
11
- from .openrouter_provider import *
12
- from .tool import *
10
+ from openrouter.llms import *
11
+ from openrouter.message import *
12
+ from openrouter.openrouter_provider import *
13
+ from openrouter.tool import *
13
14
 
14
15
 
15
16
  _base_system_prompt = """
@@ -35,9 +36,57 @@ class OpenRouterClient:
35
36
  system_prompt = _base_system_prompt
36
37
  system_prompt = system_prompt.replace("[TIME]", f"{month}/{day}/{year}")
37
38
  system_prompt = system_prompt.replace("[SYSTEM_INSTRUCTION]", prompt)
38
-
39
+
39
40
  self._system_prompt = Message(text=system_prompt, role=Role.system)
40
-
41
+
42
+ def _execute_tools(self, reply: Message, tools: list[tool_model]) -> Message:
43
+ if not reply.tool_calls:
44
+ return reply
45
+
46
+ reply_copy = deepcopy(reply)
47
+
48
+ for requested_tool in reply_copy.tool_calls:
49
+ args = requested_tool.arguments
50
+ if isinstance(args, str):
51
+ args = json.loads(args)
52
+
53
+ for tool in tools:
54
+ if tool.name == requested_tool.name:
55
+ result = tool(**args)
56
+ requested_tool.result = result
57
+ break
58
+
59
+ return reply_copy
60
+
61
+ def execute_tool(self, reply: Message, tool_index: int, tools: List[tool_model] = []) -> Message:
62
+ if not reply.tool_calls:
63
+ return reply
64
+
65
+ if tool_index < 0 or tool_index >= len(reply.tool_calls):
66
+ raise IndexError(f"Tool index {tool_index} is out of range. Available tools: {len(reply.tool_calls)}")
67
+
68
+ requested_tool = reply.tool_calls[tool_index]
69
+
70
+ args = requested_tool.arguments
71
+ if isinstance(args, str):
72
+ args = json.loads(args)
73
+
74
+ all_tools = self.tools + tools
75
+ for tool in all_tools:
76
+ if tool.name == requested_tool.name:
77
+ result = tool(**args)
78
+ requested_tool.result = result
79
+ break
80
+ else:
81
+ raise ValueError(f"Tool '{requested_tool.name}' not found in registered tools")
82
+
83
+ for i, msg in enumerate(self._memory):
84
+ if msg.id == reply.id:
85
+ self._memory[i] = reply
86
+ break
87
+
88
+ return reply
89
+
41
90
  def clear_memory(self) -> None:
42
91
  self._memory = []
43
92
 
@@ -80,14 +129,17 @@ class OpenRouterClient:
80
129
  def invoke(
81
130
  self,
82
131
  model: LLMModel,
83
- query: Message,
132
+ query: Message = None,
84
133
  tools: list[tool_model] = None,
85
134
  provider: ProviderConfig = None,
86
- temperature: float = 0.3
135
+ temperature: float = 0.3,
136
+ auto_tool_exec: bool = True
87
137
  ) -> Message:
88
138
  tools = tools or []
89
- self._memory.append(query)
139
+ if query is not None:
140
+ self._memory.append(query)
90
141
  client = OpenRouterProvider()
142
+
91
143
  reply = client.invoke(
92
144
  model=model,
93
145
  temperature=temperature,
@@ -99,28 +151,17 @@ class OpenRouterClient:
99
151
  reply.answered_by = model
100
152
  self._memory.append(reply)
101
153
 
102
- if reply.tool_calls:
103
- for requested_tool in reply.tool_calls:
104
- args = requested_tool.arguments
105
- if isinstance(args, str):
106
- args = json.loads(args)
107
-
108
- for tool in (self.tools + tools):
109
- if tool.name == requested_tool.name:
110
- result = tool(**args)
111
- requested_tool.result = result
112
- break
113
- else:
114
- return reply
115
-
154
+ if auto_tool_exec and reply.tool_calls:
155
+ reply = self._execute_tools(reply, self.tools + tools)
156
+ self._memory[-1] = reply
157
+
116
158
  reply = client.invoke(
117
159
  model=model,
160
+ temperature=temperature,
118
161
  system_prompt=self._system_prompt,
119
162
  querys=self._memory,
120
- tools=self.tools + tools,
121
163
  provider=provider
122
164
  )
123
-
124
165
  reply.answered_by = model
125
166
  self._memory.append(reply)
126
167
 
@@ -156,13 +197,15 @@ class OpenRouterClient:
156
197
  async def async_invoke(
157
198
  self,
158
199
  model: LLMModel,
159
- query: Message,
200
+ query: Message = None,
160
201
  tools: list[tool_model] = None,
161
202
  provider: ProviderConfig = None,
162
- temperature: float = 0.3
203
+ temperature: float = 0.3,
204
+ auto_tool_exec: bool = True
163
205
  ) -> Message:
164
206
  tools = tools or []
165
- self._memory.append(query)
207
+ if query is not None:
208
+ self._memory.append(query)
166
209
  client = OpenRouterProvider()
167
210
  reply = await client.async_invoke(
168
211
  model=model,
@@ -175,19 +218,8 @@ class OpenRouterClient:
175
218
  reply.answered_by = model
176
219
  self._memory.append(reply)
177
220
 
178
- if reply.tool_calls:
179
- for requested_tool in reply.tool_calls:
180
- args = requested_tool.arguments
181
- if isinstance(args, str):
182
- args = json.loads(args)
183
-
184
- for tool in (self.tools + tools):
185
- if tool.name == requested_tool.name:
186
- result = tool(**args)
187
- requested_tool.result = result
188
- break
189
- else:
190
- return reply
221
+ if auto_tool_exec and reply.tool_calls:
222
+ reply = self._execute_tools(reply, self.tools + tools)
191
223
 
192
224
  reply = await client.async_invoke(
193
225
  model=model,
@@ -248,4 +280,8 @@ class OpenRouterClient:
248
280
  provider=provider,
249
281
  json_schema=json_schema
250
282
  )
283
+
284
+ self._memory.append(Message(text=reply.model_dump_json(), role=Role.ai, answered_by=model))
285
+
251
286
  return reply
287
+
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ import json
2
3
  import os
3
4
  from dataclasses import dataclass, asdict
4
5
  from typing import List, Optional, Literal, Iterator, AsyncIterator
@@ -6,11 +7,11 @@ from typing import List, Optional, Literal, Iterator, AsyncIterator
6
7
  from dotenv import load_dotenv
7
8
  from openai import OpenAI, AsyncOpenAI
8
9
  from openai.types.chat import ChatCompletionChunk
9
- from pydantic import BaseModel
10
+ from pydantic import BaseModel, ValidationError
10
11
 
11
- from .message import Message, Role, ToolCall
12
- from .tool import tool_model
13
- from .llms import LLMModel
12
+ from openrouter.message import Message, Role, ToolCall
13
+ from openrouter.tool import tool_model
14
+ from openrouter.llms import LLMModel
14
15
 
15
16
 
16
17
 
@@ -122,7 +123,7 @@ class OpenRouterProvider:
122
123
  for tool in response.choices[0].message.tool_calls:
123
124
  reply.tool_calls.append(ToolCall(id=tool.id, name=tool.function.name, arguments=tool.function.arguments))
124
125
  return reply
125
-
126
+
126
127
  def invoke_stream(
127
128
  self,
128
129
  model: LLMModel,
@@ -219,14 +220,60 @@ class OpenRouterProvider:
219
220
  messages = self.make_prompt(system_prompt, querys)
220
221
  provider_dict = provider.to_dict() if provider else None
221
222
 
223
+ schema = json_schema.model_json_schema()
224
+
225
+ def add_additional_properties_false(obj):
226
+ if isinstance(obj, dict):
227
+ if "properties" in obj:
228
+ obj["additionalProperties"] = False
229
+ for value in obj.values():
230
+ add_additional_properties_false(value)
231
+ elif isinstance(obj, list):
232
+ for item in obj:
233
+ add_additional_properties_false(item)
234
+
235
+ def ensure_required_properties(obj):
236
+ if isinstance(obj, dict):
237
+ properties = obj.get("properties")
238
+ if isinstance(properties, dict):
239
+ keys = list(properties.keys())
240
+ existing_required = obj.get("required")
241
+ if isinstance(existing_required, list):
242
+ required_set = set(existing_required)
243
+ else:
244
+ required_set = set()
245
+ required_set.update(keys)
246
+ obj["required"] = list(required_set)
247
+ for value in obj.values():
248
+ ensure_required_properties(value)
249
+ elif isinstance(obj, list):
250
+ for item in obj:
251
+ ensure_required_properties(item)
252
+
253
+ add_additional_properties_false(schema)
254
+ ensure_required_properties(schema)
255
+
222
256
  response = self.client.chat.completions.create(
223
257
  model=model.name,
224
258
  temperature=temperature,
225
259
  messages=messages,
226
- response_format={"type": "json_schema", "json_schema": {"name": json_schema.__name__, "schema": json_schema.model_json_schema()}},
260
+ response_format={"type": "json_schema", "json_schema": {"name": json_schema.__name__, "schema": schema}},
227
261
  extra_body={"provider": provider_dict},
228
262
  )
229
263
 
230
- return json_schema.model_validate_json(response.choices[0].message.content)
231
-
232
-
264
+ content = response.choices[0].message.content
265
+
266
+ try:
267
+ return json_schema.model_validate_json(content)
268
+ except ValidationError:
269
+ formatted_content = content
270
+ try:
271
+ parsed = json.loads(content)
272
+ formatted_content = json.dumps(parsed, indent=2, ensure_ascii=False)
273
+ except json.JSONDecodeError:
274
+ pass
275
+ print("structured_output validation failed, response content:")
276
+ print(formatted_content)
277
+ raise
278
+
279
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrouter-provider
3
- Version: 1.0.1
3
+ Version: 1.0.10
4
4
  Summary: This is an unofficial wrapper of OpenRouter.
5
5
  Author-email: Keisuke Miyamto <aichiboyhighschool@gmail.com>
6
6
  Requires-Python: >=3.7
@@ -0,0 +1,10 @@
1
+ openrouter/__init__.py,sha256=xuIzdm8-l3Tmc-zrNIXTicv05c9HCMxTS9xynKpWK-Q,123
2
+ openrouter/llms.py,sha256=zmujFW5BmQA0Fm6z8skuO3ipLpaUMy1VBZxYkPE9OcM,3391
3
+ openrouter/message.py,sha256=ESI4YT6x0TuPZ0AY29ZPlBCv72KrQad1-IeNmrfGD0w,2978
4
+ openrouter/openrouter.py,sha256=T1n3Mi26StAMzJ5yNNY5ww6fympiHBfp5M4pzYPmaV8,8993
5
+ openrouter/openrouter_provider.py,sha256=NHKZjpTTBKWTYcH67XTXh8vTotV-zsuuQl2xzAjXq24,9898
6
+ openrouter/tool.py,sha256=tUUNLosz1XhzPIwY1zHXWNM3ePs7hcVD1a_W5hWTCWk,1975
7
+ openrouter_provider-1.0.10.dist-info/METADATA,sha256=NFw7weaQNpo-sEJbxrsC_DqrkJsLSJ7_bk4APhRecVY,11504
8
+ openrouter_provider-1.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ openrouter_provider-1.0.10.dist-info/top_level.txt,sha256=0jnlCcRirGeYZLm5ZbWQRUonIp4tTPl_9mq-ds_1SEo,11
10
+ openrouter_provider-1.0.10.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ openrouter
__init__.py DELETED
File without changes
@@ -1,10 +0,0 @@
1
- __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- llms.py,sha256=IjG3WgmPDbNR8zvAOYDsZvSZNmDjYfPS4IpdtlI4OI8,3535
3
- message.py,sha256=xTPzrgiVd651EeccUoxh5h1Xo4Xhu1VpAnb6NB-p9YY,2860
4
- openrouter.py,sha256=zXa6huZ4QIzJpbk9GeX4fEKJsyYwZN09ZlmbkAef5ik,7857
5
- openrouter_provider.py,sha256=dJe_kPENFfhzC9J7q4XyglpXYq-1KPFIwsgyIdHeml8,8021
6
- tool.py,sha256=tUUNLosz1XhzPIwY1zHXWNM3ePs7hcVD1a_W5hWTCWk,1975
7
- openrouter_provider-1.0.1.dist-info/METADATA,sha256=g4NGzvYX4bY8VU3c9gEAT8XWcfT0itTTY-HiOQVb3o4,11503
8
- openrouter_provider-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- openrouter_provider-1.0.1.dist-info/top_level.txt,sha256=P1gRRxgxRCkgKpO2Eru8nR9svmkGy1YlKSVavfrNIFs,58
10
- openrouter_provider-1.0.1.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- __init__
2
- llms
3
- message
4
- openrouter
5
- openrouter_provider
6
- tool
File without changes