From be43a81f701c7372de727bc62a1986f4f160c255 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan Date: Sun, 25 Oct 2026 13:13:37 +0530 Subject: [PATCH] fix(roleplay): keep the handoff settings within the binding limit Cloudflare rejected the release with 129 text bindings against a limit of 128: vars and secrets share the budget, and the two handoff vars pushed it over. ROLEPLAY_HANDOFF_FROM and ROLEPLAY_HANDOFF_TO now live in RUNTIME_FEATURE_FLAGS like the other nonsecret settings, and the roleplay settings read them through runtimeFeatureValue, so a separate var of the same name still takes precedence. The var count pin returns to 65 and now says why it exists. --- docs/cloudflare-containers.md | 15 +++++++++------ docs/roleplay.md | 7 ++++--- tests/helpers/load_cloudflare_worker.mjs | 1 + tests/outbound_inventory.json | 4 ++-- tests/test_roleplay_config_worker.mjs | 16 ++++++++++++---- tests/test_runtime_feature_flags_worker.mjs | 7 +++++-- worker/roleplay/config.mjs | 5 +++-- worker/runtime-feature-flags.mjs | 2 ++ wrangler.jsonc | 4 ++-- 9 files changed, 40 insertions(+), 21 deletions(-) diff --git a/docs/cloudflare-containers.md b/docs/cloudflare-containers.md index bf455292..265c6704 100644 --- a/docs/cloudflare-containers.md +++ b/docs/cloudflare-containers.md @@ -42,12 +42,15 @@ Wrangler `vars` in `wrangler.jsonc` already set: - `SERVER_PORT=8080` - `APP_NAME=MultiLLM Proxy` -`RUNTIME_FEATURE_FLAGS` groups seven nonsecret feature settings into one JSON -binding to leave room for provider secrets. The Container, usage ledger 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 seven names committed in `wrangler.jsonc`. +`RUNTIME_FEATURE_FLAGS` groups nine 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. Public HTTP requests receive a `308` redirect to the same HTTPS URL before any route or container work. For Container requests, the Worker overwrites diff --git a/docs/roleplay.md b/docs/roleplay.md index f0370d4f..5a341ef2 100644 --- a/docs/roleplay.md +++ b/docs/roleplay.md @@ -184,9 +184,10 @@ 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 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. 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`, 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 diff --git a/tests/helpers/load_cloudflare_worker.mjs b/tests/helpers/load_cloudflare_worker.mjs index da1b35e2..82594938 100644 --- a/tests/helpers/load_cloudflare_worker.mjs +++ b/tests/helpers/load_cloudflare_worker.mjs @@ -252,6 +252,7 @@ export async function roleplayModuleUrl() { const configDataUrl = dataModuleUrl( configSource .replace('from "../client-headers.mjs";', `from "${new URL("../../worker/client-headers.mjs", import.meta.url)}";`) + .replace('from "../runtime-feature-flags.mjs";', `from "${new URL("../../worker/runtime-feature-flags.mjs", import.meta.url)}";`) .replace('from "./routing-policy.mjs";', `from "${new URL("../../worker/roleplay/routing-policy.mjs", import.meta.url)}";`) .replace( 'from "./capacity.mjs";', diff --git a/tests/outbound_inventory.json b/tests/outbound_inventory.json index 010bc018..979cfae5 100644 --- a/tests/outbound_inventory.json +++ b/tests/outbound_inventory.json @@ -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": "df8aca19da17bf36a87b3c2c9501f7c950033490288b79a866d75f1fd323c121" + "sha256": "5998981bc7e5e7f026da9e49ed80d8de3212023e970db84549cb9976ba37c777" }, "worker/roleplay/continuation.mjs": { "kind": "non-provider", @@ -2322,7 +2322,7 @@ "worker/runtime-feature-flags.mjs": { "kind": "non-provider", "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", - "sha256": "acf379482beccbaa1faae4f1b872ee5ed613f3f3abfd600e60fb5fff1a4464fd" + "sha256": "40d379212efba252e64155364a4c26c359724c37c6c0458b5afe6c4824f18724" }, "worker/saml-identity.mjs": { "kind": "non-provider", diff --git a/tests/test_roleplay_config_worker.mjs b/tests/test_roleplay_config_worker.mjs index 2ddea206..6181b6f2 100644 --- a/tests/test_roleplay_config_worker.mjs +++ b/tests/test_roleplay_config_worker.mjs @@ -278,17 +278,25 @@ test("deployment lets DeepSeek hand pivotal turns to GLM 5.3, at most every othe // 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. - assert.equal(config.vars?.ROLEPLAY_HANDOFF_FROM, "opencode:deepseek-v4.1-flash"); - assert.deepEqual(config.vars?.ROLEPLAY_HANDOFF_TO.split(","), [ + // Grouped in RUNTIME_FEATURE_FLAGS: the Worker is at its limit of 128 text bindings. + 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 config.vars.ROLEPLAY_HANDOFF_TO.split(",")) { - assert.ok(chain.indexOf(target) > chain.indexOf(config.vars.ROLEPLAY_HANDOFF_FROM), target); + for (const target of flags.ROLEPLAY_HANDOFF_TO.split(",")) { + assert.ok(chain.indexOf(target) > chain.indexOf(flags.ROLEPLAY_HANDOFF_FROM), target); } + 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); }); test("deployment routes GLM through LinkAPI before NavyAI", async () => { diff --git a/tests/test_runtime_feature_flags_worker.mjs b/tests/test_runtime_feature_flags_worker.mjs index eaabf8b6..5c05574f 100644 --- a/tests/test_runtime_feature_flags_worker.mjs +++ b/tests/test_runtime_feature_flags_worker.mjs @@ -10,8 +10,10 @@ const config = JSON.parse(await readFile(new URL("../wrangler.jsonc", import.met const { MultiLLMProxyContainer } = await loadWorkerModule(); test("deployment groups feature settings while preserving Container flags and secrets", () => { - assert.equal(Object.keys(config.vars.RUNTIME_FEATURE_FLAGS).length, 7); - assert.equal(Object.keys(config.vars).length, 67); + assert.equal(Object.keys(config.vars.RUNTIME_FEATURE_FLAGS).length, 9); + // Vars and secrets share the Worker's limit of 128 text bindings, and the release has 127. + // Put a new nonsecret setting in RUNTIME_FEATURE_FLAGS instead of raising this count. + assert.equal(Object.keys(config.vars).length, 65); const source = { ...config.vars, ANTHROPIC_API_KEY: "synthetic-provider-secret" }; const container = new MultiLLMProxyContainer({}, source); assert.equal(container.envVars.MODEL_COOLDOWN_ENABLED, "true"); @@ -24,6 +26,7 @@ test("deployment groups feature settings while preserving Container flags and se assert.equal(container.envVars.RUNTIME_FEATURE_FLAGS, undefined); assert.equal(usageBucketsEnabled(source), true); assert.equal(runtimeFeatureValue(source, "ROLEPLAY_CANDIDATE_CONTEXT_REFIT"), "window"); + assert.equal(runtimeFeatureValue(source, "ROLEPLAY_HANDOFF_FROM"), "opencode:deepseek-v4.1-flash"); }); test("individual settings retain disabled values and precedence without mutating bindings", () => { diff --git a/worker/roleplay/config.mjs b/worker/roleplay/config.mjs index 771059bb..18686506 100644 --- a/worker/roleplay/config.mjs +++ b/worker/roleplay/config.mjs @@ -4,6 +4,7 @@ import { rankQualityEligible, } from "./routing-policy.mjs"; import { clientContextHeaders, withClientDefaults, withOpencodeSession } from "../client-headers.mjs"; +import { runtimeFeatureValue } from "../runtime-feature-flags.mjs"; import { parseRoleplayProviderLimits, resolveRoleplayCandidateLimits, @@ -743,8 +744,8 @@ export function getRoleplaySettings(env) { env.ROLEPLAY_PROVIDER_LIMITS, ), intelligenceChain: parseIntelligenceChain(env.ROLEPLAY_INTELLIGENCE_MODELS), - handoffSources: parseHandoffEntries(env.ROLEPLAY_HANDOFF_FROM), - handoffTargets: parseHandoffEntries(env.ROLEPLAY_HANDOFF_TO), + handoffSources: parseHandoffEntries(runtimeFeatureValue(env, "ROLEPLAY_HANDOFF_FROM")), + handoffTargets: parseHandoffEntries(runtimeFeatureValue(env, "ROLEPLAY_HANDOFF_TO")), handoffMinTurns: boundedInteger(env.ROLEPLAY_HANDOFF_MIN_TURNS, 2, 0, 1_000), emptyReplyFallbackEnabled: booleanSetting( env.ROLEPLAY_EMPTY_REPLY_FALLBACK_ENABLED, diff --git a/worker/runtime-feature-flags.mjs b/worker/runtime-feature-flags.mjs index ed7f0844..6bac139e 100644 --- a/worker/runtime-feature-flags.mjs +++ b/worker/runtime-feature-flags.mjs @@ -5,6 +5,8 @@ const FEATURE_NAMES = Object.freeze([ "PROMPT_CACHE_AFFINITY_ENABLED", "PROMPT_CACHE_USAGE_BUCKETS_ENABLED", "ROLEPLAY_CANDIDATE_CONTEXT_REFIT", + "ROLEPLAY_HANDOFF_FROM", + "ROLEPLAY_HANDOFF_TO", "UPSTREAM_RETRY_AFTER_ADVICE_ENABLED", ]); diff --git a/wrangler.jsonc b/wrangler.jsonc index 087bbb1c..2748bc1c 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -111,8 +111,6 @@ "ROLEPLAY_PROVIDER_LIMITS": "{}", "ROLEPLAY_AUTO_ROUTE": "intelligence", "ROLEPLAY_INTELLIGENCE_MODELS": "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,nanogpt:z-ai/glm-5.3-flash,nanogpt:z-ai/glm-5.2,cline-pass:cline-pass/glm-5.3-flash,cline-pass:cline-pass/mimo-v2.6-pro", - "ROLEPLAY_HANDOFF_FROM": "opencode:deepseek-v4.1-flash", - "ROLEPLAY_HANDOFF_TO": "opencode:glm-5.3,cline-pass:cline-pass/glm-5.3", "GLM_AUTO_OPTIMIZE": "true", "GLM_AUTO_OPTIMIZE_TRIGGER_TOKENS": "8000", "GLM_AUTO_OPTIMIZE_KEEP_RECENT_TURNS": "8", @@ -166,6 +164,8 @@ "PROMPT_CACHE_AFFINITY_ENABLED": "true", "PROMPT_CACHE_USAGE_BUCKETS_ENABLED": "true", "ROLEPLAY_CANDIDATE_CONTEXT_REFIT": "window", + "ROLEPLAY_HANDOFF_FROM": "opencode:deepseek-v4.1-flash", + "ROLEPLAY_HANDOFF_TO": "opencode:glm-5.3,cline-pass:cline-pass/glm-5.3", "UPSTREAM_RETRY_AFTER_ADVICE_ENABLED": "true", "MULTILLM_STREAM_PREFLIGHT": "strict" }