5.1 KiB
LobeHub CLI Reverse Engineering (2026-07-02)
Discovery
LobeHub has a CLI (@lobehub/cli, binary lh) that provides a REAL API to LobeHub Cloud.
This disproves the prior claim that "LobeHub = UI frontend, NOT API."
Source
- GitHub:
lobehub/lobehubmonorepo - CLI code:
apps/cli/src/ - Server routers:
apps/server/src/routers/lambda/aiChat.ts(tRPC) - Constants:
apps/cli/src/constants/urls.ts
Architecture
lh CLI → app.lobehub.com (tRPC server)
→ agent-gateway.lobehub.com (WebSocket streaming)
→ Auth: .lobehub/credentials.json (JWT/OIDC)
- Server:
app.lobehub.com(tRPC protocol) - WebSocket gateway:
agent-gateway.lobehub.com - Auth headers:
X-API-Key(API key) orOidc-Auth(JWT) - Credentials stored:
~/.lobehub/credentials.json(encrypted blob) - Settings:
~/.lobehub/settings.json
Key Commands
# Auth
lh login # Browser Device Code Flow
lh whoami # Check current user
lh login --server <url> # Custom server
# Models
lh provider list # List providers (openai, anthropic, deepseek, etc.)
lh model list <providerId> # List models for provider
lh model view <id> # Model details
# Generation
lh generate text <prompt> [options] # Text generation (single completion)
lh generate image <prompt> # Image generation
lh generate video <prompt> # Video generation
lh generate tts <text> # Text-to-speech
lh generate asr <audio-file> # Speech-to-text
# Other
lh agent # Manage agents
lh kb # Knowledge bases
lh session-group # Session groups
lh thread # Message threads
lh generate text Options
-m, --model <model> Model ID (provider/model format) (default: openai/gpt-4o-mini)
-p, --provider <provider> Provider name (derived from model if omitted)
-s, --system <prompt> System prompt
--temperature <n> Temperature (0-2)
--max-tokens <n> Maximum output tokens
--stream Enable streaming (SSE, renders incrementally)
--json Output full JSON response
--pipe Pipe mode: read additional context from stdin
CRITICAL: Always use --stream --json together
Without --json: plain text output (hard to parse).
Without --stream: --json tries to parse the entire response as one JSON object —
fails on models that return reasoning tokens (DeepSeek) with:
Unexpected token 'i', "id: undefi"... is not valid JSON
With --stream --json: JSON Lines output — each line is either:
- A string token (text chunk):
"Hello"→json.loads()gives the text - A JSON object (metadata):
{"delta":{"role":"assistant"},...},{"totalTokens":...},{"duration":...}
Stream Output Format (JSON Lines)
{"delta":{"role":"assistant"},"id":"gate-...","index":0} # initial delta
"token" # text chunk (string)
"another" # more text
{"totalTokens":27,"cost":0,...} # usage
{"duration":203,"latency":2585,"tps":29.55,...} # timing
Status markers to filter out
These appear as string tokens but are NOT text content:
SKIP_TOKENS = {"in_progress", "message_stop", "end_turn", "completed", "done", "max_tokens"}
Available Models (verified 2026-07-02)
OpenAI (via LobeHub)
openai/gpt-5.5(enabled)openai/gpt-5.4(enabled)openai/gpt-5.4-mini(enabled)openai/gpt-5.5-pro,gpt-5.4-pro,gpt-5.4-nano(disabled by default)openai/o4-mini,o3-pro,o3(disabled)
Anthropic (via LobeHub)
anthropic/claude-fable-5(enabled)anthropic/claude-opus-4-8(enabled)anthropic/claude-opus-4-7(enabled)anthropic/claude-sonnet-4-6(enabled)anthropic/claude-haiku-4-5(enabled)anthropic/claude-opus-4-6(disabled)
DeepSeek (via LobeHub)
deepseek/deepseek-v4-pro(enabled)deepseek/deepseek-v4-flash(enabled)deepseek/deepseek-r1(enabled)
Other providers available
google, glmcodingplan, kimicodingplan, moonshot, bedrock, bailiancodingplan, vertexai, azure, azureai, aihubmix, openrouter, fal, ollama, ollamacloud, vllm
Windows Subprocess Pitfall
asyncio.create_subprocess_exec("lh", ...) fails with FileNotFoundError: [WinError 2].
The lh shim in PATH is a bash script; Windows needs lh.cmd.
Fix: Use full path to lh.cmd:
LH_CMD = r"C:\Users\User\AppData\Roaming\npm\lh.cmd"
proc = await asyncio.create_subprocess_exec(LH_CMD, "generate", "text", ...)
Python Async Generator Pitfall
return value inside an async def generator = SyntaxError: 'return' with value in async generator.
Fix: Use yield for final result instead of return:
async def run_lh(...):
# ... process tokens ...
yield {"type": "done", "text": "".join(text_buf), "usage": usage}
# NO return statement