Skip to content

fix(provider): normalize GPT-OSS tool template payloads#429

Merged
Gajesh2007 merged 2 commits into
masterfrom
fix/dar-344-gpt-oss-tool-payload
Jun 20, 2026
Merged

fix(provider): normalize GPT-OSS tool template payloads#429
Gajesh2007 merged 2 commits into
masterfrom
fix/dar-344-gpt-oss-tool-payload

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Normalize GPT-OSS/Harmony tool payloads before Jinja render so malformed histories return clean 400s instead of provider 500s.
  • Translate legacy assistant function_call payloads into modern tool_calls before strict decode can drop them.
  • Scope GPT-OSS-specific tool description/schema cleanup and tool-call invariants to Harmony only, preserving Gemma 4 behavior.
  • Align GPT-OSS stop tokens with upstream generation config (<|return|>, <|endoftext|>, <|call|>) and bridge reasoning_content into the Harmony thinking field for template replay.

Fixes DAR-344.

Verification

  • swift test --filter JinjaSanitizationTests
  • swift test --filter InboundDecodeTests
  • swift test --filter MultiModelBatchSchedulerEngineTests
  • swift build
  • Live GPT-OSS inference: DARKBLOOM_LIVE_MLX_TESTS=1 ... swift test --filter JinjaSanitizationLiveTests/nullBearingToolRequestInfers
  • Live Gemma template render: DARKBLOOM_LIVE_MLX_TESTS=1 DARKBLOOM_LIVE_MLX_GEMMA=1 ... swift test --filter GemmaToolCallLiveTests/realTemplateRendersSingleBrace
  • Live Gemma generation: DARKBLOOM_LIVE_MLX_TESTS=1 DARKBLOOM_LIVE_MLX_GEMMA=1 ... swift test --filter GemmaToolCallLiveTests/realModelEmitsCleanToolCall
  • git diff --check

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jun 20, 2026 9:01pm
d-inference-console-ui-dev Ready Ready Preview Jun 20, 2026 9:01pm
d-inference-landing Ready Ready Preview Jun 20, 2026 9:01pm

Request Review

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

This PR introduces model-specific chat-template normalization and Jinja sanitization for GPT-OSS/Harmony and Gemma 4 models; no security mitigations are weakened, but two new attack-surface concerns warrant attention before merge.


Trust Boundaries Touched

  • TB-007 — Provider inference engine (primary): input normalization runs inside the inference path on adversarial consumer data.
  • TB-003 — Provider operator vs process: modelType(at:) reads config.json from the HuggingFace cache directory, which the operator controls.

Threat Assessment

Threat File(s) Finding
T-007 (provider serves manipulated outputs) MultiModelBatchSchedulerEngine.swift:306-312, 469-475 ℹ️ Neutral on the coordinator weight-hash enforcement gap (SEC-007 remains open). The normalizer runs after model load and does not affect whether the coordinator accepts an unadvertised model.
T-007 (malformed tool history causing Jinja crashes) ChatTemplateFixes.swift:74-100, MultiModelBatchSchedulerEngineError.swift:38-42 ✅ Strengthens: validateGenericToolHistory and validateHarmonyToolInvariants now reject malformed tool sequences with a well-typed 400 (invalidToolPayload) before they reach the Jinja renderer. Previously, a consumer could craft a pathological tool history that tripped a model-specific Jinja assertion and surfaced as a provider-attributed 500, which had attribution/billing side-effects.
T-028 (residual inference data in GPU memory) BatchScheduler.swift:365-376 ℹ️ Neutral — effectiveEOSTokenIds changes the token set used to terminate generation for certain models; no impact on GPU buffer zeroing (still unmitigated).
T-041 (cross-tenant prefix-cache TTFT oracle) BatchScheduler.swift:533-545 ℹ️ Neutral — modelType field added to LoadSnapshot is used only for template dispatch, not for cache key construction or tenant isolation.

