7 Commits
Author SHA1 Message Date
tazlin 0822891e84 docs: wording, structure, and chat accuracy
Broad editorial pass over docs/ plus a nav update in zensical.toml.

- Wording and style: replace "carries"/"answers" phrasing with plainer terms, use periods instead of semicolons, and split long paragraphs.
- Structure: add subheadings and tables in the chat, moderation, kudos, model reference and style contract docs. Add "Code map" and "Contract tests" sections to several reference pages. Move the code maps in chat_front.md and chat_requests.md to the end of those pages. Reorder the minimal chat worker page so the procedure comes before the source listing.
- Content fixes:
  - Document the 404 ChatJobAborted vs ChatProgressNotStreaming progress responses.
  - Document the availability refusal table (503/404/400).
  - Document fail-fast admission and queue-position refresh.
  - Fix the stream parser example to check response.ok and error on a missing [DONE].
  - Fix the style contract's max_length ceiling (16384).
  - Note that the kudos heartbeat metrics are quorum-node only.
  - Clarify tool_choice, strict conformance and capability caveats in the tutorial.
- Navigation: reorder Tutorial and Topics in the docs landing page and zensical nav. Exclude /openai-chat-local/ from builds.
- Chat doc summaries were rewritten in several front matter blocks and the generated README tables updated to match.
2026-11-02 20:38:38 -05:00
tazlin b3eb8a8877 docs: adjust copy for legibility, tone and readability
- Reworded the chat worker how-to, tutorial, wire examples and the explanation page so each sentence states one thing in plain terms
- The same pass over the docstrings, comments and log messages of the minimal chat worker tool and the chat test helpers
- Renamed the wire-example tests' answered_by field to response_example
2026-11-02 13:31:00 -05:00
tazlin 9d26500362 docs: add checked chat wire examples and link the reference pages to them
- docs/reference/chat_wire_examples.md holds the JSON bodies of native requesters, OpenAI front clients and text workers, with a sequence diagram of one chat job
- test_chat_wire_examples.py and test_chat_front_wire_examples.py capture each response body live and compare it with the page: keys, the values of rc, code, type, state, finish_reason, kind, reason and booleans, and the JSON type of every other scalar; documented requests are posted, and worker request keys are checked against the route's parser and models
- tests/integration/_wire_examples.py holds the page parsing and comparison both test modules share
- serve_chat_jobs.md opens with one chat job in full; it, chat_requests.md and chat_front.md include wire examples through pymdownx.snippets sections, so a shown body cannot drift from the checked one
- chat_requests.md and chat_front.md link each major section to its examples; the 202 description states when message and warnings appear, and the front's error descriptions carry the 404 type and metadata.rc
2026-11-02 13:30:59 -05:00
tazlin f26a7ba5b0 docs: describe koboldcpp 1.124's embedded chat worker and text worker details
- serve_chat_jobs.md: the koboldcpp worked example follows release 1.124, which declares chat on every pop, pops from aihorde.net, declares only the prompt_overflow_refused guarantee, estimates fit from the request JSON and faults with prompt_overflow or reply_refused; step 1 and the example verify the declaration through worker details
- first_chat_worker.md: the tutorial runs on aihorde.net with a model name no other worker serves, lists the release feature set, and sends its forced tool request as best_effort, since no release koboldcpp worker declares tool_choice_enforced
- chat_requests.md: koboldcpp faults a refused reply only after ChatSubmitInvalid, ChatSubmitNonconformant or ChatModelMismatch
- send_chat_completions.md: a strict request no worker can serve ends with 404 no_worker_supports_features
2026-11-02 13:30:58 -05:00
tazlin d8068eea4b feat(chat): stream worker progress to clients through a retained Redis stream
A streamed request without tools or a non-text response format gets its content as the worker generates it, about one chunk a second (ADR 0024).

