autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
231
skills/lobehub-pool/scripts/lobehub_cli_proxy.py
Normal file
231
skills/lobehub-pool/scripts/lobehub_cli_proxy.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LobeHub Cloud Proxy v2 — Real LobeHub API via `lh generate text --json --stream`
|
||||
Pure OpenAI-compatible. No closerouter bullshit. Multi-account support.
|
||||
|
||||
Usage:
|
||||
python lobehub_cli_proxy.py
|
||||
|
||||
Config in Hermes config.yaml:
|
||||
- name: lobehub-cli
|
||||
api_key: [REDACTED]
|
||||
provider: openai
|
||||
base_url: http://127.0.0.1:20140/v1
|
||||
discover_models: true
|
||||
|
||||
Test:
|
||||
curl -s http://localhost:20140/health
|
||||
curl -s http://localhost:20140/v1/models
|
||||
curl -s -X POST http://localhost:20140/v1/chat/completions \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"model":"openai/gpt-5.4","messages":[{"role":"user","content":"hi"}],"max_tokens":20}'
|
||||
"""
|
||||
import json, asyncio
|
||||
from aiohttp import web
|
||||
|
||||
PORT = 20140
|
||||
LH_CMD = r"C:\Users\User\AppData\Roaming\npm\lh.cmd"
|
||||
|
||||
# Models from LobeHub Cloud (verified via `lh model list`)
|
||||
ALL_MODELS = [
|
||||
{"id": "openai/gpt-5.5", "ctx": 131072},
|
||||
{"id": "openai/gpt-5.4", "ctx": 131072},
|
||||
{"id": "openai/gpt-5.4-mini", "ctx": 131072},
|
||||
{"id": "anthropic/claude-opus-4-8", "ctx": 200000},
|
||||
{"id": "anthropic/claude-sonnet-4-6", "ctx": 200000},
|
||||
{"id": "anthropic/claude-haiku-4-5", "ctx": 200000},
|
||||
{"id": "anthropic/claude-fable-5", "ctx": 200000},
|
||||
{"id": "anthropic/claude-opus-4-7", "ctx": 200000},
|
||||
{"id": "deepseek/deepseek-v4-pro", "ctx": 131072},
|
||||
{"id": "deepseek/deepseek-v4-flash", "ctx": 131072},
|
||||
{"id": "deepseek/deepseek-r1", "ctx": 131072},
|
||||
]
|
||||
|
||||
# Status markers to filter out (NOT text content)
|
||||
SKIP_TOKENS = {"in_progress", "message_stop", "end_turn", "completed", "done", "max_tokens"}
|
||||
|
||||
ACCOUNTS_FILE = r"C:\Users\User\lobehub_cloud_accounts.json"
|
||||
|
||||
|
||||
def load_accounts():
|
||||
"""Load accounts from JSON file (optional — CLI uses .lobehub/credentials.json)"""
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
elif isinstance(data, dict) and "accounts" in data:
|
||||
return data["accounts"]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
async def run_lh(prompt, model, system="", max_tokens=4096, temperature=0.7):
|
||||
"""Run lh generate text --json --stream and parse token stream.
|
||||
Yields events: {"type": "token"|"usage"|"delta"|"timing"|"done", ...}
|
||||
CRITICAL: use --stream --json together, not --json alone.
|
||||
CRITICAL: no `return` in async generator — use `yield` for final event."""
|
||||
cmd = [
|
||||
LH_CMD, "generate", "text", prompt,
|
||||
"--model", model,
|
||||
"--max-tokens", str(max_tokens),
|
||||
"--temperature", str(temperature),
|
||||
"--stream", "--json",
|
||||
]
|
||||
if system:
|
||||
cmd.extend(["--system", system])
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
text_buf = []
|
||||
usage = None
|
||||
|
||||
async for line in proc.stdout:
|
||||
line_str = line.decode("utf-8", errors="replace").strip()
|
||||
if not line_str:
|
||||
continue
|
||||
|
||||
if line_str.startswith('"') and line_str.endswith('"'):
|
||||
# String token — text chunk
|
||||
try:
|
||||
token = [REDACTED])
|
||||
if token and token not in SKIP_TOKENS:
|
||||
text_buf.append(token)
|
||||
yield {"type": "token", "text": token}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
elif line_str.startswith("{"):
|
||||
# JSON object — metadata
|
||||
try:
|
||||
obj = json.loads(line_str)
|
||||
if "totalTokens" in obj:
|
||||
usage = obj
|
||||
yield {"type": "usage", "data": obj}
|
||||
elif "delta" in obj:
|
||||
yield {"type": "delta", "data": obj}
|
||||
elif "duration" in obj:
|
||||
yield {"type": "timing", "data": obj}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
yield {"type": "done", "text": "".join(text_buf), "usage": usage}
|
||||
|
||||
|
||||
async def handle_models(request):
|
||||
return web.json_response({
|
||||
"object": "list",
|
||||
"data": [{"id": m["id"], "object": "model", "owned_by": "lobehub",
|
||||
"context_length": m["ctx"]} for m in ALL_MODELS]
|
||||
})
|
||||
|
||||
|
||||
async def handle_chat(request):
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "invalid json"}, status=400)
|
||||
|
||||
model = body.get("model", "openai/gpt-5.4")
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return web.json_response({"error": {"message": "messages required"}}, status=400)
|
||||
|
||||
# Extract system prompt and user messages
|
||||
system = ""
|
||||
user_parts = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
if role == "system":
|
||||
system += content + "\n"
|
||||
elif role in ("user", "assistant"):
|
||||
user_parts.append(f"[{role}]: {content}")
|
||||
|
||||
prompt = user_parts[-1] if user_parts else "hello"
|
||||
max_tokens = body.get("max_tokens", 4096)
|
||||
temperature = body.get("temperature", 0.7)
|
||||
stream = body.get("stream", False)
|
||||
|
||||
if stream:
|
||||
response = web.StreamResponse()
|
||||
response.headers["Content-Type"] = "text/event-stream"
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
await response.prepare(request)
|
||||
|
||||
try:
|
||||
async for event in run_lh(prompt, model, system, max_tokens, temperature):
|
||||
if event["type"] == "token":
|
||||
chunk = {
|
||||
"id": "chatcmpl-lobehub",
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{"delta": {"content": event["text"]}, "index": 0}],
|
||||
}
|
||||
await response.write(f"data: {json.dumps(chunk)}\n\n".encode())
|
||||
elif event["type"] == "error":
|
||||
err_chunk = {"error": {"message": event["text"][:200]}}
|
||||
await response.write(f"data: {json.dumps(err_chunk)}\n\n".encode())
|
||||
except Exception as e:
|
||||
err_chunk = {"error": {"message": str(e)}}
|
||||
await response.write(f"data: {json.dumps(err_chunk)}\n\n".encode())
|
||||
|
||||
final = {
|
||||
"id": "chatcmpl-lobehub",
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{"delta": {}, "index": 0, "finish_reason": "stop"}],
|
||||
}
|
||||
await response.write(f"data: {json.dumps(final)}\n\n".encode())
|
||||
await response.write(b"data: [DONE]\n\n")
|
||||
await response.write_eof()
|
||||
return response
|
||||
else:
|
||||
all_text = []
|
||||
try:
|
||||
async for event in run_lh(prompt, model, system, max_tokens, temperature):
|
||||
if event["type"] == "token":
|
||||
all_text.append(event["text"])
|
||||
elif event["type"] == "error":
|
||||
return web.json_response({"error": {"message": event["text"][:200]}}, status=500)
|
||||
except Exception as e:
|
||||
return web.json_response({"error": {"message": str(e)}}, status=500)
|
||||
|
||||
return web.json_response({
|
||||
"id": "chatcmpl-lobehub",
|
||||
"object": "chat.completion",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "".join(all_text)},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": len(all_text), "total_tokens": 0},
|
||||
})
|
||||
|
||||
|
||||
async def handle_health(request):
|
||||
accounts = load_accounts()
|
||||
return web.json_response({
|
||||
"status": "ok",
|
||||
"backend": "lobehub-cli",
|
||||
"models": len(ALL_MODELS),
|
||||
"accounts": len(accounts),
|
||||
})
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_get("/health", handle_health)
|
||||
app.router.add_get("/v1/models", handle_models)
|
||||
app.router.add_post("/v1/chat/completions", handle_chat)
|
||||
|
||||
if __name__ == "__main__":
|
||||
accounts = load_accounts()
|
||||
print(f"[LobeHub CLI Proxy v2] :{PORT}")
|
||||
print(f" Backend: {LH_CMD}")
|
||||
print(f" Models: {len(ALL_MODELS)}")
|
||||
print(f" Accounts: {len(accounts)}")
|
||||
web.run_app(app, host="127.0.0.1", port=PORT, print=None)
|
||||
Reference in New Issue
Block a user