Disable Hugging Face reasoning replay

Disable Hugging Face prior reasoning replay for Chat Completions while preserving streamed reasoning output.
This commit is contained in:
Ali Khokhar 2026-07-06 20:25:07 -07:00 committed by GitHub
parent 62c0480eed
commit 950aba393d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 93 additions and 25 deletions

View file

@ -379,7 +379,8 @@ Gateway providers such as Vercel AI Gateway, Hugging Face, Cohere, and GitHub
Models stay thin when their documented OpenAI-compatible Chat Completions
behavior matches shared transport policy. Provider-specific gateway quirks, such
as Cohere's supported `reasoning_effort` values, GitHub's API headers/catalog
filtering, and unsupported compatibility fields, stay in that provider package.
filtering, Hugging Face's disabled prior reasoning replay, and unsupported
compatibility fields, stay in that provider package.
### Adding A Provider

View file

@ -269,7 +269,7 @@ Create a Hugging Face token with Inference Providers permission at [huggingface.
Set `MODEL` to a Hugging Face model slug such as `huggingface/openai/gpt-oss-120b:fastest`, `huggingface/Qwen/Qwen3-Coder-480B-A35B-Instruct:fastest`, or `huggingface/deepseek-ai/DeepSeek-R1:fastest`.
Hugging Face routes through the OpenAI-compatible router at `https://router.huggingface.co/v1`. FCC uses the shared OpenAI-chat transport and preserves request `extra_body` for Hugging Face provider options.
Hugging Face routes through the OpenAI-compatible router at `https://router.huggingface.co/v1`. FCC uses the shared OpenAI-chat transport, preserves request `extra_body` for Hugging Face provider options, and does not replay prior hidden reasoning into Chat Completions because that request field is not documented by Hugging Face. New reasoning emitted by the upstream is still shown as Claude thinking.
If your existing repo `.env` or `~/.fcc/.env` uses the old voice setting `HF_TOKEN`, `fcc-server`/`fcc-init` renames it to `HUGGINGFACE_API_KEY`. Explicit `FCC_ENV_FILE` files are not rewritten automatically; rename the key there manually.

View file

@ -1,19 +1,14 @@
"""Hugging Face Inference Providers implementation."""
from copy import deepcopy
from typing import Any
from core.anthropic import ReasoningReplayMode, build_base_request_body
from core.anthropic.conversion import OpenAIConversionError
from providers.base import ProviderConfig
from providers.defaults import HUGGINGFACE_DEFAULT_BASE
from providers.transports.openai_chat import (
OpenAIChatRequestPolicy,
OpenAIChatTransport,
build_openai_chat_request_body,
)
_REQUEST_POLICY = OpenAIChatRequestPolicy(
provider_name="HUGGINGFACE",
include_extra_body=True,
)
from providers.exceptions import InvalidRequestError
from providers.transports.openai_chat import OpenAIChatTransport
class HuggingFaceProvider(OpenAIChatTransport):
@ -30,8 +25,23 @@ class HuggingFaceProvider(OpenAIChatTransport):
def _build_request_body(
self, request: Any, thinking_enabled: bool | None = None
) -> dict:
return build_openai_chat_request_body(
request,
thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
policy=_REQUEST_POLICY,
)
"""Build a Hugging Face Chat Completions body.
Hugging Face's router documents reasoning controls for supported models
but not prior-turn reasoning replay through ``messages[].reasoning_content``.
Keep replay disabled while still allowing new streamed reasoning output
to map back to Claude thinking in the shared OpenAI-chat stream adapter.
"""
try:
body = build_base_request_body(
request,
reasoning_replay=ReasoningReplayMode.DISABLED,
)
except OpenAIConversionError as exc:
raise InvalidRequestError(str(exc)) from exc
request_extra = getattr(request, "extra_body", None)
if isinstance(request_extra, dict) and request_extra:
body["extra_body"] = deepcopy(request_extra)
return body

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "free-claude-code"
version = "3.4.5"
version = "3.4.6"
description = "Middleware between Claude Code CLI (Anthropic API) and NVIDIA NIM"
readme = "README.md"
requires-python = ">=3.14.0"

View file

@ -5,14 +5,22 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from core.anthropic import ReasoningReplayMode
from providers.base import ProviderConfig
from providers.huggingface import HUGGINGFACE_DEFAULT_BASE, HuggingFaceProvider
class MockMessage:
def __init__(self, role, content):
def __init__(self, role, content, reasoning_content=None):
self.role = role
self.content = content
self.reasoning_content = reasoning_content
class MockBlock:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class MockRequest:
@ -89,9 +97,7 @@ def test_init_strips_trailing_slash(huggingface_config):
def test_build_request_body_keeps_max_tokens(huggingface_provider):
with patch(
"providers.transports.openai_chat.request_policy.build_base_request_body"
) as mock_convert:
with patch("providers.huggingface.client.build_base_request_body") as mock_convert:
mock_convert.return_value = {
"model": "openai/gpt-oss-120b:fastest",
"messages": [{"role": "user", "name": "alice", "content": "hi"}],
@ -100,17 +106,68 @@ def test_build_request_body_keeps_max_tokens(huggingface_provider):
body = huggingface_provider._build_request_body(MockRequest())
mock_convert.assert_called_once()
assert (
mock_convert.call_args.kwargs["reasoning_replay"]
is ReasoningReplayMode.DISABLED
)
assert body["messages"][0].get("name") == "alice"
assert body["max_tokens"] == 42
assert "max_completion_tokens" not in body
def test_build_request_body_preserves_caller_extra_body(huggingface_provider):
req = MockRequest(extra_body={"provider": "auto", "bill_to": "my-org"})
extra_body = {"provider": "auto", "routing": {"bill_to": "my-org"}}
req = MockRequest(extra_body=extra_body)
body = huggingface_provider._build_request_body(req)
assert body["extra_body"] == {"provider": "auto", "bill_to": "my-org"}
assert body["extra_body"] == extra_body
assert body["extra_body"] is not extra_body
assert body["extra_body"]["routing"] is not extra_body["routing"]
def test_build_request_body_does_not_replay_prior_thinking_blocks(
huggingface_provider,
):
req = MockRequest(
system=None,
messages=[
MockMessage(
"assistant",
[
MockBlock(type="thinking", thinking="hidden prior thought"),
MockBlock(type="text", text="visible answer"),
],
)
],
)
body = huggingface_provider._build_request_body(req)
assert body["messages"] == [{"role": "assistant", "content": "visible answer"}]
assert "reasoning_content" not in body["messages"][0]
assert "hidden prior thought" not in str(body)
def test_build_request_body_does_not_replay_top_level_reasoning_content(
huggingface_provider,
):
req = MockRequest(
system=None,
messages=[
MockMessage(
"assistant",
"visible answer",
reasoning_content="hidden prior reasoning",
)
],
)
body = huggingface_provider._build_request_body(req)
assert body["messages"] == [{"role": "assistant", "content": "visible answer"}]
assert "hidden prior reasoning" not in str(body)
@pytest.mark.asyncio

2
uv.lock generated
View file

@ -561,7 +561,7 @@ wheels = [
[[package]]
name = "free-claude-code"
version = "3.4.5"
version = "3.4.6"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },