Imported from sagemoyi/moagent (
extensions/custom-endpoint/AGENTS.md). Install upstream withnpx skills add sagemoyi/moagent --skill custom-endpoint. Copyright stays with the author.
Module: 自定义端点 (custom-endpoint)
Session owner: 自定义端点
Status: shipped (0.5.0+) — usable; refine matching / UX as needed.
Global rules: rootAGENTS.md.
Purpose
Let the moagent profile use custom model endpoints (relays, local OpenAI-compatible servers, Claude-compatible proxies) without hand-editing JSON.
Goals:
- Interactive add via
/login→「自定义端点」 and/custom-endpoint - Multi-protocol: Completions / Responses / Anthropic Messages
- Auto-fill context window, maxTokens, reasoning, thinkingLevelMap from models.dev (lab-first)
- Make max / xhigh thinking levels actually selectable (unlike painful OpenClaw custom-model setups)
Owns / does not own
| Owns | Does not own |
|---|---|
extensions/custom-endpoint/* |
Coding Pi ~/.pi/agent/models.json |
Writing moagent models.json + per-provider keys in auth.json |
Built-in Pi provider OAuth (Anthropic/OpenAI/…) — that is /login core + setup login step |
models.dev fetch/cache/match + thinkingLevelMap generation |
Web UI model picker (uses same profile models; coordinate with web/ only if UX changes) |
Meta-provider moagent-custom (login entry only, no inference) |
Growing install.sh with endpoint Q&A |
Primary paths (repo)
extensions/custom-endpoint/
├── index.ts # registerProvider(moagent-custom) + thin entrypoint
├── commands.ts # /custom-endpoint command dispatch + refresh orchestration
├── wizard.ts # multi-step add/update flow
├── ui.ts # AuthInteraction ↔ WizardUI adapters
├── endpoint-url.ts # URL normalize + metadata/link-local/DNS guard
├── remote-models.ts # bounded /models fetch + safe manual redirects
├── endpoint-store.ts # multi-file preflight + refresh merge operations
├── catalog.ts # 兼容重导出门面
├── catalog/client.ts # models.dev fetch/cache/timeout
├── catalog/matcher.ts # normalize/index/score + 可替换匹配数据表
├── catalog/thinking.ts # thinkingLevelMap + model config enrich
├── models-json.ts # models.json + auth.json + managed list I/O
├── paths.ts # getAgentDir()-relative paths
├── types.ts
└── AGENTS.md
examples/custom-endpoint-models.json # shape template (not live secrets)
Package: listed under package.json → pi.extensions → ./extensions (auto-discovered as subdir index.ts).
Runtime files (moagent home only, not git)
| Path | Role |
|---|---|
~/.moagent/agent/models.json |
Provider baseUrl / api / models[] metadata |
~/.moagent/agent/auth.json |
API keys (type: api_key) per real provider id |
~/.moagent/agent/moagent-custom-endpoints.json |
Managed provider id list for list/remove/refresh |
~/.moagent/agent/cache/models-dev-api.json |
models.dev api.json + models.json cache (TTL 24h) |
Model entries may include non-secret moagentModelsDevKey (e.g. moonshotai/kimi-k3) for /custom-endpoint refresh. Web 人工编辑 catalog 字段时会写 moagentManualFields[];refresh 必须跳过这些字段,同时继续刷新未人工覆盖的 metadata。
Do not commit real keys. Prefer auth.json over literal apiKey in models.json (ENV/$VAR style preserved if already present).
Commands / UX
| Entry | Behavior |
|---|---|
/login → 自定义端点 |
Meta-provider moagent-custom apiKey.login runs runAddEndpointWizard |
/custom-endpoint / status / list |
Show managed providers |
/custom-endpoint add |
Same wizard via extension UI |
/custom-endpoint remove [id] |
Drop from models.json + auth + managed list |
/custom-endpoint refresh [id] |
Re-enrich models from models.dev (lab keys / match) |
| After write | Open /model; thinking level UI shows max/xhigh when mapped |
Wizard flow (summary)
- Provider id (e.g.
na,my-proxy) - Base URL
- API protocol (default Completions; suggest Responses for api.openai.com; Anthropic for Claude-ish URLs)
- API key →
auth.json - Model ids: try
GET {base}/modelsif key present, else manual - Per model: models.dev match → preview ctx/out/reasoning/levels → write
Protocols (v1)
api |
When |
|---|---|
openai-completions |
Default — most relays / Ollama / vLLM (/v1/chat/completions) |
openai-responses |
Official GPT / endpoints that speak /v1/responses |
anthropic-messages |
Claude-compatible Messages API |
No Google in v1 (out of scope unless Moyi asks).
Default compat for Completions: supportsDeveloperRole: false, maxTokensField: "max_tokens", supportsReasoningEffort: true when any model has reasoning.
models.dev matching (lab-first)
Problem: same basename appears on many resellers with conflicting reasoning_options (overclaiming max/xhigh).
Pipeline:
- Normalize id (basename, alnum fold)
- Prefer canonical
models.json(~lab catalog) for identity / context / output / modalities - Prefer lab provider row in
api.json(deepseek,moonshotai,zhipuai, …) forreasoning_options - Family hints:
kimi*→moonshotai,glm*→zhipuai,qwen*→alibaba,mimo*→xiaomi,deepseek*→deepseek, … - Ambiguous → UI pick among top candidates
- No match (AA): ask reasoning? if yes →
reasoning: true+thinkingLevelMap.max = "max"
thinkingLevelMap vs Pi
Pi behavior (getSupportedThinkingLevels): with reasoning: true, off–high default on; xhigh / max only appear if explicitly mapped (non-null).
So lab efforts that include max / xhigh must be written into thinkingLevelMap or the UI never offers them.
Meta-provider moagent-custom
- Registered via
createProviderso it appears in/login check/resolvealways unconfigured → not a usable inference provider- Stream stubs error if mis-selected
- Login success stores a placeholder under
moagent-custom; real keys go under the user-chosen provider id
Overlap with setup login step
| Layer | Role |
|---|---|
配置向导 steps/login.ts |
Full Pi /login loop (OAuth + API key providers + this meta-provider) before memory |
| This module | Endpoint wizard + models.json enrich when user picks 自定义端点 or /custom-endpoint add |
Shared: WizardUI / AuthInteraction patterns; setup uses auth-interaction.ts + model-runtime.ts under moagent-setup; custom-endpoint has its own ui.ts for AuthInteraction from createProvider login.
Do not reimplement OAuth for Anthropic/OpenAI here — use Pi /login / setup login step.
Test checklist
PI_CODING_AGENT_DIR=~/.moagent/agent pi install .
# interactive
moagent
# /login → 自定义端点
# or: /custom-endpoint add
# /custom-endpoint list
# /model → pick provider/model; set thinking to max if mapped
# headless catalog sanity (optional): load catalog.ts via pi -e selftest
Verify:
- models.json provider has correct
api/baseUrl/contextWindow/maxTokens - reasoning models have
thinkingLevelMap.max(andxhighwhen lab lists it) - auth.json has key under provider id, not only
moagent-custom -
moagent-custom-endpoints.jsonlists the provider - Coding profile
~/.pi/agentunchanged
Decisions
- Persist as official Pi
models.json(hot-reload on/model), not a parallel runtime registry. - Keys in
auth.json, not repo. - models.dev lab rows for thinking metadata; never “longest reseller effort list wins”.
- Completions default; Responses first-class but not default.
- Meta-provider only for login discoverability.
- Unmatched + reasoning → expose
max(product choice AA). - Prefer
/v1/modelsdiscovery when key present (AA).
Refactor status (2026-08-02)
-
2026-08-03:Web 完整模型编辑器写入
moagentManualFields[]标记人工覆盖;扩展侧mergeRefreshedModels()按最新模型重读后跳过这些字段,防/custom-endpoint refresh把 context/maxTokens/reasoning/thinking/cost 等手工值改回 catalog,同时仍保留未知字段和未覆盖 metadata。 -
index.ts已收敛为薄入口;命令调度、端点网络边界、组合存储分别移入commands.ts/remote-models.ts/endpoint-store.ts。 -
/models发现改为手动、限次 redirect:每一跳重新做 URL + DNS 校验,阻断云元数据/链路本地,跨 origin 不转发 API key;响应按 1 MiB 有界读取,结构、模型数量与 id 长度均有守卫。 -
向导无 scheme 地址默认
http://(适配 Ollama/vLLM/CPA),拒绝凭据/query/fragment;models.dev 每次操作只加载一次,refresh 在 await 后重读 provider 并按 model id 合并,保留并发变化与未知高级字段。 -
managed state 文件“缺失迁移”与“已存在但为空”已区分;models/auth/managed 写前统一预检结构,新增 URL、redirect、凭据转发、托管空状态、refresh merge、损坏 auth 回归测试。
-
models.json/auth.json/ managed state 已接入extensions/lib/config-io.ts:tmp 创建即 0600、fsync+rename;损坏 JSON 先备份并拒绝静默清空;removeApiKey 不再把 auth.json 退回 0644。 -
catalog 已拆成 client / matcher / thinking 三层,旧
catalog.ts仅作兼容门面;所有外部 fetch 都有默认 10 秒超时,匹配数据表可替换,死的PI_LEVELS已删除。 -
catalog matcher / thinking-level 映射及 client 超时边界已由 root
node --test直接覆盖;hintLab/scoreMatch为纯函数测试导出。 -
ExtensionContext 的 string[] select bridge 使用共享
selectById,AuthPrompt/WizardUI 选择结果不再依赖显示 label 前缀。
Open issues / debt
- Extension
inputis not secret-masked (keys visible in slash-command path;/loginsecret type is better) -
modelRegistry.runtimeis used by setup login, not here — keep boundary clear - Fuzzy match still imperfect for heavily aliased gateway ids
- No Google / Azure-specific protocols yet
- Refresh does not re-probe remote
/modelslist (metadata only) - Optional: setup-only shortcut that only opens custom-endpoint wizard
Related modules
| Module | Relation |
|---|---|
Root AGENTS.md |
Module map entry「自定义端点」 |
moagent-setup |
login step order + full auth UX |
web |
Lists models from same moagent profile |
| Pi docs | models.md, custom-provider.md (upstream) |