New Attack Surface Not Covered by Existing Threats

1. Operator-controlled config.json injected into model-dispatch logic (TB-003)

BatchScheduler.modelType(at:) (BatchScheduler.swift:579-585) reads config.json from the HuggingFace cache with try? Data(contentsOf:) and extracts model_type as a raw String. This value then flows directly into GPTOSSHarmonyTemplateFix.isHarmonyModelHint() and Gemma4TemplateFix.applies() to select which normalization/EOS-token logic runs.

The operator has unrestricted write access to ~/.cache/huggingface. By setting "model_type": "gptoss" in config.json for a non-GPT-OSS model, the operator can force the Harmony template fix to run on an unintended model — this will:

  • Inject <|return|>, <|endoftext|>, <|call|> as EOS tokens for a model whose tokenizer assigns those strings to different or nonexistent IDs (silent no-op via compactMap, so safe);
  • Run bridgeReasoningContentToThinking on all assistant messages for that model, potentially altering prompt semantics without changing the advertised model hash.

This is a T-012 / T-007 variant not explicitly covered: an operator can manipulate inference behavior (template logic, EOS stops) without touching weight files, remaining invisible to the coordinator's weight-hash check. The weight hash covers *.safetensors and config.json per WeightHasher.swiftif config.json is included in the weight hash, this is fully mitigated. Please confirm WeightHasher hashes config.json; the threat model notes it does (config.json is listed in the file set). If that's the case, a tampered config.json will surface as a weight-hash mismatch in the next heartbeat — but only if SEC-007 is closed (coordinator enforcement is currently fail-open).

Recommendation: Add a // SECURITY: config.json covered by WeightHasher comment at BatchScheduler.modelType(at:) to make the dependency explicit for future reviewers, and track this as a blocker on SEC-007.


2. isHarmonyModelHint matches on model_type substring — no allowlist

GPTOSSHarmonyTemplateFixes.swift:40-46:

normalized.contains("gpt-oss") || normalized.contains("gpt_oss") || normalized.contains("gptoss")

A consumer-supplied model field in the request (request.model at MultiModelBatchSchedulerEngine.swift:306) flows into ChatTemplateFixContext.modelId and is tested by this substring check. A consumer who requests model: "gpt-oss-poisoned-custom" will trigger Harmony template normalization (including EOS token injection and thinking bridging) on whatever model is actually loaded, regardless of whether it is a Harmony model. This is a T-007 / ADV-002 concern (consumer manipulates inference behavior via model name).

The practical impact is limited — the EOS tokens may not resolve in non-Harmony tokenizers (compactMap drops them) — but bridgeReasoningContentToThinking and validateHarmonyToolInvariants (single-tool-call-per-message check) will run unconditionally, potentially rejecting valid multi-tool-call requests with a 400 for non-Harmony models if a consumer names their model with "gpt-oss".

Recommendation: Gate modelId-based matching on a coordinator-registered model catalog check (i.e., verify the loaded model is of the expected family, not just the consumer-supplied string), or add a note that modelId here is the coordinator-assigned model identifier (not consumer free-text) and cannot be attacker-influenced.


Open Findings Addressed

None of the tracked SEC-* findings are resolved by this PR. SEC-007 (weight-hash enforcement fail-open) remains open and is now a dependency of the config.json → template-dispatch trust assumption noted above.


🔐 Threat model: docs/threat-model.yaml · Updates on each push to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56661f01ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +120 to +121
messages: normalizeJinjaMessagesForTemplate(messages, modelId: request.model),
tools: normalizeJinjaToolsForTemplate(toolSpecs, modelId: request.model),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Thread modelType into the fallback template recount

