GenerateOptions.prefill had no production setter and both adapters
rejected it with LlmError('UNSUPPORTED') — its entire observable
behavior was two throws, each pinned by one adapter test. DeepSeek's
chat-prefix completion is a Beta feature on a base URL neither adapter
targets. ToolSchema.strict was threaded through defineTool, the
registry's schemas() allowlist, the deepseek wire mapping, a per-tool
payload-patching pass in the pi-ai adapter, and a tool-catalog render
row, yet no shipped tool set it and the internal endpoint story for
strict mode was never built.
Remove both fields end-to-end: the vocabulary in dsh-llm, the adapter
guards and wire branches, the dsh-tools threading, the tool-catalog
Strict row, the pinning tests, the core.md pastes, the adapter README
rows, and the cookbook line that used prefill as the UNSUPPORTED
example (now stated generically). The pi-ai payload fixup keeps the
half with a job: pi-ai stamps strict:false on every serialized tool,
so the fixup scrubs it unconditionally for wire parity with the
hand-rolled twin (per-tool set/delete machinery gone). temperature/
stop/maxTokens are untouched — honored end-to-end by both adapters.
Each knob returns with its first real producer: prefill with an
adapter that implements chat-prefix completion, strict with a tool
that wants it and a beta-endpoint story.
RFC: docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
(moved from proposed/, amended to shipped reality); the content-block
vocabulary RFC's consequence line now records prefill as producer-gated.
5.0 KiB
RFC: Drop GenerateOptions.prefill and ToolSchema.strict — request knobs with no working end-to-end path
Status: implemented (proposed and accepted 2026-07-04)
Problem
Two request-contract knobs rode the whole request pipeline, yet neither could do anything:
prefill(packages/llm/llm/src/types.ts) had no production setter — the loop assemblesmodel/system/tools/messagesplussessionId/signal, and the compaction backend adds onlymaxTokens— and BOTH adapters rejected it:packages/llm/llm-deepseek/src/serialize.tsandpackages/llm/llm-pi-ai/src/adapter.tseach threwLlmError('UNSUPPORTED')on a non-undefinedprefill. The field's entire observable behavior was two throws, each pinned by one adapter test. DeepSeek's chat-prefix completion is a Beta feature on a base URL neither adapter targets.strict(ToolSchema, same file) was threaded throughDefineToolOptions/defineTool(packages/core/tools/src/schema.ts), the registry'sschemas()allowlist (packages/core/tools/src/index.ts), the deepseek wire mapping (packages/llm/llm-deepseek/src/serialize.ts, whose wire-type note recorded that strict mode requires the/betabase URL the adapter does not use), a per-tool payload-patching pass inpackages/llm/llm-pi-ai/src/adapter.ts, and a conditionalStrict:row in the tool-catalog renderer (scripts/gen-tool-catalog.ts). No shipped tool set it —rgacross everytool-*package src andexamples/found zerostrict:producers; the only setters were dsh-tools unit tests.
Both knobs were adapter-symmetric, so removal shed them from both twins together — the twin-adapter design is untouched.
Decision
prefillis removed fromGenerateOptions, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in core.md, and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance (adding-an-llm-adapter.md) states the rule generically — aGenerateOptionsfield your provider cannot honor throwsLlmError(..., 'UNSUPPORTED')— instead of using prefill as the example. The content-block vocabulary RFC's consequences record prefill as producer-gated rather than as having a home, per implemented/AGENTS.md.strictis removed fromToolSchema,DefineToolOptions,defineTool, theschemas()allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer'sStrict:row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stampsstrict: falseon every serialized tool; the hand-rolled twin sends no such field, so the scrub survives for wire parity, pinned by its serializer test). The setter tests and the core.md paste line are gone; bothGenerateOptionsandToolSchemakeep their rows inscripts/type-equiv.manifest.json, since each type survives minus a field.
This RFC deliberately does NOT touch temperature, stop, or maxTokens: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on agent/request.
Why not keep them?
"An explicit UNSUPPORTED throw is honest contract behavior" — but a knob whose only implementation across both twins is rejection promises nothing, and deleting it upgrades the failure mode: an accidental setter becomes a compile error instead of a runtime throw. "Strict schema adherence is an officially documented provider feature with complete plumbing" — but a knob is not product surface until a shipped tool sets it AND an endpoint honors it; today neither is true. Each returns with its first real producer: prefill together with an adapter that implements chat-prefix completion (and a stated policy for adapters that do not), strict together with a tool that wants it and a beta-endpoint story.
Acceptance criteria
rg prefillreturns only RFC records (this one and the content-block vocabulary RFC's producer-gated consequence); a tool-schema-scopedrg strictreturns only this RFC, the surviving pi-ai scrub, and unrelated prose such asstrictEqual.- Both adapters compile and their contract tests pass without the guards; the pi-ai fixup still scrubs the library's strict default (wire parity pinned by its serializer tests).
- Doc pastes and the type-equiv manifest in sync;
pnpm run doc-syncgreen.
Risks
The shipped hook bridges set no request fields at all, and a request-mutating plugin (an agent/request waterfall listener) would reach for temperature/stop (kept, working), not a field adapters reject. If chat-prefix completion or strict mode become product features, the re-add lands with the adapter/endpoint work, where the contract can say what actually happens rather than "everyone throws".