fix(memhub): turn model thinking off on the SDK write path
Profiles default to disable_thinking, but build_sdk_config never told the SDK. DeepSeek Flash spent the SDK's 2000 agent_max_tokens thinking and answered finish_reason=length with no content (EMPTY_RESPONSE). The provider mapping moves into disable_thinking_params, shared by attach_disable_thinking and the SDK's llm.extra_body. The SDK sends extra_body on every chat call, and the coding/working writers build their LLMProvider from the same llm config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
423cb00dbf
commit
ad05303d23
3 files changed
+132
-16
No files matched your search
@@ -36,7 +36,11 @@ from one_memory.hy_graph_admin import (
|
||||
)
|
||||
from one_memory.hy_sdk_types import HySdkRuntimeError
|
||||
from one_memory.local_embedding import LocalEmbedder
|
||||
from one_memory.model_profiles import ModelProfileSnapshot, ProviderKind
|
||||
from one_memory.model_profiles import (
|
||||
ModelProfileSnapshot,
|
||||
ProviderKind,
|
||||
disable_thinking_params,
|
||||
)
|
||||
|
||||
_RUNTIME_LOCK = threading.RLock()
|
||||
BUILTIN_RETRIEVAL_PROFILE_ID = "builtin-retrieval"
|
||||
@@ -244,14 +248,22 @@ def build_sdk_config(
|
||||
embedding_provider, embedding_endpoint, embedding_model, dimensions
|
||||
)
|
||||
|
||||
llm: dict[str, Any] = {
|
||||
"provider": "openai",
|
||||
"model": profile.model,
|
||||
"api_key": generation_key,
|
||||
"base_url": generation_endpoint,
|
||||
}
|
||||
# The SDK sends `extra_body` on every chat call, chat and coding/working
|
||||
# alike. Without it DeepSeek spent the whole agent_max_tokens on thinking
|
||||
# and answered finish_reason=length with no content.
|
||||
thinking_off = disable_thinking_params(profile)
|
||||
if thinking_off:
|
||||
llm["extra_body"] = thinking_off
|
||||
|
||||
return MemoryConfig.from_dict(
|
||||
{
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"model": profile.model,
|
||||
"api_key": generation_key,
|
||||
"base_url": generation_endpoint,
|
||||
},
|
||||
"llm": llm,
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
"model": (
|
||||
|
||||
@@ -1136,20 +1136,31 @@ class ModelProfileStore:
|
||||
)
|
||||
|
||||
|
||||
def disable_thinking_params(
|
||||
profile: ModelProfile | ModelProfileSnapshot,
|
||||
) -> dict[str, object]:
|
||||
"""Provider-specific thinking-off params, empty unless the profile opts in.
|
||||
|
||||
The one mapping for every request Core builds itself and for the SDK's
|
||||
`llm.extra_body`, so a model that thinks on one path thinks on none.
|
||||
"""
|
||||
if not profile.disable_thinking:
|
||||
return {}
|
||||
if profile.provider_kind is ProviderKind.OPENAI_COMPATIBLE:
|
||||
if urlparse(profile.base_url or "").hostname == "api.deepseek.com":
|
||||
return {"thinking": {"type": "disabled"}}
|
||||
return {"reasoning_effort": "none"}
|
||||
if profile.provider_kind is ProviderKind.OLLAMA:
|
||||
return {"think": False}
|
||||
return {}
|
||||
|
||||
|
||||
def attach_disable_thinking(
|
||||
profile: ModelProfile | ModelProfileSnapshot,
|
||||
payload: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
"""Attach provider-specific thinking-off params when the profile opts in."""
|
||||
if not profile.disable_thinking:
|
||||
return payload
|
||||
if profile.provider_kind is ProviderKind.OPENAI_COMPATIBLE:
|
||||
if urlparse(profile.base_url or "").hostname == "api.deepseek.com":
|
||||
payload["thinking"] = {"type": "disabled"}
|
||||
else:
|
||||
payload["reasoning_effort"] = "none"
|
||||
elif profile.provider_kind is ProviderKind.OLLAMA:
|
||||
payload["think"] = False
|
||||
payload.update(disable_thinking_params(profile))
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from one_memory.hy_sdk_runtime import build_sdk_config
|
||||
from one_memory.imports.models import (
|
||||
EvidenceInput,
|
||||
EvidenceOrigin,
|
||||
@@ -17,6 +18,7 @@ from one_memory.imports.models import (
|
||||
from one_memory.imports.storage import EvidenceStore
|
||||
from one_memory.memory_batch_extraction import MemoryBatchExtractor
|
||||
from one_memory.memory_qa import _answer_request
|
||||
from one_memory.model_probe import _structured_request
|
||||
from one_memory.model_profiles import (
|
||||
ModelProfileInput,
|
||||
ModelProfileStore,
|
||||
@@ -243,3 +245,94 @@ def test_ollama_batch_sends_think_false_only_when_disabled(tmp_path) -> None:
|
||||
)
|
||||
assert payloads[0]["think"] is False
|
||||
assert "reasoning_effort" not in payloads[0]
|
||||
|
||||
|
||||
def _deepseek(store, secrets, *, disable_thinking: bool):
|
||||
return store.create(
|
||||
ModelProfileInput(
|
||||
name=f"deepseek-{disable_thinking}",
|
||||
provider_kind=ProviderKind.OPENAI_COMPATIBLE,
|
||||
base_url="https://api.deepseek.com/v1",
|
||||
model="deepseek-v4-flash",
|
||||
api_key=SecretStr("sk-test"),
|
||||
embedding_model="embedding-model",
|
||||
embedding_provider_kind=ProviderKind.LOCAL_ONNX,
|
||||
disable_thinking=disable_thinking,
|
||||
),
|
||||
secrets,
|
||||
)
|
||||
|
||||
|
||||
def test_sdk_config_turns_deepseek_thinking_off_like_the_probe(tmp_path) -> None:
|
||||
"""DeepSeek Flash spent the SDK's 2000 agent tokens thinking: EMPTY_RESPONSE.
|
||||
|
||||
The SDK sends `llm.extra_body` on every chat call (chat and the shared
|
||||
coding/working provider), and the probe must ask the model the same way.
|
||||
"""
|
||||
database = tmp_path / "one-memory.sqlite3"
|
||||
secrets = InMemorySecretStore()
|
||||
store = ModelProfileStore(database)
|
||||
profile = _deepseek(store, secrets, disable_thinking=True)
|
||||
snapshot = store.snapshot(profile.profile_id, profile.revision)
|
||||
|
||||
config = build_sdk_config(snapshot, api_key="sk-test", data_dir=tmp_path / "data")
|
||||
|
||||
assert config.llm.extra_body == {"thinking": {"type": "disabled"}}
|
||||
_m, _u, _h, probe = _structured_request(profile, "sk-test")
|
||||
assert probe["thinking"] == config.llm.extra_body["thinking"]
|
||||
assert "reasoning_effort" not in probe
|
||||
|
||||
|
||||
def test_sdk_config_sends_no_thinking_fields_unless_the_profile_opts_in(tmp_path) -> None:
|
||||
database = tmp_path / "one-memory.sqlite3"
|
||||
secrets = InMemorySecretStore()
|
||||
store = ModelProfileStore(database)
|
||||
profile = _deepseek(store, secrets, disable_thinking=False)
|
||||
snapshot = store.snapshot(profile.profile_id, profile.revision)
|
||||
|
||||
config = build_sdk_config(snapshot, api_key="sk-test", data_dir=tmp_path / "data")
|
||||
|
||||
assert not config.llm.extra_body
|
||||
|
||||
|
||||
def test_sdk_config_maps_other_providers_through_the_same_table(tmp_path) -> None:
|
||||
database = tmp_path / "one-memory.sqlite3"
|
||||
secrets = InMemorySecretStore()
|
||||
store = ModelProfileStore(database)
|
||||
openai = store.create(
|
||||
ModelProfileInput(
|
||||
name="openai",
|
||||
provider_kind=ProviderKind.OPENAI_COMPATIBLE,
|
||||
base_url="https://api.example.com/v1",
|
||||
model="gpt-test",
|
||||
api_key=SecretStr("sk-test"),
|
||||
embedding_model="embedding-model",
|
||||
embedding_provider_kind=ProviderKind.LOCAL_ONNX,
|
||||
),
|
||||
secrets,
|
||||
)
|
||||
ollama = store.create(
|
||||
ModelProfileInput(
|
||||
name="ollama",
|
||||
provider_kind=ProviderKind.OLLAMA,
|
||||
base_url="http://127.0.0.1:11434",
|
||||
model="qwen",
|
||||
embedding_model="embedding-model",
|
||||
embedding_provider_kind=ProviderKind.LOCAL_ONNX,
|
||||
),
|
||||
secrets,
|
||||
)
|
||||
|
||||
openai_config = build_sdk_config(
|
||||
store.snapshot(openai.profile_id, openai.revision),
|
||||
api_key="sk-test",
|
||||
data_dir=tmp_path / "data",
|
||||
)
|
||||
ollama_config = build_sdk_config(
|
||||
store.snapshot(ollama.profile_id, ollama.revision),
|
||||
api_key=None,
|
||||
data_dir=tmp_path / "data",
|
||||
)
|
||||
|
||||
assert openai_config.llm.extra_body == {"reasoning_effort": "none"}
|
||||
assert ollama_config.llm.extra_body == {"think": False}
|
||||
Reference in new issue
Block a user