Skip to content

OriginRouter CLI v0.5.0

Choose a tag to compare

@chengaoyan chengaoyan released this 08 Oct 09:11
· 13 commits to main since this release

OriginRouter CLI v0.5.0

Tag: v0.5.0 · npm: @originrouter/cli@0.5.0 · Requires: Node.js 22+


Highlights

Claude model routing: five slots, one per model family

Claude Code resolves its built-in subagents by model family — an agent
declaring model: opus looks up ANTHROPIC_DEFAULT_OPUS_MODEL, model: haiku
looks up ANTHROPIC_DEFAULT_HAIKU_MODEL, and so on. The CLI previously had only
two slots (main and small), so four of the five families had no way to be
pointed anywhere except the primary model.

Claude routing is now one primary model plus four independently-settable
auxiliary slots
:

Slot Alias Environment variable
main originrouter-claude-model ANTHROPIC_MODEL
opus originrouter-claude-opus ANTHROPIC_DEFAULT_OPUS_MODEL
sonnet originrouter-claude-sonnet ANTHROPIC_DEFAULT_SONNET_MODEL
haiku originrouter-claude-haiku ANTHROPIC_DEFAULT_HAIKU_MODEL
fable originrouter-claude-fable ANTHROPIC_DEFAULT_FABLE_MODEL

Any auxiliary slot can inherit the primary model — which is the default, and
means the family follows the primary everywhere, including when you later switch
the primary's provider. Set one explicitly with a model id, or leave it alone:

originrouter route set claude \
  --provider my-provider \
  --main-model claude-opus-5-5 \
  --haiku claude-haiku-4-5

--opus, --sonnet, --haiku and --fable each accept a model id or the
literal inherit. Omitting a flag also means inherit.

All five slots must share one provider, and that is now enforced across every
slot rather than just main-versus-small.

Two environment variables are no longer injected

  • ANTHROPIC_SMALL_FAST_MODEL — replaced by the per-family defaults above.
  • CLAUDE_CODE_SUBAGENT_MODEL — removed. It collapsed every model-less
    subagent onto the primary model; per-family defaults make it unnecessary.

The old originrouter-claude-fast-model alias is gone.

Sending and stopping no longer flicker

The CLI used to report "terminal activity" on a two-second timer whenever the
terminal's rendered output changed. Interactive TUIs redraw constantly, so this
produced a steady stream of terminal.activity events — in one measured session,
tens of thousands of identical rows, which crowded real turn events out of the
App's most-recent-100 window and made the send/stop button flip between states.
The reporter has been removed; turn state now comes only from actual turn events.

Sessions are valid on every device, at every scale

New sessions are identified by claude-<32 hex> instead of
claude-<millisecond timestamp>. The old id was only unique within one machine,
and the server keys sessions with UNIQUE (session_id) and no account column —
so two devices starting in the same millisecond collided and the second user's
conversation was left with no session row at all. 122 bits of randomness
removes the collision rather than narrowing its window.

Live events encrypt once, not once per viewer

When several devices are watching the same session, updates are now sealed a
single time under a per-session group key and fanned out by the relay, instead
of being re-encrypted for every recipient. Request/response traffic stays
pairwise. Group keys live in <state-dir>/session-group-keys/ (directory mode
0700, file mode 0600).

This also fixes a scale problem: subscribing to 40 sessions took roughly six
seconds and delayed the first history page by around eighteen. Subscribers now
receive every key they are missing in a single frame and report the keys they
already hold, so a re-subscribe in the steady state sends nothing at all.


Fixed

The Local API port no longer ratchets away from the default

local-api.json records the port the daemon last bound, not a port anyone
asked to keep — but it was being treated as authoritative. On a host where 7437
was taken — a second OriginRouter user on a shared Linux box, say — the daemon
moved to 7438, wrote 7438 back, and then preferred 7438 for good. Each restart
could push it one further away from the port the App and the CLI's own commands
expect.

Port selection is now precedence-ordered and fallback-aware:

  1. an explicit --local-port / ORIGINROUTER_LOCAL_PORT (an instruction — never
    silently substituted; 0 means "kernel chooses" and outranks everything below)
  2. local-api.json marked portSource: "operator" (set via
    local api set-port — the only way to pick a port on a launchd/systemd
    service install, where the supervisor starts the daemon with no flags)
  3. the last recorded port
  4. the default, 7437
  5. the first free port above

When the daemon lands somewhere other than the port that was requested, it now
says so loudly, naming the command that pins a port — because under a service
manager its own stderr goes to logs/daemon.err.log where nobody will read it.
Directly related: originrouter daemon refuses to start a second instance beside
a live one, instead of drifting to the next free port and rewriting the endpoint
App pairing reads. Use originrouter daemon status|restart|stop for a managed
daemon.

A busy port no longer blocks startup

A port recorded in local-api.json was being read as if you had asked for it
explicitly, and an explicit port never falls back — so a port that happened to be
taken turned into a failure to start at all. A recorded port is now treated as a
preference, not a demand.

Ports are read strictly

A port value used to be parsed loosely, so "80abc" was read as 80, "80.5" as
80, and "0x10" as 0. A typo became a plausible wrong port instead of an error.
Ports now have to be plain decimal digits, and anything else is rejected with the
value quoted back.

--local-port 0 asks the kernel for a free port

--local-port 0 (or ORIGINROUTER_LOCAL_PORT=0) means "any free port, you
choose", which is what a throwaway or test home wants. It works as before, and it
now overrides the recorded and default ports rather than letting a leftover
local-api.json answer a question you explicitly left open.

Agent presence is a fact, not an inference

The cloud previously inferred whether an agent was still attached from its turn
status. A session that stopped while the relay was down could therefore linger as
"active" indefinitely. Presence is now reported explicitly
(agent.session.presence), and on every relay reconnect the daemon reconciles
what it has attached against what the catalog still believes is live,
detaching anything that is gone.

Conversations stop reordering, and stop getting stuck as "running"

  • A conversation's activity timestamp is now a high-water mark
    (MAX(last_activity_at, ?)), so a late duplicate finish event can no longer
    move it backwards and flip the card to a previous run.
  • The "latest run" join prefers a still-open run over a finished one.
  • On exit, the conversation status is synced first, before the event drain
    and bridge teardown, and is bounded by a timeout. Previously the process could
    be reaped before the sync ran, leaving a conversation stuck on "running"
    forever — visible in neither the history nor the session list.

Provider conversation ids are recorded, and no longer split the session

The provider's own conversation id used to be written into the conversation id,
producing two keys for one run (claude:<uuid> versus claude-<timestamp>) with
no column joining them. Those conversations matched no session row and appeared
in neither the App's history nor its session list. The value now travels
separately as native_session_id, and is only overwritten by a non-empty value,
so an early empty report cannot erase an id recorded by an earlier run.

Long Claude conversations page back without re-parsing

The Claude transcript is parsed once per file revision and cached by
(path, size, mtime) instead of being re-read and re-parsed on every page.

Shell completion matches the current flags

Pressing <TAB> after route set or on a provider command offered
--small-model and --small-fast-model, which this version no longer accepts, so
the suggestion could only lead to an error. Completion now lists the flags that
actually work, including the four per-family ones.


Upgrading

Upgrading is automatic; no data migration is performed and no action is required
for most users.

One behavior change to know about. If you had set claude.small to a
cheaper, faster model, the Haiku family now uses your primary model instead,
which is more expensive. There is no automatic migration — the value is dropped.
If you were relying on that, set it back explicitly:

originrouter route set claude --provider <name> --haiku <cheap-model>

The CLI warns about a leftover claude.small when you run
originrouter env print and originrouter doctor, and points at this command.
Your old setting is kept on disk, unused, so you can roll back if you need to.

The first run after upgrading restarts the routing proxy once. This is expected
and clears itself — no action needed.



OriginRouter CLI v0.5.0

标签: v0.5.0 · npm: @originrouter/cli@0.5.0 · 依赖: Node.js 22+


重点

Claude 模型路由:五个槽位,每个模型家族一个

Claude Code 是按模型家族来解析内置子智能体的 —— 声明 model: opus 的智能体去读
ANTHROPIC_DEFAULT_OPUS_MODEL,model: haiku 的去读
ANTHROPIC_DEFAULT_HAIKU_MODEL,以此类推。而 CLI 此前只有两个槽位(main 和
small),所以五个家族里有四个除了主模型之外无处可指。