When a GPT-OSS model is served under a local alias that does not contain gpt-oss, the production render still enables Harmony-specific normalization because streamChatCompletion passes the loaded slot's modelType, but this fallback recount only passes request.model. In the missing-usage/cancelled-stream path, the same tool payload that rendered successfully can therefore be re-rendered as non-Harmony; with GPT-OSS schema cleanup skipped, applyChatTemplate can throw and the try? here returns a 0 prompt-token floor, underbilling those requests. Thread the captured slot modelType through promptTokenFloor so this path really mirrors production.

Useful? React with 👍 / 👎.

@Gajesh2007

Copy link
Copy Markdown
Member Author

@codex review

@Gajesh2007
Gajesh2007 merged commit 68f5514 into master Jun 20, 2026
18 of 20 checks passed

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated Code Review — Layr-Labs/d-inference#

Verdict: REQUEST_CHANGES

Security — 1 finding(s)

  • 🔵 [INFO] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:82 — Function name validation only checks for empty after trimming whitespace
    • Suggestion: Add validation for function name length limits and character restrictions to prevent potential template injection or parsing issues

Performance — 5 finding(s) (2 blocking)

  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:54-101 — JSON parsing and serialization in request normalization path
    • Suggestion: Consider caching parsed JSON or moving normalization to background thread for large payloads
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:67 — Enumerated map creates new array even when no changes needed
    • Suggestion: Use lazy enumeration or early return when no function_call fields detected
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:95-173 — Recursive schema normalization creates deep object copies
    • Suggestion: Use copy-on-write or in-place mutation to avoid unnecessary allocations
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:125-144 — Multiple iterations over schema arrays for different union keys
    • Suggestion: Combine oneOf/anyOf/allOf processing into single loop
  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/Inference/MultiModelBatchSchedulerEngine.swift:306-313 — ChatTemplateFixContext created multiple times per request
    • Suggestion: Create context once and reuse across normalization calls

Type_diligence — 13 finding(s) (4 blocking)

  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:58 — map[string]any used for JSON manipulation without type safety
    • Suggestion: Consider using a typed struct for message normalization or add explicit type assertions with ok checks
  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:62 — map[string]any used for messages array without type safety
    • Suggestion: Consider using a typed struct for message normalization or add explicit type assertions with ok checks
  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:67 — map[string]any used for individual message without type safety
    • Suggestion: Consider using a typed struct for message normalization or add explicit type assertions with ok checks
  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:81 — map[string]any used for function call without type safety
    • Suggestion: Consider using a typed struct for function call normalization or add explicit type assertions with ok checks
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/ChatTemplateFixes.swift:23 — map[string]any used for messages without type safety
    • Suggestion: Consider using a typed struct for message representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/ChatTemplateFixes.swift:38 — map[string]any used for tools without type safety
    • Suggestion: Consider using a typed struct for tool representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:15 — map[string]any used for messages without type safety
    • Suggestion: Consider using a typed struct for message representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:23 — map[string]any used for tools without type safety
    • Suggestion: Consider using a typed struct for tool representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:78 — map[string]any used for tool spec without type safety
    • Suggestion: Consider using a typed struct for tool specification or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:82 — map[string]any used for function without type safety
    • Suggestion: Consider using a typed struct for function representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:104 — map[string]any used for schema object without type safety
    • Suggestion: Consider using a typed struct for schema representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/Gemma4TemplateFixes.swift:15 — map[string]any used for messages without type safety
    • Suggestion: Consider using a typed struct for message representation or add explicit type assertions
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/Gemma4TemplateFixes.swift:21 — map[string]any used for tools without type safety
    • Suggestion: Consider using a typed struct for tool representation or add explicit type assertions