- `stream: true` is stored as the stream hint in `chat_extras` and reaches the worker as `payload.stream_progress`, so a worker posts deltas only when a client streams
- The pop writes a lease for a streamed chat generation on the chat signal database with the worker, the owner's key digest, the assigned model and the byte cap, so the progress route authenticates without a Postgres read
- `POST /v2/generate/text/progress`: a Lua append (`horde/chat_progress.lua`, beside its module) checks the lease, the sequence and the caps atomically, acknowledges an identical retry and refuses a gap with the expected `seq`; accepted deltas go to `horde:chat:stream:<request id>`, and each entry carries the lease's worker and model
- A faulted streamed generation gets an ended marker and its lease is released; a successful submit releases the lease and leaves the stream to expire; the signal channel does not carry text
- The front relays only requests that stream without tools or a non-text `response_format`, since the other replies are verified whole at submit, and forwards `stream: true` for those alone when it has a stream reader
- One `DeltaStreamReader` per process reads every registered request's stream with a blocking `XREAD` on `HORDE_OAI_STREAM_REDIS_DB` and dispatches entries to per-request queues; a failed read resumes from the last entry read
- The relay sends the role chunk at the first delta and one content chunk per delta, reconciles with the final reply by equality or prefix, and ends with `ChatStreamDiverged` on a mismatch, a replaced attempt, an ended marker or a sequence gap after exposure; every other case, and a stream that did not send anything before the final reply, is delivered whole
- `system_fingerprint` is computed once at the first delta from the entry's worker and model and set on every chunk of the stream
- The generic `NotFound` is registered with the API error handler, so its refusal carries an `rc`; fakeredis with Lua joins the dev group so the append script is unit-tested
- Tests cover the lease, the append script, the route's refusals, the relay outcomes, the fingerprint, and a stream deleted, expired or recreated with lower ids before and after exposure
- The explanation's brief and the first-completion tutorial state that a plain streamed request receives its text as the worker posts it
2026-10-30 23:46:07 -04:00
tazlin ae799932b1 feat(chat): align request validation with OpenAI
Where OpenAI refuses a request shape, the Horde refuses it with OpenAI's wording and `param`, so a client written against OpenAI fails the same way on both services.

- The front drops unknown keys inside messages and their nested objects, inside `tools` definitions and inside the `tool_choice` object before validation, so they are not forwarded; the top level and every other object (`horde`, `stream_options`, `response_format`) keep refusing unknown keys, and `tool_calls` on a message other than an assistant message is refused; OpenAI's API draws the same line, and the OpenAI SDK replays its accumulated streamed reply with fields it adds (ADR 0029 amended)
- Shared rules in `chat_contract`, applied by the native route and the front: `tool_choice` or `parallel_tool_calls` without tools is refused; an assistant message's tool calls are answered at once, each id exactly once, by tool messages that answer only that message; a strict `json_schema` sets `additionalProperties: false` and lists every property in `required` at every object
- `horde.openai_guards`, off by default, turns on the OpenAI refusals the Horde relaxes on purpose, such as a `json_object` request whose messages do not mention JSON; it acts at admission and is not stored
- A request that forwards tools beside a response format needs the `tools_with_response_format` capability under either mode, so a worker that cannot apply the format to the final answer does not receive it and the front reports the declaration gap
- Schemas are bounded at `MAX_CHAT_SCHEMA_NESTING` levels during validation, before serialisation can exceed the recursion limit; NUL is refused in schema keys; tool-call names and arguments in the history count toward the text byte cap; the response-format schema does not count as prompt in the fit and usage estimates, since the reference worker installs a grammar and does not render it; more tools than the limit is `UnsupportedChatParameter`
- A body holding `NaN`, `Infinity` or `-Infinity`, which are not JSON, is refused like a body that is not JSON; `refuse_json_constant` in `horde/chat_contract.py` is the parse hook for the front's body, the native chat body on both the Content-Length and chunked paths, and reply parsing
- `ChatJsonSchema` reads its schema from the public key `schema` only, so `schema_` is an unknown key and refused with its path as `param`
- Tests cover each rule on both routes and both body paths; the references, how-tos, tutorial and explanation describe the rules
2026-10-30 23:46:05 -04:00
tazlin 9453d62052 docs(chat): tutorials, how-tos and explanation for chat integrators
- Two tutorials open `docs/tutorial/`: a client's first chat completion with the OpenAI SDK (model choice, completion, stream, tool round, `json_schema` reply) and a worker's first chat job with koboldcpp (declaration, registration, a chat and a tool request on the native route)
- How-tos for sending chat completions from an OpenAI client, adding chat to a web frontend and serving chat jobs from a text worker
- `docs/explanation/chat_requests.md` covers the template problem, each design choice with its decision record and the rejected alternative, and what a client can and cannot rely on
- `chat` joins the topic vocabulary; the navigation lists the tutorial class, the pages and the chat front and outbound requests references; indexes regenerated
2026-10-30 23:46:04 -04:00