Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/cloudflare-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ Wrangler `vars` in `wrangler.jsonc` already set:
- `SERVER_PORT=8080`
- `APP_NAME=MultiLLM Proxy`

`RUNTIME_FEATURE_FLAGS` groups nine nonsecret settings into one JSON binding to
`RUNTIME_FEATURE_FLAGS` groups seven nonsecret settings into one JSON binding to
leave room for provider secrets. Worker vars and secrets share a limit of 128
text bindings, and Cloudflare rejects an upload that exceeds it. The Container,
usage ledger, roleplay settings and roleplay session read these grouped
settings. An individual binding with the same setting name takes precedence,
including an explicit disabled value. Keep credentials in separate Worker
secrets; the grouped binding accepts only the nine names committed in
`wrangler.jsonc`. Add a new nonsecret setting to the group rather than as a
separate var.
secrets; the grouped binding accepts only the nine names listed in
`worker/runtime-feature-flags.mjs`. Add a new nonsecret setting to the group
rather than as a separate var.

Public HTTP requests receive a `308` redirect to the same HTTPS URL before any
route or container work. For Container requests, the Worker overwrites
Expand Down
59 changes: 38 additions & 21 deletions docs/roleplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ variants.

`roleplay:intelligence` mirrors the `auto:intelligence` chat route. It tries a
fixed list strictly in order across providers, without adaptive reordering:
MiMo-V2.6-Pro on NanoGPT, then on ClinePass; then GLM-5.3-Flash, GLM-5.3 and
GLM-5.2, each on NanoGPT, ClinePass (no GLM-5.2), OpenCode Go and NavyAI.
GLM-5.3, GLM-5.3-Flash and GLM-5.2, each on NanoGPT, ClinePass (no GLM-5.2),
OpenCode Go and NavyAI; then MiMo-V2.6-Pro on NanoGPT, then on ClinePass.
Pay-as-you-go gateways (OpenRouter, LinkAPI) are not in the default list.
Entries whose provider has no key are skipped, a cooling-down entry moves out
of the way, and NanoGPT keys rotate within an entry before the next one. The
Expand All @@ -162,18 +162,33 @@ usual safe-fallback statuses advance to the next entry. Set
`provider:model` entries to replace the list. ClinePass uses `CLINE_API_KEY`
and Cline's `reasoning.effort` field, up to `xhigh`.

Production sets it (`wrangler.jsonc`, 2026-10-25) to DeepSeek V4.1 Flash, GLM-5.3 and
MiMo-V2.6-Pro on OpenCode Go, then GLM-5.3 on ClinePass, then MiMo-V2.6-Pro, GLM-5.3,
GLM-5.3-Flash and GLM-5.2 on NanoGPT, then GLM-5.3-Flash and MiMo-V2.6-Pro on ClinePass.
In 24 test turns on OpenCode Go (scene continuation, conflict, out-of-character recall and
a 20k-token lorebook lookup) every model answered and kept to the card. DeepSeek V4.1 Flash
was fastest (median 5.2 s a turn), wrote nearly as well as GLM-5.3 and used about a
thirtieth of GLM's allowance per turn, so it leads. GLM-5.3 wrote the strongest scenes
(8.3 s). MiMo-V2.6-Pro was slowest (13.5 s), with the shortest replies and some
point-of-view slips. Go meters each model against its own monthly allowance. ClinePass GLM-5.3 gave its first words in about 4 s; MiMo on NanoGPT
took 59 s, and 152 s on an adult turn, so a turn that falls back to it is slow. OpenCode Go
refused every request on 2026-10-05; the key it used then has since expired. NavyAI's free plan was
refusing every request, so it is left out.
Production sets it (`wrangler.jsonc`, 2026-10-29) to GLM-5.3 on OpenCode Go, ClinePass
and NanoGPT, then DeepSeek V4.1 Flash on OpenCode Go, then GLM-5.3-Flash and GLM-5.2 on
NanoGPT and GLM-5.3-Flash on ClinePass, then MiMo-V2.6-Pro on OpenCode Go, NanoGPT and
ClinePass. GLM-5.3 is the recommended model for `roleplay:auto`.

The order follows a test on 2026-10-29: three multi-turn roleplays with planted card
rules (a heist whose characters each have a fixed rule, a secret that must stay hidden
until proof is shown, and an out-of-character time skip with a status block to update),
two runs each on OpenCode Go, every reply read and scored out of 5. GLM-5.3 averaged 4.33
with no hard rule breaks and took 21 to 41 s a reply. DeepSeek V4.1 Flash, which led the
chain from 2026-10-25, averaged 3.25: it answered in 6 to 10 s but reasoned far less
(0.7k to 3k characters against GLM-5.3's 11k to 30k), broke a character rule and came
close to confessing the secret. GLM-5.3-Flash averaged 3.92 with three hard rule breaks,
and MiMo-V2.6-Pro 2.83: among other faults it wrote a line for the user, so it is last.
Kimi K3.1 Pro scored highest (4.67) but took 72 to 143 s a reply and is not in the chain.
One reader scored two runs per scenario, so the sample is small.

The cost is OpenCode Go usage. In a test on 2026-10-25 a GLM-5.3 turn used about 30
times as much of the Go allowance as a DeepSeek V4.1 Flash turn, so leading with GLM-5.3
reaches Go's [usage limits](opencode-go.md#subscription-and-upstream-limits) much
sooner. When Go refuses GLM-5.3, the turn moves to GLM-5.3 on ClinePass and NanoGPT, then
to DeepSeek V4.1 Flash. On 2026-10-29 only OpenCode Go answered: ClinePass returned 403
`ENTITLEMENT_ERROR` for every model and NanoGPT returned 429 `model_cooldown`. Both
statuses advance to the next entry and pause that entry for the session (5 minutes after a
403, 1 minute after a 429). When it works, ClinePass GLM-5.3 gives its first words in about
4 s; MiMo on NanoGPT took 59 s, and 152 s on an adult turn, so a turn that falls back to it
is slow. NavyAI's free plan was refusing every request, so it is left out.

With `ROLEPLAY_AUTO_ROUTE=intelligence`, plain `roleplay:auto` turns (and requests
without a roleplay model) use the same chain. Turns that set `routing.mode` or
Expand All @@ -184,10 +199,12 @@ configured key. Leave the variable unset for the adaptive behaviour above.

`ROLEPLAY_HANDOFF_FROM` names chain entries that may pass a turn on, and
`ROLEPLAY_HANDOFF_TO` the entries that take it; both use the
`ROLEPLAY_INTELLIGENCE_MODELS` format and are unset by default. Production sets
both inside `RUNTIME_FEATURE_FLAGS`; a separate var of the same name takes
precedence. Production lets DeepSeek V4.1 Flash on OpenCode Go hand off to
GLM-5.3 on OpenCode Go, then on ClinePass. A source request carries one internal function, `multillm_hand_off_turn`,
`ROLEPLAY_INTELLIGENCE_MODELS` format and are unset by default. Set them inside
`RUNTIME_FEATURE_FLAGS`; a separate var of the same name takes precedence. A
source hands off only to targets that come after it in the chain. Production
let DeepSeek V4.1 Flash hand off to GLM-5.3 from 2026-10-25 to 2026-10-29, while
DeepSeek led the chain. Now that GLM-5.3 comes first, no target follows DeepSeek,
so production sets neither. A source request carries one internal function, `multillm_hand_off_turn`,
whose arguments are a reason (`deeper_reasoning` or `stronger_prose`), short notes
and an optional draft. When the source calls it instead of writing, the Worker
discards that attempt (its frames are held until visible text appears, so nothing
Expand Down Expand Up @@ -629,7 +646,7 @@ Non-secret tuning variables:
| `ROLEPLAY_PROVIDER_MODELS` | `{}` | JSON provider-specific Kimi/GLM ID or ordered fallback IDs |
| `ROLEPLAY_PROVIDER_FAMILIES` | `{}` | JSON provider-to-family allowlists; an empty list disables that provider |
| `ROLEPLAY_AUTO_ROUTE` | `adaptive` | `intelligence` sends plain `roleplay:auto` turns through the `roleplay:intelligence` chain |
| `ROLEPLAY_INTELLIGENCE_MODELS` | MiMo-V2.6-Pro, then GLM-5.3-Flash, GLM-5.3 and GLM-5.2 | Ordered `provider:model` list for `roleplay:intelligence`; providers `nanogpt`, `cline-pass`, `opencode`, `navyai`, `linkapi`, `openrouter` |
| `ROLEPLAY_INTELLIGENCE_MODELS` | GLM-5.3, then GLM-5.3-Flash and GLM-5.2, then MiMo-V2.6-Pro | Ordered `provider:model` list for `roleplay:intelligence`; providers `nanogpt`, `cline-pass`, `opencode`, `navyai`, `linkapi`, `openrouter` |
| `ROLEPLAY_PROVIDER_LIMITS` | `{}` | JSON provider/family context and output overrides |
| `ROLEPLAY_QUALITY_LATENCY_PREMIUM_PERCENT` | `20` | Largest allowed full GLM-5.3 p95 TTFB and total-latency premium over Flash |
| `ROLEPLAY_QUALITY_MIN_SAMPLES` | `3` | Successful samples required for both full and Flash before promotion |
Expand All @@ -651,8 +668,8 @@ Non-secret tuning variables:
| `ROLEPLAY_UPSTREAM_HEADER_TIMEOUT_MS` | `90000` | Maximum wait for provider response headers before advancing to the next tier |
| `ROLEPLAY_PRE_RESPONSE_FALLBACK_ENABLED` | `true` | Advance after a provider transport rejection or header timeout; client aborts never advance |
| `ROLEPLAY_PROVIDER_ERROR_FALLBACK_ENABLED` | `true` | Advance only when a 500/502/504 JSON response explicitly identifies both its error code and type as `provider_error`; unknown 5xx responses remain fail-closed |
| `ROLEPLAY_HANDOFF_FROM` | unset | Chain entries offered `multillm_hand_off_turn`; production uses `opencode:deepseek-v4.1-flash` |
| `ROLEPLAY_HANDOFF_TO` | unset | Chain entries that take a handed-off turn, in chain order; production uses GLM-5.3 on OpenCode Go and ClinePass |
| `ROLEPLAY_HANDOFF_FROM` | unset | Chain entries offered `multillm_hand_off_turn`; production sets none |
| `ROLEPLAY_HANDOFF_TO` | unset | Chain entries after the source that take a handed-off turn, in chain order; production sets none |
| `ROLEPLAY_HANDOFF_MIN_TURNS` | `2` | At most one handoff per this many turns in a session; `0` or `1` lifts the cap |
| `ROLEPLAY_EMPTY_REPLY_FALLBACK_ENABLED` | `true` | Start a reply with no visible text over on the next candidate, at most twice |
| `ROLEPLAY_REFUSAL_FALLBACK_ENABLED` | `true` | Privately regenerate an exact GLM-5.3 Flash refusal trigger with the configured Uncensored Flash candidate; rejected output is neither streamed nor stored |
Expand Down
4 changes: 2 additions & 2 deletions tests/outbound_inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -2127,7 +2127,7 @@
"worker/roleplay/config.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "5998981bc7e5e7f026da9e49ed80d8de3212023e970db84549cb9976ba37c777"
"sha256": "bf7bd2fa10ad817990ceeafd538424a22eccb45166e52d8b6a0a65c1a7aa608a"
},
"worker/roleplay/continuation.mjs": {
"kind": "non-provider",
Expand Down Expand Up @@ -2187,7 +2187,7 @@
"worker/roleplay/model-selection.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "87e782e35c4ad4ee6e5df2c0264e9cb9b8118298bc8be0f418c270385b7ee576"
"sha256": "403fd6d2b7d1e7e467d7f2227c8cdb4ba9dd38c86a2fe79e783666d4e372c425"
},
"worker/roleplay/nonstream-repair.mjs": {
"kind": "non-provider",
Expand Down
44 changes: 17 additions & 27 deletions tests/test_roleplay_config_worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -246,24 +246,24 @@ test("deployment defaults roleplay generation to high reasoning", async () => {
assert.equal(config.vars?.ROLEPLAY_DEFAULT_REASONING_EFFORT, "high");
});

test("deployment starts roleplay on OpenCode Go, then Cline Pass, and keeps NanoGPT on the subscription", async () => {
test("deployment starts roleplay on GLM 5.3 and keeps NanoGPT on the subscription", async () => {
const configUrl = new URL("../wrangler.jsonc", import.meta.url);
const config = JSON.parse(await readFile(configUrl, "utf8"));

// Measured 2026-10-25 on OpenCode Go (24 roleplay turns, every one answered): DeepSeek V4.1 Flash
// was fastest (median 5.2 s) and wrote nearly as well as GLM 5.3 for about a thirtieth of its
// allowance, so it leads; GLM 5.3 (8.3 s, the strongest scenes) is next, then MiMo-V2.6-Pro
// (13.5 s, the shortest replies). Cline Pass GLM 5.3 follows them; MiMo on NanoGPT is next.
// Scored 2026-10-29 on OpenCode Go (3 multi-turn roleplays with planted card rules, 2 runs each):
// GLM 5.3 averaged 4.33 of 5 with no hard rule breaks, DeepSeek V4.1 Flash 3.25 (it broke a
// character rule and nearly confessed a secret) and MiMo-V2.6-Pro 2.83 (it wrote the user's
// lines). GLM 5.3 leads on every provider; DeepSeek follows it, and MiMo is last.
assert.deepEqual(config.vars?.ROLEPLAY_INTELLIGENCE_MODELS.split(","), [
"opencode:deepseek-v4.1-flash",
"opencode:glm-5.3",
"opencode:mimo-v2.6-pro",
"cline-pass:cline-pass/glm-5.3",
"nanogpt:xiaomi/mimo-v2.6-pro",
"nanogpt:z-ai/glm-5.3",
"opencode:deepseek-v4.1-flash",
"nanogpt:z-ai/glm-5.3-flash",
"nanogpt:z-ai/glm-5.2",
"cline-pass:cline-pass/glm-5.3-flash",
"opencode:mimo-v2.6-pro",
"nanogpt:xiaomi/mimo-v2.6-pro",
"cline-pass:cline-pass/mimo-v2.6-pro",
]);
// The subscription-only key cannot pay for :fast, so NanoGPT requests stay on the subscription.
Expand All @@ -272,31 +272,21 @@ test("deployment starts roleplay on OpenCode Go, then Cline Pass, and keeps Nano
assert.equal(config.vars?.NANOGPT_SPEED_ROUTING, "");
});

test("deployment lets DeepSeek hand pivotal turns to GLM 5.3, at most every other turn", async () => {
test("deployment offers no handoff now that GLM 5.3 leads", async () => {
const configUrl = new URL("../wrangler.jsonc", import.meta.url);
const config = JSON.parse(await readFile(configUrl, "utf8"));

// Measured 2026-10-25: with the tool offered, DeepSeek V4.1 Flash wrote all 12 ordinary turns
// itself and handed off 4 of 6 climax and multi-faction planning turns, each with a full draft.
// Grouped in RUNTIME_FEATURE_FLAGS: the Worker is at its limit of 128 text bindings.
// A source hands off only to targets later in the chain. DeepSeek V4.1 Flash now follows every
// GLM 5.3 entry, so a DeepSeek handoff could never reach one. A separate var would take
// precedence over RUNTIME_FEATURE_FLAGS, so neither setting may exist in either place.
const flags = config.vars?.RUNTIME_FEATURE_FLAGS;
assert.equal(config.vars?.ROLEPLAY_HANDOFF_FROM, undefined);
assert.equal(config.vars?.ROLEPLAY_HANDOFF_TO, undefined);
assert.equal(flags?.ROLEPLAY_HANDOFF_FROM, "opencode:deepseek-v4.1-flash");
assert.deepEqual(flags?.ROLEPLAY_HANDOFF_TO.split(","), [
"opencode:glm-5.3",
"cline-pass:cline-pass/glm-5.3",
]);
// ROLEPLAY_HANDOFF_MIN_TURNS keeps its default of 2.
assert.equal(config.vars?.ROLEPLAY_HANDOFF_MIN_TURNS, undefined);
const chain = config.vars?.ROLEPLAY_INTELLIGENCE_MODELS.split(",");
for (const target of flags.ROLEPLAY_HANDOFF_TO.split(",")) {
assert.ok(chain.indexOf(target) > chain.indexOf(flags.ROLEPLAY_HANDOFF_FROM), target);
for (const name of ["ROLEPLAY_HANDOFF_FROM", "ROLEPLAY_HANDOFF_TO"]) {
assert.equal(config.vars?.[name], undefined, name);
assert.equal(flags?.[name], undefined, name);
}
const settings = getRoleplaySettings(config.vars);
assert.deepEqual([...settings.handoffSources], ["opencode:deepseek-v4.1-flash"]);
assert.deepEqual([...settings.handoffTargets], ["opencode:glm-5.3", "cline-pass:cline-pass/glm-5.3"]);
assert.equal(settings.handoffMinTurns, 2);
assert.equal(settings.handoffSources.size, 0);
assert.equal(settings.handoffTargets.size, 0);
});

test("deployment routes GLM through LinkAPI before NavyAI", async () => {
Expand Down
Loading
Loading