Pārlūkot izejas kodu

refactor(llm): drop the inert request knobs — prefill and strict

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.
Tianyi Cui 3 mēneši atpakaļ
vecāks
revīzija
5a8234643a

+ 1 - 1
docs/cookbook/adding-an-llm-adapter.md

@@ -27,7 +27,7 @@ Registration is effect-based (HMR-safe); one adapter per model name — duplicat
 - Allocate block `index`es in first-seen stream order; reuse the index for every delta of the same block.
 - Errors have exactly two sanctioned paths: THROW from `stream()` (transport and protocol failures — use `LlmError` with a stable code), or end the stream with `finish {kind: 'error' | 'aborted'}` (provider in-band failures). Consumers handle both; pick per failure class and document it.
 - Honor `options.signal` (pass it to fetch / your SDK).
-- `prefill` and other unsupported `GenerateOptions` fields: throw `LlmError(..., 'UNSUPPORTED')` rather than silently dropping.
+- A `GenerateOptions` field your provider cannot honor (e.g. a `stop` list on a provider without stop sequences): throw `LlmError(..., 'UNSUPPORTED')` rather than silently dropping it.
 
 Provider-specific request knobs (thinking modes, effort levels) belong in the ADAPTER's Config, not in `GenerateOptions` — the core vocabulary stays provider-neutral.
 

+ 0 - 3
docs/core-data-structures/core.md

@@ -130,8 +130,6 @@ interface GenerateOptions {
   system?: string
   /** Tool schemas (adapters map to the provider's `tools` field). */
   tools?: ToolSchema[]
-  /** Assistant prefix continuation (prefill). */
-  prefill?: ContentBlock[]
   temperature?: number
   maxTokens?: number
   /**
@@ -180,7 +178,6 @@ interface ToolSchema {
   description: string
   /** JSON Schema object for the arguments. */
   parameters: Record<string, unknown>
-  strict?: boolean
 }
 ```
 

+ 1 - 1
docs/rfc/README.md

@@ -52,7 +52,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | Title | First proposed |
 |---|---|
 | [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 |
-| [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](proposed/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 |
 | [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](proposed/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 |
 | [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
 | [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 |
@@ -119,6 +118,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 |
 | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 |
 | [Drop the `image` content block until a path can honor it](implemented/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 |
+| [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](implemented/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 |
 | [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 |
 
 ### Architecture

+ 1 - 1
docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md

@@ -16,6 +16,6 @@ In-session context injection (`context/message`, `steering/message`) renders as
 
 ## Consequences
 
-- Reasoning and prefill have a home without provider contortions. Multimodal content deliberately has NO core block type: the core set is limited to blocks every shipping path honors, and a multimodal feature adds its block type through the merge-extensible map in the same coordinated change that maps it in the adapters, surfaces it in the UI bridges, and prices it in compaction — see [the drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md). Block cache hints likewise have no core field: DeepSeek prompt caching is automatic, so no shipping adapter can transmit a hint; a caching feature adds a `cache` field together with the adapter that honors it — see [the producer-less-variants RFC](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md).
+- Reasoning has a home without provider contortions. Multimodal content deliberately has NO core block type: the core set is limited to blocks every shipping path honors, and a multimodal feature adds its block type through the merge-extensible map in the same coordinated change that maps it in the adapters, surfaces it in the UI bridges, and prices it in compaction — see [the drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md). Block cache hints likewise have no core field: DeepSeek prompt caching is automatic, so no shipping adapter can transmit a hint; a caching feature adds a `cache` field together with the adapter that honors it — see [the producer-less-variants RFC](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md). Assistant-prefix continuation (prefill) likewise has no request field: DeepSeek's chat-prefix completion is a Beta feature on a base URL neither shipping adapter targets, so a prefill feature adds `GenerateOptions.prefill` together with the adapter that honors it — see [the inert-request-knobs RFC](../simplification/2026-07-04-drop-inert-request-knobs.md).
 - Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
 - IDs that cross package boundaries are branded (`CallId`, `SessionId`, `AgentId`) — nominal typing at zero runtime cost.

+ 33 - 0
docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md

@@ -0,0 +1,33 @@
+# 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 assembles `model`/`system`/`tools`/`messages` plus `sessionId`/`signal`, and the compaction backend adds only `maxTokens` — and BOTH adapters rejected it: `packages/llm/llm-deepseek/src/serialize.ts` and `packages/llm/llm-pi-ai/src/adapter.ts` each threw `LlmError('UNSUPPORTED')` on a non-undefined `prefill`. 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 through `DefineToolOptions`/`defineTool` (`packages/core/tools/src/schema.ts`), the registry's `schemas()` 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 `/beta` base URL the adapter does not use), a per-tool payload-patching pass in `packages/llm/llm-pi-ai/src/adapter.ts`, and a conditional `Strict:` row in the tool-catalog renderer (`scripts/gen-tool-catalog.ts`). No shipped tool set it — `rg` across every `tool-*` package src and `examples/` found zero `strict:` 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](../architecture/2026-06-13-twin-llm-adapters.md) is untouched.
+
+## Decision
+
+- `prefill` is removed from `GenerateOptions`, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in [core.md](../../../core-data-structures/core.md), and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance ([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md)) states the rule generically — a `GenerateOptions` field your provider cannot honor throws `LlmError(..., 'UNSUPPORTED')` — instead of using prefill as the example. The [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record prefill as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md).
+- `strict` is removed from `ToolSchema`, `DefineToolOptions`, `defineTool`, the `schemas()` allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer's `Strict:` row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stamps `strict: false` on 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; both `GenerateOptions` and `ToolSchema` keep their rows in `scripts/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 prefill` returns only RFC records (this one and the [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s producer-gated consequence); a tool-schema-scoped `rg strict` returns only this RFC, the surviving pi-ai scrub, and unrelated prose such as `strictEqual`.
+- 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-sync` green.
+
+## 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".

