145 lines
5.1 KiB
Markdown
145 lines
5.1 KiB
Markdown
# 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/lobehub` monorepo
|
|
- 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) or `Oidc-Auth` (JWT)
|
|
- Credentials stored: `~/.lobehub/credentials.json` (encrypted blob)
|
|
- Settings: `~/.lobehub/settings.json`
|
|
|
|
## Key Commands
|
|
|
|
```bash
|
|
# 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:
|
|
1. A string token (text chunk): `"Hello"` → `json.loads()` gives the text
|
|
2. 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:
|
|
```python
|
|
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`:
|
|
```python
|
|
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`:
|
|
```python
|
|
async def run_lh(...):
|
|
# ... process tokens ...
|
|
yield {"type": "done", "text": "".join(text_buf), "usage": usage}
|
|
# NO return statement
|
|
```
|