From 5cbe6c50209d1b857125fac588f77ba2be98793f Mon Sep 17 00:00:00 2001 From: host Date: Wed, 12 Aug 2026 13:32:16 +0300 Subject: [PATCH] =?UTF-8?q?Pipe:=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0?= =?UTF-8?q?=D1=82=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B5=20?= =?UTF-8?q?OpenAI=20chat.completion=20=D1=81=20usage=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20Token=20Tracker=20(v2.8.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openrouter_auto_router_v2.7.1.py | 135 ++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 19 deletions(-) diff --git a/openrouter_auto_router_v2.7.1.py b/openrouter_auto_router_v2.7.1.py index 9feae5f..add1fc8 100644 --- a/openrouter_auto_router_v2.7.1.py +++ b/openrouter_auto_router_v2.7.1.py @@ -1,14 +1,15 @@ """ title: OpenRouter Auto Router author: OpenAI -version: 2.7.1 -description: Автоматическая маршрутизация OpenRouter с поддержкой tools. +version: 2.8.0 +description: Автоматическая маршрутизация OpenRouter с поддержкой tools и прокидыванием usage для Token Tracker. """ import asyncio import inspect import json import re +import time import httpx from pydantic import BaseModel, Field @@ -315,6 +316,74 @@ class Pipe: return payload + def _accumulate_usage(self, total_usage, data): + """Суммирует usage по всем итерациям tool-calling.""" + if not isinstance(data, dict): + return + + usage = data.get("usage") + if not isinstance(usage, dict): + return + + total_usage["prompt_tokens"] += int(usage.get("prompt_tokens", 0) or 0) + total_usage["completion_tokens"] += int(usage.get("completion_tokens", 0) or 0) + total_usage["total_tokens"] += int(usage.get("total_tokens", 0) or 0) + + cost = usage.get("cost") + if isinstance(cost, (int, float)): + total_usage["cost"] += float(cost) + + prompt_details = usage.get("prompt_tokens_details") or {} + if isinstance(prompt_details, dict): + total_usage["cached_tokens"] += int( + prompt_details.get("cached_tokens", 0) or 0 + ) + total_usage["cache_write_tokens"] += int( + prompt_details.get("cache_write_tokens", 0) or 0 + ) + + completion_details = usage.get("completion_tokens_details") or {} + if isinstance(completion_details, dict): + total_usage["reasoning_tokens"] += int( + completion_details.get("reasoning_tokens", 0) or 0 + ) + + def _build_usage_payload(self, total_usage): + """Собирает usage в формате OpenAI chat.completion для Token Tracker.""" + return { + "prompt_tokens": total_usage["prompt_tokens"], + "completion_tokens": total_usage["completion_tokens"], + "total_tokens": total_usage["total_tokens"], + "cost": total_usage["cost"], + "prompt_tokens_details": { + "cached_tokens": total_usage["cached_tokens"], + "cache_write_tokens": total_usage["cache_write_tokens"], + }, + "completion_tokens_details": { + "reasoning_tokens": total_usage["reasoning_tokens"], + }, + } + + def _build_router_content(self, answer, route, model): + """Собирает финальный текст ответа с префиксом маршрутизатора.""" + if not self.valves.SHOW_ROUTER_INFO: + return answer + + route_names = { + "default": "💰 Обычный запрос", + "coding": "👨‍💻 Код / Linux / Docker", + "vision": "🖼️ Изображение / скриншот", + "complex": "🧠 Сложная задача", + } + route_name = route_names.get(route, route) + + return ( + f"**🤖 Auto Router** \n" + f"Маршрут: **{route_name}** \n" + f"Модель: `{model}`\n\n" + f"{answer}" + ) + async def _run_tool_callable(self, callable_fn, arguments, name): """Вызывает sync/async callable и ограничивает результат.""" try: @@ -453,7 +522,10 @@ class Pipe: __user__: dict = None, __tools__: dict = None, ): - """Маршрутизирует запрос и выполняет цикл tool calling.""" + """Маршрутизирует запрос и выполняет цикл tool calling. + + Возвращает OpenAI-совместимый dict формата chat.completion, + чтобы Token Tracker Filter мог извлечь usage (prompt/completion/cost).""" if not isinstance(body, dict): return "❌ **Некорректный формат запроса.**" @@ -478,6 +550,20 @@ class Pipe: final_content = "" stopped_reason = "" + # Аккумуляторы usage по всем итерациям tool-calling + total_usage = { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "cost": 0.0, + "reasoning_tokens": 0, + "cached_tokens": 0, + "cache_write_tokens": 0, + } + response_id = f"auto-router-{int(time.time())}" + response_created = int(time.time()) + response_model_id = model + timeout = httpx.Timeout(self.valves.REQUEST_TIMEOUT) try: @@ -488,6 +574,16 @@ class Pipe: ) data = await self._request(payload, client) + # Суммируем usage и обновляем метаданные ответа + if isinstance(data, dict): + self._accumulate_usage(total_usage, data) + if isinstance(data.get("id"), str): + response_id = data["id"] + if isinstance(data.get("created"), (int, float)): + response_created = int(data["created"]) + if isinstance(data.get("model"), str): + response_model_id = data["model"] + try: message = data["choices"][0]["message"] except (KeyError, IndexError, TypeError): @@ -577,20 +673,21 @@ class Pipe: trace_text = self._format_tool_trace(tool_trace) answer = f"{answer}\n\n---\n\n{trace_text}" if answer else trace_text - if not self.valves.SHOW_ROUTER_INFO: - return answer + content = self._build_router_content(answer, route, model) - route_names = { - "default": "💰 Обычный запрос", - "coding": "👨‍💻 Код / Linux / Docker", - "vision": "🖼️ Изображение / скриншот", - "complex": "🧠 Сложная задача", - } - route_name = route_names.get(route, route) - - return ( - f"**🤖 Auto Router** \n" - f"Маршрут: **{route_name}** \n" - f"Модель: `{model}`\n\n" - f"{answer}" - ) \ No newline at end of file + # Возвращаем OpenAI chat.completion dict — Token Tracker Filter + # прочитает usage и покажет токены/стоимость/баланс. + return { + "id": response_id, + "object": "chat.completion", + "created": response_created, + "model": response_model_id, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": content}, + "finish_reason": "stop", + } + ], + "usage": self._build_usage_payload(total_usage), + } \ No newline at end of file