+ 0 - 33
docs/rfc/proposed/simplification/2026-07-04-drop-inert-request-knobs.md

@@ -1,33 +0,0 @@
-# RFC: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path
-
-Status: proposed
-
-## Problem
-
-Two request-contract knobs ride the whole request pipeline, yet neither can do anything today:
-
-- **`prefill`** (`packages/llm/llm/src/types.ts`) has no production setter — the loop assembles `model`/`system`/`tools`/`messages` plus `sessionId`/`signal`, and the compaction backend adds only `maxTokens` — and BOTH adapters reject it: `packages/llm/llm-deepseek/src/serialize.ts` and `packages/llm/llm-pi-ai/src/adapter.ts` each throw `LlmError('UNSUPPORTED')` on a non-undefined `prefill`. The field's entire observable behavior is 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) is threaded through `DefineToolOptions`/`defineTool` (`packages/core/tools/src/schema.ts`), the registry's `schemas()` allowlist (`packages/core/tools/src/index.ts`), the deepseek wire mapping (`packages/llm/llm-deepseek/src/serialize.ts`, whose wire-type note records that strict mode requires the `/beta` base URL the adapter does not use), and a per-tool payload-patching pass in `packages/llm/llm-pi-ai/src/adapter.ts`. No shipped tool sets it — `rg` across every `tool-*` package src and `examples/` finds zero `strict:` producers; the only setters are dsh-tools unit tests.
-
-Both knobs are adapter-symmetric, so removal sheds them from both twins together — the [twin-adapter design](../../implemented/architecture/2026-06-13-twin-llm-adapters.md) is untouched.
-
-## Proposal
-
-- Remove `prefill` from `GenerateOptions`, both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste lines in [core.md](../../../core-data-structures/core.md), the adapter README rows documenting the rejection, and the cookbook line using prefill as the UNSUPPORTED example ([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md)); amend the [content-block vocabulary RFC](../../implemented/architecture/2026-06-11-content-block-vocabulary.md)'s consequence line naming prefill as having a home, per [implemented/AGENTS.md](../../implemented/AGENTS.md).
-- Remove `strict` from `ToolSchema`, `DefineToolOptions`, `defineTool`, and the `schemas()` allowlist; drop the deepseek serializer branch; simplify the pi-ai payload fixup to the unconditional scrub of pi-ai's own strict default (that half exists for wire parity with the hand-rolled twin and survives); drop the setter tests and the core.md paste line.
-
-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 prefill` and a tool-schema-scoped `rg strict` return only this RFC (and unrelated prose such as `strictEqual`).
-- 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-sync` green.
-
-## 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".

