Compare commits
4 commits
dea5747737
...
3fea146d74
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fea146d74 | |||
| 17623652ba | |||
| 3d992041e9 | |||
| 82163f7a2a |
2 changed files with 79 additions and 19 deletions
|
|
@ -62,3 +62,7 @@ AI Agent (Cline 等) が「なぜか思い通りに動かない」時の強力
|
||||||
1. **Agent対応の判断**: `d` でダンプを見れば、モデルが正しく「ツール呼び出し(JSON)」を行っているか一目瞭然です。
|
1. **Agent対応の判断**: `d` でダンプを見れば、モデルが正しく「ツール呼び出し(JSON)」を行っているか一目瞭然です。
|
||||||
2. **30bモデルの運用**: RTX 3060 で 30b クラスを動かす際、`s` と `l` の分析により「あ、これはQ3量子化じゃないと無理だ」といった戦略的な判断が可能になります。
|
2. **30bモデルの運用**: RTX 3060 で 30b クラスを動かす際、`s` と `l` の分析により「あ、これはQ3量子化じゃないと無理だ」といった戦略的な判断が可能になります。
|
||||||
3. **長考への耐性**: 600秒のタイムアウト設定により、巨大モデルの深い思考(Reasoning)を途中で遮断しません。
|
3. **長考への耐性**: 600秒のタイムアウト設定により、巨大モデルの深い思考(Reasoning)を途中で遮断しません。
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
sudo apt install -y python3-pip
|
||||||
|
pip install httpx uvicorn fastapi --break-system-packages
|
||||||
|
|
|
||||||
94
oproxy.py
94
oproxy.py
|
|
@ -1,5 +1,5 @@
|
||||||
# バージョン情報: Python 3.12+ / FastAPI 0.115.0 / uvicorn 0.30.0 / httpx 0.28.0
|
# バージョン情報: Python 3.12+ / FastAPI 0.115.0 / uvicorn 0.30.0 / httpx 0.28.0
|
||||||
# [2026-02-10] 3060(12GB)戦術支援:全機能完全統合・非短縮版
|
# [2026-02-14] 3060(12GB)戦術支援:Mattermost連携・文字化け完全対策・全ロジック非短縮版
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
@ -35,6 +35,11 @@ CONFIG = {
|
||||||
"models_cache": [],
|
"models_cache": [],
|
||||||
"loop": None,
|
"loop": None,
|
||||||
"vram_total": 12.0,
|
"vram_total": 12.0,
|
||||||
|
# --- Mattermost 連携設定 ---
|
||||||
|
"webhook_url_secret": "https://mm.ka.sugeee.com/hooks/ctjisw6ugjg85nhddo9ox9zt4c",
|
||||||
|
"webhook_url": "https://mm.ka.sugeee.com/hooks/7d4793kdufyad8afi9x9wof13r",
|
||||||
|
"enable_webhook": True,
|
||||||
|
"max_log_len": 4000
|
||||||
}
|
}
|
||||||
|
|
||||||
# 高速抽出用正規表現
|
# 高速抽出用正規表現
|
||||||
|
|
@ -49,7 +54,44 @@ def pulse(char, color=C_RESET):
|
||||||
print(f"{color}{char}{C_RESET}", end="", flush=True)
|
print(f"{color}{char}{C_RESET}", end="", flush=True)
|
||||||
|
|
||||||
|
|
||||||
# --- 🧠 戦略的モデル分析 (2行分割レイアウト) ---
|
# --- 🛰️ Mattermost 送信エンジン (文字化け対策済み) ---
|
||||||
|
async def post_to_mattermost(model_name, path, req_body, resp_text):
|
||||||
|
"""通信完了後にバックグラウンドでMattermostへログを飛ばす。生日本語を保持する。"""
|
||||||
|
if not CONFIG["webhook_url"] or not CONFIG["enable_webhook"]:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# リクエストのデコード(生日本語を維持)
|
||||||
|
try:
|
||||||
|
req_obj = json.loads(req_body)
|
||||||
|
req_clean = json.dumps(req_obj, indent=2, ensure_ascii=False)
|
||||||
|
except:
|
||||||
|
req_clean = req_body.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
# レスポンスのカットオフ
|
||||||
|
resp_display = (
|
||||||
|
resp_text if len(resp_text) < CONFIG["max_log_len"]
|
||||||
|
else resp_text[:CONFIG["max_log_len"]] + "\n\n*(長文のため中略...)*"
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"username": f"oproxy:{model_name}",
|
||||||
|
"icon_url": "https://ollama.com/public/ollama.png",
|
||||||
|
"text": (
|
||||||
|
f"### 🛡️ LLM Traffic Log: `{path}`\n"
|
||||||
|
f"- **Model:** `{model_name}` | **Time:** {datetime.now().strftime('%H:%M:%S')}\n"
|
||||||
|
f"#### 📥 Request preview\n```json\n{req_clean[:800]}\n```\n"
|
||||||
|
f"#### 📤 Response\n{resp_display}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
# ensure_ascii=False で Mattermost に生の日本語を届ける
|
||||||
|
encoded_payload = json.dumps(payload, ensure_ascii=False).encode('utf-8')
|
||||||
|
await client.post(CONFIG["webhook_url"], content=encoded_payload, headers={"Content-Type": "application/json"})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n{C_RED}[Webhook Fail] {e}{C_RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 🧠 戦略的モデル分析 (2行分割レイアウト・非短縮版) ---
|
||||||
async def fetch_detailed_models():
|
async def fetch_detailed_models():
|
||||||
"""Ollamaの内部情報を深掘りし、メタデータを完全取得する"""
|
"""Ollamaの内部情報を深掘りし、メタデータを完全取得する"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -192,10 +234,17 @@ async def sticky_proxy(path: str, request: Request):
|
||||||
|
|
||||||
print(f"{get_ts()} {C_WHITE}/{path: <10}{C_RESET} ", end="", flush=True)
|
print(f"{get_ts()} {C_WHITE}/{path: <10}{C_RESET} ", end="", flush=True)
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
|
|
||||||
|
# 送信時にどのモデルか判別するための抽出
|
||||||
|
model_name = "unknown"
|
||||||
|
try:
|
||||||
|
model_name = json.loads(body).get("model", "unknown")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if do_dump:
|
if do_dump:
|
||||||
print(
|
print(
|
||||||
f"\n{C_YELLOW}{'=' * 60}\n[DUMP REQUEST: {request.method} /{path}]\n{'=' * 60}{C_RESET}"
|
f"\n{C_YELLOW}{'=' * 60}\n[DUMP REQUEST: {model_name}]\n{'=' * 60}{C_RESET}"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
print(json.dumps(json.loads(body), indent=2, ensure_ascii=False))
|
print(json.dumps(json.loads(body), indent=2, ensure_ascii=False))
|
||||||
|
|
@ -223,26 +272,33 @@ async def sticky_proxy(path: str, request: Request):
|
||||||
headers=headers,
|
headers=headers,
|
||||||
) as response:
|
) as response:
|
||||||
pulse("v", C_GREEN)
|
pulse("v", C_GREEN)
|
||||||
|
|
||||||
|
full_text_buffer = []
|
||||||
async for chunk in response.aiter_bytes():
|
async for chunk in response.aiter_bytes():
|
||||||
if do_dump:
|
# チャンクから正規表現で高速テキスト抽出
|
||||||
# チャンクから正規表現で高速テキスト抽出(リアルタイム表示)
|
raw_data = chunk.decode(errors="ignore")
|
||||||
raw_data = chunk.decode(errors="ignore")
|
matches = RE_CONTENT.findall(raw_data)
|
||||||
matches = RE_CONTENT.findall(raw_data)
|
for m in matches:
|
||||||
for m in matches:
|
try:
|
||||||
try:
|
# 【最重要】Unicodeエスケープを戻し、化けないようにUTF-8で再構成
|
||||||
text = m.encode().decode(
|
text = m.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf-8')
|
||||||
"unicode_escape", errors="ignore"
|
full_text_buffer.append(text)
|
||||||
)
|
if do_dump:
|
||||||
print(
|
print(f"{C_WHITE}{text}{C_RESET}", end="", flush=True)
|
||||||
f"{C_WHITE}{text}{C_RESET}", end="", flush=True
|
except:
|
||||||
)
|
# 万が一デコードに失敗した場合はマッチした文字列をそのまま保持
|
||||||
except:
|
full_text_buffer.append(m)
|
||||||
pass
|
|
||||||
|
|
||||||
pulse("v", C_GREEN)
|
pulse("v", C_GREEN)
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
|
# 通信完了後に非同期タスクとしてWebhook送信
|
||||||
|
if full_text_buffer:
|
||||||
|
combined_text = "".join(full_text_buffer)
|
||||||
|
asyncio.create_task(post_to_mattermost(
|
||||||
|
model_name, path, body, combined_text
|
||||||
|
))
|
||||||
|
|
||||||
if do_dump:
|
if do_dump:
|
||||||
print(f"\n{C_GREEN}{'=' * 60}{C_RESET}")
|
print(f"\n{C_GREEN}{'=' * 60}{C_RESET}")
|
||||||
pulse("*", C_YELLOW)
|
pulse("*", C_YELLOW)
|
||||||
|
|
@ -300,4 +356,4 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# メインスレッドでUvicorn、サブスレッドで入力待ち
|
# メインスレッドでUvicorn、サブスレッドで入力待ち
|
||||||
threading.Thread(target=input_handler, daemon=True).start()
|
threading.Thread(target=input_handler, daemon=True).start()
|
||||||
uvicorn.run(app, host="127.0.0.1", port=args.local, log_level="error")
|
uvicorn.run(app, host="127.0.0.1", port=args.local, log_level="error")
|
||||||
Loading…
Reference in a new issue