Skip to content

Drop the duplicated trailing user turn in Foundation Models sessions - #217

Merged
mattt merged 2 commits into
huggingface:mainfrom
james-333i:fix-duplicate-trailing-prompt
Sep 4, 2026
Merged

Drop the duplicated trailing user turn in Foundation Models sessions#217
mattt merged 2 commits into
huggingface:mainfrom
james-333i:fix-duplicate-trailing-prompt

Conversation

@james-333i

Copy link
Copy Markdown
Contributor

LanguageModelSession appends the prompt entry to its transcript before invoking the model. The Foundation Models adapter then passes that transcript to FoundationModels.LanguageModelSession and also sends the prompt through respond and streamResponse, which appends it again. Every FM-backed turn therefore reaches the model with the final user message duplicated.

The MLX adapter shows the intended contract. It renders the chat from session.transcript alone and treats the prompt parameter as a fallbackPrompt. FM's API requires passing the prompt separately, so the fix on this adapter is to drop a trailing transcript prompt that matches the outgoing prompt before conversion.

This is directly observable on OS 27 by decoding the rendered prompt of a custom LanguageModel backend, where the user turn appears twice. The same transcript construction applies to SystemLanguageModel on OS 26, where it wastes prompt tokens on every turn.

LanguageModelSession appends the prompt entry to its transcript before
invoking the model. The Foundation Models adapter then passes that
transcript to FoundationModels.LanguageModelSession and also sends the
prompt through respond/streamResponse, which appends it again, so every
FM-backed turn reaches the model with the final user message duplicated.

The MLX adapter shows the intended contract: it renders the chat from
session.transcript alone and treats the prompt parameter as a fallback.
FM's API requires passing the prompt separately, so the fix on this
adapter is to drop a trailing transcript prompt that matches the
outgoing prompt before converting.

Verified by decoding the rendered prompt of a custom LanguageModel
backend on OS 27, where the doubled turn is directly observable; the
same transcript construction applies to SystemLanguageModel on OS 26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new helper can drop a trailing transcript prompt based on text-only comparison and may inadvertently remove structured prompt context that the outgoing Prompt parameter cannot represent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes prompt duplication when using the Foundation Models–backed SystemLanguageModel by ensuring the outgoing prompt is not effectively included twice (once via LanguageModelSession.transcript, and again via the FM respond/streamResponse APIs).

Changes:

  • Drops a trailing transcript prompt entry when it matches the outgoing prompt before converting the transcript to FoundationModels.Transcript.
  • Applies the fix consistently to both respond and streamResponse paths in SystemLanguageModel.
File summaries
File Description
Sources/AnyLanguageModel/Models/SystemLanguageModel.swift Avoids duplicating the final user prompt in FM session construction by trimming a matching trailing transcript prompt.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +370 to +383
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
func fmTranscriptDroppingDuplicatePrompt(_ transcript: Transcript, prompt: Prompt) -> Transcript {
guard let lastEntry = transcript.last, case .prompt(let lastPrompt) = lastEntry else {
return transcript
}
let lastText = lastPrompt.segments.compactMap { segment -> String? in
if case .text(let textSegment) = segment { return textSegment.content }
return nil
}.joined()
guard lastText == prompt.description else {
return transcript
}
return Transcript(entries: transcript.dropLast())
}
@mattt

mattt commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Nice catch. I pushed a small follow-up that fixes the formatting for the linter and limits the drop to prompt entries whose segments are all text, so a structured segment can never go missing. Merging now. Thanks, @james-333i!

@mattt
mattt merged commit 803b99b into huggingface:main Sep 4, 2026
11 checks passed
james-333i added a commit to james-333i/AnyLanguageModel that referenced this pull request Sep 4, 2026
Brings in the merged huggingface#195, huggingface#205, huggingface#212 and huggingface#217 along with the follow-ups
applied on merge (all-text prompt drop, boolean and number probe items).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants