Fork philholden/partialupdate locally, remove Cloudflare AI Gateway dependency, add a generic OpenAI-compatible provider pointing at Featherless AI (GLM-5.2, 256K context) with Valkey cross-process concurrency coordination.
A Cloudflare Worker that generates its own UI on the fly. The LLM responds with HTML (not markdown), enabling SVG, CSS, JS, forms, and interactive apps inside chat. Uses a custom delimiter protocol for streaming partial updates to connected clients via WebSocket.
CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_AI_GATEWAY_ID)LLM_QUEUE_LIMIT=5, serial drain via llmDrainRunning flag (1 active LLM call per chat)cloudflare-gateway, workers-ai, gemini-direct, inception-directhttps://api.featherless.ai/v1)zai-org/GLM-5.2 (256K context, cost 4 on Featherless)_ConcurrencyLedger + cross-process Valkey ledger (Redis-compatible, port 6390)featherless:concurrency:held, featherless:concurrency:slots, featherless:concurrency:slot_costsFeatherless plans reserve concurrent inference capacity rather than billing per token. Requests have model-size-derived concurrency costs, and a request returns HTTP 429 if it would push used_cost above limit. Without coordination, three apps (Hermes, GTC, partialupdate) hitting Featherless simultaneously will collide.
partialupdate calls Featherless API directly. No awareness of Hermes/GTC usage.
Point partialupdate at http://localhost:8642/v1/chat/completions. Has Featherless concurrency guard.
hermes proxy (:8645)Credential-attaching forwarder, no agent loop, raw passthrough.
partialupdate calls Featherless directly (simple, no agent loop interference), but performs a Valkey preflight check before each LLM call. Same cross-process coordination pattern already used by Hermes and GTC, reimplemented in TypeScript for the Worker runtime.
pu:. Valkey failures fail open — partialupdate's serial queue is the backstop.
llmQueuedrainLlmQueue() picks up next item (serial, 1 at a time per chat)EVAL Lua reserve script with cost=4 (GLM-5.2), prefix pu:{doId}:{uuid}fetch() Featherless /v1/chat/completions with stream: trueparseSseTextStream() → UpdateStreamParser → WebSocket broadcastEVAL Lua release script with reservation ID (in finally block)openai-direct Provider TypeScriptsrc/env.ts — Add new env types
// Add to MODEL_PROVIDER union:
| "openai-direct"
// Add new fields to AppEnv:
OPENAI_API_KEY?: string;
OPENAI_BASE_URL?: string; // default: https://api.featherless.ai/v1
OPENAI_MODEL?: string; // default: zai-org/GLM-5.2
OPENAI_MAX_TOKENS?: number | string;
OPENAI_TEMPERATURE?: number | string;
OPENAI_MODEL_SETTINGS?: Record<unknown> | string;
src/index.ts — Add streamOpenAIDirectResponse() function
async function* streamOpenAIDirectResponse(
env: AppEnv,
messages: LlmMessage[],
): AsyncIterable<string> {
const baseUrl = env.OPENAI_BASE_URL || "https://api.featherless.ai/v1";
const model = env.OPENAI_MODEL || "zai-org/GLM-5.2";
const maxTokens = numberEnv(env.OPENAI_MAX_TOKENS, 8192);
const temperature = numberEnv(env.OPENAI_TEMPERATURE, 0.7);
const modelSettings = parseGatewayModelSettings(env.OPENAI_MODEL_SETTINGS);
const response = await fetch(`${baseUrl}/chat/completions`, {
body: JSON.stringify({
...modelSettings,
messages: gatewayChatMessagesFromMessages(messages),
model,
stream: true,
max_tokens: maxTokens,
temperature,
}),
headers: {
Authorization: `Bearer ${env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
method: "POST",
});
if (!response.ok || !response.body) {
console.warn("OpenAI direct did not return a stream", {
model, status: response.status, statusText: response.statusText,
body: await response.text().catch(() => ""),
});
yield fallbackUpdate(lastUserMessage(messages), "");
return;
}
yield* parseSseTextStream(response.body);
}
Wire into streamModelResponse() dispatch — add before the Cloudflare Gateway fallback:
if (provider === "openai-direct" && env.OPENAI_API_KEY) {
yield* streamOpenAIDirectResponse(env, messages);
return;
}
src/index.ts — Add Valkey preflight in drainLlmQueue() or streamModelResponse()
Before the fetch() call in streamOpenAIDirectResponse, add Valkey reserve. After stream completes (or errors), add Valkey release in a finally block.
The existing Hermes/GTC Valkey coordination uses atomic Lua scripts executed via EVAL. We port the same scripts to TypeScript, using the same Redis keys and same reservation ID format. This ensures all three apps coordinate through the same shared ledger.
// Cloudflare Workers don't have a native Redis client, but we can
// use fetch() to talk to Valkey via its HTTP interface, or use a
// TCP socket via connect() from "cloudflare:sockets".
//
// Option A: Valkey RESP over TCP (cloudflare:sockets)
// - Workers support outbound TCP via cloudflare:sockets
// - Implement minimal RESP protocol (INLINE commands)
// - EVAL script as a single RESP array
//
// Option B: Redis HTTP proxy (if available)
// - Some Redis setups expose an HTTP interface
// - Not standard for Valkey, would need a sidecar
//
// Recommended: Option A (TCP via cloudflare:sockets)
| App | Prefix | Example |
|---|---|---|
| Hermes | hermes: | hermes:{pid}:{tid}:{uuid12} |
| GroktoCrawl | gtc: | gtc:{pid}:{uuid12} |
| partialupdate | pu: | pu:{doId}:{uuid12} |
-- KEYS[1] = featherless:concurrency:held
-- KEYS[2] = featherless:concurrency:slots
-- KEYS[3] = featherless:concurrency:slot_costs
-- ARGV[1] = cost (4 for GLM-5.2)
-- ARGV[2] = reservation_id
-- ARGV[3] = now (unix ms)
-- ARGV[4] = stale_seconds (1800)
-- ARGV[5] = remote_limit (from Featherless /account/concurrency)
-- ARGV[6] = remote_used (from Featherless /account/concurrency)
-- 1. Garbage-collect stale reservations
local stale_cutoff = tonumber(ARGV[3]) - (tonumber(ARGV[4]) * 1000)
local stale = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', stale_cutoff)
for _, sid in ipairs(stale) do
local scost = redis.call('HGET', KEYS[3], sid)
if scost then
redis.call('DECRBY', KEYS[1], tonumber(scost))
redis.call('HDEL', KEYS[3], sid)
end
end
redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', stale_cutoff)
-- 2. Check capacity
local held = tonumber(redis.call('GET', KEYS[1]) or '0')
local limit = tonumber(ARGV[5])
local used = tonumber(ARGV[6])
local available = limit - used - held
if available < tonumber(ARGV[1]) then
return 0 -- denied
end
-- 3. Reserve
redis.call('INCRBY', KEYS[1], tonumber(ARGV[1]))
redis.call('ZADD', KEYS[2], tonumber(ARGV[3]), ARGV[2])
redis.call('HSET', KEYS[3], ARGV[2], tonumber(ARGV[1]))
return 1 -- granted
-- KEYS[1] = featherless:concurrency:held
-- KEYS[2] = featherless:concurrency:slots
-- KEYS[3] = featherless:concurrency:slot_costs
-- ARGV[1] = reservation_id
local cost = redis.call('HGET', KEYS[3], ARGV[1])
if not cost then return 0 end -- already released or unknown
redis.call('DECRBY', KEYS[1], tonumber(cost))
redis.call('ZREM', KEYS[2], ARGV[1])
redis.call('HDEL', KEYS[3], ARGV[1])
return 1
Before each reserve, partialupdate fetches GET https://api.featherless.ai/account/concurrency to get limit and used_cost. This is a lightweight call (JSON snapshot). The result is cached for 5 seconds to avoid excessive polling.
"vars": {
"MODEL_PROVIDER": "openai-direct",
"OPENAI_BASE_URL": "https://api.featherless.ai/v1",
"OPENAI_MODEL": "zai-org/GLM-5.2",
"OPENAI_MAX_TOKENS": 8192,
"OPENAI_TEMPERATURE": 0.7,
// Valkey coordination
"VALKEY_URL": "localhost:6390",
"VALKEY_CONCURRENCY_STALE_SECONDS": 1800,
// ... existing vars
}
OPENAI_API_KEY=rc_8fc960939d6331abb6a20e6b6d09178f344b1ddf942037bb0f1629d078539e3b
# openai-direct provider (Featherless, OpenRouter, local Ollama, etc.)
MODEL_PROVIDER=openai-direct
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.featherless.ai/v1
OPENAI_MODEL=zai-org/GLM-5.2
OPENAI_MAX_TOKENS=8192
OPENAI_TEMPERATURE=0.7
# Valkey cross-process concurrency coordination (optional but recommended)
# If unset, partialupdate fails open (no coordination with Hermes/GTC)
VALKEY_URL=localhost:6390
VALKEY_CONCURRENCY_STALE_SECONDS=1800
npm install in partialupdate repo.dev.vars with Featherless API keynpm run dev — Worker starts on http://localhost:8787/c/chat/debug) — verify LLM messages contain GLM-5.2 responsesdocker exec groktocrawl-valkey-1 redis-cli ping → PONGredis-cli -p 6390 GET featherless:concurrency:heldheld increments by 4 during callheld returns to previous valueSET featherless:concurrency:held 8 (if limit is 8)SET featherless:concurrency:held 0docker stop groktocrawl-valkey-1docker start groktocrawl-valkey-1redis-cli -p 6390 ZRANGE featherless:concurrency:slots 0 -1 WITHSCOREShermes:... and pu:... reservation IDs appear| Risk | Impact | Mitigation |
|---|---|---|
| Cloudflare Workers can't do raw TCP to Valkey | Valkey coordination impossible from Worker runtime | Primary: Use cloudflare:sockets API (GA since 2024, supports outbound TCP). Fallback: Run a tiny HTTP→Redis bridge sidecar (Node.js, ~50 lines) on localhost. Last resort: Skip Valkey, rely on serial queue only (acceptable for single-user local dev). |
| GLM-5.2 doesn't follow partialupdate's protocol well | Broken HTML output, missing delimiters | README says Gemini 3 Flash works best. GLM-5.2 is strong at code/HTML. Test with simple prompts first ("make a tic tac toe game"). If protocol adherence is poor, try deepseek-ai/DeepSeek-V4-Pro or moonshotai/Kimi-K2.7-Code as alternatives. |
| Featherless 429 despite Valkey coordination | LLM call fails, fallback update shown | Valkey ledger is advisory — TOCTOU race still possible if remote used_cost changes between snapshot and API call. The 5-second cache window is the risk window. Mitigation: on 429, retry once after refreshing /account/concurrency. |
| Stale reservation leak (Worker crashes between reserve and release) | Held capacity never freed, blocks other apps | Stale cleanup runs on every reserve attempt (GCs reservations older than 1800s). Same as Hermes/GTC. The TTL is the backstop. |
| Wrangler dev vs deployed Worker behavior differs | Works locally, breaks deployed | For now this is local-dev only (per README's own warning). No deployment planned. If deployed later, Valkey URL must be reachable from Cloudflare edge — would need a public Redis endpoint or Cloudflare Tunnel. |
cloudflare:socketsThe biggest unknown is whether cloudflare:sockets (the Workers TCP API) works in wrangler dev (local miniflare runtime). If it doesn't, we need a fallback:
http://localhost:6391/eval instead of raw TCP.Recommendation: Start without Valkey (Phase 1). Get the provider working. Add Valkey as Phase 2 only if collisions are observed. The user confirmed Valkey should be "low priority / strict strapped down."
openai-direct provider to env.ts + index.tsstreamModelResponse() dispatchnpm install && npm run dev