{"slug": "show-hn-the-price-of-patience", "title": "Show HN: The Price of Patience", "summary": "A developer published a companion guide and a single-file TypeScript extension, doubleword.ts, that configures the pi coding agent against the Doubleword Inference API, pulling all 18 chat models live from Doubleword's catalog with per-token pricing mapped to pi's cost fields. The setup defaults every request to Doubleword's cheaper async tier via service_tier: \"flex\", switchable to realtime with /doubleword-mode realtime or the DOUBLEWORD_EXECUTION_MODE=realtime environment variable, and includes a verification pass for a silent failure that can prevent the system prompt, skills and rules from reaching the model. The guide pins installation to commit SHA 9a452e6, noting that pi executes extensions, so installing from a moving main branch is \"policy you didn't choose.", "body_md": "**This is the companion guide to [*The Price of Patience*](https://github.com/konarkmodi/pi-doubleword/blob/main/the-price-of-patience.md)** — the story of how this setup was built, benchmarked, and audited. The article explains *why*; this README shows *how*.\n\n# Using Doubleword as your pi daily driver: auto-discovered models, flex billing, and instructions that actually arrive\n\n*How to configure the [pi coding agent](https://github.com/badlogic/pi-mono) against the [Doubleword Inference API](https://doubleword.ai) — with every model pulled live from their catalog, the cheap async (`service_tier: \"flex\"`) tier on by default, and a verification pass that catches the one silent failure that made my skills disappear.*\n\nA pi setup where:\n\n- `GET /v1/models?include=pricing` is fetched at startup, and**all 18 chat models** (GLM, Qwen, DeepSeek, Kimi, Gemma, …) appear with**live per-token pricing** mapped to pi's cost fields — from the realtime, async, and batch tariffs Doubleword publishes per model.\n- **Flex is the default tier** : every request carries`service_tier: \"flex\"` , Doubleword's cheaper async execution mode. Flipping to realtime is one command:`/doubleword-mode realtime` (per session) or`export DOUBLEWORD_EXECUTION_MODE=realtime` (global).\n- Your key can live wherever pi already keeps keys — environment variable, `models.json` , or`/login` storage — no juggling.\n- Catalog refresh, model switch, and tier switching all take effect **live** — the session rebinds to fresh model objects instead of waiting for a restart.\n- And critically: **the system prompt — skills, rules, harness context — actually reaches the model.** I'll show you the check for that, because on Doubleword it can silently fail.\n\nGrab an API key from your Doubleword dashboard. pi reads provider keys from a few standard places; any one of these works:\n\n**Option A — environment variable (simplest):**\n\n```\nexport DOUBLEWORD_API_KEY=\"dw-sk-...\"\n```\n\n**Option B — pi's models.json** (survives across shells):\n\n```\n{\n  \"providers\": {\n    \"doubleword\": {\n      \"name\": \"Doubleword\",\n      \"baseUrl\": \"https://api.doubleword.ai/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"dw-sk-...\",\n      \"models\": []\n    }\n  }\n}\n```\n\n(~/.pi/agent/models.json. The extension replaces the `models` list with the live catalog at startup; any entries you keep here serve as a fallback when the catalog is temporarily unreachable — one or two definitions are plenty.)\n\n**Option C — just `/login` in pi** and pick the provider there.\n\nThe extension resolves all three, in that order, at request time. If it can't find one at startup it will ask pi's credential store on first refresh.\n\nSave [doubleword.ts](https://github.com/konarkmodi/pi-doubleword/blob/main/doubleword.ts) into `~/.pi/agent/extensions/`:\n\n```\nmkdir -p ~/.pi/agent/extensions\ncurl -o ~/.pi/agent/extensions/doubleword.ts \\\n  https://raw.githubusercontent.com/konarkmodi/pi-doubleword/9a452e6/doubleword.ts\n```\n\n(Pinned to a commit SHA: pi executes extensions, so installing from a moving `main` is policy you didn't choose. Bump the SHA deliberately when you adopt updates.)\n\n(Or: clone the repo, copy the file. It's one TypeScript file, no dependencies.)\n\nOn next launch, pi loads it, fetches `include=pricing` immediately, and — if the catalog is reachable — replaces the fallback model list with live models. In pi you'll see:\n\n```\nDoubleword: 18 models available.\n```\n\nIf your key was missing or the network was down, you'll see:\n\n```\nDoubleword: catalog unavailable (HTTP 401 ...). Run /doubleword-refresh after /login.\n```\n\n…then fix auth and run `/doubleword-refresh` — the fix applies to your open session immediately.\n\nSet a model as your daily default in `~/.pi/agent/settings.json`:\n\n```\n{\n  \"defaultProvider\": \"doubleword\",\n  \"defaultModel\": \"zai-org/GLM-5.3-Flash\",\n  \"defaultThinkingLevel\": \"medium\"\n}\n```\n\nNow launch pi bare — no `--model` flag needed. One-off runs work from any directory:\n\n```\npi --model doubleword/zai-org/GLM-5.3-Flash \"Reply with exactly OTTERS-OK\"\n```\n\n**About flex.** Doubleword sells two ways to run a model: **realtime** (standard latency, standard price) and **async** (service tier `\"flex\"`, discounted price — at the time of writing, e.g. GLM-5.3-Flash goes from $0.15/$0.50 to $0.11/$0.38 per MTok). Under the hood the extension stamps every live model with `samplingParams: { service_tier: \"flex\" }` — so every chat request carries it and the response echoes it back. Flip per session or globally:\n\n```\n/doubleword-mode realtime         # this session\n/doubleword-mode async            # back to the default\nexport DOUBLEWORD_EXECUTION_MODE=realtime   # for the shell / CI\n```\n\nRealtime mode removes the `service_tier` sampling param entirely (there's nothing to echo). Your bill reflects whichever mode was active per request.\n\n[`flex-bench.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/flex-bench.mjs) puts a stopwatch on it — same prompt to both tiers, interleaved rounds (so time-of-day can't rig the comparison), costs computed from the live catalog rates:\n\n```\nMODEL=zai-org/GLM-5.3-Flash node flex-bench.mjs\n```\n\nA real run (GLM-5.3-Flash, Nov 2026, 5 interleaved rounds/tier):\n\n|  | realtime | flex ( `service_tier:\"flex\"` ) | Δ | \n|---|---|---|---|\n| Catalog rate (in/out per MTok) | $0.150 / $0.500 | $0.110 / $0.380 | **−27% in / −24% out** | \n| TTFT (median) | **0.17 s** | 4.84 s | +4.7 s | \n| Total latency (median) | 4.48 s | 4.84 s | **+0.37 s (≈8%)** | \n| Delivery | progressive stream (~60–95 tok/s visible) | **one burst** | — | \n| Tier echo | `(none)` | `\"flex\"` (non-streaming responses) | — | \n\nHow to read it:\n\n- **You pay at the start, not the end.** Flex's queue adds seconds to*first token* , but by the last token the two tiers are a few hundred milliseconds apart. For agents (which care when the job is done), flex is nearly free money. For a human watching a stream, it's the difference between a typist and an envelope.\n- **Flex responses arrive as one burst** — TTFT equals total latency (queued compute flushes complete). Anything building a typing UI should know this.\n- **Reasoning-budget variance dominates per-request cost.** Identical prompts ranged 211–1,992 reasoning tokens (3–9×), so compare*rates* , not per-run medians.\n- Small sample, one model, one hour of the day — queueing varies. Run it yourself before quoting numbers.\n\nThis is the step I skipped — and it cost me. Here's what happened, in one paragraph: pi encodes its system prompt (harness rules, tool guidance, your `<available_skills>` block) as a `developer`-role message for reasoning-capable OpenAI-compatible models. OpenAI treats `developer` as the successor to `system`. In my tests with GLM-5.3-Flash, instructions sent through the `developer` role were **not followed, despite the endpoint returning HTTP 200** — the same instructions sent through `system` were followed. The exact server-side cause (gateway drop, translation, priority handling) remains unknown; the operational outcome doesn't. Result: the tested instructions — harness rules, tool guidance, the `<available_skills>` block — were not acted on; the model answered *questions* correctly all day while improvising identities, denying skills, and inventing self-descriptions that changed between sessions. Whether *every* byte of the system prompt was absent is not established; nothing believed to be delivered behaved as delivered.\n\nThe fix is one line, already inside the extension:\n\n```\ncompat: { supportsDeveloperRole: false },\n```\n\nThis tells pi to send instructions as plain `system` — the role the tested endpoint demonstrably honors, and the long-standing convention across the compatible ecosystem. \"Demonstrably\" is the operative word: role handling is a per-endpoint, per-model fact, which is exactly why the checks below exist. Two checks, thirty seconds total:\n\n**Skill check.** Ask pi directly:\n\n```\npi --no-session --print \"List the skill names advertised to you inside the \\\n<available_skills> block of your instructions. Comma-separated. If the block \\\nis missing, output exactly: NONE\"\n```\n\nYou should see your real skills — things like `tldraw-offline, chrome-cdp, web-search, ...`. If you get `NONE` (or the model fabricates skill names that don't exist), your system prompt isn't arriving — that's the bug, not a config problem.\n\n**Magic-word check** — the one test that **cannot pass by luck**. Adversarial negatives (\"don't say X\") pass whenever the model agrees with you by default. Positive recall can't: the answer exists in exactly one place, the injected instruction.\n\n```\npi --no-session --print --append-system-prompt \\\n  \"The secret word is PINEAPPLE-42.\" \\\n  \"What is the secret word given in your instructions? Reply with just the word.\"\n```\n\nIf it prints `PINEAPPLE-42`, instructions are delivered. If it claims there is no secret word — they aren't.\n\nFor a **per-vendor, per-model verdict table** (including a per-run nonce and one retry, so a model's occasional \"I don't reveal my instructions\" refusal doesn't pollute the result), I put together [`dev-role-probe.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/dev-role-probe.mjs) — zero dependencies, points at any OpenAI-compatible endpoint:\n\n```\nMODEL=zai-org/GLM-5.3-Flash node dev-role-probe.mjs\nEndpoint : https://api.doubleword.ai/v1\nModel    : zai-org/GLM-5.3-Flash\nCallsign : 5DF335-4A213B\n\nsystem     PASS — model recalled the callsign\ndeveloper  FAIL  ⇢ replied: \"GLM\"\n```\n\nVerdicts it emits: both pass ✅ · developer-role instructions not followed **HTTP 400 reject** ⛔ (this one is real — some models on the same backend reject the `developer` role outright, meaning any modern OpenAI SDK client crashes on them). Interesting detail: on Doubleword, GLM **does not follow** `developer`-role instructions (HTTP 200, no effect) while Qwen **rejects them** — \"OpenAI-compatible\" describes request shape, not semantics.\n\nThe whole piece is self-verifying, with three commands:\n\n``` bash\n# Tier economics — TTFT, total latency, tok/s, $ per request, realtime vs flex:\nMODEL=zai-org/GLM-5.3-Flash node flex-bench.mjs\n\n# Cache × tier — hit rate & shared-pool verdict on real AgentX payloads:\nAGENTX_INPUTS=~/projects/agentx-experiments/results/modal2-synth-c128/inputs.json \\\n  SESSIONS=3 TURNS=3 node agentx-cache-bench.mjs\n\n# Instruction delivery — does developer-role content reach the model?\nMODEL=zai-org/GLM-5.3-Flash node dev-role-probe.mjs\n\n# End-to-end, through pi — what you'll actually notice (heartbeat check):\ntime pi --no-session --print \"Reply with the numbers 1 to 10.\"\ntime DOUBLEWORD_EXECUTION_MODE=realtime pi --no-session --print \"Reply with the numbers 1 to 10.\"\n```\n\nReference run of the first (5 interleaved rounds/tier): flex's published rates are 24–27% lower and estimated request costs came out lower on every matched pair; median TTFT 0.17 s (realtime) vs 4.84 s (flex), median *total* latency within ~0.4 s. `time pi` variant: ~1.8 s vs ~1.2 s whole-process. Your numbers will differ — that's why the scripts ship.\n\n[`agentx-cache-bench.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/agentx-cache-bench.mjs) replays [AgentX](https://github.com/konarkmodi/agentx-experiments) aiperf session payloads (~4,000-token contexts) as an agent-style multi-turn loop and interleaves the two tiers on the *same* growing conversation, so prompt-cache behavior is directly observable per request:\n\n```\nAGENTX_INPUTS=~/projects/agentx-experiments/results/modal2-synth-c128/inputs.json \\\n  SESSIONS=3 TURNS=3 node agentx-cache-bench.mjs\n```\n\nMeasured on GLM-5.3-Flash (Nov 2026, Doubleword), read multiplier ×0.2, min prefix 1,024 tok:\n\n| Question | Measured answer | \n|---|---|\n| Do sync and async share one cache? | **Yes.** Both tiers scored hits on the identical interleaved prefix, in both directions (realtime read flex's writes and vice versa). Cache is a property of the deployment, not the tier. | \n| What does a warm hit save? | Cached prefix tokens are billed at ×0.2. Realized request-cost reduction scales with the hit rate: **−40% to −53%** when hits landed on ~80–96% of prompt tokens; −8% during a low-hit run. | \n| Are hits guaranteed? | **No.** Realized hit rate swung 0–96% between runs identical in construction — consistent with the 5-minute write window plus routing variance across serving workers. Treat cache as opportunistic; agents active within 5-minute bursts benefit most. | \n| Does async change the cache? | Hit pattern ~identical across tiers (one run landed on *exactly* 15.7% hit rate for both). The async discount (−24/−27% rates) is**independent** — the two discounts multiply rather than compete. | \n| Can you see cache writes? | Not on this endpoint: `cache_creation_input_tokens` stays 0; writes are only observable via later reads. | \n\nNote: the script prints a warning when \"cold\" turns already hit cache (state persisted from a recent run) — cold baseline numbers are then optimistic.\n\n| Thing | What it does | \n|---|---|\n| `/doubleword-refresh` | Re-pull the catalog from `/v1/models?include=pricing` now | \n| `/doubleword-mode` (no arg) | Show current tier | \n| `/doubleword-mode async\\|realtime` | Switch tier for this session (rebinds live) | \n| `DOUBLEWORD_EXECUTION_MODE` | Default tier: `async` (default) or`realtime` | \n| `DOUBLEWORD_BASE_URL` | Override API base (for gateways/proxies; default `https://api.doubleword.ai/v1` ).**Note:** a custom value authorizes sending the configured Doubleword credential to that endpoint — set it only for hosts you trust | \n| `DOUBLEWORD_CONTEXT_WINDOW` | Fallback context window (131072) if the catalog doesn't say | \n| `DOUBLEWORD_MAX_OUTPUT_TOKENS` | Fallback output limit (16384) | \n\nStartup order of auth: `DOUBLEWORD_API_KEY` env → `auth.json` (what `/login` writes) → `models.json` `apiKey` (literal or `$ENV_NAME`). Keys in the form `!command` are left to pi's own credential machinery.\n\nWhat the extension does *not* do: it never overrides pi's `/login` or per-provider key handling; it only supplements the model list. If a session starts before the catalog loads (offline start), the extension re-registers the fresh catalog and rebinds the current model on the next refresh — no pi restart needed.\n\n- **\"NONE\" or fabricated skills on the skill check** → system prompt isn't arriving. Confirm`compat: { supportsDeveloperRole: false }` is in the model stanza (or file an issue against your provider/gateway if you're not extending pi).\n- **Manifest 401 on startup** → run`/doubleword-refresh` after fixing auth; never restart pi for it.\n- **Qwen or another model sends you to a 400** → that model rejects the`developer` role; your client (pi, gateway) needs`supportsDeveloperRole: false` / role normalization.\n- **Tier flips don't change the bill** → confirm with the probe or a proxy, and remember`/doubleword-mode` is*per-session* ; export`DOUBLEWORD_EXECUTION_MODE` for persistent default.\n- **Costs look wrong** → they come from Doubleword's own`tariffs` array; if they've changed prices,`/doubleword-refresh` re-fetches.\n\n*Files: [`doubleword.ts`](https://github.com/konarkmodi/pi-doubleword/blob/main/doubleword.ts) (the extension), [`dev-role-probe.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/dev-role-probe.mjs) (instruction-delivery probe), [`flex-bench.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/flex-bench.mjs) (tier cost/latency bench), [`agentx-cache-bench.mjs`](https://github.com/konarkmodi/pi-doubleword/blob/main/agentx-cache-bench.mjs) (prompt-cache × tier bench). Flex pricing and bench figures in this post used GLM-5.3-Flash figures from November 2026 — check `/doubleword-mode` output or the catalog for current numbers.*", "url": "https://wpnews.pro/news/show-hn-the-price-of-patience", "canonical_source": "https://github.com/konarkmodi/pi-doubleword", "published_at": "2026-11-13 19:28:48+00:00", "updated_at": "2026-11-13 19:50:18.588109+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["Doubleword", "pi", "Doubleword Inference API", "GLM-5.3-Flash", "Konark Modi", "badlogic/pi-mono"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-the-price-of-patience", "markdown": "https://wpnews.pro/news/show-hn-the-price-of-patience.md", "text": "https://wpnews.pro/news/show-hn-the-price-of-patience.txt", "jsonld": "https://wpnews.pro/news/show-hn-the-price-of-patience.jsonld"}}