现在 Claude 路由是一个主模型 + 四个可独立指定的辅助槽位:

槽位 别名 环境变量
main originrouter-claude-model ANTHROPIC_MODEL
opus originrouter-claude-opus ANTHROPIC_DEFAULT_OPUS_MODEL
sonnet originrouter-claude-sonnet ANTHROPIC_DEFAULT_SONNET_MODEL
haiku originrouter-claude-haiku ANTHROPIC_DEFAULT_HAIKU_MODEL
fable originrouter-claude-fable ANTHROPIC_DEFAULT_FABLE_MODEL

任一辅助槽位都可以继承主模型 —— 这是默认行为,意味着该家族处处跟随主模型,包括你
之后更换主模型 provider 的情况。要单独指定就用模型 id 显式设置,不管它就保持继承:

originrouter route set claude \
  --provider my-provider \
  --main-model claude-opus-5-5 \
  --haiku claude-haiku-4-5

--opus、--sonnet、--haiku、--fable 各接受一个模型 id 或字面量 inherit。不传
该参数同样等于 inherit。

五个槽位必须共用同一个 provider,而且这条约束现在覆盖所有槽位,不再只是
main 与 small 之间。

两个环境变量不再注入

  • ANTHROPIC_SMALL_FAST_MODEL —— 由上面的按家族默认值取代。
  • CLAUDE_CODE_SUBAGENT_MODEL —— 已移除。它会把所有未声明模型的子智能体都塌缩到
    主模型上;有了按家族的默认值,它已无必要。

旧的 originrouter-claude-fast-model 别名已不存在。

发送/停止按钮不再闪烁

CLI 原来每两秒检查一次终端渲染输出是否变化,一变就上报一次"终端活动"。交互式 TUI
在不断重绘,于是产生源源不断的 terminal.activity 事件 —— 在一次实测会话里产生了
数万条完全相同的记录,把真实的轮次事件挤出了 App 的"最近 100 条"窗口,并让发送/停止
按钮来回跳状态。该上报器已删除;轮次状态现在只来自真实的轮次事件。

会话在任何设备、任何规模下都有效

新会话用 claude-<32 位十六进制> 标识,不再用 claude-<毫秒时间戳>。旧 id 只在一台
机器内唯一,而服务端用 UNIQUE (session_id) 建键且不含账号列 —— 所以两台设备在同一
毫秒启动会话就会撞键,第二个用户的会话连一行 session 记录都没有。122 位随机数把
碰撞直接消除,而不是仅仅缩小它的时间窗口。

实时事件只加密一次,而不是每个订阅者各一次

当多台设备同时观看同一个会话时,更新现在用每会话的群组密钥只封装一次,由中继分发,
而不是为每个接收方各加密一遍。请求/响应流量仍走两两配对加密。群组密钥存放在
<state-dir>/session-group-keys/(目录权限 0700,文件权限 0600)。

这同时修掉一个规模问题:订阅 40 个会话原先要花约 6 秒,并把首页历史延迟约 18 秒。现在
订阅者会在一帧里收到所有缺失的密钥,并上报自己已持有的密钥,因此稳态下重新订阅什么都不发。


修复

本地 API 端口不再逐次偏离默认值

local-api.json 记录的是守护进程上次绑定的端口,而不是任何人要求保留的端口 —— 但它
被当成了权威。在一台 7437 被占用的机器上(比如共享 Linux 机器上的第二个 OriginRouter
用户),守护进程会挪到 7438,把 7438 写回去,然后从此偏好 7438。每次重启都可能把它推得
离 App 和 CLI 自身命令所期望的端口更远一步。

端口选择现在是按优先级排序且带 fallback 的:

  1. 显式的 --local-port / ORIGINROUTER_LOCAL_PORT(这是指令,绝不被静默替换;0
    表示"内核来选",且优先级高于以下全部)
  2. 标了 portSource: "operator" 的 local-api.json(由 local api set-port 写入 ——
    在 launchd/systemd 服务安装下,supervisor 启动守护进程时不带任何参数,这是唯一能
    指定端口的途径)
  3. 上次记录的端口
  4. 默认值 7437
  5. 往上第一个空闲端口

