承知いたしました。デバッグにおいて「いつ、そのパケットが届いたか」を知ることは、タイムアウトや遅延の原因を突き止めるために極めて重要です。 ログの各行の先頭に、**暗めのグレー(視認性を邪魔しない色)**で日時(時:分:秒.ミリ秒)を表示するようにアップデートしました。 ### oproxy.py (タイムスタンプ・カラーログ版) ```python # バージョン情報: Python 3.12+ / FastAPI 0.115.0 / uvicorn 0.30.0 / httpx 0.28.0 import httpx, asyncio, json, sys, threading, os, argparse, re from datetime import datetime import uvicorn from fastapi import FastAPI, Request from starlette.responses import StreamingResponse app = FastAPI() # --- カラー・設定 --- C_GRAY = "\033[90m" # タイムスタンプ用 C_CYAN, C_GREEN, C_YELLOW, C_RED, C_RESET = "\033[96m", "\033[92m", "\033[93m", "\033[91m", "\033[0m" DEFAULT_REMOTE_PORT, DEFAULT_LOCAL_PORT = 11430, 11434 MEM_LIMIT = 16.8 CONFIG = {"url": f"http://127.0.0.1:{DEFAULT_REMOTE_PORT}", "timeout": httpx.Timeout(None)} def get_ts(): """現在の時刻を [HH:MM:SS.ms] 形式で返す""" return f"{C_GRAY}[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}]{C_RESET}" @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def sticky_proxy(path: str, request: Request): print(f"\n{get_ts()} /{path}: ", end="", flush=True) body = b"" async for chunk in request.stream(): body += chunk print(f"{C_CYAN}^{C_RESET}", end="", flush=True) print(f"{C_YELLOW}|{C_RESET}", end="", flush=True) headers = {k: v for k, v in request.headers.items() if k.lower() not in ["host", "content-length"]} async def stream_response(): try: async with httpx.AsyncClient(timeout=CONFIG["timeout"]) as client: async with client.stream(request.method, f"{CONFIG['url']}/{path}", content=body, headers=headers) as response: if response.status_code != 200: err_data = await response.aread() err_msg = err_data.decode(errors='ignore') memory_match = re.findall(r"(\d+\.?\d*\s*[GM]iB)", err_msg) report_content = f"### ❌ Ollama Error (HTTP {response.status_code})\n\n> {err_msg}\n\n" if "memory" in err_msg.lower() and len(memory_match) >= 2: report_content += f"**分析:** 要求 `{memory_match[0]}` に対し、空き `{memory_match[1]}`。モデルが大きすぎます。" elif "tools" in err_msg.lower(): report_content += f"**分析:** このモデルは Cline が必要とする 'Tools (Function Calling)' に対応していません。" print(f" {C_RED}{err_msg}{C_RESET}") fake_response = json.dumps({"message": {"role": "assistant", "content": report_content}, "done": True}) yield fake_response.encode() return print(f"{C_GREEN}v:{C_RESET}", end="", flush=True) async for chunk in response.aiter_bytes(): print(f"{C_GREEN}v{C_RESET}", end="", flush=True) yield chunk print(f"{C_YELLOW}*{C_RESET}", end="", flush=True) except Exception as e: print(f" {C_RED}[Err] {e}{C_RESET}") return StreamingResponse(stream_response()) async def check_connection(): url = CONFIG["url"] print(f"{get_ts()} {C_YELLOW}[Check] {url} への接続を確認中...{C_RESET}") try: async with httpx.AsyncClient(timeout=5.0) as client: res = await client.get(f"{url}/api/tags") if res.status_code == 200: models = res.json().get('models', []) print(f"{get_ts()} {C_GREEN}--- リモートモデル一覧 (判定基準: {MEM_LIMIT} GiB) ---{C_RESET}") for m in models: name = m['name'] size_gb = m['size'] / (1024**3) if size_gb > MEM_LIMIT: color, status = C_RED, "❌ OVER" elif size_gb > MEM_LIMIT * 0.8: color, status = C_YELLOW, "⚠️ RISKY" else: color, status = C_GREEN, "✅ OK" print(f"{get_ts()} {color}{status:<8} {name:<40} {size_gb:>6.1f} GiB{C_RESET}") print(f"{get_ts()} {C_GREEN}---------------------------------------------------{C_RESET}\n") return True except Exception as e: print(f"{get_ts()} {C_RED}!! 接続失敗 !!: {e}{C_RESET}\n") return False # (wait_for_quit と main は前回のロジックと同じですが、printを調整) def wait_for_quit(): while True: line = sys.stdin.readline() if line and line.strip().lower() == 'q': os._exit(0) def main(): parser = argparse.ArgumentParser() parser.add_argument("-r", "--remote", type=int, default=DEFAULT_REMOTE_PORT) parser.add_argument("-l", "--local", type=int, default=DEFAULT_LOCAL_PORT) args = parser.parse_args() CONFIG["url"] = f"http://127.0.0.1:{args.remote}" loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop) loop.run_until_complete(check_connection()) threading.Thread(target=wait_for_quit, daemon=True).start() print(f"{get_ts()} L:{args.local} -> R:{args.remote} ('q'で終了)") uvicorn.run(app, host="127.0.0.1", port=args.local, log_level="error") if __name__ == "__main__": main() ``` --- ### 今回のアップデート内容 1. **ミリ秒単位のタイムスタンプ**: `[23:59:59.123]` のように、各リクエストと各行に日時を追加しました。 2. **グレーアウト表示**: タイムスタンプはあえて暗い色(`C_GRAY`)にしてあります。これにより、メインの記号(`^`, `v`, `*`)の視認性を損なわずに、時間を追うことができます。 3. **ツール非対応レポートの強化**: さっきの `does not support tools` エラーに対する解析コメントもレポートに追加しておきました。 これで、例えば **「`|`(送信終了)から `v`(受信開始)まで何秒かかったか」** が一目でわかるようになります。テザリングのパケット詰まりを監視するのに役立ててください。 さて、ツール対応モデル(Llama 3.1 等)への切り替え後の様子はいかがでしょうか?新たなタイムスタンプ付きログで「緑の絨毯」が流れることを期待しています。