Skip to content

feat: carry the tools column through to the trainer - #58

Merged
Neonkraft merged 1 commit into
mainfrom
feat/sft-tool-declarations
Aug 27, 2026
Merged

Neonkraft merged 1 commit into
mainfrom
feat/sft-tool-declarations

Conversation

@KonstiNik

Copy link
Copy Markdown
Member

Summary

columns_to_keep=["messages"] in src/post_training/methods/sft.py drops any row-level tools column before the trainer sees it, and MESSAGES_FEATURES declares only role/content, so a structured message.tool_calls field is removed by the schema cast as well. The qwen3 template's tool branches — the # Tools preamble, the tool_calls loop, the tool-role <tool_response> block — are therefore unreachable through this pipeline: no row can carry a declaration to them. This PR carries the column.

TRL reads it as a JSON string (json.loads(tools) if isinstance(tools, str) else tools, trl/trainer/sft_trainer.py), so tools is declared Value("string") rather than as a nested tool schema, and datasets fills it with null for a source that has none — so a tools-carrying dataset still concatenates with one that has none.

_classify_row now renders with tools= too, parsed the way TRL parses it. That half is not cosmetic: the row filter measures against max_length, and a declaration emits the entire # Tools preamble ahead of the conversation — 5,382 tokens on the worst row of the pool we're preparing. A filter rendering without it measures a shorter row than the one truncation is applied to, and keeps rows whose assistant span the trainer then cuts. Carrying the column without this would introduce that, which is why both are in one PR.

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Performance
  • Documentation
  • Maintenance

Validation

pytest tests/ — 132 pass, 4 new. Ruff 0.9.10 and black 25.1.0 clean at the pinned pre-commit versions.

Three of the four new tests fail against main; the fourth pins concatenate_datasets' null-filling for a mixed schema, which holds either way and is there to catch a silent regression rather than to prove this change.

The load-bearing one is test_a_tools_declaration_counts_toward_max_length: the same row with the cap set to its bare rendered length is kept without tools and rejected with them.

columns_to_keep dropped it and MESSAGES_FEATURES never declared it, so no row
could reach the template's tool branches. TRL reads the column as a JSON string,
so Value("string") is enough — no nested tool schema in Features, and datasets
fills null for sources that have none.

The filter renders with tools too. A declaration emits the template's whole
"# Tools" preamble, so rendering without it would measure a shorter row than the
one max_length is applied to.
@KonstiNik
KonstiNik requested a review from Neonkraft August 27, 2026 07:08

@Neonkraft Neonkraft left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ignore the nitpick. LGTM :)

truncated_span_action=mc.truncated_span_action,
),
columns_to_keep=["messages"],
columns_to_keep=["messages", "tools"],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Move this up to a constant COLUMNS_TO_KEEP, similar to MESSAGE_FEATURES, please :)

@Neonkraft
Neonkraft merged commit 13bd157 into main Aug 27, 2026
2 checks passed
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.

2 participants