{"owner":"yihong0618","repo":"bilingual_book_maker","hasSkills":true,"totalSkillsCount":3,"totalTokensCount":6157,"categories":["anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/bbm-plan/references/prompt-files.md",".agents/skills/bbm-plan/references/providers.md",".agents/skills/bbm-plan/SKILL.md"],"skills":{".agents/skills/bbm-plan/references/prompt-files.md":"# Prompt files: lint contract and git hygiene\n\nLoaded from SKILL.md's intake once a candidate `prompt*` file is found **and\nthe user has said to use it**. Do not lint before asking. The format itself,\nwith examples, is on `docs/en/prompt.md`; this file is the checklist to run\nagainst the user's file before the first paid run.\n\n## The contract (`book_maker/cli.py:parse_prompt_arg`)\n\n- `.json`: an object with **only** the keys `user` (required), `system` and\n  `style` (both optional). Any other key is rejected outright.\n- The `user` template must contain the literal placeholder `{text}`.\n  `{language}` is optional.\n- `.txt` becomes the user template as-is, same `{text}` rule.\n- `.md` is read as the PromptDown **block** form by the repo's own parser\n  (no package involved): a `## System Message`, an optional `## Style`,\n  and a `## Conversation` whose turns open with `**User:**` on its own\n  line. The `| Role | Content |` table form is refused with an error\n  naming the file and the block form. The repo's `prompt_md.prompt.md`\n  sample is a valid block-form template to copy. `## Developer Message`\n  is accepted as another name for the system section.\n- Unknown `{placeholders}` in the `user` template are refused at parse\n  time, before anything is paid; `{text}`, `{language}` and `{crlf}` are\n  the full set. A literal brace is `{{` / `}}`.\n\n```markdown\n# Translation Prompt\n\n## System Message\n\nYou are a professional translator. Keep the register of the original.\n\n## Conversation\n\n**User:**\nPlease translate the following text into {language}:\n\n{text}\n```\n\nFix or report lint problems before the paid run. The CLI would reject the\nfile at run start anyway, but a traceback after the user has already\napproved the cost is the wrong place to learn about a missing `{text}`.\n\n## Keep them out of git\n\nPrompt files are the user's personal voice and often carry character names\nor other personal terminology — same handling as `.env`. If the working directory is a\nrepo:\n\n```bash\ngit check-ignore prompt.json .env\n```\n\nAdd whatever comes back uncovered to `.git/info/exclude`. That file is\nlocal-only; never edit the project's tracked `.gitignore` for this.\n\n## Where the register goes\n\nA style instruction belongs in the `style` section (or the `system`\nmessage), stated once where a window starts — the run places it with the\nstanding instructions itself, never repeated per request. The `user`\ntemplate is sent for every unit — every word in it is paid for on every\nrequest of the book. A user-written style also replaces the model's own\nstyle notes in session handoffs.\n",".agents/skills/bbm-plan/references/providers.md":"# Routes: model name → endpoint shape → flags\n\nLoaded from `references/route-setup.md` step 3. The user pages cover the\nflags themselves: `docs/en/llm-args.md` (model, endpoint, format, keys),\n`docs/en/model_lang.md` (per-vendor addresses), `docs/en/providers.md` (the\nprovider file's fields, the image and classify models, which key is sent\nwhere). This file carries only what the agent needs beyond them: which\nshape to try, how to probe it, and what each route can do.\n\n## The one rule that decides everything\n\nThere is no model-name whitelist. A route is three flags plus the model id\nthe endpoint itself uses:\n\n```\n--api_base \"$ROOT\"  --key \"$KEY\"  [--api_format ...]  --model \"$MODEL\"\n```\n\n`--api_format` is inferred from the `--api_base` host first (`*.anthropic.com`\nmeans `anthropic`, anything else `openai`), then from the model id (`claude`\nor `anthropic` in it means `anthropic`). Pass it only to correct a wrong\nguess. Any model id reaches any endpoint; nothing has to be registered.\n\n| the endpoint speaks | flags |\n|---|---|\n| the OpenAI shape | `--api_base \"$ROOT/v1\" --model \"$MODEL\"` |\n| the anthropic shape, on an anthropic.com host | `--api_base \"$ROOT\" --model \"$MODEL\"` |\n| the anthropic shape, on a gateway domain | the same plus `--api_format anthropic` |\n| an entry in `bbm_providers.json` / `~/.bbm/providers.json` | `--provider NAME`, optionally `--model \"$MODEL\"` |\n| the OrcaRouter gateway | `--model orcarouter`, no `--api_base` |\n| nothing: a local Codex sidecar on the user's plan | `--api_format codex`, no key, no base (`references/route-setup.md` step 4) |\n\nOn the openai format `--model` may be left out; it defaults to\n`gpt-6-luna`. Every other format wants an id, and the anthropic format\nerrors without one.\n\n`codex` is the one route with no endpoint to probe. It is not a model id and\nnot a host, so none of the probes below apply to it. Spell it\n`--api_format codex`; `--model codex` is the same route under an older\nspelling.\n\nA gateway asked for the anthropic shape it does not serve answers 404, and\nthe run stops naming `--api_format openai` as the fix. `--api_base` may be\npasted with its path (`.../v1/chat/completions`); the path is trimmed.\n\nGroq, xAI, a LiteLLM proxy and every aggregator are reached through the\nOpenAI shape. The first three are also `--api_format groq` / `xai` /\n`litellm`, which is the same OpenAI route with that vendor's address filled\nin, so everything the OpenAI translator does — the structured-output probe,\ncontext, async, batch — applies to them.\n\nGemini and Qwen are the two exceptions: each has a translator of its own\nbecause neither protocol is the OpenAI shape. `--api_format gemini` talks to\nthe Gemini API (native constrained decoding, its own chat history, paced by\n`--interval`), and `--api_format qwen` talks to Qwen-MT on DashScope, whose\nrequest states a source and target language instead of carrying a prompt.\nBoth vendors *also* serve an OpenAI-compatible base, which the `openai`\nroute reaches — use that when the run needs the OpenAI translator's batch\nand session-context machinery.\n\n## `--provider NAME`: the same route, written once\n\nThe fields and the lookup order are on `docs/en/providers.md`; what matters\nfor the probe:\n\n- The `providers` wrapper is required; the loader reads nothing from a file\n  without it. A project `bbm_providers.json` wins over\n  `~/.bbm/providers.json` on a shared name; a name in neither falls back to\n  the shipped example, with a warning naming the address and key variable.\n- `api_style` is the format (`openai`, `anthropic`, `gemini`, `qwen`,\n  `groq`, `xai`, `litellm`; `claude` in older files). Any other\n  OpenAI-compatible host is `openai` with its address in `base_url`.\n- `default_models` becomes `--model` when it holds one id and\n  `--model_list` when it holds several. `env_key` is read for the key ahead\n  of `BBM_API_KEY` and the format's own variables. **Explicit flags win**,\n  so `--provider nvidia --model <id>` keeps the user's model.\n- A key is bound to its address: the entry's `env_key` is not sent when an\n  `--api_base` moves the run elsewhere. The `img_*` and `classify_*` fields\n  follow the same rule (`docs/en/providers.md#which-key-goes-where`); the\n  EPUB flow adds a `classify_model` only when the user names a classifier.\n\n## `--model orcarouter`: a gateway with no address to type\n\n`--model orcarouter` sends the run to OrcaRouter's OpenAI-shaped endpoint\nand asks for its smart-routing model, `orcarouter/auto`. It is a supported\nroute, not a legacy alias, so nothing is rewritten, and the key comes from\n`BBM_ORCAROUTER_API_KEY`.\n\n`orcarouter/auto` is the default, not a pin — to send the run to one named\nmodel instead of the smart router, use that entry:\n`--provider orcarouter --model <id>`. A probe reads the address\n(`https://api.orcarouter.ai/v1`) from there; it is never a flag you pass.\n\n## Binding `$KEY`, `$ROOT`, `$MODEL` from the entry before any probe\n\nThe entry decides which key variable to read. Do **not** take \"whichever\nkey is set\" — `.env` is sourced into a shell that may already export other\nproviders' keys from `~/.zshenv`, and a stale one would route the run to an\nendpoint the user never chose. Exit before curl when anything is missing:\nan empty bearer token produces a 401 that reads like a bad key.\n\n```bash\nroute_env() {   # $1 = provider name, as in bbm_providers.json / ~/.bbm/providers.json\n  eval \"$(python3 - \"$1\" <<'EOF'\nimport json, os, pathlib, shlex, sys\nname = sys.argv[1]; entry = None\nfor f in (pathlib.Path.home()/\".bbm\"/\"providers.json\", pathlib.Path(\"bbm_providers.json\")):\n    if f.is_file():\n        entry = json.load(open(f)).get(\"providers\", {}).get(name, entry)\nif entry is None:\n    print(f'echo \"no provider entry named {name}\" >&2; return 1'); sys.exit()\nstyle = entry.get(\"api_style\")\n# gemini and qwen speak their own protocols: the probes below would send this\n# entry's key to the wrong endpoint in the wrong shape, so refuse rather than\n# guess a shape for them.\nOPENAI_SHAPED = {\n    \"openai\": \"https://api.openai.com\",\n    \"groq\": \"https://api.groq.com/openai\",\n    \"xai\": \"https://api.x.ai\",\n    \"litellm\": \"http://localhost:4000\",\n}\nif style in (\"gemini\", \"qwen\"):\n    print(f'echo \"provider {name}: api_style {style!r} is a protocol of its own; run it with --provider {name}, these probes do not apply\" >&2; return 1'); sys.exit()\nif style not in OPENAI_SHAPED and style not in (\"anthropic\", \"claude\"):\n    print(f'echo \"provider {name}: api_style {style!r} is not a format; the loader refuses this entry\" >&2; return 1'); sys.exit()\nshape = \"anthropic\" if style in (\"anthropic\", \"claude\") else \"openai\"\ndefault_root = \"https://api.anthropic.com\" if shape == \"anthropic\" else OPENAI_SHAPED[style]\nroot = (entry.get(\"base_url\") or default_root).rstrip(\"/\")\nroot = root[:-3] if root.endswith(\"/v1\") else root\nkey_var = entry.get(\"env_key\") or \"BBM_API_KEY\"\nmodel = (entry.get(\"default_models\") or [\"\"])[0] or (\"gpt-6-luna\" if shape == \"openai\" else \"\")\nprint(f\"SHAPE={shape} ROOT={shlex.quote(root)} MODEL={shlex.quote(model)} KEY_VAR={key_var}\")\nEOF\n)\"\n  [ -n \"${SHAPE:-}\" ] || return 1\n  KEY=\"$(printenv \"$KEY_VAR\" 2>/dev/null || true)\"   # same in bash and zsh; .env is exported by set -a\n  [ -n \"$MODEL\" ] || { echo \"entry $1 names no model (the anthropic format needs one)\" >&2; return 1; }\n  [ -n \"$KEY\" ]   || { echo \"$KEY_VAR is unset — fill .env\" >&2; return 1; }\n}\n```\n\n`$ROOT` comes out with no trailing `/v1` whatever the entry wrote, so every\nprobe path below is spelled in full. On a gateway the key belongs to the\n*gateway*, not the model's vendor: a Claude model reached over the OpenAI\nshape reads the gateway entry's `env_key`, because that is the shape being\nspoken.\n\n## Shapes, and how to probe each\n\n`$ROOT` is the scheme+host with no `/v1` (`route_env` guarantees it), so\nevery path below is written out in full. Each probe is one tiny call, a\nfraction of a cent.\n\n**No token cap on the OpenAI shape.** `max_tokens: 1` looks thrifty and is a\nfalse negative twice over: gateways reject caps below their own floor\n(measured: `max_tokens must be greater than 2` on *every* model of one\ngateway), and OpenAI's own o-series/gpt-5 models reject `max_tokens`\noutright in favour of `max_completion_tokens`. A probe must test one thing.\nThe repo's own probes send no cap for exactly this reason\n(`translator/capabilities.py`, `probe_structured_output` and\n`probe_model_route`); match them. A capped probe against gpt-5.6-luna came\nback \"'max_tokens' is not supported with this model\" and confirmed nothing.\nThe reply is a few tokens of \"Hi!\".\n\n**OpenAI shape** — the universal one. Most gateways serve every model they\nhost on it, whoever made the model.\n\n```bash\ncurl -sS \"$ROOT/v1/chat/completions\" \\\n  -H \"Authorization: Bearer $KEY\" -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.choices[0]`. → `--api_base \"$ROOT/v1\" --key \"$KEY\"\n--model \"$MODEL\"`\n\n**Anthropic shape**. `max_tokens` is *mandatory* here, unlike above; 16 is\npast every floor seen so far.\n\n```bash\ncurl -sS \"$ROOT/v1/messages\" \\\n  -H \"x-api-key: $KEY\" -H 'anthropic-version: 2023-06-01' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"max_tokens\":16,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.content[0]`. → `--api_base \"$ROOT\" --key \"$KEY\"\n--model \"$MODEL\"` (add `--api_format anthropic` when the host is not\nanthropic.com). The reply's `model` field echoes the id the endpoint actually\nresolved to (`claude-haiku-4.5` → `claude-haiku-4-5-20251001`). Real Anthropic requires\n`x-api-key`; gateways commonly accept `Authorization: Bearer` too, so try\n`x-api-key` first and Bearer second.\n\n**`--api_base` for this route takes the root, not `/v1`**: the SDK appends\n`/v1/messages` itself. Passing `https://host/v1` used to produce\n`/v1/v1/messages` and a 403 reading \"HTTP node only allows access to\ninference API paths\"; a trailing `/v1` is now trimmed automatically (with a\nprinted note), so either form works — but say `https://host` and mean it.\n\n**Gemini** has a route of its own — `--api_format gemini`, over the Gemini\nAPI — which the OpenAI-shape and anthropic-shape probes here do not reach.\nTo probe from a shell, use its OpenAI-compatible base instead,\n`https://generativelanguage.googleapis.com/v1beta/openai/`, with the\nOpenAI-shape call above; that base is also the one to run against when the\ntranslation wants batching or session context. **Qwen-MT** is the same\nstory at `https://dashscope.aliyuncs.com/compatible-mode/v1`.\n\n## Inferring which shape to try first, from the model name\n\n| model name starts with | try first | then |\n|---|---|---|\n| `claude-` | OpenAI if a gateway base is set; else anthropic | the other one |\n| anything else (`gpt-`, `o1`, `o3`, `gemini-`, `grok-`, `llama`, `qwen`, …) | OpenAI | — |\n\nOpenAI first at a gateway because aggregators serve Claude and Gemini\nmodels on `/chat/completions` too. Go native only when the endpoint is\nAnthropic's own, or when the gateway rejects the OpenAI shape.\n\n**The chat call is the verdict; the listing is only a hint.** `GET\n$ROOT/v1/models` (Bearer auth) is free on OpenAI-shaped endpoints and\nreturns `{\"data\":[{\"id\":…}]}`, but gateways routinely serve a model they do\nnot list, or list it under another id. A name missing from the listing is\nno reason to give up: run the chat probe, and a reply is the answer. Fetch\nthe listing when the probe fails, to tell a typo'd id from an unsupported\npath; both return 404, and only the listing separates them. The run does\nthe same (`translator/capabilities.py`, `probe_model_route`, once at the\nfirst paid call). Some gateways add `supported_endpoint_types` per row (on\none aggregator every row read `['openai', 'anthropic']`); when it is there\nit answers the shape question outright.\n\n## Capability caveats per route\n\n| `--api_format` | translation | plan-mode classification |\n|---|---|---|\n| `openai` (any host) | schema when the probe says `strict`, else delimiter | yes |\n| `anthropic` | delimiter (no structured-output work was done for it) | yes, via the prompt rung |\n| `codex` | one turn per unit, on a thread that is itself the context window | yes, via the prompt rung — the sidecar compiles no schema |\n| `google`, `deepl`, `deeplfree`, `caiyun`, `tencent`, `customapi` | translation only | only with a classifier of its own (`--classify-model`, or an entry's `classify_model`) |\n\nClassification capability does not gate the default flow — `--plan-classify\nagent` makes no API call, you are the classifier. It matters only when the\nuser names a classifier (`references/epub-plan-mode.md`, \"A classifier the\nuser names\") and that classifier is the translating model.\n\nThe translation services have one channel, and it translates\nwhatever it is handed. They cannot be asked a question.\n\n## What the run's own probe does later\n\nAt first paid use, the OpenAI-shaped translator sends a one-key schema probe\nand grades the endpoint `strict` / `shape` / `json` / unsupported. Only\n`strict` gets a schema for *translation* — the translation schema pins the\ntarget language as a value constraint, so an endpoint that ignores values\nwould drop it. Everything else falls back to the delimiter method, which is\nfine and prints one yellow line. This is capability discovery, not an error;\ndo not report it to the user as a failure.\n",".agents/skills/bbm-plan/SKILL.md":"---\nname: bbm-plan\ndescription: Translate a book into a bilingual edition with bilingual_book_maker (\"bbm\") - an EPUB through plan mode (greedy partition, an agent-reviewed classification plan, then a full resumable run), a PDF into a bilingual, reflowable EPUB (--to-epub), or a TXT, SRT or Markdown file. Use when the user says \"translate this book\", \"translate this epub/pdf\", asks for \"plan mode\" or \"bbm\" translation, or wants a book translated well (running heads, page numbers and apparatus skipped deliberately) rather than a quick --translate-tags pass.\n---\n\n# bbm-plan: translate a book with bilingual_book_maker\n\nThis file is the router. Do the intake below, then load **one** flow file\nfor the book in hand and follow it. The other flow files stay unread.\n\nRepo: the repository this skill ships in (`make_book.py` at its root). Run\nevery command from the repo root. The user documentation is the MkDocs site\nin `docs/en/` of this checkout; the flow files point at the page to read for\nanything they do not spell out. `python make_book.py --help` is the\nauthority on every flag, and `docs/en/cmd.md` lists them all in one place.\n\n## Your stance\n\nYou are the trained CLI operator. You pick every flag from the flow file's\nrecommendations and state each choice with a one-line reason; the\nrecommended defaults are what you pass unless the user asks for something\nelse or the book argues otherwise. The user hands over a book, credentials\nand (if they have one) a prompt file, approves the plan and the cost, and\ngets a bilingual book back. They are never asked to experiment with flags,\nhalt semantics or resume mechanics.\n\n**The EPUB flow uses `--plan-classify agent`.** The plan arrives with its\nuncertain signatures set to `\"action\": null`, and the translate run\nrefuses to start while any null remains, so *you*, the coding agent, own\nthe classification against the real samples. Do it in the main agent with\nfull session context; never delegate plan editing to a subagent or a\nsmall/fast model. The one exception is the user naming a classifier\nthemselves (Jev, or a model): then follow \"A classifier the user names\" in\n`references/epub-plan-mode.md`, and still read its skips.\n\nAll state lives on disk (`bbm_providers.json`, `.env`, the flow's own\nworking files, the resume cache, `run.log`), so any step can be redone\nafter a crash or in a new session.\n\n## Intake\n\n1. **Route and credentials: always read `references/route-setup.md`**\n   before the first command. It probes what the user already has, asks one\n   question (route, model, prompt file), binds the `ROUTE` and `CONTEXT`\n   arrays every flow uses, and probes the endpoint for a sub-cent before\n   anything is paid. **Keys never go on the command line**: the handoff\n   block reprints the whole command, and argparse echoes it back on a\n   mistyped flag. If a key ever reaches the terminal, say so at once and\n   tell the user to rotate it.\n2. **Book path** and **target language** (`--language`, e.g. `zh-hans`,\n   `ja`, `Simplified Chinese`). For a small language the tables may not\n   know, pass both halves: `--language \"ain:Ainu\"` (tag before the colon,\n   name after; the tag list is `docs/en/languages.md`). `--source_lang` states\n   the source language for a book whose short lines or names could be\n   misdetected.\n3. **Their prompt file**, asked in the route question. If they hand one\n   over, lint it before the first paid run: `references/prompt-files.md`.\n   If they say no, offer one sentence of what a style instruction would buy\n   them and move on. Independently, look for an existing template in the\n   book's directory and the repo root (`prompt.json`, `prompt.txt`,\n   `prompt*.md`, `prompt_template*`). **A candidate must carry a diff**: in\n   a git repo only untracked or modified-against-HEAD files count; cleanly\n   tracked `prompt*` files are the repo's shipped examples, not the user's\n   voice. Found one? **Ask before doing anything with it**; never adopt or\n   ignore it silently.\n4. Anything they want changed from the recommended defaults: a visual style\n   for the translation, specific chapters or pages only.\n\n**Bilingual is the assumption; do not ask.** The output keeps every\noriginal next to its translation unless the user asked for a\ntranslated-only book in so many words (\"just the Chinese\", \"replace the\noriginal\", \"not bilingual\", \"single language\"). *\"Translate this book to\nChinese\"* is not that request; it is the ordinary ask, and it means a\nbilingual book. Never infer single-language output (`--single_translate`)\nfrom the target language being named, from the book being short, or from\nthe user sounding brisk. Say which one you are producing in the same line\nwhere you state the flag choices, so a user who wanted the other one can\nsay so before anything is paid for.\n\n## Branch by the book\n\n| the book is | read, and follow | the user page behind it |\n|---|---|---|\n| an `.epub` | `references/epub-plan-mode.md` | `docs/en/formats/epub.md` |\n| a `.pdf` | `references/pdf-route.md` | `docs/en/features/pdf-to-epub.md` |\n| a `.md` Datalab wrote (`{N}------` page separators), or any `.md` the user wants as an EPUB | `references/pdf-route.md` (`--to-epub` on the `.md`) | `docs/en/features/pdf-to-epub.md` |\n| a `.txt`, `.srt` or `.md` | `references/plain-formats.md` | `docs/en/formats/txt.md`, `docs/en/formats/srt.md`, `docs/en/formats/md.md` |\n\nA file with another extension is not a book this tool reads; say so. A\nfolder of chapters is several books: ask whether they want one run per\nfile.\n\n## Stating the choices\n\nBefore the first paid run, one short block to the user:\n\n- the command you will run, with one line per non-default flag saying why;\n- bilingual or translated-only, in so many words;\n- the checkpoint the flow file puts before the full run (what the user\n  will be shown, and when), and any test run you skip or add;\n- which allowance is spent: the API key's account, or the ChatGPT plan on\n  the codex route.\n\nThen wait for the approval of the cost, and run.\n\n## Context hygiene\n\n- **Never** let a translation run stream into the conversation: every paid\n  run gets `--quiet` where the format takes it *and* a log-file redirect\n  (`> run.log 2>&1`), in the background, polled with `tail -5 run.log`.\n- Everything the next step needs is on disk; nothing critical lives only in\n  conversation.\n- **Compaction threshold**, *your* context, not the run's: for a small book\n  do not compact at all. Only compact when context is genuinely pressured\n  (roughly 70% used), at most once, at the natural boundary: after the\n  flow's checkpoint, before the full run.\n\n## Reference files\n\nLoad each one only when the step that needs it runs.\n\n- **`references/route-setup.md`**: credential probe, the route question,\n  `ROUTE`/`CONTEXT` by format, the endpoint probe, the codex route, local\n  models, the route and context flag menu, route failure lines. Every flow.\n- **`references/providers.md`**: route table, probe recipes (`route_env`),\n  per-format capability caveats. Read from route-setup whenever the\n  endpoint is not a plain OpenAI-shaped host.\n- **`references/prompt-files.md`**: the `--prompt` lint checklist and\n  keeping a user's prompt out of git.\n- **`references/epub-plan-mode.md`**: plan, classify, optional smoke, full\n  run, halt/resume, deliver, failure lines. EPUB only.\n- **`references/pdf-route.md`**: the whole PDF flow. PDF only.\n- **`references/plain-formats.md`**: the default command per TXT, SRT and\n  Markdown file, deliver, failure lines.\n"},"files":{".agents/skills/bbm-plan/references/prompt-files.md":"# Prompt files: lint contract and git hygiene\n\nLoaded from SKILL.md's intake once a candidate `prompt*` file is found **and\nthe user has said to use it**. Do not lint before asking. The format itself,\nwith examples, is on `docs/en/prompt.md`; this file is the checklist to run\nagainst the user's file before the first paid run.\n\n## The contract (`book_maker/cli.py:parse_prompt_arg`)\n\n- `.json`: an object with **only** the keys `user` (required), `system` and\n  `style` (both optional). Any other key is rejected outright.\n- The `user` template must contain the literal placeholder `{text}`.\n  `{language}` is optional.\n- `.txt` becomes the user template as-is, same `{text}` rule.\n- `.md` is read as the PromptDown **block** form by the repo's own parser\n  (no package involved): a `## System Message`, an optional `## Style`,\n  and a `## Conversation` whose turns open with `**User:**` on its own\n  line. The `| Role | Content |` table form is refused with an error\n  naming the file and the block form. The repo's `prompt_md.prompt.md`\n  sample is a valid block-form template to copy. `## Developer Message`\n  is accepted as another name for the system section.\n- Unknown `{placeholders}` in the `user` template are refused at parse\n  time, before anything is paid; `{text}`, `{language}` and `{crlf}` are\n  the full set. A literal brace is `{{` / `}}`.\n\n```markdown\n# Translation Prompt\n\n## System Message\n\nYou are a professional translator. Keep the register of the original.\n\n## Conversation\n\n**User:**\nPlease translate the following text into {language}:\n\n{text}\n```\n\nFix or report lint problems before the paid run. The CLI would reject the\nfile at run start anyway, but a traceback after the user has already\napproved the cost is the wrong place to learn about a missing `{text}`.\n\n## Keep them out of git\n\nPrompt files are the user's personal voice and often carry character names\nor other personal terminology — same handling as `.env`. If the working directory is a\nrepo:\n\n```bash\ngit check-ignore prompt.json .env\n```\n\nAdd whatever comes back uncovered to `.git/info/exclude`. That file is\nlocal-only; never edit the project's tracked `.gitignore` for this.\n\n## Where the register goes\n\nA style instruction belongs in the `style` section (or the `system`\nmessage), stated once where a window starts — the run places it with the\nstanding instructions itself, never repeated per request. The `user`\ntemplate is sent for every unit — every word in it is paid for on every\nrequest of the book. A user-written style also replaces the model's own\nstyle notes in session handoffs.\n",".agents/skills/bbm-plan/references/providers.md":"# Routes: model name → endpoint shape → flags\n\nLoaded from `references/route-setup.md` step 3. The user pages cover the\nflags themselves: `docs/en/llm-args.md` (model, endpoint, format, keys),\n`docs/en/model_lang.md` (per-vendor addresses), `docs/en/providers.md` (the\nprovider file's fields, the image and classify models, which key is sent\nwhere). This file carries only what the agent needs beyond them: which\nshape to try, how to probe it, and what each route can do.\n\n## The one rule that decides everything\n\nThere is no model-name whitelist. A route is three flags plus the model id\nthe endpoint itself uses:\n\n```\n--api_base \"$ROOT\"  --key \"$KEY\"  [--api_format ...]  --model \"$MODEL\"\n```\n\n`--api_format` is inferred from the `--api_base` host first (`*.anthropic.com`\nmeans `anthropic`, anything else `openai`), then from the model id (`claude`\nor `anthropic` in it means `anthropic`). Pass it only to correct a wrong\nguess. Any model id reaches any endpoint; nothing has to be registered.\n\n| the endpoint speaks | flags |\n|---|---|\n| the OpenAI shape | `--api_base \"$ROOT/v1\" --model \"$MODEL\"` |\n| the anthropic shape, on an anthropic.com host | `--api_base \"$ROOT\" --model \"$MODEL\"` |\n| the anthropic shape, on a gateway domain | the same plus `--api_format anthropic` |\n| an entry in `bbm_providers.json` / `~/.bbm/providers.json` | `--provider NAME`, optionally `--model \"$MODEL\"` |\n| the OrcaRouter gateway | `--model orcarouter`, no `--api_base` |\n| nothing: a local Codex sidecar on the user's plan | `--api_format codex`, no key, no base (`references/route-setup.md` step 4) |\n\nOn the openai format `--model` may be left out; it defaults to\n`gpt-6-luna`. Every other format wants an id, and the anthropic format\nerrors without one.\n\n`codex` is the one route with no endpoint to probe. It is not a model id and\nnot a host, so none of the probes below apply to it. Spell it\n`--api_format codex`; `--model codex` is the same route under an older\nspelling.\n\nA gateway asked for the anthropic shape it does not serve answers 404, and\nthe run stops naming `--api_format openai` as the fix. `--api_base` may be\npasted with its path (`.../v1/chat/completions`); the path is trimmed.\n\nGroq, xAI, a LiteLLM proxy and every aggregator are reached through the\nOpenAI shape. The first three are also `--api_format groq` / `xai` /\n`litellm`, which is the same OpenAI route with that vendor's address filled\nin, so everything the OpenAI translator does — the structured-output probe,\ncontext, async, batch — applies to them.\n\nGemini and Qwen are the two exceptions: each has a translator of its own\nbecause neither protocol is the OpenAI shape. `--api_format gemini` talks to\nthe Gemini API (native constrained decoding, its own chat history, paced by\n`--interval`), and `--api_format qwen` talks to Qwen-MT on DashScope, whose\nrequest states a source and target language instead of carrying a prompt.\nBoth vendors *also* serve an OpenAI-compatible base, which the `openai`\nroute reaches — use that when the run needs the OpenAI translator's batch\nand session-context machinery.\n\n## `--provider NAME`: the same route, written once\n\nThe fields and the lookup order are on `docs/en/providers.md`; what matters\nfor the probe:\n\n- The `providers` wrapper is required; the loader reads nothing from a file\n  without it. A project `bbm_providers.json` wins over\n  `~/.bbm/providers.json` on a shared name; a name in neither falls back to\n  the shipped example, with a warning naming the address and key variable.\n- `api_style` is the format (`openai`, `anthropic`, `gemini`, `qwen`,\n  `groq`, `xai`, `litellm`; `claude` in older files). Any other\n  OpenAI-compatible host is `openai` with its address in `base_url`.\n- `default_models` becomes `--model` when it holds one id and\n  `--model_list` when it holds several. `env_key` is read for the key ahead\n  of `BBM_API_KEY` and the format's own variables. **Explicit flags win**,\n  so `--provider nvidia --model <id>` keeps the user's model.\n- A key is bound to its address: the entry's `env_key` is not sent when an\n  `--api_base` moves the run elsewhere. The `img_*` and `classify_*` fields\n  follow the same rule (`docs/en/providers.md#which-key-goes-where`); the\n  EPUB flow adds a `classify_model` only when the user names a classifier.\n\n## `--model orcarouter`: a gateway with no address to type\n\n`--model orcarouter` sends the run to OrcaRouter's OpenAI-shaped endpoint\nand asks for its smart-routing model, `orcarouter/auto`. It is a supported\nroute, not a legacy alias, so nothing is rewritten, and the key comes from\n`BBM_ORCAROUTER_API_KEY`.\n\n`orcarouter/auto` is the default, not a pin — to send the run to one named\nmodel instead of the smart router, use that entry:\n`--provider orcarouter --model <id>`. A probe reads the address\n(`https://api.orcarouter.ai/v1`) from there; it is never a flag you pass.\n\n## Binding `$KEY`, `$ROOT`, `$MODEL` from the entry before any probe\n\nThe entry decides which key variable to read. Do **not** take \"whichever\nkey is set\" — `.env` is sourced into a shell that may already export other\nproviders' keys from `~/.zshenv`, and a stale one would route the run to an\nendpoint the user never chose. Exit before curl when anything is missing:\nan empty bearer token produces a 401 that reads like a bad key.\n\n```bash\nroute_env() {   # $1 = provider name, as in bbm_providers.json / ~/.bbm/providers.json\n  eval \"$(python3 - \"$1\" <<'EOF'\nimport json, os, pathlib, shlex, sys\nname = sys.argv[1]; entry = None\nfor f in (pathlib.Path.home()/\".bbm\"/\"providers.json\", pathlib.Path(\"bbm_providers.json\")):\n    if f.is_file():\n        entry = json.load(open(f)).get(\"providers\", {}).get(name, entry)\nif entry is None:\n    print(f'echo \"no provider entry named {name}\" >&2; return 1'); sys.exit()\nstyle = entry.get(\"api_style\")\n# gemini and qwen speak their own protocols: the probes below would send this\n# entry's key to the wrong endpoint in the wrong shape, so refuse rather than\n# guess a shape for them.\nOPENAI_SHAPED = {\n    \"openai\": \"https://api.openai.com\",\n    \"groq\": \"https://api.groq.com/openai\",\n    \"xai\": \"https://api.x.ai\",\n    \"litellm\": \"http://localhost:4000\",\n}\nif style in (\"gemini\", \"qwen\"):\n    print(f'echo \"provider {name}: api_style {style!r} is a protocol of its own; run it with --provider {name}, these probes do not apply\" >&2; return 1'); sys.exit()\nif style not in OPENAI_SHAPED and style not in (\"anthropic\", \"claude\"):\n    print(f'echo \"provider {name}: api_style {style!r} is not a format; the loader refuses this entry\" >&2; return 1'); sys.exit()\nshape = \"anthropic\" if style in (\"anthropic\", \"claude\") else \"openai\"\ndefault_root = \"https://api.anthropic.com\" if shape == \"anthropic\" else OPENAI_SHAPED[style]\nroot = (entry.get(\"base_url\") or default_root).rstrip(\"/\")\nroot = root[:-3] if root.endswith(\"/v1\") else root\nkey_var = entry.get(\"env_key\") or \"BBM_API_KEY\"\nmodel = (entry.get(\"default_models\") or [\"\"])[0] or (\"gpt-6-luna\" if shape == \"openai\" else \"\")\nprint(f\"SHAPE={shape} ROOT={shlex.quote(root)} MODEL={shlex.quote(model)} KEY_VAR={key_var}\")\nEOF\n)\"\n  [ -n \"${SHAPE:-}\" ] || return 1\n  KEY=\"$(printenv \"$KEY_VAR\" 2>/dev/null || true)\"   # same in bash and zsh; .env is exported by set -a\n  [ -n \"$MODEL\" ] || { echo \"entry $1 names no model (the anthropic format needs one)\" >&2; return 1; }\n  [ -n \"$KEY\" ]   || { echo \"$KEY_VAR is unset — fill .env\" >&2; return 1; }\n}\n```\n\n`$ROOT` comes out with no trailing `/v1` whatever the entry wrote, so every\nprobe path below is spelled in full. On a gateway the key belongs to the\n*gateway*, not the model's vendor: a Claude model reached over the OpenAI\nshape reads the gateway entry's `env_key`, because that is the shape being\nspoken.\n\n## Shapes, and how to probe each\n\n`$ROOT` is the scheme+host with no `/v1` (`route_env` guarantees it), so\nevery path below is written out in full. Each probe is one tiny call, a\nfraction of a cent.\n\n**No token cap on the OpenAI shape.** `max_tokens: 1` looks thrifty and is a\nfalse negative twice over: gateways reject caps below their own floor\n(measured: `max_tokens must be greater than 2` on *every* model of one\ngateway), and OpenAI's own o-series/gpt-5 models reject `max_tokens`\noutright in favour of `max_completion_tokens`. A probe must test one thing.\nThe repo's own probes send no cap for exactly this reason\n(`translator/capabilities.py`, `probe_structured_output` and\n`probe_model_route`); match them. A capped probe against gpt-5.6-luna came\nback \"'max_tokens' is not supported with this model\" and confirmed nothing.\nThe reply is a few tokens of \"Hi!\".\n\n**OpenAI shape** — the universal one. Most gateways serve every model they\nhost on it, whoever made the model.\n\n```bash\ncurl -sS \"$ROOT/v1/chat/completions\" \\\n  -H \"Authorization: Bearer $KEY\" -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.choices[0]`. → `--api_base \"$ROOT/v1\" --key \"$KEY\"\n--model \"$MODEL\"`\n\n**Anthropic shape**. `max_tokens` is *mandatory* here, unlike above; 16 is\npast every floor seen so far.\n\n```bash\ncurl -sS \"$ROOT/v1/messages\" \\\n  -H \"x-api-key: $KEY\" -H 'anthropic-version: 2023-06-01' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"max_tokens\":16,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.content[0]`. → `--api_base \"$ROOT\" --key \"$KEY\"\n--model \"$MODEL\"` (add `--api_format anthropic` when the host is not\nanthropic.com). The reply's `model` field echoes the id the endpoint actually\nresolved to (`claude-haiku-4.5` → `claude-haiku-4-5-20251001`). Real Anthropic requires\n`x-api-key`; gateways commonly accept `Authorization: Bearer` too, so try\n`x-api-key` first and Bearer second.\n\n**`--api_base` for this route takes the root, not `/v1`**: the SDK appends\n`/v1/messages` itself. Passing `https://host/v1` used to produce\n`/v1/v1/messages` and a 403 reading \"HTTP node only allows access to\ninference API paths\"; a trailing `/v1` is now trimmed automatically (with a\nprinted note), so either form works — but say `https://host` and mean it.\n\n**Gemini** has a route of its own — `--api_format gemini`, over the Gemini\nAPI — which the OpenAI-shape and anthropic-shape probes here do not reach.\nTo probe from a shell, use its OpenAI-compatible base instead,\n`https://generativelanguage.googleapis.com/v1beta/openai/`, with the\nOpenAI-shape call above; that base is also the one to run against when the\ntranslation wants batching or session context. **Qwen-MT** is the same\nstory at `https://dashscope.aliyuncs.com/compatible-mode/v1`.\n\n## Inferring which shape to try first, from the model name\n\n| model name starts with | try first | then |\n|---|---|---|\n| `claude-` | OpenAI if a gateway base is set; else anthropic | the other one |\n| anything else (`gpt-`, `o1`, `o3`, `gemini-`, `grok-`, `llama`, `qwen`, …) | OpenAI | — |\n\nOpenAI first at a gateway because aggregators serve Claude and Gemini\nmodels on `/chat/completions` too. Go native only when the endpoint is\nAnthropic's own, or when the gateway rejects the OpenAI shape.\n\n**The chat call is the verdict; the listing is only a hint.** `GET\n$ROOT/v1/models` (Bearer auth) is free on OpenAI-shaped endpoints and\nreturns `{\"data\":[{\"id\":…}]}`, but gateways routinely serve a model they do\nnot list, or list it under another id. A name missing from the listing is\nno reason to give up: run the chat probe, and a reply is the answer. Fetch\nthe listing when the probe fails, to tell a typo'd id from an unsupported\npath; both return 404, and only the listing separates them. The run does\nthe same (`translator/capabilities.py`, `probe_model_route`, once at the\nfirst paid call). Some gateways add `supported_endpoint_types` per row (on\none aggregator every row read `['openai', 'anthropic']`); when it is there\nit answers the shape question outright.\n\n## Capability caveats per route\n\n| `--api_format` | translation | plan-mode classification |\n|---|---|---|\n| `openai` (any host) | schema when the probe says `strict`, else delimiter | yes |\n| `anthropic` | delimiter (no structured-output work was done for it) | yes, via the prompt rung |\n| `codex` | one turn per unit, on a thread that is itself the context window | yes, via the prompt rung — the sidecar compiles no schema |\n| `google`, `deepl`, `deeplfree`, `caiyun`, `tencent`, `customapi` | translation only | only with a classifier of its own (`--classify-model`, or an entry's `classify_model`) |\n\nClassification capability does not gate the default flow — `--plan-classify\nagent` makes no API call, you are the classifier. It matters only when the\nuser names a classifier (`references/epub-plan-mode.md`, \"A classifier the\nuser names\") and that classifier is the translating model.\n\nThe translation services have one channel, and it translates\nwhatever it is handed. They cannot be asked a question.\n\n## What the run's own probe does later\n\nAt first paid use, the OpenAI-shaped translator sends a one-key schema probe\nand grades the endpoint `strict` / `shape` / `json` / unsupported. Only\n`strict` gets a schema for *translation* — the translation schema pins the\ntarget language as a value constraint, so an endpoint that ignores values\nwould drop it. Everything else falls back to the delimiter method, which is\nfine and prints one yellow line. This is capability discovery, not an error;\ndo not report it to the user as a failure.\n",".agents/skills/bbm-plan/SKILL.md":"---\nname: bbm-plan\ndescription: Translate a book into a bilingual edition with bilingual_book_maker (\"bbm\") - an EPUB through plan mode (greedy partition, an agent-reviewed classification plan, then a full resumable run), a PDF into a bilingual, reflowable EPUB (--to-epub), or a TXT, SRT or Markdown file. Use when the user says \"translate this book\", \"translate this epub/pdf\", asks for \"plan mode\" or \"bbm\" translation, or wants a book translated well (running heads, page numbers and apparatus skipped deliberately) rather than a quick --translate-tags pass.\n---\n\n# bbm-plan: translate a book with bilingual_book_maker\n\nThis file is the router. Do the intake below, then load **one** flow file\nfor the book in hand and follow it. The other flow files stay unread.\n\nRepo: the repository this skill ships in (`make_book.py` at its root). Run\nevery command from the repo root. The user documentation is the MkDocs site\nin `docs/en/` of this checkout; the flow files point at the page to read for\nanything they do not spell out. `python make_book.py --help` is the\nauthority on every flag, and `docs/en/cmd.md` lists them all in one place.\n\n## Your stance\n\nYou are the trained CLI operator. You pick every flag from the flow file's\nrecommendations and state each choice with a one-line reason; the\nrecommended defaults are what you pass unless the user asks for something\nelse or the book argues otherwise. The user hands over a book, credentials\nand (if they have one) a prompt file, approves the plan and the cost, and\ngets a bilingual book back. They are never asked to experiment with flags,\nhalt semantics or resume mechanics.\n\n**The EPUB flow uses `--plan-classify agent`.** The plan arrives with its\nuncertain signatures set to `\"action\": null`, and the translate run\nrefuses to start while any null remains, so *you*, the coding agent, own\nthe classification against the real samples. Do it in the main agent with\nfull session context; never delegate plan editing to a subagent or a\nsmall/fast model. The one exception is the user naming a classifier\nthemselves (Jev, or a model): then follow \"A classifier the user names\" in\n`references/epub-plan-mode.md`, and still read its skips.\n\nAll state lives on disk (`bbm_providers.json`, `.env`, the flow's own\nworking files, the resume cache, `run.log`), so any step can be redone\nafter a crash or in a new session.\n\n## Intake\n\n1. **Route and credentials: always read `references/route-setup.md`**\n   before the first command. It probes what the user already has, asks one\n   question (route, model, prompt file), binds the `ROUTE` and `CONTEXT`\n   arrays every flow uses, and probes the endpoint for a sub-cent before\n   anything is paid. **Keys never go on the command line**: the handoff\n   block reprints the whole command, and argparse echoes it back on a\n   mistyped flag. If a key ever reaches the terminal, say so at once and\n   tell the user to rotate it.\n2. **Book path** and **target language** (`--language`, e.g. `zh-hans`,\n   `ja`, `Simplified Chinese`). For a small language the tables may not\n   know, pass both halves: `--language \"ain:Ainu\"` (tag before the colon,\n   name after; the tag list is `docs/en/languages.md`). `--source_lang` states\n   the source language for a book whose short lines or names could be\n   misdetected.\n3. **Their prompt file**, asked in the route question. If they hand one\n   over, lint it before the first paid run: `references/prompt-files.md`.\n   If they say no, offer one sentence of what a style instruction would buy\n   them and move on. Independently, look for an existing template in the\n   book's directory and the repo root (`prompt.json`, `prompt.txt`,\n   `prompt*.md`, `prompt_template*`). **A candidate must carry a diff**: in\n   a git repo only untracked or modified-against-HEAD files count; cleanly\n   tracked `prompt*` files are the repo's shipped examples, not the user's\n   voice. Found one? **Ask before doing anything with it**; never adopt or\n   ignore it silently.\n4. Anything they want changed from the recommended defaults: a visual style\n   for the translation, specific chapters or pages only.\n\n**Bilingual is the assumption; do not ask.** The output keeps every\noriginal next to its translation unless the user asked for a\ntranslated-only book in so many words (\"just the Chinese\", \"replace the\noriginal\", \"not bilingual\", \"single language\"). *\"Translate this book to\nChinese\"* is not that request; it is the ordinary ask, and it means a\nbilingual book. Never infer single-language output (`--single_translate`)\nfrom the target language being named, from the book being short, or from\nthe user sounding brisk. Say which one you are producing in the same line\nwhere you state the flag choices, so a user who wanted the other one can\nsay so before anything is paid for.\n\n## Branch by the book\n\n| the book is | read, and follow | the user page behind it |\n|---|---|---|\n| an `.epub` | `references/epub-plan-mode.md` | `docs/en/formats/epub.md` |\n| a `.pdf` | `references/pdf-route.md` | `docs/en/features/pdf-to-epub.md` |\n| a `.md` Datalab wrote (`{N}------` page separators), or any `.md` the user wants as an EPUB | `references/pdf-route.md` (`--to-epub` on the `.md`) | `docs/en/features/pdf-to-epub.md` |\n| a `.txt`, `.srt` or `.md` | `references/plain-formats.md` | `docs/en/formats/txt.md`, `docs/en/formats/srt.md`, `docs/en/formats/md.md` |\n\nA file with another extension is not a book this tool reads; say so. A\nfolder of chapters is several books: ask whether they want one run per\nfile.\n\n## Stating the choices\n\nBefore the first paid run, one short block to the user:\n\n- the command you will run, with one line per non-default flag saying why;\n- bilingual or translated-only, in so many words;\n- the checkpoint the flow file puts before the full run (what the user\n  will be shown, and when), and any test run you skip or add;\n- which allowance is spent: the API key's account, or the ChatGPT plan on\n  the codex route.\n\nThen wait for the approval of the cost, and run.\n\n## Context hygiene\n\n- **Never** let a translation run stream into the conversation: every paid\n  run gets `--quiet` where the format takes it *and* a log-file redirect\n  (`> run.log 2>&1`), in the background, polled with `tail -5 run.log`.\n- Everything the next step needs is on disk; nothing critical lives only in\n  conversation.\n- **Compaction threshold**, *your* context, not the run's: for a small book\n  do not compact at all. Only compact when context is genuinely pressured\n  (roughly 70% used), at most once, at the natural boundary: after the\n  flow's checkpoint, before the full run.\n\n## Reference files\n\nLoad each one only when the step that needs it runs.\n\n- **`references/route-setup.md`**: credential probe, the route question,\n  `ROUTE`/`CONTEXT` by format, the endpoint probe, the codex route, local\n  models, the route and context flag menu, route failure lines. Every flow.\n- **`references/providers.md`**: route table, probe recipes (`route_env`),\n  per-format capability caveats. Read from route-setup whenever the\n  endpoint is not a plain OpenAI-shaped host.\n- **`references/prompt-files.md`**: the `--prompt` lint checklist and\n  keeping a user's prompt out of git.\n- **`references/epub-plan-mode.md`**: plan, classify, optional smoke, full\n  run, halt/resume, deliver, failure lines. EPUB only.\n- **`references/pdf-route.md`**: the whole PDF flow. PDF only.\n- **`references/plain-formats.md`**: the default command per TXT, SRT and\n  Markdown file, deliver, failure lines.\n"},"items":[{"name":"prompt-files.md","path":".agents/skills/bbm-plan/references/prompt-files.md","rawUrl":"https://raw.githubusercontent.com/yihong0618/bilingual_book_maker/HEAD/.agents/skills/bbm-plan/references/prompt-files.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Prompt files: lint contract and git hygiene\n\nLoaded from SKILL.md's intake once a candidate `prompt*` file is found **and\nthe user has said to use it**. Do not lint before asking. The format itself,\nwith examples, is on `docs/en/prompt.md`; this file is the checklist to run\nagainst the user's file before the first paid run.\n\n## The contract (`book_maker/cli.py:parse_prompt_arg`)\n\n- `.json`: an object with **only** the keys `user` (required), `system` and\n  `style` (both optional). Any other key is rejected outright.\n- The `user` template must contain the literal placeholder `{text}`.\n  `{language}` is optional.\n- `.txt` becomes the user template as-is, same `{text}` rule.\n- `.md` is read as the PromptDown **block** form by the repo's own parser\n  (no package involved): a `## System Message`, an optional `## Style`,\n  and a `## Conversation` whose turns open with `**User:**` on its own\n  line. The `| Role | Content |` table form is refused with an error\n  naming the file and the block form. The repo's `prompt_md.prompt.md`\n  sample is a valid block-form template to copy. `## Developer Message`\n  is accepted as another name for the system section.\n- Unknown `{placeholders}` in the `user` template are refused at parse\n  time, before anything is paid; `{text}`, `{language}` and `{crlf}` are\n  the full set. A literal brace is `{{` / `}}`.\n\n```markdown\n# Translation Prompt\n\n## System Message\n\nYou are a professional translator. Keep the register of the original.\n\n## Conversation\n\n**User:**\nPlease translate the following text into {language}:\n\n{text}\n```\n\nFix or report lint problems before the paid run. The CLI would reject the\nfile at run start anyway, but a traceback after the user has already\napproved the cost is the wrong place to learn about a missing `{text}`.\n\n## Keep them out of git\n\nPrompt files are the user's personal voice and often carry character names\nor other personal terminology — same handling as `.env`. If the working directory is a\nrepo:\n\n```bash\ngit check-ignore prompt.json .env\n```\n\nAdd whatever comes back uncovered to `.git/info/exclude`. That file is\nlocal-only; never edit the project's tracked `.gitignore` for this.\n\n## Where the register goes\n\nA style instruction belongs in the `style` section (or the `system`\nmessage), stated once where a window starts — the run places it with the\nstanding instructions itself, never repeated per request. The `user`\ntemplate is sent for every unit — every word in it is paid for on every\nrequest of the book. A user-written style also replaces the model's own\nstyle notes in session handoffs.\n","isInternal":false,"tokens":617,"sizeBytes":2594},{"name":"providers.md","path":".agents/skills/bbm-plan/references/providers.md","rawUrl":"https://raw.githubusercontent.com/yihong0618/bilingual_book_maker/HEAD/.agents/skills/bbm-plan/references/providers.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Routes: model name → endpoint shape → flags\n\nLoaded from `references/route-setup.md` step 3. The user pages cover the\nflags themselves: `docs/en/llm-args.md` (model, endpoint, format, keys),\n`docs/en/model_lang.md` (per-vendor addresses), `docs/en/providers.md` (the\nprovider file's fields, the image and classify models, which key is sent\nwhere). This file carries only what the agent needs beyond them: which\nshape to try, how to probe it, and what each route can do.\n\n## The one rule that decides everything\n\nThere is no model-name whitelist. A route is three flags plus the model id\nthe endpoint itself uses:\n\n```\n--api_base \"$ROOT\"  --key \"$KEY\"  [--api_format ...]  --model \"$MODEL\"\n```\n\n`--api_format` is inferred from the `--api_base` host first (`*.anthropic.com`\nmeans `anthropic`, anything else `openai`), then from the model id (`claude`\nor `anthropic` in it means `anthropic`). Pass it only to correct a wrong\nguess. Any model id reaches any endpoint; nothing has to be registered.\n\n| the endpoint speaks | flags |\n|---|---|\n| the OpenAI shape | `--api_base \"$ROOT/v1\" --model \"$MODEL\"` |\n| the anthropic shape, on an anthropic.com host | `--api_base \"$ROOT\" --model \"$MODEL\"` |\n| the anthropic shape, on a gateway domain | the same plus `--api_format anthropic` |\n| an entry in `bbm_providers.json` / `~/.bbm/providers.json` | `--provider NAME`, optionally `--model \"$MODEL\"` |\n| the OrcaRouter gateway | `--model orcarouter`, no `--api_base` |\n| nothing: a local Codex sidecar on the user's plan | `--api_format codex`, no key, no base (`references/route-setup.md` step 4) |\n\nOn the openai format `--model` may be left out; it defaults to\n`gpt-6-luna`. Every other format wants an id, and the anthropic format\nerrors without one.\n\n`codex` is the one route with no endpoint to probe. It is not a model id and\nnot a host, so none of the probes below apply to it. Spell it\n`--api_format codex`; `--model codex` is the same route under an older\nspelling.\n\nA gateway asked for the anthropic shape it does not serve answers 404, and\nthe run stops naming `--api_format openai` as the fix. `--api_base` may be\npasted with its path (`.../v1/chat/completions`); the path is trimmed.\n\nGroq, xAI, a LiteLLM proxy and every aggregator are reached through the\nOpenAI shape. The first three are also `--api_format groq` / `xai` /\n`litellm`, which is the same OpenAI route with that vendor's address filled\nin, so everything the OpenAI translator does — the structured-output probe,\ncontext, async, batch — applies to them.\n\nGemini and Qwen are the two exceptions: each has a translator of its own\nbecause neither protocol is the OpenAI shape. `--api_format gemini` talks to\nthe Gemini API (native constrained decoding, its own chat history, paced by\n`--interval`), and `--api_format qwen` talks to Qwen-MT on DashScope, whose\nrequest states a source and target language instead of carrying a prompt.\nBoth vendors *also* serve an OpenAI-compatible base, which the `openai`\nroute reaches — use that when the run needs the OpenAI translator's batch\nand session-context machinery.\n\n## `--provider NAME`: the same route, written once\n\nThe fields and the lookup order are on `docs/en/providers.md`; what matters\nfor the probe:\n\n- The `providers` wrapper is required; the loader reads nothing from a file\n  without it. A project `bbm_providers.json` wins over\n  `~/.bbm/providers.json` on a shared name; a name in neither falls back to\n  the shipped example, with a warning naming the address and key variable.\n- `api_style` is the format (`openai`, `anthropic`, `gemini`, `qwen`,\n  `groq`, `xai`, `litellm`; `claude` in older files). Any other\n  OpenAI-compatible host is `openai` with its address in `base_url`.\n- `default_models` becomes `--model` when it holds one id and\n  `--model_list` when it holds several. `env_key` is read for the key ahead\n  of `BBM_API_KEY` and the format's own variables. **Explicit flags win**,\n  so `--provider nvidia --model <id>` keeps the user's model.\n- A key is bound to its address: the entry's `env_key` is not sent when an\n  `--api_base` moves the run elsewhere. The `img_*` and `classify_*` fields\n  follow the same rule (`docs/en/providers.md#which-key-goes-where`); the\n  EPUB flow adds a `classify_model` only when the user names a classifier.\n\n## `--model orcarouter`: a gateway with no address to type\n\n`--model orcarouter` sends the run to OrcaRouter's OpenAI-shaped endpoint\nand asks for its smart-routing model, `orcarouter/auto`. It is a supported\nroute, not a legacy alias, so nothing is rewritten, and the key comes from\n`BBM_ORCAROUTER_API_KEY`.\n\n`orcarouter/auto` is the default, not a pin — to send the run to one named\nmodel instead of the smart router, use that entry:\n`--provider orcarouter --model <id>`. A probe reads the address\n(`https://api.orcarouter.ai/v1`) from there; it is never a flag you pass.\n\n## Binding `$KEY`, `$ROOT`, `$MODEL` from the entry before any probe\n\nThe entry decides which key variable to read. Do **not** take \"whichever\nkey is set\" — `.env` is sourced into a shell that may already export other\nproviders' keys from `~/.zshenv`, and a stale one would route the run to an\nendpoint the user never chose. Exit before curl when anything is missing:\nan empty bearer token produces a 401 that reads like a bad key.\n\n```bash\nroute_env() {   # $1 = provider name, as in bbm_providers.json / ~/.bbm/providers.json\n  eval \"$(python3 - \"$1\" <<'EOF'\nimport json, os, pathlib, shlex, sys\nname = sys.argv[1]; entry = None\nfor f in (pathlib.Path.home()/\".bbm\"/\"providers.json\", pathlib.Path(\"bbm_providers.json\")):\n    if f.is_file():\n        entry = json.load(open(f)).get(\"providers\", {}).get(name, entry)\nif entry is None:\n    print(f'echo \"no provider entry named {name}\" >&2; return 1'); sys.exit()\nstyle = entry.get(\"api_style\")\n# gemini and qwen speak their own protocols: the probes below would send this\n# entry's key to the wrong endpoint in the wrong shape, so refuse rather than\n# guess a shape for them.\nOPENAI_SHAPED = {\n    \"openai\": \"https://api.openai.com\",\n    \"groq\": \"https://api.groq.com/openai\",\n    \"xai\": \"https://api.x.ai\",\n    \"litellm\": \"http://localhost:4000\",\n}\nif style in (\"gemini\", \"qwen\"):\n    print(f'echo \"provider {name}: api_style {style!r} is a protocol of its own; run it with --provider {name}, these probes do not apply\" >&2; return 1'); sys.exit()\nif style not in OPENAI_SHAPED and style not in (\"anthropic\", \"claude\"):\n    print(f'echo \"provider {name}: api_style {style!r} is not a format; the loader refuses this entry\" >&2; return 1'); sys.exit()\nshape = \"anthropic\" if style in (\"anthropic\", \"claude\") else \"openai\"\ndefault_root = \"https://api.anthropic.com\" if shape == \"anthropic\" else OPENAI_SHAPED[style]\nroot = (entry.get(\"base_url\") or default_root).rstrip(\"/\")\nroot = root[:-3] if root.endswith(\"/v1\") else root\nkey_var = entry.get(\"env_key\") or \"BBM_API_KEY\"\nmodel = (entry.get(\"default_models\") or [\"\"])[0] or (\"gpt-6-luna\" if shape == \"openai\" else \"\")\nprint(f\"SHAPE={shape} ROOT={shlex.quote(root)} MODEL={shlex.quote(model)} KEY_VAR={key_var}\")\nEOF\n)\"\n  [ -n \"${SHAPE:-}\" ] || return 1\n  KEY=\"$(printenv \"$KEY_VAR\" 2>/dev/null || true)\"   # same in bash and zsh; .env is exported by set -a\n  [ -n \"$MODEL\" ] || { echo \"entry $1 names no model (the anthropic format needs one)\" >&2; return 1; }\n  [ -n \"$KEY\" ]   || { echo \"$KEY_VAR is unset — fill .env\" >&2; return 1; }\n}\n```\n\n`$ROOT` comes out with no trailing `/v1` whatever the entry wrote, so every\nprobe path below is spelled in full. On a gateway the key belongs to the\n*gateway*, not the model's vendor: a Claude model reached over the OpenAI\nshape reads the gateway entry's `env_key`, because that is the shape being\nspoken.\n\n## Shapes, and how to probe each\n\n`$ROOT` is the scheme+host with no `/v1` (`route_env` guarantees it), so\nevery path below is written out in full. Each probe is one tiny call, a\nfraction of a cent.\n\n**No token cap on the OpenAI shape.** `max_tokens: 1` looks thrifty and is a\nfalse negative twice over: gateways reject caps below their own floor\n(measured: `max_tokens must be greater than 2` on *every* model of one\ngateway), and OpenAI's own o-series/gpt-5 models reject `max_tokens`\noutright in favour of `max_completion_tokens`. A probe must test one thing.\nThe repo's own probes send no cap for exactly this reason\n(`translator/capabilities.py`, `probe_structured_output` and\n`probe_model_route`); match them. A capped probe against gpt-5.6-luna came\nback \"'max_tokens' is not supported with this model\" and confirmed nothing.\nThe reply is a few tokens of \"Hi!\".\n\n**OpenAI shape** — the universal one. Most gateways serve every model they\nhost on it, whoever made the model.\n\n```bash\ncurl -sS \"$ROOT/v1/chat/completions\" \\\n  -H \"Authorization: Bearer $KEY\" -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.choices[0]`. → `--api_base \"$ROOT/v1\" --key \"$KEY\"\n--model \"$MODEL\"`\n\n**Anthropic shape**. `max_tokens` is *mandatory* here, unlike above; 16 is\npast every floor seen so far.\n\n```bash\ncurl -sS \"$ROOT/v1/messages\" \\\n  -H \"x-api-key: $KEY\" -H 'anthropic-version: 2023-06-01' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"max_tokens\":16,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.content[0]`. → `--api_base \"$ROOT\" --key \"$KEY\"\n--model \"$MODEL\"` (add `--api_format anthropic` when the host is not\nanthropic.com). The reply's `model` field echoes the id the endpoint actually\nresolved to (`claude-haiku-4.5` → `claude-haiku-4-5-20251001`). Real Anthropic requires\n`x-api-key`; gateways commonly accept `Authorization: Bearer` too, so try\n`x-api-key` first and Bearer second.\n\n**`--api_base` for this route takes the root, not `/v1`**: the SDK appends\n`/v1/messages` itself. Passing `https://host/v1` used to produce\n`/v1/v1/messages` and a 403 reading \"HTTP node only allows access to\ninference API paths\"; a trailing `/v1` is now trimmed automatically (with a\nprinted note), so either form works — but say `https://host` and mean it.\n\n**Gemini** has a route of its own — `--api_format gemini`, over the Gemini\nAPI — which the OpenAI-shape and anthropic-shape probes here do not reach.\nTo probe from a shell, use its OpenAI-compatible base instead,\n`https://generativelanguage.googleapis.com/v1beta/openai/`, with the\nOpenAI-shape call above; that base is also the one to run against when the\ntranslation wants batching or session context. **Qwen-MT** is the same\nstory at `https://dashscope.aliyuncs.com/compatible-mode/v1`.\n\n## Inferring which shape to try first, from the model name\n\n| model name starts with | try first | then |\n|---|---|---|\n| `claude-` | OpenAI if a gateway base is set; else anthropic | the other one |\n| anything else (`gpt-`, `o1`, `o3`, `gemini-`, `grok-`, `llama`, `qwen`, …) | OpenAI | — |\n\nOpenAI first at a gateway because aggregators serve Claude and Gemini\nmodels on `/chat/completions` too. Go native only when the endpoint is\nAnthropic's own, or when the gateway rejects the OpenAI shape.\n\n**The chat call is the verdict; the listing is only a hint.** `GET\n$ROOT/v1/models` (Bearer auth) is free on OpenAI-shaped endpoints and\nreturns `{\"data\":[{\"id\":…}]}`, but gateways routinely serve a model they do\nnot list, or list it under another id. A name missing from the listing is\nno reason to give up: run the chat probe, and a reply is the answer. Fetch\nthe listing when the probe fails, to tell a typo'd id from an unsupported\npath; both return 404, and only the listing separates them. The run does\nthe same (`translator/capabilities.py`, `probe_model_route`, once at the\nfirst paid call). Some gateways add `supported_endpoint_types` per row (on\none aggregator every row read `['openai', 'anthropic']`); when it is there\nit answers the shape question outright.\n\n## Capability caveats per route\n\n| `--api_format` | translation | plan-mode classification |\n|---|---|---|\n| `openai` (any host) | schema when the probe says `strict`, else delimiter | yes |\n| `anthropic` | delimiter (no structured-output work was done for it) | yes, via the prompt rung |\n| `codex` | one turn per unit, on a thread that is itself the context window | yes, via the prompt rung — the sidecar compiles no schema |\n| `google`, `deepl`, `deeplfree`, `caiyun`, `tencent`, `customapi` | translation only | only with a classifier of its own (`--classify-model`, or an entry's `classify_model`) |\n\nClassification capability does not gate the default flow — `--plan-classify\nagent` makes no API call, you are the classifier. It matters only when the\nuser names a classifier (`references/epub-plan-mode.md`, \"A classifier the\nuser names\") and that classifier is the translating model.\n\nThe translation services have one channel, and it translates\nwhatever it is handed. They cannot be asked a question.\n\n## What the run's own probe does later\n\nAt first paid use, the OpenAI-shaped translator sends a one-key schema probe\nand grades the endpoint `strict` / `shape` / `json` / unsupported. Only\n`strict` gets a schema for *translation* — the translation schema pins the\ntarget language as a value constraint, so an endpoint that ignores values\nwould drop it. Everything else falls back to the delimiter method, which is\nfine and prints one yellow line. This is capability discovery, not an error;\ndo not report it to the user as a failure.\n","isInternal":false,"tokens":3672,"sizeBytes":13461},{"name":"SKILL.md","path":".agents/skills/bbm-plan/SKILL.md","rawUrl":"https://raw.githubusercontent.com/yihong0618/bilingual_book_maker/HEAD/.agents/skills/bbm-plan/SKILL.md","title":"bbm-plan","category":"anthropic-skill","format":"markdown","content":"---\nname: bbm-plan\ndescription: Translate a book into a bilingual edition with bilingual_book_maker (\"bbm\") - an EPUB through plan mode (greedy partition, an agent-reviewed classification plan, then a full resumable run), a PDF into a bilingual, reflowable EPUB (--to-epub), or a TXT, SRT or Markdown file. Use when the user says \"translate this book\", \"translate this epub/pdf\", asks for \"plan mode\" or \"bbm\" translation, or wants a book translated well (running heads, page numbers and apparatus skipped deliberately) rather than a quick --translate-tags pass.\n---\n\n# bbm-plan: translate a book with bilingual_book_maker\n\nThis file is the router. Do the intake below, then load **one** flow file\nfor the book in hand and follow it. The other flow files stay unread.\n\nRepo: the repository this skill ships in (`make_book.py` at its root). Run\nevery command from the repo root. The user documentation is the MkDocs site\nin `docs/en/` of this checkout; the flow files point at the page to read for\nanything they do not spell out. `python make_book.py --help` is the\nauthority on every flag, and `docs/en/cmd.md` lists them all in one place.\n\n## Your stance\n\nYou are the trained CLI operator. You pick every flag from the flow file's\nrecommendations and state each choice with a one-line reason; the\nrecommended defaults are what you pass unless the user asks for something\nelse or the book argues otherwise. The user hands over a book, credentials\nand (if they have one) a prompt file, approves the plan and the cost, and\ngets a bilingual book back. They are never asked to experiment with flags,\nhalt semantics or resume mechanics.\n\n**The EPUB flow uses `--plan-classify agent`.** The plan arrives with its\nuncertain signatures set to `\"action\": null`, and the translate run\nrefuses to start while any null remains, so *you*, the coding agent, own\nthe classification against the real samples. Do it in the main agent with\nfull session context; never delegate plan editing to a subagent or a\nsmall/fast model. The one exception is the user naming a classifier\nthemselves (Jev, or a model): then follow \"A classifier the user names\" in\n`references/epub-plan-mode.md`, and still read its skips.\n\nAll state lives on disk (`bbm_providers.json`, `.env`, the flow's own\nworking files, the resume cache, `run.log`), so any step can be redone\nafter a crash or in a new session.\n\n## Intake\n\n1. **Route and credentials: always read `references/route-setup.md`**\n   before the first command. It probes what the user already has, asks one\n   question (route, model, prompt file), binds the `ROUTE` and `CONTEXT`\n   arrays every flow uses, and probes the endpoint for a sub-cent before\n   anything is paid. **Keys never go on the command line**: the handoff\n   block reprints the whole command, and argparse echoes it back on a\n   mistyped flag. If a key ever reaches the terminal, say so at once and\n   tell the user to rotate it.\n2. **Book path** and **target language** (`--language`, e.g. `zh-hans`,\n   `ja`, `Simplified Chinese`). For a small language the tables may not\n   know, pass both halves: `--language \"ain:Ainu\"` (tag before the colon,\n   name after; the tag list is `docs/en/languages.md`). `--source_lang` states\n   the source language for a book whose short lines or names could be\n   misdetected.\n3. **Their prompt file**, asked in the route question. If they hand one\n   over, lint it before the first paid run: `references/prompt-files.md`.\n   If they say no, offer one sentence of what a style instruction would buy\n   them and move on. Independently, look for an existing template in the\n   book's directory and the repo root (`prompt.json`, `prompt.txt`,\n   `prompt*.md`, `prompt_template*`). **A candidate must carry a diff**: in\n   a git repo only untracked or modified-against-HEAD files count; cleanly\n   tracked `prompt*` files are the repo's shipped examples, not the user's\n   voice. Found one? **Ask before doing anything with it**; never adopt or\n   ignore it silently.\n4. Anything they want changed from the recommended defaults: a visual style\n   for the translation, specific chapters or pages only.\n\n**Bilingual is the assumption; do not ask.** The output keeps every\noriginal next to its translation unless the user asked for a\ntranslated-only book in so many words (\"just the Chinese\", \"replace the\noriginal\", \"not bilingual\", \"single language\"). *\"Translate this book to\nChinese\"* is not that request; it is the ordinary ask, and it means a\nbilingual book. Never infer single-language output (`--single_translate`)\nfrom the target language being named, from the book being short, or from\nthe user sounding brisk. Say which one you are producing in the same line\nwhere you state the flag choices, so a user who wanted the other one can\nsay so before anything is paid for.\n\n## Branch by the book\n\n| the book is | read, and follow | the user page behind it |\n|---|---|---|\n| an `.epub` | `references/epub-plan-mode.md` | `docs/en/formats/epub.md` |\n| a `.pdf` | `references/pdf-route.md` | `docs/en/features/pdf-to-epub.md` |\n| a `.md` Datalab wrote (`{N}------` page separators), or any `.md` the user wants as an EPUB | `references/pdf-route.md` (`--to-epub` on the `.md`) | `docs/en/features/pdf-to-epub.md` |\n| a `.txt`, `.srt` or `.md` | `references/plain-formats.md` | `docs/en/formats/txt.md`, `docs/en/formats/srt.md`, `docs/en/formats/md.md` |\n\nA file with another extension is not a book this tool reads; say so. A\nfolder of chapters is several books: ask whether they want one run per\nfile.\n\n## Stating the choices\n\nBefore the first paid run, one short block to the user:\n\n- the command you will run, with one line per non-default flag saying why;\n- bilingual or translated-only, in so many words;\n- the checkpoint the flow file puts before the full run (what the user\n  will be shown, and when), and any test run you skip or add;\n- which allowance is spent: the API key's account, or the ChatGPT plan on\n  the codex route.\n\nThen wait for the approval of the cost, and run.\n\n## Context hygiene\n\n- **Never** let a translation run stream into the conversation: every paid\n  run gets `--quiet` where the format takes it *and* a log-file redirect\n  (`> run.log 2>&1`), in the background, polled with `tail -5 run.log`.\n- Everything the next step needs is on disk; nothing critical lives only in\n  conversation.\n- **Compaction threshold**, *your* context, not the run's: for a small book\n  do not compact at all. Only compact when context is genuinely pressured\n  (roughly 70% used), at most once, at the natural boundary: after the\n  flow's checkpoint, before the full run.\n\n## Reference files\n\nLoad each one only when the step that needs it runs.\n\n- **`references/route-setup.md`**: credential probe, the route question,\n  `ROUTE`/`CONTEXT` by format, the endpoint probe, the codex route, local\n  models, the route and context flag menu, route failure lines. Every flow.\n- **`references/providers.md`**: route table, probe recipes (`route_env`),\n  per-format capability caveats. Read from route-setup whenever the\n  endpoint is not a plain OpenAI-shaped host.\n- **`references/prompt-files.md`**: the `--prompt` lint checklist and\n  keeping a user's prompt out of git.\n- **`references/epub-plan-mode.md`**: plan, classify, optional smoke, full\n  run, halt/resume, deliver, failure lines. EPUB only.\n- **`references/pdf-route.md`**: the whole PDF flow. PDF only.\n- **`references/plain-formats.md`**: the default command per TXT, SRT and\n  Markdown file, deliver, failure lines.\n","frontmatter":{"name":"bbm-plan","description":"Translate a book into a bilingual edition with bilingual_book_maker (\"bbm\") - an EPUB through plan mode (greedy partition, an agent-reviewed classification plan, then a full resumable run), a PDF into a bilingual, reflowable EPUB (--to-epub), or a TXT, SRT or Markdown file. Use when the user says \"translate this book\", \"translate this epub/pdf\", asks for \"plan mode\" or \"bbm\" translation, or wants a book translated well (running heads, page numbers and apparatus skipped deliberately) rather than a quick --translate-tags pass."},"isInternal":false,"tokens":1868,"sizeBytes":7506}],"systemPromptSnippet":"<yakaai_skills repo=\"yihong0618/bilingual_book_maker\">\n<!-- File: .agents/skills/bbm-plan/references/prompt-files.md (Tokens: ~617 | Category: anthropic-skill) -->\n# Prompt files: lint contract and git hygiene\n\nLoaded from SKILL.md's intake once a candidate `prompt*` file is found **and\nthe user has said to use it**. Do not lint before asking. The format itself,\nwith examples, is on `docs/en/prompt.md`; this file is the checklist to run\nagainst the user's file before the first paid run.\n\n## The contract (`book_maker/cli.py:parse_prompt_arg`)\n\n- `.json`: an object with **only** the keys `user` (required), `system` and\n  `style` (both optional). Any other key is rejected outright.\n- The `user` template must contain the literal placeholder `{text}`.\n  `{language}` is optional.\n- `.txt` becomes the user template as-is, same `{text}` rule.\n- `.md` is read as the PromptDown **block** form by the repo's own parser\n  (no package involved): a `## System Message`, an optional `## Style`,\n  and a `## Conversation` whose turns open with `**User:**` on its own\n  line. The `| Role | Content |` table form is refused with an error\n  naming the file and the block form. The repo's `prompt_md.prompt.md`\n  sample is a valid block-form template to copy. `## Developer Message`\n  is accepted as another name for the system section.\n- Unknown `{placeholders}` in the `user` template are refused at parse\n  time, before anything is paid; `{text}`, `{language}` and `{crlf}` are\n  the full set. A literal brace is `{{` / `}}`.\n\n```markdown\n# Translation Prompt\n\n## System Message\n\nYou are a professional translator. Keep the register of the original.\n\n## Conversation\n\n**User:**\nPlease translate the following text into {language}:\n\n{text}\n```\n\nFix or report lint problems before the paid run. The CLI would reject the\nfile at run start anyway, but a traceback after the user has already\napproved the cost is the wrong place to learn about a missing `{text}`.\n\n## Keep them out of git\n\nPrompt files are the user's personal voice and often carry character names\nor other personal terminology — same handling as `.env`. If the working directory is a\nrepo:\n\n```bash\ngit check-ignore prompt.json .env\n```\n\nAdd whatever comes back uncovered to `.git/info/exclude`. That file is\nlocal-only; never edit the project's tracked `.gitignore` for this.\n\n## Where the register goes\n\nA style instruction belongs in the `style` section (or the `system`\nmessage), stated once where a window starts — the run places it with the\nstanding instructions itself, never repeated per request. The `user`\ntemplate is sent for every unit — every word in it is paid for on every\nrequest of the book. A user-written style also replaces the model's own\nstyle notes in session handoffs.\n\n<!-- File: .agents/skills/bbm-plan/references/providers.md (Tokens: ~3672 | Category: anthropic-skill) -->\n# Routes: model name → endpoint shape → flags\n\nLoaded from `references/route-setup.md` step 3. The user pages cover the\nflags themselves: `docs/en/llm-args.md` (model, endpoint, format, keys),\n`docs/en/model_lang.md` (per-vendor addresses), `docs/en/providers.md` (the\nprovider file's fields, the image and classify models, which key is sent\nwhere). This file carries only what the agent needs beyond them: which\nshape to try, how to probe it, and what each route can do.\n\n## The one rule that decides everything\n\nThere is no model-name whitelist. A route is three flags plus the model id\nthe endpoint itself uses:\n\n```\n--api_base \"$ROOT\"  --key \"$KEY\"  [--api_format ...]  --model \"$MODEL\"\n```\n\n`--api_format` is inferred from the `--api_base` host first (`*.anthropic.com`\nmeans `anthropic`, anything else `openai`), then from the model id (`claude`\nor `anthropic` in it means `anthropic`). Pass it only to correct a wrong\nguess. Any model id reaches any endpoint; nothing has to be registered.\n\n| the endpoint speaks | flags |\n|---|---|\n| the OpenAI shape | `--api_base \"$ROOT/v1\" --model \"$MODEL\"` |\n| the anthropic shape, on an anthropic.com host | `--api_base \"$ROOT\" --model \"$MODEL\"` |\n| the anthropic shape, on a gateway domain | the same plus `--api_format anthropic` |\n| an entry in `bbm_providers.json` / `~/.bbm/providers.json` | `--provider NAME`, optionally `--model \"$MODEL\"` |\n| the OrcaRouter gateway | `--model orcarouter`, no `--api_base` |\n| nothing: a local Codex sidecar on the user's plan | `--api_format codex`, no key, no base (`references/route-setup.md` step 4) |\n\nOn the openai format `--model` may be left out; it defaults to\n`gpt-6-luna`. Every other format wants an id, and the anthropic format\nerrors without one.\n\n`codex` is the one route with no endpoint to probe. It is not a model id and\nnot a host, so none of the probes below apply to it. Spell it\n`--api_format codex`; `--model codex` is the same route under an older\nspelling.\n\nA gateway asked for the anthropic shape it does not serve answers 404, and\nthe run stops naming `--api_format openai` as the fix. `--api_base` may be\npasted with its path (`.../v1/chat/completions`); the path is trimmed.\n\nGroq, xAI, a LiteLLM proxy and every aggregator are reached through the\nOpenAI shape. The first three are also `--api_format groq` / `xai` /\n`litellm`, which is the same OpenAI route with that vendor's address filled\nin, so everything the OpenAI translator does — the structured-output probe,\ncontext, async, batch — applies to them.\n\nGemini and Qwen are the two exceptions: each has a translator of its own\nbecause neither protocol is the OpenAI shape. `--api_format gemini` talks to\nthe Gemini API (native constrained decoding, its own chat history, paced by\n`--interval`), and `--api_format qwen` talks to Qwen-MT on DashScope, whose\nrequest states a source and target language instead of carrying a prompt.\nBoth vendors *also* serve an OpenAI-compatible base, which the `openai`\nroute reaches — use that when the run needs the OpenAI translator's batch\nand session-context machinery.\n\n## `--provider NAME`: the same route, written once\n\nThe fields and the lookup order are on `docs/en/providers.md`; what matters\nfor the probe:\n\n- The `providers` wrapper is required; the loader reads nothing from a file\n  without it. A project `bbm_providers.json` wins over\n  `~/.bbm/providers.json` on a shared name; a name in neither falls back to\n  the shipped example, with a warning naming the address and key variable.\n- `api_style` is the format (`openai`, `anthropic`, `gemini`, `qwen`,\n  `groq`, `xai`, `litellm`; `claude` in older files). Any other\n  OpenAI-compatible host is `openai` with its address in `base_url`.\n- `default_models` becomes `--model` when it holds one id and\n  `--model_list` when it holds several. `env_key` is read for the key ahead\n  of `BBM_API_KEY` and the format's own variables. **Explicit flags win**,\n  so `--provider nvidia --model <id>` keeps the user's model.\n- A key is bound to its address: the entry's `env_key` is not sent when an\n  `--api_base` moves the run elsewhere. The `img_*` and `classify_*` fields\n  follow the same rule (`docs/en/providers.md#which-key-goes-where`); the\n  EPUB flow adds a `classify_model` only when the user names a classifier.\n\n## `--model orcarouter`: a gateway with no address to type\n\n`--model orcarouter` sends the run to OrcaRouter's OpenAI-shaped endpoint\nand asks for its smart-routing model, `orcarouter/auto`. It is a supported\nroute, not a legacy alias, so nothing is rewritten, and the key comes from\n`BBM_ORCAROUTER_API_KEY`.\n\n`orcarouter/auto` is the default, not a pin — to send the run to one named\nmodel instead of the smart router, use that entry:\n`--provider orcarouter --model <id>`. A probe reads the address\n(`https://api.orcarouter.ai/v1`) from there; it is never a flag you pass.\n\n## Binding `$KEY`, `$ROOT`, `$MODEL` from the entry before any probe\n\nThe entry decides which key variable to read. Do **not** take \"whichever\nkey is set\" — `.env` is sourced into a shell that may already export other\nproviders' keys from `~/.zshenv`, and a stale one would route the run to an\nendpoint the user never chose. Exit before curl when anything is missing:\nan empty bearer token produces a 401 that reads like a bad key.\n\n```bash\nroute_env() {   # $1 = provider name, as in bbm_providers.json / ~/.bbm/providers.json\n  eval \"$(python3 - \"$1\" <<'EOF'\nimport json, os, pathlib, shlex, sys\nname = sys.argv[1]; entry = None\nfor f in (pathlib.Path.home()/\".bbm\"/\"providers.json\", pathlib.Path(\"bbm_providers.json\")):\n    if f.is_file():\n        entry = json.load(open(f)).get(\"providers\", {}).get(name, entry)\nif entry is None:\n    print(f'echo \"no provider entry named {name}\" >&2; return 1'); sys.exit()\nstyle = entry.get(\"api_style\")\n# gemini and qwen speak their own protocols: the probes below would send this\n# entry's key to the wrong endpoint in the wrong shape, so refuse rather than\n# guess a shape for them.\nOPENAI_SHAPED = {\n    \"openai\": \"https://api.openai.com\",\n    \"groq\": \"https://api.groq.com/openai\",\n    \"xai\": \"https://api.x.ai\",\n    \"litellm\": \"http://localhost:4000\",\n}\nif style in (\"gemini\", \"qwen\"):\n    print(f'echo \"provider {name}: api_style {style!r} is a protocol of its own; run it with --provider {name}, these probes do not apply\" >&2; return 1'); sys.exit()\nif style not in OPENAI_SHAPED and style not in (\"anthropic\", \"claude\"):\n    print(f'echo \"provider {name}: api_style {style!r} is not a format; the loader refuses this entry\" >&2; return 1'); sys.exit()\nshape = \"anthropic\" if style in (\"anthropic\", \"claude\") else \"openai\"\ndefault_root = \"https://api.anthropic.com\" if shape == \"anthropic\" else OPENAI_SHAPED[style]\nroot = (entry.get(\"base_url\") or default_root).rstrip(\"/\")\nroot = root[:-3] if root.endswith(\"/v1\") else root\nkey_var = entry.get(\"env_key\") or \"BBM_API_KEY\"\nmodel = (entry.get(\"default_models\") or [\"\"])[0] or (\"gpt-6-luna\" if shape == \"openai\" else \"\")\nprint(f\"SHAPE={shape} ROOT={shlex.quote(root)} MODEL={shlex.quote(model)} KEY_VAR={key_var}\")\nEOF\n)\"\n  [ -n \"${SHAPE:-}\" ] || return 1\n  KEY=\"$(printenv \"$KEY_VAR\" 2>/dev/null || true)\"   # same in bash and zsh; .env is exported by set -a\n  [ -n \"$MODEL\" ] || { echo \"entry $1 names no model (the anthropic format needs one)\" >&2; return 1; }\n  [ -n \"$KEY\" ]   || { echo \"$KEY_VAR is unset — fill .env\" >&2; return 1; }\n}\n```\n\n`$ROOT` comes out with no trailing `/v1` whatever the entry wrote, so every\nprobe path below is spelled in full. On a gateway the key belongs to the\n*gateway*, not the model's vendor: a Claude model reached over the OpenAI\nshape reads the gateway entry's `env_key`, because that is the shape being\nspoken.\n\n## Shapes, and how to probe each\n\n`$ROOT` is the scheme+host with no `/v1` (`route_env` guarantees it), so\nevery path below is written out in full. Each probe is one tiny call, a\nfraction of a cent.\n\n**No token cap on the OpenAI shape.** `max_tokens: 1` looks thrifty and is a\nfalse negative twice over: gateways reject caps below their own floor\n(measured: `max_tokens must be greater than 2` on *every* model of one\ngateway), and OpenAI's own o-series/gpt-5 models reject `max_tokens`\noutright in favour of `max_completion_tokens`. A probe must test one thing.\nThe repo's own probes send no cap for exactly this reason\n(`translator/capabilities.py`, `probe_structured_output` and\n`probe_model_route`); match them. A capped probe against gpt-5.6-luna came\nback \"'max_tokens' is not supported with this model\" and confirmed nothing.\nThe reply is a few tokens of \"Hi!\".\n\n**OpenAI shape** — the universal one. Most gateways serve every model they\nhost on it, whoever made the model.\n\n```bash\ncurl -sS \"$ROOT/v1/chat/completions\" \\\n  -H \"Authorization: Bearer $KEY\" -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.choices[0]`. → `--api_base \"$ROOT/v1\" --key \"$KEY\"\n--model \"$MODEL\"`\n\n**Anthropic shape**. `max_tokens` is *mandatory* here, unlike above; 16 is\npast every floor seen so far.\n\n```bash\ncurl -sS \"$ROOT/v1/messages\" \\\n  -H \"x-api-key: $KEY\" -H 'anthropic-version: 2023-06-01' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"model\":\"'\"$MODEL\"'\",\"max_tokens\":16,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n```\n\nPasses when the body has `.content[0]`. → `--api_base \"$ROOT\" --key \"$KEY\"\n--model \"$MODEL\"` (add `--api_format anthropic` when the host is not\nanthropic.com). The reply's `model` field echoes the id the endpoint actually\nresolved to (`claude-haiku-4.5` → `claude-haiku-4-5-20251001`). Real Anthropic requires\n`x-api-key`; gateways commonly accept `Authorization: Bearer` too, so try\n`x-api-key` first and Bearer second.\n\n**`--api_base` for this route takes the root, not `/v1`**: the SDK appends\n`/v1/messages` itself. Passing `https://host/v1` used to produce\n`/v1/v1/messages` and a 403 reading \"HTTP node only allows access to\ninference API paths\"; a trailing `/v1` is now trimmed automatically (with a\nprinted note), so either form works — but say `https://host` and mean it.\n\n**Gemini** has a route of its own — `--api_format gemini`, over the Gemini\nAPI — which the OpenAI-shape and anthropic-shape probes here do not reach.\nTo probe from a shell, use its OpenAI-compatible base instead,\n`https://generativelanguage.googleapis.com/v1beta/openai/`, with the\nOpenAI-shape call above; that base is also the one to run against when the\ntranslation wants batching or session context. **Qwen-MT** is the same\nstory at `https://dashscope.aliyuncs.com/compatible-mode/v1`.\n\n## Inferring which shape to try first, from the model name\n\n| model name starts with | try first | then |\n|---|---|---|\n| `claude-` | OpenAI if a gateway base is set; else anthropic | the other one |\n| anything else (`gpt-`, `o1`, `o3`, `gemini-`, `grok-`, `llama`, `qwen`, …) | OpenAI | — |\n\nOpenAI first at a gateway because aggregators serve Claude and Gemini\nmodels on `/chat/completions` too. Go native only when the endpoint is\nAnthropic's own, or when the gateway rejects the OpenAI shape.\n\n**The chat call is the verdict; the listing is only a hint.** `GET\n$ROOT/v1/models` (Bearer auth) is free on OpenAI-shaped endpoints and\nreturns `{\"data\":[{\"id\":…}]}`, but gateways routinely serve a model they do\nnot list, or list it under another id. A name missing from the listing is\nno reason to give up: run the chat probe, and a reply is the answer. Fetch\nthe listing when the probe fails, to tell a typo'd id from an unsupported\npath; both return 404, and only the listing separates them. The run does\nthe same (`translator/capabilities.py`, `probe_model_route`, once at the\nfirst paid call). Some gateways add `supported_endpoint_types` per row (on\none aggregator every row read `['openai', 'anthropic']`); when it is there\nit answers the shape question outright.\n\n## Capability caveats per route\n\n| `--api_format` | translation | plan-mode classification |\n|---|---|---|\n| `openai` (any host) | schema when the probe says `strict`, else delimiter | yes |\n| `anthropic` | delimiter (no structured-output work was done for it) | yes, via the prompt rung |\n| `codex` | one turn per unit, on a thread that is itself the context window | yes, via the prompt rung — the sidecar compiles no schema |\n| `google`, `deepl`, `deeplfree`, `caiyun`, `tencent`, `customapi` | translation only | only with a classifier of its own (`--classify-model`, or an entry's `classify_model`) |\n\nClassification capability does not gate the default flow — `--plan-classify\nagent` makes no API call, you are the classifier. It matters only when the\nuser names a classifier (`references/epub-plan-mode.md`, \"A classifier the\nuser names\") and that classifier is the translating model.\n\nThe translation services have one channel, and it translates\nwhatever it is handed. They cannot be asked a question.\n\n## What the run's own probe does later\n\nAt first paid use, the OpenAI-shaped translator sends a one-key schema probe\nand grades the endpoint `strict` / `shape` / `json` / unsupported. Only\n`strict` gets a schema for *translation* — the translation schema pins the\ntarget language as a value constraint, so an endpoint that ignores values\nwould drop it. Everything else falls back to the delimiter method, which is\nfine and prints one yellow line. This is capability discovery, not an error;\ndo not report it to the user as a failure.\n\n<!-- File: .agents/skills/bbm-plan/SKILL.md (Tokens: ~1868 | Category: anthropic-skill) -->\n---\nname: bbm-plan\ndescription: Translate a book into a bilingual edition with bilingual_book_maker (\"bbm\") - an EPUB through plan mode (greedy partition, an agent-reviewed classification plan, then a full resumable run), a PDF into a bilingual, reflowable EPUB (--to-epub), or a TXT, SRT or Markdown file. Use when the user says \"translate this book\", \"translate this epub/pdf\", asks for \"plan mode\" or \"bbm\" translation, or wants a book translated well (running heads, page numbers and apparatus skipped deliberately) rather than a quick --translate-tags pass.\n---\n\n# bbm-plan: translate a book with bilingual_book_maker\n\nThis file is the router. Do the intake below, then load **one** flow file\nfor the book in hand and follow it. The other flow files stay unread.\n\nRepo: the repository this skill ships in (`make_book.py` at its root). Run\nevery command from the repo root. The user documentation is the MkDocs site\nin `docs/en/` of this checkout; the flow files point at the page to read for\nanything they do not spell out. `python make_book.py --help` is the\nauthority on every flag, and `docs/en/cmd.md` lists them all in one place.\n\n## Your stance\n\nYou are the trained CLI operator. You pick every flag from the flow file's\nrecommendations and state each choice with a one-line reason; the\nrecommended defaults are what you pass unless the user asks for something\nelse or the book argues otherwise. The user hands over a book, credentials\nand (if they have one) a prompt file, approves the plan and the cost, and\ngets a bilingual book back. They are never asked to experiment with flags,\nhalt semantics or resume mechanics.\n\n**The EPUB flow uses `--plan-classify agent`.** The plan arrives with its\nuncertain signatures set to `\"action\": null`, and the translate run\nrefuses to start while any null remains, so *you*, the coding agent, own\nthe classification against the real samples. Do it in the main agent with\nfull session context; never delegate plan editing to a subagent or a\nsmall/fast model. The one exception is the user naming a classifier\nthemselves (Jev, or a model): then follow \"A classifier the user names\" in\n`references/epub-plan-mode.md`, and still read its skips.\n\nAll state lives on disk (`bbm_providers.json`, `.env`, the flow's own\nworking files, the resume cache, `run.log`), so any step can be redone\nafter a crash or in a new session.\n\n## Intake\n\n1. **Route and credentials: always read `references/route-setup.md`**\n   before the first command. It probes what the user already has, asks one\n   question (route, model, prompt file), binds the `ROUTE` and `CONTEXT`\n   arrays every flow uses, and probes the endpoint for a sub-cent before\n   anything is paid. **Keys never go on the command line**: the handoff\n   block reprints the whole command, and argparse echoes it back on a\n   mistyped flag. If a key ever reaches the terminal, say so at once and\n   tell the user to rotate it.\n2. **Book path** and **target language** (`--language`, e.g. `zh-hans`,\n   `ja`, `Simplified Chinese`). For a small language the tables may not\n   know, pass both halves: `--language \"ain:Ainu\"` (tag before the colon,\n   name after; the tag list is `docs/en/languages.md`). `--source_lang` states\n   the source language for a book whose short lines or names could be\n   misdetected.\n3. **Their prompt file**, asked in the route question. If they hand one\n   over, lint it before the first paid run: `references/prompt-files.md`.\n   If they say no, offer one sentence of what a style instruction would buy\n   them and move on. Independently, look for an existing template in the\n   book's directory and the repo root (`prompt.json`, `prompt.txt`,\n   `prompt*.md`, `prompt_template*`). **A candidate must carry a diff**: in\n   a git repo only untracked or modified-against-HEAD files count; cleanly\n   tracked `prompt*` files are the repo's shipped examples, not the user's\n   voice. Found one? **Ask before doing anything with it**; never adopt or\n   ignore it silently.\n4. Anything they want changed from the recommended defaults: a visual style\n   for the translation, specific chapters or pages only.\n\n**Bilingual is the assumption; do not ask.** The output keeps every\noriginal next to its translation unless the user asked for a\ntranslated-only book in so many words (\"just the Chinese\", \"replace the\noriginal\", \"not bilingual\", \"single language\"). *\"Translate this book to\nChinese\"* is not that request; it is the ordinary ask, and it means a\nbilingual book. Never infer single-language output (`--single_translate`)\nfrom the target language being named, from the book being short, or from\nthe user sounding brisk. Say which one you are producing in the same line\nwhere you state the flag choices, so a user who wanted the other one can\nsay so before anything is paid for.\n\n## Branch by the book\n\n| the book is | read, and follow | the user page behind it |\n|---|---|---|\n| an `.epub` | `references/epub-plan-mode.md` | `docs/en/formats/epub.md` |\n| a `.pdf` | `references/pdf-route.md` | `docs/en/features/pdf-to-epub.md` |\n| a `.md` Datalab wrote (`{N}------` page separators), or any `.md` the user wants as an EPUB | `references/pdf-route.md` (`--to-epub` on the `.md`) | `docs/en/features/pdf-to-epub.md` |\n| a `.txt`, `.srt` or `.md` | `references/plain-formats.md` | `docs/en/formats/txt.md`, `docs/en/formats/srt.md`, `docs/en/formats/md.md` |\n\nA file with another extension is not a book this tool reads; say so. A\nfolder of chapters is several books: ask whether they want one run per\nfile.\n\n## Stating the choices\n\nBefore the first paid run, one short block to the user:\n\n- the command you will run, with one line per non-default flag saying why;\n- bilingual or translated-only, in so many words;\n- the checkpoint the flow file puts before the full run (what the user\n  will be shown, and when), and any test run you skip or add;\n- which allowance is spent: the API key's account, or the ChatGPT plan on\n  the codex route.\n\nThen wait for the approval of the cost, and run.\n\n## Context hygiene\n\n- **Never** let a translation run stream into the conversation: every paid\n  run gets `--quiet` where the format takes it *and* a log-file redirect\n  (`> run.log 2>&1`), in the background, polled with `tail -5 run.log`.\n- Everything the next step needs is on disk; nothing critical lives only in\n  conversation.\n- **Compaction threshold**, *your* context, not the run's: for a small book\n  do not compact at all. Only compact when context is genuinely pressured\n  (roughly 70% used), at most once, at the natural boundary: after the\n  flow's checkpoint, before the full run.\n\n## Reference files\n\nLoad each one only when the step that needs it runs.\n\n- **`references/route-setup.md`**: credential probe, the route question,\n  `ROUTE`/`CONTEXT` by format, the endpoint probe, the codex route, local\n  models, the route and context flag menu, route failure lines. Every flow.\n- **`references/providers.md`**: route table, probe recipes (`route_env`),\n  per-format capability caveats. Read from route-setup whenever the\n  endpoint is not a plain OpenAI-shaped host.\n- **`references/prompt-files.md`**: the `--prompt` lint checklist and\n  keeping a user's prompt out of git.\n- **`references/epub-plan-mode.md`**: plan, classify, optional smoke, full\n  run, halt/resume, deliver, failure lines. EPUB only.\n- **`references/pdf-route.md`**: the whole PDF flow. PDF only.\n- **`references/plain-formats.md`**: the default command per TXT, SRT and\n  Markdown file, deliver, failure lines.\n</yakaai_skills>"}