+ 8 - 9
packages/core/tools/src/index.ts

@@ -306,20 +306,19 @@ export class ToolRegistry extends Service {
 
   /**
    * Return all registered tool schemas — exactly the model-facing fields
-   * (`name`, `description`, `parameters`, and `strict` when set), as sent to the
-   * model via the system-prompt assembly. Constructed EXPLICITLY rather than by
-   * stripping known non-schema members: a `ToolDefinition` also carries
-   * `execute` and the optional `presentCall`/`presentResult` UI callbacks, and
-   * those (especially the functions) must never leak into a model request. An
-   * allowlist can't drift when a new non-schema member is added to the
-   * definition; a denylist (rest-destructure) would silently leak it.
+   * (`name`, `description`, `parameters`), as sent to the model via the
+   * system-prompt assembly. Constructed EXPLICITLY rather than by stripping
+   * known non-schema members: a `ToolDefinition` also carries `execute` and the
+   * optional `presentCall`/`presentResult` UI callbacks, and those (especially
+   * the functions) must never leak into a model request. An allowlist can't
+   * drift when a new non-schema member is added to the definition; a denylist
+   * (rest-destructure) would silently leak it.
    */
   schemas(): ToolSchema[] {
-    return [...this.store.values()].map(({ name, description, parameters, strict }): ToolSchema => ({
+    return [...this.store.values()].map(({ name, description, parameters }): ToolSchema => ({
       name,
       description,
       parameters: structuredClone(parameters),
-      ...strict !== undefined ? { strict } : {},
     }))
   }
 

+ 0 - 3
packages/core/tools/src/schema.ts

@@ -312,8 +312,6 @@ export interface DefineToolOptions<S extends SchemaSpec> {
    * free for the same replay reason. See {@link ToolResultView}.
    */
   presentResult?(args: InferArgs<S>, result: ToolResult): ToolResultView | undefined
-  /** Whether the tool requires structured output (default false). */
-  strict?: boolean
 }
 
 /**
@@ -355,7 +353,6 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
     name: options.name,
     description: options.description,
     parameters: schemaSpecToJsonSchema(options.parameters) as unknown as Record<string, unknown>,
-    ...options.strict !== undefined ? { strict: options.strict } : {},
     async execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn> {
       // Validate the model-generated args before the typed body runs. On
       // mismatch we throw ToolArgsError; the registry turns it into an

+ 0 - 11
packages/core/tools/tests/gen-tool-catalog.spec.ts

@@ -103,15 +103,4 @@ describe('gen-tool-catalog render', () => {
     expect(md).toContain('```json')
     expect(md).toContain('Source: [`packages/demo/tool-demo/src/index.ts`]')
   })
-
-  it('renders the strict flag when a schema sets it', () => {
-    const catalog: ToolCatalog = [
-      {
-        pkg: '@deepseek-ai/dsh-tool-demo',
-        source: 'packages/demo/tool-demo/src/index.ts',
-        schemas: [{ name: 'demo', description: '', parameters: { type: 'object', properties: {} }, strict: true }],
-      },
-    ]
-    expect(render(catalog)).toContain('Strict: `true`')
-  })
 })

+ 0 - 50
packages/core/tools/tests/tools.spec.ts

@@ -62,18 +62,6 @@ describe('ToolRegistry', () => {
     expect(schema.execute).toBeUndefined()
   })
 
-  it('schemas() preserves `strict` when set (allowlist keeps the model-facing fields)', async () => {
-    const ctx = await setup()
-    ctx.tools.register(defineTool({
-      name: 'strict-tool',
-      description: 'd',
-      parameters: { x: { type: 'string', required: true } },
-      strict: true,
-      async execute() { return [] },
-    }))
-    expect(ctx.tools.schemas()[0]).toMatchObject({ name: 'strict-tool', strict: true })
-  })
-
   it('executes a tool and returns its content', async () => {
     const ctx = await setup()
     ctx.tools.register(echoTool)
@@ -611,44 +599,6 @@ describe('schema DSL edge cases', () => {
     })
   })
 
-  it('defineTool passes through strict flag when set to true', () => {
-    const tool = defineTool({
-      name: 'strict-tool',
-      description: 'A strict tool',
-      parameters: { input: { type: 'string' } },
-      strict: true,
-      async execute(args) {
-        return [{ type: 'text' as const, text: args.input ?? '' }]
-      },
-    })
-    expect(tool.strict).toBe(true)
-  })
-
-  it('defineTool omits strict when not provided', () => {
-    const tool = defineTool({
-      name: 'non-strict-tool',
-      description: 'A non-strict tool',
-      parameters: { input: { type: 'string' } },
-      async execute(args) {
-        return [{ type: 'text' as const, text: args.input ?? '' }]
-      },
-    })
-    expect('strict' in tool).toBe(false)
-  })
-
-  it('defineTool strict=false is included', () => {
-    const tool = defineTool({
-      name: 'explicitly-non-strict',
-      description: 'Explicitly non-strict',
-      parameters: { input: { type: 'string' } },
-      strict: false,
-      async execute(args) {
-        return [{ type: 'text' as const, text: args.input ?? '' }]
-      },
-    })
-    expect(tool.strict).toBe(false)
-  })
-
   it('handles enum and default together in one property', () => {
     const spec = {
       level: { type: 'string', enum: ['low', 'high'], default: 'low' },

+ 0 - 2
packages/llm/llm-deepseek/README.md

@@ -28,12 +28,10 @@ A second, independent implementation of the same seam exists in `@deepseek-ai/ds
 - Streaming only (`stream_options.include_usage` always on). `usage` may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to `[DONE]`, so `usage` always precedes `finish` and nothing follows `finish`.
 - The first thinking-mode chunk carries `reasoning_content: ""` — handled (no spurious reasoning block).
 - **Reasoning passback rule**: on assistant turns that carried tool calls, `reasoning_content` is serialized back in history (required by the API in thinking mode); on tool-call-free turns it is dropped (ignored anyway — saves tokens).
-- `strict` on tool schemas passes through (officially Beta; the public API wants the `/beta` base URL for it, the internal endpoint accepts it directly).
 - Cache accounting: `cacheReadTokens` ← `prompt_cache_hit_tokens` / `prompt_tokens_details.cached_tokens`; DeepSeek reports no cache-write metric.
 
 ## Limitations (MVP, documented deliberately)
 
-- `prefill` throws `LlmError('UNSUPPORTED')` — DeepSeek's chat-prefix completion is a Beta feature on the `/beta` base URL; future work.
 - `tool_choice` is not mapped (not part of the core vocabulary).
 
 ## Errors

+ 1 - 15
packages/llm/llm-deepseek/src/serialize.ts

@@ -15,7 +15,6 @@
  * @module dsh-llm-deepseek/serialize
  */
 
-import { LlmError } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
 import type { WireMessage, WireRequest, WireTool } from './types.ts'
 
@@ -98,19 +97,8 @@ export function serializeMessages(messages: Message[]): WireMessage[] {
   return wire
 }
 
-/**
- * Build the full wire request. Throws `LlmError('UNSUPPORTED')` for
- * `prefill` (DeepSeek's chat-prefix completion is a Beta feature on a
- * different base URL — see README).
- */
+/** Build the full wire request. */
 export function serializeRequest(options: GenerateOptions, defaults: RequestDefaults = {}): WireRequest {
-  if (options.prefill !== undefined) {
-    throw new LlmError(
-      'prefill is not supported by the DeepSeek adapter (Beta chat-prefix completion is future work)',
-      'UNSUPPORTED',
-    )
-  }
-
   const messages: WireMessage[] = []
   if (options.system !== undefined) {
     messages.push({ role: 'system', content: options.system })
@@ -123,8 +111,6 @@ export function serializeRequest(options: GenerateOptions, defaults: RequestDefa
       name: tool.name,
       description: tool.description,
       parameters: tool.parameters,
-      // strict is officially supported (Beta); pass the tool author's choice.
-      ...tool.strict !== undefined ? { strict: tool.strict } : {},
     },
   }))
 

+ 0 - 2
packages/llm/llm-deepseek/src/types.ts

@@ -78,8 +78,6 @@ export interface WireTool {
     name: string
     description: string
     parameters: Record<string, unknown>
-    /** Beta: strict schema adherence (official: requires the /beta base URL). */
-    strict?: boolean
   }
 }
 

+ 4 - 15
packages/llm/llm-deepseek/tests/serialize.spec.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import { CallId, LlmError } from '@deepseek-ai/dsh-llm'
+import { CallId } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
 import { serializeMessages, serializeRequest } from '@deepseek-ai/dsh-llm-deepseek'
 
@@ -155,17 +155,17 @@ describe('serializeRequest', () => {
     expect(wire.stop).toEqual(['END'])
   })
 
-  it('maps tools with strict passthrough', () => {
+  it('maps tools to the wire function shape', () => {
     const wire = serializeRequest(request({
       messages: history,
       tools: [
         { name: 'a', description: 'A', parameters: { type: 'object', properties: {} } },
-        { name: 'b', description: 'B', parameters: { type: 'object', properties: {} }, strict: true },
+        { name: 'b', description: 'B', parameters: { type: 'object', properties: { x: { type: 'string' } } } },
       ],
     }))
     expect(wire.tools).toEqual([
       { type: 'function', function: { name: 'a', description: 'A', parameters: { type: 'object', properties: {} } } },
-      { type: 'function', function: { name: 'b', description: 'B', parameters: { type: 'object', properties: {} }, strict: true } },
+      { type: 'function', function: { name: 'b', description: 'B', parameters: { type: 'object', properties: { x: { type: 'string' } } } } },
     ])
   })
 
@@ -185,17 +185,6 @@ describe('serializeRequest', () => {
     expect(wire.thinking).toBeUndefined()
     expect(wire.reasoning_effort).toBeUndefined()
   })
-
-  it('rejects prefill with an UNSUPPORTED LlmError', () => {
-    expect(() => serializeRequest(request({ prefill: [{ type: 'text', text: 'Sure' }] })))
-      .toThrow(LlmError)
-    try {
-      serializeRequest(request({ prefill: [] }))
-      expect.unreachable()
-    } catch (error) {
-      expect((error as LlmError).code).toBe('UNSUPPORTED')
-    }
-  })
 })
 
 describe('review fixes: assistant content shapes', () => {

+ 2 - 2
packages/llm/llm-pi-ai/README.md

@@ -9,7 +9,7 @@ DeepSeek adapter for the harness LLM seam backed by [`@earendil-works/pi-ai`](ht
 - pi-ai hands tool-call `arguments` around as **parsed objects**; the harness keeps raw JSON strings. The adapter patches replay payloads back to the original raw strings before sending them, and re-stringifies parsed output tool calls at `block-end`.
 - pi-ai reports failures as **in-stream error events** (it never throws mid-stream); these map to `finish {kind:'error'|'aborted'}` chunks — the protocol's other sanctioned error path besides throwing (which llm-deepseek uses).
 - pi-ai folds reasoning tokens into `usage.output`; there is no separate reasoning count to map.
-- pi-ai's options omit some DeepSeek/OpenAI-compatible details; the adapter uses its `onPayload` hook to preserve the harness contract (`stop`, per-tool `strict`, omitted reasoning effort, raw replayed tool arguments).
+- pi-ai's options omit some DeepSeek/OpenAI-compatible details; the adapter uses its `onPayload` hook to preserve the harness contract (`stop`, scrubbing pi-ai's own per-tool `strict` default — the hand-rolled twin sends no such field — omitted reasoning effort, raw replayed tool arguments).
 
 ## Config
 
@@ -31,7 +31,7 @@ pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time depe
 
 ## Limitations
 
-Same MVP contract as llm-deepseek: `prefill` throws `UNSUPPORTED`, `tool_choice` is not mapped.
+Same MVP contract as llm-deepseek: `tool_choice` is not mapped.
 
 ## Testing
 

+ 10 - 24
packages/llm/llm-pi-ai/src/adapter.ts

@@ -13,9 +13,9 @@
 
 import { stream as piStream } from '@earendil-works/pi-ai'
 import type { Model } from '@earendil-works/pi-ai'
-import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
+import { LlmAdapter } from '@deepseek-ai/dsh-llm'
 import { CallId } from '@deepseek-ai/dsh-llm'
-import type { GenerateOptions, StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm'
+import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
 import { toPiContext, toStreamChunks } from './convert.ts'
 
 /** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
@@ -61,7 +61,7 @@ export function buildModel(modelId: string, options: PiAiAdapterOptions): Model<
 }
 
 type Payload = {
-  tools?: { function?: { name?: unknown; strict?: unknown } }[]
+  tools?: { function?: { strict?: unknown } }[]
   messages?: {
     role?: unknown
     tool_calls?: { id?: unknown; function?: { arguments?: unknown } }[]
@@ -81,10 +81,6 @@ function rawToolArguments(options: GenerateOptions): Map<CallId, string> {
   return raw
 }
 
-function strictByToolName(tools: ToolSchema[] | undefined): Map<string, boolean | undefined> {
-  return new Map((tools ?? []).map(tool => [tool.name, tool.strict]))
-}
-
 function patchPayload(payload: unknown, options: GenerateOptions, reasoning: PiAiReasoning | undefined): unknown {
   /* v8 ignore next -- pi-ai onPayload always receives an object; tolerate unusual future hooks defensively */
   if (typeof payload !== 'object' || payload === null) return payload
@@ -97,16 +93,13 @@ function patchPayload(payload: unknown, options: GenerateOptions, reasoning: PiA
     body.stop = options.stop
   }
 
-  const strictByName = strictByToolName(options.tools)
+  // pi-ai stamps its own `strict` default on every serialized tool; the
+  // harness tool contract has no strict field and the hand-rolled twin sends
+  // none, so scrub it for wire parity.
   for (const tool of body.tools ?? []) {
     /* v8 ignore next -- malformed pi-ai payload guard: real tool entries always carry function */
     if (tool.function === undefined) continue
-    const name = tool.function.name
-    /* v8 ignore next -- malformed pi-ai payload guard: real function entries always carry a string name */
-    if (typeof name !== 'string') continue
-    const strict = strictByName.get(name)
-    if (strict === undefined) delete tool.function.strict
-    else tool.function.strict = strict
+    delete tool.function.strict
   }
 
   const rawById = rawToolArguments(options)
@@ -131,9 +124,9 @@ function patchPayload(payload: unknown, options: GenerateOptions, reasoning: PiA
  *
  * Implementation notes:
  * - `onPayload` patches provider payload details pi-ai cannot express directly:
- *   stop sequences, per-tool strict, omitted reasoning effort, and raw replayed
- *   tool-call arguments.
- * - `prefill` throws UNSUPPORTED (same contract as dsh-llm-deepseek).
+ *   stop sequences, scrubbing pi-ai's own per-tool `strict` default (the
+ *   hand-rolled twin sends no such field), omitted reasoning effort, and raw
+ *   replayed tool-call arguments.
  * - pi-ai reports request failures as in-stream error events; convert.ts
  *   maps them to `finish {kind:'error'|'aborted'}` chunks rather than
  *   throwing — both are sanctioned StreamChunk error paths.
@@ -144,13 +137,6 @@ export class PiAiAdapter extends LlmAdapter {
   }
 
   async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
-    if (options.prefill !== undefined) {
-      throw new LlmError(
-        'prefill is not supported by the pi-ai adapter',
-        'UNSUPPORTED',
-      )
-    }
-
     const model = buildModel(options.model, this.options)
     // Undefined config means "provider default" (DeepSeek: thinking ENABLED),
     // matching llm-deepseek's omission semantics. pi-ai derives the wire

+ 11 - 20
packages/llm/llm-pi-ai/tests/adapter.spec.ts

@@ -2,7 +2,7 @@ import { createServer } from 'node:http'
 import type { IncomingMessage, Server, ServerResponse } from 'node:http'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from 'cordis'
-import LlmService, { CallId, LlmError } from '@deepseek-ai/dsh-llm'
+import LlmService, { CallId } from '@deepseek-ai/dsh-llm'
 import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
 import { buildModel, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
 import { assemble } from './assemble.ts'
@@ -149,26 +149,26 @@ describe('PiAiAdapter against a mock server', () => {
     expect(server.requests[0]).toMatchObject({ stop: ['END'] })
   })
 
-  it('preserves per-tool strict exactly through onPayload', async () => {
+  it('scrubs pi-ai\'s own per-tool strict default through onPayload', async () => {
     const server = await mockServer([{ events: textEvents }])
     const ctx = await harness(server.url)
     await assemble(ctx,{
       model: 'deepseek-v4-flash',
       messages: [],
       tools: [
-        { name: 'strict_true', description: 'true', parameters: {}, strict: true },
-        { name: 'strict_false', description: 'false', parameters: {}, strict: false },
-        { name: 'strict_omitted', description: 'omitted', parameters: {} },
+        { name: 'alpha', description: 'a', parameters: {} },
+        { name: 'beta', description: 'b', parameters: {} },
       ],
     })
 
+    // pi-ai stamps `strict` on every serialized tool function; the harness
+    // contract has none and the hand-rolled twin sends no such field, so the
+    // payload fixup must have deleted it from every tool.
     const request = server.requests[0] as { tools: { function: { name: string; strict?: boolean } }[] }
-    expect(request.tools.map(tool => [tool.function.name, tool.function.strict])).toEqual([
-      ['strict_true', true],
-      ['strict_false', false],
-      ['strict_omitted', undefined],
-    ])
-    expect('strict' in request.tools[2]!.function).toBe(false)
+    expect(request.tools.map(tool => tool.function.name)).toEqual(['alpha', 'beta'])
+    for (const tool of request.tools) {
+      expect('strict' in tool.function).toBe(false)
+    }
   })
 
   it('preserves raw replayed tool-call arguments in the provider payload', async () => {
@@ -209,15 +209,6 @@ describe('PiAiAdapter against a mock server', () => {
     expect(result.finish).toMatchObject({ kind: 'error', code })
   })
 
-  it('rejects prefill with UNSUPPORTED', async () => {
-    const ctx = await harness('http://127.0.0.1:1')
-    await expect(assemble(ctx,{
-      model: 'deepseek-v4-flash',
-      messages: [],
-      prefill: [{ type: 'text', text: 'Sure' }],
-    })).rejects.toThrow(LlmError)
-  })
-
   it('registers/unregisters models on the llm service (HMR safety)', async () => {
     const ctx = new Context()
     await ctx.plugin(LlmService)

+ 0 - 3
packages/llm/llm/src/types.ts

@@ -163,7 +163,6 @@ export interface ToolSchema {
   description: string
   /** JSON Schema object for the arguments. */
   parameters: Record<string, unknown>
-  strict?: boolean
 }
 
 /** A single model request, fully assembled. */
@@ -174,8 +173,6 @@ export interface GenerateOptions {
   system?: string
   /** Tool schemas (adapters map to the provider's `tools` field). */
   tools?: ToolSchema[]
-  /** Assistant prefix continuation (prefill). */
-  prefill?: ContentBlock[]
   temperature?: number
   maxTokens?: number
   /**

+ 0 - 1
scripts/gen-tool-catalog.ts

@@ -220,7 +220,6 @@ export async function collectToolCatalog(packages: ToolPackage[] = TOOL_PACKAGES
 function renderTool(schema: ToolSchema, source: string): string[] {
   const out = [`### \`${schema.name}\``, '']
   if (schema.description) out.push(schema.description, '')
-  if (schema.strict !== undefined) out.push(`Strict: \`${String(schema.strict)}\``, '')
   out.push('```json', JSON.stringify(schema.parameters, null, 2), '```', '')
   out.push(`Source: [\`${source}\`](../../${source})`, '')
   return out