feat: opencode zen adapter + first-run config generation, fix stats/stream bugs

- adapters/opencode.lua: opencode.ai zen free pool adapter — sends the
  opencode client User-Agent (zen fingerprints clients by UA; non-official
  clients hit FreeUsageLimitError); pairs with api_key: public
- config: no config file ships in the repo; first run generates a default
  config at the -config path with a random admin key, loopback listen and a
  keyless zen source (config.EnsureDefault); remove config.example.yaml
- lua: seed bundled adapters from the embedded FS instead of a hardcoded
  name list
- ui: widen model kind select (chat was clipped to 'cha')
- phase 5 bugfixes: stats ms/s bucket mixing, cleanScopes nil, ctx.Err
  guards, direct-path ModelAvailable, empty stream body failure,
  bestImageModel rewrite, transform failure recording, Core.mu, timer,
  effective model for tool-calls
This commit is contained in:
JianFeeeee
2026-08-13 12:25:07 +08:00
parent d06210204b
commit 2bc1d0e67a
22 changed files with 910 additions and 148 deletions

View File

@ -64,27 +64,32 @@ Extracted and independently evolved from the multi-source LLM adapter layer of
## Quick start
```bash
cp config.example.yaml config.yaml # edit your sources & keys
GOMODCACHE=... GOPROXY=off go build -tags luajit -o llmsproxy ./cmd/llmsproxy
./llmsproxy -config config.yaml
./llmsproxy -config config.yaml # first run generates a default config and prints a random admin key
```
> Depends on [golua](https://github.com/aarzilli/golua) (LuaJIT bindings).
> You **must** build with `-tags luajit`; otherwise the built-in superset
> gopher-lua path is used (behavior differs slightly).
> **No config is shipped in the repo** (config files carry real keys). On
> first run the binary generates a default config at the `-config` path: a
> random admin key (printed to the startup log), loopback-only `127.0.0.1:8080`,
> and a keyless opencode zen source ready to chat. Rotate the admin key in the
> WebUI after first login.
```bash
# no key -> 401
curl http://127.0.0.1:8080/v1/models
# one-shot
curl -H "Authorization: Bearer sk-gw-local-0001" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}' \
# one-shot ($KEY = the admin key printed at first startup)
curl -H "Authorization: Bearer $KEY" \
-d '{"model":"AUTO","messages":[{"role":"user","content":"hi"}]}' \
http://127.0.0.1:8080/v1/chat/completions
# streaming
curl -N -H "Authorization: Bearer sk-gw-local-0001" \
-d '{"model":"deepseek-v4-flash","stream":true,"messages":[{"role":"user","content":"hi"}]}' \
curl -N -H "Authorization: Bearer $KEY" \
-d '{"model":"AUTO","stream":true,"messages":[{"role":"user","content":"hi"}]}' \
http://127.0.0.1:8080/v1/chat/completions
```
@ -93,11 +98,11 @@ gateway key as the API key.
## Configuration
See [`config.example.yaml`](config.example.yaml). Core fields:
Generated on first run at the `-config` path (default `./config.yaml`). Core fields:
```yaml
listen: 127.0.0.1:8080 # bind address (keep internal/loopback)
gateway_keys: [sk-gw-0001] # initial admin key seed, migrated to the store on first start
gateway_keys: [sk-gw-<random>] # initial admin key seed (random per install), migrated to the store on first start
default_model: AUTO # when model is unroutable, follow the AUTO chain
adapter_dir: adapters # Lua adapter dir; created+seeded if missing, read-only otherwise
runtime_file: runtime.json # WebUI-edited sources/keys/AUTO chain persist here
@ -282,12 +287,19 @@ Shared helpers: `hmac_sha256_hex(key, data)`, `sha256_hex(data)`,
### Built-in adapters
`openai` `deepseek` `anthropic` `gemini` `github` `groq` `mistral` `ollama`
`kimicode`.
`kimicode` `opencode`.
`anthropic`/`gemini`/`ollama` include multimodal conversion
(`image_url` → their native format); with `disable_thinking` the `deepseek`
adapter sets `extra_body.thinking.type` to `disabled`.
**opencode** targets the opencode.ai zen free pool
(`https://opencode.ai/zen/v1`): zen fingerprints clients by User-Agent and
routes non-official UAs (curl, Go's default) into an anonymous pool that
hits `FreeUsageLimitError`. The adapter always sends the opencode client UA;
combined with `api_key: "public"` (the keyless official client actually sends
`Bearer public`) it gets the free pool, e.g. `deepseek-v4-flash-free`.
**kimicode** demonstrates `build_headers`: the cloud validates the calling
app, so you HMAC-sign timestamp+URL+body with `meta.app_secret` and add
`X-App-Sign`-style headers. Configure `sources[].meta.{app_id, app_secret,