Additive_complexity — 6 finding(s) (2 blocking)

  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/InboundChatNormalization.swift:49-101 — Complex legacy function_call normalization could be simplified with helper functions
    • Suggestion: Extract validation logic into smaller helper functions like validateFunctionCall() and buildToolCall() to reduce cognitive load
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/ChatTemplateFixes.swift:1-120 — New abstraction layer with only two implementations may be premature
    • Suggestion: Consider inlining the GPT-OSS and Gemma4 logic directly into the calling sites until more model families require fixes
  • 🟡 [MEDIUM] provider-swift/Sources/ProviderCore/Inference/GPTOSSHarmonyTemplateFixes.swift:82-174 — Schema normalization logic duplicates JSON traversal patterns
    • Suggestion: Extract a generic JSON schema normalizer that takes transformation rules as parameters to reduce the recursive traversal duplication
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/Gemma4TemplateFixes.swift:1-29 — Gemma4TemplateFix is a complete no-op placeholder
    • Suggestion: Remove this file until actual Gemma4-specific fixes are needed, or add a comment explaining why the placeholder is necessary
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/BatchScheduler.swift:535-571 — Model config parsing logic mixed into scheduler
    • Suggestion: Move modelType parsing to a dedicated config utility class to separate concerns
  • 🔵 [INFO] provider-swift/Sources/ProviderCore/Inference/BatchSchedulerTypes.swift:90 — Added modelType field increases struct complexity
    • Suggestion: Consider if modelType could be derived from existing fields or passed separately to avoid expanding the LoadSnapshot struct

25 finding(s) total, 8 blocking. Verdict: REQUEST_CHANGES.

🤖 Automated review by Centaur · DAR-186

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f684700f89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +119 to 123
let fixContext = ChatTemplateFixContext(modelId: request.model)
guard let ids = try? tokenizer.inner.applyChatTemplate(
messages: sanitizeJinjaMessages(messages),
tools: sanitizeJinjaTools(toolSpecs),
messages: ChatTemplateFixes.normalizeMessages(messages, context: fixContext),
tools: ChatTemplateFixes.normalizeTools(toolSpecs, context: fixContext),
additionalContext: additionalContext

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass model type into prompt-token floor

When a GPT-OSS/Harmony model is served under an alias that does not contain gpt-oss, the main render path uses the loaded slot's modelType to enable the Harmony fixes, but this recovery path builds the fix context from request.model only. If a stream is cancelled after output or otherwise misses the usage chunk, promptTokenFloor re-renders the same prompt without the GPT-OSS normalizations and the try? can fall back to 0 for the tool/reasoning payloads this patch is handling, so the completion settles with zero prompt tokens despite having performed prefill work. Thread the loaded model type through this helper so the billing floor matches the production render path.

Useful? React with 👍 / 👎.

root["messages"] = try messages.enumerated().map { index, value in
guard var message = value as? [String: Any] else { return value }
guard (message["role"] as? String) == "assistant" else { return value }
guard message["tool_calls"] == nil else { return value }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat null legacy tool_calls as absent

When a client serializes optional fields and sends a legacy assistant function_call with tool_calls: null or tool_calls: [], this guard treats the placeholder as an existing modern tool call and skips the conversion. The strict decoder then drops the unknown function_call (or keeps no calls), leaving any following role:function result orphaned, so the Harmony validation/template path still rejects the legacy transcript this normalizer is meant to repair.

Useful? React with 👍 / 👎.

Comment on lines +471 to +474
let fixContext = ChatTemplateFixContext(modelId: request.model)
let tokens = try tokenizer.inner.applyChatTemplate(
messages: sanitizeJinjaMessages(messages),
tools: sanitizeJinjaTools(tools),
messages: ChatTemplateFixes.normalizeMessages(messages, context: fixContext),
tools: ChatTemplateFixes.normalizeTools(tools, context: fixContext),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve template fixes with the loaded model type

For /apply-template calls against a resident GPT-OSS model exposed under an alias that does not include gpt-oss, this context never includes the registry entry's modelType, unlike the production streaming path. The endpoint can therefore render the same tool/reasoning transcript without the Harmony-specific normalizations and fail even though normal inference for that alias succeeds; carry the resolved model type alongside the tokenizer for this utility path before choosing template fixes.

Useful? React with 👍 / 👎.

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