nav job mode: whole-menu titles as one nested list; Kimi Code API backend

- translate.py: new "nav" job mode (Hello.modes opt-in) — the whole
  navigation hierarchy crosses as one nested Markdown list of pending
  titles, decomposed back by align_nav: item count/depth must match or
  the job is rejected wholesale (titles fall back to scoped jobs);
  items failing title checks individually are skipped to scoped jobs.
  Dispatched ahead of per-title jobs; a lone pending title stays scoped.
- article jobs carry the already-translated menu title and parent title
  as contexts, so the injected heading can match the menu while the
  model may adapt the in-article title to the content.
- llm_translator.py: nav mode + nav_prompt; article prompt takes the
  title/location context; API keys from per-provider env vars only
  (KIMI/MOONSHOT/OPENAI_API_KEY, each sent only to its own host;
  LLM_API_KEY generic) — no CLI flag, no config file; Kimi Code /coding
  endpoint support (sampling fields dropped, reasoning_effort from
  config, field-proven with k3-256k at low effort); errors include the
  response body; verbose per-job logging with the raw response incl.
  thinking (stripped from results); Kimi models announce all languages.
This commit is contained in:
2026-09-21 14:00:51 +00:00
parent 911e28efbe
commit b1ce15f3cc
5 changed files with 386 additions and 101 deletions
+35 -8
View File
@@ -98,7 +98,7 @@ class Hello(msgspec.Struct, tag="hello"):
modes: list[str] = ["segments"] # job granularities accepted
```
Three job modes, in increasing granularity:
Four job modes, in increasing granularity:
- **`segments`** — the current protocol, unchanged: `Job.texts` carries
prose segments (markup never crosses the wire), `Result.texts` returns
@@ -122,14 +122,29 @@ Three job modes, in increasing granularity:
original Markdown (the chunk sequence is recoverable server-side via
`node.chunks`); `Result.texts` carries one element, the full translated
Markdown. The server decomposes (below) and stores per chunk.
- **`nav`** — the whole navigation hierarchy. `Job.texts` carries one
element, a nested Markdown list of every node title still pending for
the language (`- Title`, indented by depth, in menu order);
`Result.texts` carries one element, the translated list. The server
decomposes by list structure (`align_nav`): item count and nesting
depth must match the source item for item, then each item is stored as
a per-title fragment under its title's chunk hash.
Titles are jobs like any other in all modes (`kind="title"` keeps its
article-opening context rule; in `markdown` mode a title crosses as
plain text, since it carries no markup by construction). Additionally, an
plain text, since it carries no markup by construction) — but for
nav-capable connections a single `nav` job names the entire menu first:
one round trip instead of one per page, with siblings, parents and
children translating in sight of each other. A structurally mangled list
is rejected wholesale and the titles fall back to scoped title jobs.
Additionally, an
`article` job carries the page title injected as a `# {title}` line at
the top when the render would inject it (the body has no h1 of its own):
the title translates in document context and the opening paragraphs see
the heading. The heading's pair in the decomposed result becomes the
the heading. The menu title's and parent node's existing translations
ride along as `Job.contexts` ("" where none), so the heading can match
the menu while the model may still adapt the in-article title to the
content. The heading's pair in the decomposed result becomes the
title fragment (heading text only, never stored as a body chunk).
### Dispatch and validation
@@ -175,20 +190,32 @@ A second client script next to `scripts/translator.py` speaking the
`markdown` and `article` modes. Internally it targets the **OpenAI
Chat Completions API shape** (`POST /v1/chat/completions`): ollama
serves it at `:11434/v1`, llama.cpp's server likewise, and hosted APIs
(OpenAI and compatible providers) natively — `base_url` + `model` +
optional API key in the client's config selects local GPU, local CPU or
a remote model, with backend quirks (ollama's `think: false`,
`num_predict` cap, per-model sampling) in a per-model config section.
(OpenAI and compatible providers) natively — `--base-url` + `--model`
selects local GPU, local CPU or a remote model, the API key comes from
the standard per-provider environment variable (`KIMI_API_KEY`,
`MOONSHOT_API_KEY`, `OPENAI_API_KEY`, each sent only to its own
provider's host; `LLM_API_KEY` for anything else) — deliberately never
a CLI flag or a config file — and backend quirks (ollama's
`think: false`, `num_predict` cap, per-model sampling) live in the
script's `DEFAULT_CONFIG`.
How the client drives its LLM is its internal matter; the wire protocol
above is the contract.
Field-proven backends: the local qwen3.8:27b of the trials above, and
the **Kimi Code API** (`--base-url https://api.kimi.com/coding` resp.
`api.kimi.ai`, `--model k3-256k`): the `/coding` endpoint fixes sampling
internally (the client drops `temperature`/`top_p` for it — they 400)
and runs `reasoning_effort: low` from the config, which produces good
translations at a fraction of the default (high) effort's latency and
quota; thinking output is logged verbatim but stripped from the result.
The client announces in `Hello`:
- `model`: the model string it is actually serving (e.g. `qwen3.8:27b`)
- `langs`: from its per-model language table — for the shipped qwen3.8
configuration the site languages as configured server-side
(de, es, fi, pt, zh; Finnish flagged as the weakest, patch-covered)
- `modes`: `["markdown", "article"]` for a structure-proven model,
- `modes`: `["markdown", "article", "nav"]` for a structure-proven model,
`["markdown"]` for one that is only trusted in scoped mode
The Seed-X client is untouched and announces `["segments"]` (implicitly,