Widen the empty-array probe to merged item-start tokens - #212
Conversation
Byte-pair vocabularies put most item-start probability on merged tokens, such as a quote fused with the first word or carrying a leading space. The probe offered only the bare single-character tokens, so the comparison against the closing bracket was noise and small models closed arrays the prompt asked them to fill. Admit every token whose first non-whitespace character starts the item type, and treat any token that trims to the closing bracket as a close.
There was a problem hiding this comment.
🟡 Changes recommended
The probe-widening logic is applied to string/object/array but boolean/number item-start tokens remain inconsistent with the new “first non-whitespace” approach, which can keep the probe noisy for those array element types.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves the “empty array” decision probe in ConstrainedJSONGenerator so models using BPE vocabularies can more reliably choose between closing [] vs starting the first element by accounting for merged/whitespace-prefixed tokens.
Changes:
- Expands the close decision to treat any token that trims to
]as a close candidate. - Broadens item-start token sets for
.string/.object/.arrayby including tokens whose first non-whitespace character is the relevant structural prefix. - Adds helper utilities to derive token sets (
tokensMatchingTrimmed,tokensStarting(with:)) used by the probe.
File summaries
| File | Description |
|---|---|
| Sources/AnyLanguageModel/Shared/StructuredGeneration.swift | Widens empty-array probe token candidates for both close (]) and item-starts to better reflect BPE merged tokens. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| case .array: | ||
| return [try Self.singleToken(for: "[", backend: backend)] | ||
| return tokensStarting(with: "[") | ||
| case .boolean: | ||
| var tokens = Set<Int>() | ||
| for literal in ["true", "false"] { |
| private mutating func sampleWhetherToCloseEmptyArray( | ||
| items: GenerationSchema.Node | ||
| ) async throws -> Bool { | ||
| let closeToken = try Self.singleToken(for: "]", backend: backend) | ||
| let closeTokens = tokensMatchingTrimmed("]") | ||
| var allowed = try itemStartTokens(for: items) | ||
| allowed.insert(closeToken) | ||
| guard !allowed.isEmpty else { | ||
| allowed.formUnion(closeTokens) | ||
| guard !allowed.isEmpty, !closeTokens.isEmpty else { | ||
| return false | ||
| } | ||
| let token = try await backend.sample(from: allowed) | ||
| return token == closeToken | ||
| return closeTokens.contains(token) | ||
| } |
| /// Tokens whose text equals `text` after trimming surrounding whitespace. | ||
| private func tokensMatchingTrimmed(_ text: String) -> Set<Int> { | ||
| var tokens = Set<Int>() | ||
| for token in 0 ..< backend.vocabSize { | ||
| if backend.isSpecialToken(token) { continue } | ||
| guard let tokenText = backend.tokenText(token) else { continue } | ||
| if tokenText.trimmingCharacters(in: .whitespacesAndNewlines) == text { |
Boolean starts admit any token that is a prefix of true or false after leading whitespace, and number starts look at the first non-whitespace character, matching the string, object, and array cases. Adds a test with merged item-start and close tokens.
|
Merging now. I pushed a follow-up that applies the same rule to boolean and number items and adds a test with merged tokens. Caching the vocab scans can be a follow-up if it shows up in profiles. Thanks, @james-333i! |
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>
Byte-pair vocabularies put most item-start probability on merged tokens, such as a quote fused with the first word or carrying a leading space. The probe offered only the bare single-character tokens, so the comparison against the closing bracket was noise and small models closed arrays the prompt asked them to fill.
Admit every token whose first non-whitespace character starts the item type, and treat any token that trims to the closing bracket as a close.