当守护进程最终落在与所请求不同的端口上时,它现在会大声说明,并给出固定端口的命令 ——
因为在服务管理器下,它自己的 stderr 会写进 logs/daemon.err.log,没人会去看。直接相关
的一点:originrouter daemon 拒绝在已有存活实例时再启动第二个,而不是悄悄飘到下一个空闲
端口并改掉 App 配对读取的地址。要管理服务安装的守护进程,请用
originrouter daemon status|restart|stop。

端口被占用不再导致无法启动

local-api.json 里记录的端口此前被当成"你显式要求的端口"来读,而显式端口从不 fallback
—— 于是一个恰好被占用的端口就变成了完全起不来。现在记录的端口只被当作偏好,而不是要求。

端口改为严格解析

端口值此前是宽松解析的,所以 "80abc" 被读成 80、"80.5" 被读成 80、"0x10" 被读成
0。一个笔误变成了"看起来很合理"的错误端口,而不是一个报错。现在端口必须是纯十进制数字,
其余一律拒绝并把原值回显出来。

--local-port 0 向内核要一个空闲端口

--local-port 0(或 ORIGINROUTER_LOCAL_PORT=0)的含义是"随便哪个空闲端口,你来选",
这正是临时环境或测试环境所需要的。它和以前一样可用,并且现在会覆盖记录端口和默认端口,
而不是让一份残留的 local-api.json 去回答一个你明确留空的问题。

Agent 在线状态是事实,而不是推断

云端此前是从轮次状态推断智能体是否还在连着。因此一个在 relay 掉线期间停止的会话可能
无限期地停留在"活跃"。现在状态是显式上报的(agent.session.presence),并且每次 relay
重连时守护进程都会把"自己已挂载的"与"目录仍认为存活的"做一次对账,把已经不存在的显式
置为已断开。

会话不再乱序,也不会卡在 "running"

  • 会话的活动时间戳现在是高水位(MAX(last_activity_at, ?)),因此一条迟到的重复完成
    事件不再能把它往回推、让卡片翻回上一次运行。
  • "最近一次运行"的连接会优先选择仍在进行中的运行,而不是已结束的。
  • 退出时,会话状态同步现在最先执行,排在事件排空和 bridge 拆除之前,并带超时上限。
    此前进程可能在同步跑起来之前就被回收,导致会话永远卡在 "running" —— 历史和会话列表
    里都看不到。

Provider 对话 id 会被记录,且不再割裂会话

Provider 自己的对话 id 原来会被写进会话 id 里,于是一次运行产生两个键
(claude:<uuid> 与 claude-<timestamp>),且没有任何列能把它们连起来。这些会话匹配
不到任何 session 行,既不出现在 App 的历史里,也不出现在会话列表里。现在该值单独以
native_session_id 传输,且只在非空时才覆盖,因此早期发出的空上报不会抹掉同一会话更早
一次运行记录下的 id。

长的 Claude 对话翻页不再重新解析

Claude 转录文件现在按文件版本解析一次,并按 (路径, 大小, mtime) 缓存,而不是每翻一页就
重新读取并重新解析整个文件。

Shell 补全与实际参数一致

在 route set 之后或 provider 命令上敲 <TAB>,此前会推荐本版本已不再接受的
--small-model 和 --small-fast-model,所以那条建议只会导向报错。现在补全会列出真正
可用的参数,包括四个按家族的参数。


升级

升级是自动的;不做数据迁移,多数用户无需任何操作。

有一个行为变化需要知道。 如果你曾把 claude.small 设为一个更便宜的快速模型,那么
Haiku 家族现在会改用你的主模型,也就是更贵。没有自动迁移 —— 该值会被丢弃。如果你
原来依赖它,请显式设回去:

originrouter route set claude --provider <name> --haiku <cheap-model>

当你运行 originrouter env print 和 originrouter doctor 时,CLI 会对遗留的
claude.small 发出警告,并指向这条命令。你原来的设置会保留在磁盘上、不再被使用,所以需要
时仍可回滚。

升级后首次运行会重启一次路由代理。这是预期行为,会自行恢复,无需任何操作。