Skip to content

feat: Support stop sequences for plain-text HTTP and CLI generation #483

Description

@Arthur-Ficial

Value: Text agent frameworks and parser-oriented prompts often stop at a protocol delimiter. Supporting that behavior allows existing workflows to connect without removing a parameter or consuming text that belongs to the next stage. It also gives shell users an explicit boundary for extracting one answer from a locally generated text protocol.

Problem today: Sources/Core/ChatRequestValidator.swift:57 rejects every non-null stop; Sources/Server.swift:119 advertises it as unsupported, and Tests/integration/openapi_spec_test.py:522 locks in that rejection. Apple's sampling options do not need to grow a native stop field for apfel to provide an output-side boundary over the existing stream. A concrete client is LangChain's classic ReAct builder, which binds a stop delimiter by default: libs/langchain/langchain_classic/agents/react/agent.py:137.

Proposal: Accept OpenAI's string or string-array stop form for ordinary text generation, and expose repeatable CLI --stop. Implement a pure incremental matcher shared by HTTP and CLI. Hold back only the suffix that could become a delimiter; emit preceding text promptly, omit the matched delimiter and subsequent content, and cooperatively cancel generation after a match. Non-streaming requests should collect through the same mechanism so they can stop work early instead of generating the full response and cutting it afterward.

Use finish_reason:"stop" for a matched boundary. Limit the number and total size of delimiters, define earliest-match/tie behavior, and reject empty strings. Initially reject combinations with active tool calling or schema/JSON-constrained generation unless their structural validity is explicitly preserved. For text ReAct prompts, this request must be accepted:

curl --fail-with-body http://localhost:11434/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"apple-foundationmodel","messages":[{"role":"user","content":"Write one short action, then a line beginning Observation."}],"stop":["\nObservation"],"stream":true}'

Acceptance criteria:

  • Write failing matcher tests before implementation for every split position, overlapping delimiters, shared prefixes, multiple matches, Unicode, and EOF with an unmatched prefix.
  • Streaming and non-streaming produce identical delivered text for the same generated snapshots; no fragment of a completed delimiter reaches stdout/SSE.
  • A match cancels the producer and releases the request's concurrency permit; already generated but withheld text is never emitted afterward.
  • Validation covers string/array forms, null, empty values, excessive delimiters, and unsupported output-mode combinations; CLI flag tests cover all invalid combinations.
  • Add a LangChain-shaped request regression and live HTTP/CLI smoke coverage; distinguish actual observed generation from delivered-output accounting in usage documentation.

Golden-goal alignment: Removes a concrete OpenAI client rejection and adds a composable text boundary to the UNIX tool.

Effort: M

Risks / open questions:

  • FoundationModels cancellation is cooperative; do not promise zero extra internal work after the first matched delimiter.
  • Prefix withholding adds bounded output latency, and cumulative framework snapshots need a tested Unicode-safe adapter.
  • Supporting text stop sequences does not imply support for log probabilities, penalties, or other unavailable sampling controls.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestserverThe OpenAI-compatible HTTP server surface

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions