diff --git a/openrouter_auto_router.py b/openrouter_auto_router.py index 789939e..a5b5188 100644 --- a/openrouter_auto_router.py +++ b/openrouter_auto_router.py @@ -105,9 +105,9 @@ class Pipe: "complex bug", "complex problem", "multi-agent", "multiple services", "distributed system", "design a system", "build a system", "agentic", } - if any(self._contains_phrase(lowered, item) for item in complex_phrases) or any( - word.startswith("архитектур") for word in words - ): + if any(self._contains_phrase(lowered, item) for item in complex_phrases): + return "complex" + if any(word.startswith("архитектур") for word in words): return "complex" coding_words = { @@ -128,11 +128,9 @@ class Pipe: "перепиши код", "рефакторинг", "stack trace", "error log", "виртуальная машина", "reverse proxy", "nginx proxy manager", "pull request", } - coding_stems = {"программ", "функци", "библиотек", "ошиб", "сертификат"} - if words.intersection(coding_words): return "coding" - if any(word.startswith(stem) for word in words for stem in coding_stems): + if any(word.startswith(stem) for word in words for stem in {"программ", "функци", "библиотек", "ошиб", "сертификат"}): return "coding" if any(self._contains_phrase(lowered, item) for item in coding_phrases): return "coding" @@ -189,13 +187,15 @@ class Pipe: raise RuntimeError("Request failed without a captured error") def _parse_tool_arguments(self, raw_arguments): - """Разбирает JSON-аргументы вызова инструмента.""" + """Разбирает JSON-аргументы инструмента.""" if raw_arguments is None: return {} if isinstance(raw_arguments, dict): return raw_arguments - if not isinstance(raw_arguments, str) or not raw_arguments.strip(): - return {} if isinstance(raw_arguments, str) else None + if not isinstance(raw_arguments, str): + return None + if not raw_arguments.strip(): + return {} try: arguments = json.loads(raw_arguments) except json.JSONDecodeError: @@ -225,7 +225,7 @@ class Pipe: result.append({"type": "function", "function": spec}) return result - def _build_payload(self, body, model, messages, tools_map): + def _build_payload(self, body, model, messages, tools_map, tool_iteration=0): """Собирает payload для OpenRouter.""" payload = dict(body) payload.update({"model": model, "messages": messages, "stream": False}) @@ -235,24 +235,33 @@ class Pipe: payload["max_tokens"] = self.valves.MAX_TOKENS for field in {"user", "reasoning_effort", "metadata", "store"}: payload.pop(field, None) + if not self.valves.ENABLE_TOOLS: for field in {"tools", "tool_choice", "parallel_tool_calls"}: payload.pop(field, None) - elif not payload.get("tools"): - specs = self._extract_tool_specs(tools_map) - if specs: - payload["tools"] = specs + else: + if not payload.get("tools"): + specs = self._extract_tool_specs(tools_map) + if specs: + payload["tools"] = specs + + # После первого tool call модель должна иметь возможность + # завершить ответ обычным текстом, даже если Open WebUI + # передал tool_choice="required" или фиксированный tool. + if tool_iteration > 0: + payload["tool_choice"] = "auto" + return payload - async def _execute_tool_call(self, tool_call, tools_map): + async def _execute_tool_call(self, call, tools_map): """Выполняет tool call; фатальна только ошибка tool_call_id.""" - if not isinstance(tool_call, dict): + if not isinstance(call, dict): return {"fatal": True, "result": "Ошибка: некорректный формат tool call."} - call_id = tool_call.get("id") + call_id = call.get("id") if not isinstance(call_id, str) or not call_id.strip(): return {"fatal": True, "result": "Ошибка: отсутствует корректный tool_call_id."} - function = tool_call.get("function") + function = call.get("function") if not isinstance(function, dict): return {"fatal": False, "tool_call_id": call_id, "name": "unknown", "arguments": {}, "result": "Ошибка: отсутствует описание функции."} name = function.get("name") @@ -327,11 +336,11 @@ class Pipe: try: async with httpx.AsyncClient(timeout=timeout) as client: - for _ in range(self.valves.MAX_TOOL_ITERATIONS): - data = await self._request( - self._build_payload(body, model, messages, tools_map), - client, + for iteration in range(self.valves.MAX_TOOL_ITERATIONS): + payload = self._build_payload( + body, model, messages, tools_map, iteration ) + data = await self._request(payload, client) try: message = data["choices"][0]["message"] except (KeyError, IndexError, TypeError): @@ -350,21 +359,25 @@ class Pipe: break messages.append({"role": "assistant", "content": message.get("content"), "tool_calls": tool_calls}) - fatal_stop = False + stop = False for call in tool_calls: function = call.get("function", {}) if isinstance(call, dict) else {} - key = json.dumps({"name": function.get("name"), "arguments": function.get("arguments")}, ensure_ascii=False, sort_keys=True) + key = json.dumps( + {"name": function.get("name"), "arguments": function.get("arguments")}, + ensure_ascii=False, + sort_keys=True, + ) repeated[key] = repeated.get(key, 0) + 1 if repeated[key] > self.valves.MAX_SAME_TOOL_CALLS: stopped_reason = "⚠️ Один и тот же tool call повторился слишком много раз." - fatal_stop = True + stop = True break execution = await self._execute_tool_call(call, tools_map) if execution["fatal"]: stopped_reason = execution["result"] final_content = content - fatal_stop = True + stop = True break trace.append(execution) messages.append({ @@ -373,7 +386,7 @@ class Pipe: "name": execution["name"], "content": execution["result"], }) - if fatal_stop: + if stop: break else: stopped_reason = f"⚠️ Достигнут лимит циклов tool calling: {self.valves.MAX_TOOL_ITERATIONS}."