From 942f6328919d6a41836cb47cd1def544b8b04be1 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 8 Feb 2026 01:47:43 +0900 Subject: [PATCH] README.md --- NEWS.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 NEWS.md create mode 100644 README.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..79fee60 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,128 @@ +承知いたしました。デバッグにおいて「いつ、そのパケットが届いたか」を知ることは、タイムアウトや遅延の原因を突き止めるために極めて重要です。 + +ログの各行の先頭に、**暗めのグレー(視認性を邪魔しない色)**で日時(時:分:秒.ミリ秒)を表示するようにアップデートしました。 + +### 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 等)への切り替え後の様子はいかがでしょうか?新たなタイムスタンプ付きログで「緑の絨毯」が流れることを期待しています。 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..780dca3 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +これまでの試行錯誤と、この「凄そうなツール」に詰め込んだ機能をすべて盛り込んだ `README.md` を作成しました。 + +このプロキシは、単なる通信の中継器ではなく、**「リモートサーバーの限界を可視化し、Clineを快適に動かすための管制塔」**としての役割を担っています。 + +--- + +## README.md + +# Ollama Debugging Proxy (oproxy) + +リモートサーバー上の Ollama とローカルの Cline を繋ぎ、通信状態やサーバーリソース、モデルの適合性をリアルタイムに可視化するための高機能プロキシツールです。 + +### 🌟 主な機能 + +* **戦力分析 (Startup Scan)**: 起動時にリモートの全モデルをスキャンし、以下の2点から「Clineで動くか」を自動判定します。 +* **メモリ判定**: サーバーの空きメモリ(16.8 GiB基準)に収まるか。 +* **ツール判定**: Clineの操作に必要な `Tools (Function Calling)` に対応しているか。 + + +* **通信の可視化 (Real-time Logs)**: +* `^`: リクエスト送信(Cline → Proxy → Ollama) +* `v`: レスポンス受信(Ollama → Proxy → Cline) +* `|` / `*`: パケットの区切りと完了を視認。 +* **タイムスタンプ**: ミリ秒単位のログで、遅延が発生している箇所を特定。 + + +* **エラー翻訳**: Ollamaが返す不親切な JSON エラーを解析し、Clineのチャット画面上に「何が原因で、どう対策すべきか」の Markdown レポートとして流し込みます。 +* **物理的表示の安定性**: 絵文字の幅によるズレを排除した「背景色付きバッジ」システムにより、ターミナル上での完璧な整列を実現。 + +--- + +### 🚀 使い方 + +#### 1. 起動 + +```bash +python oproxy.py -r 11433 -l 11434 + +``` + +* `-r`: リモートの Ollama ポート(SSHトンネル等のポート) +* `-l`: Cline が接続するローカルポート + +#### 2. リストの確認 + +起動時に表示されるリストで **`READY`** と出ているモデルを確認してください。 + +* `READY` (緑): メモリ・機能ともに合格。 +* `TOOL` (黄): メモリは足りるが、ファイル操作等ができない可能性あり。 +* `MEM` (赤): メモリ不足でロードに失敗します。 + +#### 3. Cline の設定 + +Cline の設定画面で `Base URL` を `http://127.0.0.1:11434` に設定し、リストで確認した最適なモデル名を入力してください。 + +--- + +### 🛠 必要要件 + +* **Python**: 3.12以上推奨 +* **依存ライブラリ**: +* `fastapi` +* `uvicorn` +* `httpx` + + + +--- + +### 🎨 ログ表示の見方 + +```text +[HH:MM:SS.ms] READY [TOOL] モデル名 (サイズ) GiB +[HH:MM:SS.ms] /api/chat: ^^^^|v:vvvvvvvvv* + +``` + +* `^` が出たまま止まる場合:アップロード(テザリング等の上り)がボトルネック。 +* `|` の後に `v` がなかなか出ない場合:サーバー側の推論待ち、またはメモリのロード中。 +* `v` が少しずつ出る場合:ストリーミング中。