Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .flocks/flocks.json.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"provider": {},
"default_models": {
"default_parameters": {
"reasoning_effort": "high"
}
},
"mcp": {},
"channels": {},
"plugin": [],
Expand Down
42 changes: 40 additions & 2 deletions flocks/config/config_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@


_FALLBACK_CONFIG_TEMPLATES: Dict[str, Dict[str, Any]] = {
"flocks.json": {},
"flocks.json": {
"default_models": {
"default_parameters": {
"reasoning_effort": "high",
},
},
},
".secret.json": {},
"mcp_list.json": {
"version": "1.0.0",
Expand Down Expand Up @@ -454,6 +460,31 @@ def get_all_model_settings(cls) -> Dict[str, Dict[str, Any]]:
data = cls._read_raw()
return data.get("model_settings", {})

@classmethod
def get_effective_model_default_parameters(
cls,
provider_id: str,
model_id: str,
) -> Dict[str, Any]:
"""Resolve global defaults with model-specific overrides."""
data = cls._read_raw()
result: Dict[str, Any] = {}
default_models = data.get("default_models")
global_parameters = (
default_models.get("default_parameters", {})
if isinstance(default_models, dict)
else {}
)
if isinstance(global_parameters, dict):
result.update(global_parameters)

settings = data.get("model_settings", {})
setting = settings.get(f"{provider_id}/{model_id}", {})
model_parameters = setting.get("default_parameters", {})
if isinstance(model_parameters, dict):
result.update(model_parameters)
return result

# ------------------------------------------------------------------
# Default models (default_models section)
# ------------------------------------------------------------------
Expand Down Expand Up @@ -503,7 +534,14 @@ def delete_default_model(cls, model_type: str) -> bool:
def get_all_default_models(cls) -> Dict[str, Dict[str, Any]]:
"""Get all default model configs."""
data = cls._read_raw()
return data.get("default_models", {})
defaults = data.get("default_models")
if not isinstance(defaults, dict):
return {}
return {
model_type: config
for model_type, config in defaults.items()
if model_type != "default_parameters" and isinstance(config, dict)
}

# ------------------------------------------------------------------
# Runtime model fallbacks (fallback_providers section)
Expand Down
11 changes: 11 additions & 0 deletions flocks/provider/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ def update_setting(
model_id=model_id,
)

def get_effective_default_parameters(
self,
provider_id: str,
model_id: str,
) -> Dict[str, Any]:
"""Get inherited default parameters for a provider model."""
return ConfigWriter.get_effective_model_default_parameters(
provider_id,
model_id,
)

# ==================== Default Models ====================

def get_default_model(
Expand Down
17 changes: 9 additions & 8 deletions flocks/provider/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
# ---------------------------------------------------------------------------
DEFAULT_THINKING_BUDGET = 16000
DEFAULT_OUTPUT_BUFFER = 8192
DEFAULT_KIMI_K3_REASONING_EFFORT = "max"
DEFAULT_REASONING_EFFORT = "high"
DEFAULT_KIMI_K3_REASONING_EFFORT = DEFAULT_REASONING_EFFORT
KIMI_K3_REASONING_EFFORTS = frozenset({"low", "high", "max"})

_GENERIC_CHAT_REASONING_EXTRA_BODY_KEYS = {
Expand Down Expand Up @@ -76,15 +77,15 @@ def _resolve_reasoning_enabled(provider_id: str, model_id: str) -> Optional[bool


def _resolve_reasoning_effort(provider_id: str, model_id: str) -> Optional[str]:
"""Read a model-level reasoning effort from flocks.json."""
"""Read the effective reasoning effort from flocks.json."""
try:
from flocks.provider.model_manager import get_model_manager

setting = get_model_manager().get_setting(provider_id, model_id)
if not setting:
return None

value = (setting.default_parameters or {}).get("reasoning_effort")
default_parameters = get_model_manager().get_effective_default_parameters(
provider_id,
model_id,
)
value = default_parameters.get("reasoning_effort")
return value.strip().lower() if isinstance(value, str) else None
except Exception as exc:
log.debug("options.reasoning_effort_setting_lookup_failed", {
Expand Down Expand Up @@ -370,7 +371,7 @@ def build_provider_options(
# -- OpenAI reasoning (o1 / o3 / gpt-5) --------------------------------
elif provider_id == "openai":
if reasoning_enabled is not False and any(tag in model_lower for tag in ("o1", "o3", "gpt-5")):
options["reasoningEffort"] = "medium"
options["reasoningEffort"] = reasoning_effort or DEFAULT_REASONING_EFFORT

# -- Google Gemini thinking ---------------------------------------------
elif provider_id == "google":
Expand Down
49 changes: 49 additions & 0 deletions tests/config/test_config_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,35 @@ def test_get_all_model_settings(self, temp_project):
assert "anthropic/claude-sonnet" in all_settings
assert len(all_settings) == 2

def test_effective_default_parameters_follow_scope_precedence(self, temp_project):
from flocks.config.config_writer import ConfigWriter

data = ConfigWriter._read_raw()
data["default_models"] = {
"default_parameters": {
"reasoning_effort": "low",
"temperature": 0.1,
},
}
data["model_settings"] = {
"anthropic/claude-sonnet-4-5": {
"default_parameters": {
"reasoning_effort": "high",
}
}
}
ConfigWriter._write_raw(data)

parameters = ConfigWriter.get_effective_model_default_parameters(
"anthropic",
"claude-sonnet-4-5",
)

assert parameters == {
"reasoning_effort": "high",
"temperature": 0.1,
}

def test_model_settings_preserve_other_sections(self, temp_project):
from flocks.config.config_writer import ConfigWriter
ConfigWriter.set_model_setting("openai", "gpt-4o", {"enabled": True})
Expand Down Expand Up @@ -439,6 +468,26 @@ def test_get_all_default_models(self, temp_project):
assert "text-embedding" in all_defaults
assert len(all_defaults) == 2

def test_get_all_default_models_excludes_default_parameters(self, temp_project):
from flocks.config.config_writer import ConfigWriter

data = ConfigWriter._read_raw()
data["default_models"] = {
"default_parameters": {"reasoning_effort": "medium"},
"llm": {
"provider_id": "anthropic",
"model_id": "claude-sonnet",
},
}
ConfigWriter._write_raw(data)

assert ConfigWriter.get_all_default_models() == {
"llm": {
"provider_id": "anthropic",
"model_id": "claude-sonnet",
}
}

def test_default_models_preserve_other_sections(self, temp_project):
from flocks.config.config_writer import ConfigWriter
ConfigWriter.set_default_model("llm", "anthropic", "claude")
Expand Down
28 changes: 26 additions & 2 deletions tests/provider/test_provider_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_moonshot_kimi_k3_uses_default_reasoning_effort(self):
resolve_max_tokens=False,
)

assert options["extra_body"] == {"reasoning_effort": "max"}
assert options["extra_body"] == {"reasoning_effort": "high"}

def test_kimi_k27_forces_thinking_even_when_toggle_is_disabled(self):
options = provider_options.build_provider_options(
Expand Down Expand Up @@ -127,7 +127,7 @@ def test_kimi_k3_uses_reasoning_effort_instead_of_thinking(self):
resolve_max_tokens=False,
)

assert options["extra_body"] == {"reasoning_effort": "max"}
assert options["extra_body"] == {"reasoning_effort": "high"}
assert "thinking" not in options["extra_body"]

def test_kimi_k3_respects_supported_reasoning_effort(self):
Expand Down Expand Up @@ -443,3 +443,27 @@ def test_openai_reasoning_can_be_disabled(self):
)

assert "reasoningEffort" not in options

def test_openai_reasoning_defaults_to_high(self):
options = provider_options.build_provider_options(
"openai",
"gpt-5.4",
resolve_max_tokens=False,
)

assert options["reasoningEffort"] == "high"

def test_openai_uses_configured_reasoning_effort(self, monkeypatch):
monkeypatch.setattr(
provider_options,
"_resolve_reasoning_effort",
lambda *_args: "low",
)

options = provider_options.build_provider_options(
"openai",
"gpt-5.4",
resolve_max_tokens=False,
)

assert options["reasoningEffort"] == "low"
4 changes: 2 additions & 2 deletions tests/provider/test_thinking_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def _expected_generic_chat_extra_body(
if "mimo" in model_lower:
return MIMO_THINKING_EXTRA_BODY
if is_kimi_k3_model(model_id):
return {"reasoning_effort": "max"}
return {"reasoning_effort": "high"}
if is_kimi_k27_code_model(model_id):
return KIMI_THINKING_EXTRA_BODY
if "kimi" in model_lower:
Expand Down Expand Up @@ -444,7 +444,7 @@ def test_anthropic_transport_still_uses_thinking_field(
("kimi-k2.6-uncatalogued", KIMI_THINKING_EXTRA_BODY),
("kimi-k2.7-code", KIMI_THINKING_EXTRA_BODY),
("kimi-k2.7-code-highspeed", KIMI_THINKING_EXTRA_BODY),
("kimi-k3", {"reasoning_effort": "max"}),
("kimi-k3", {"reasoning_effort": "high"}),
("mimo-v2.5-pro-uncatalogued", MIMO_THINKING_EXTRA_BODY),
("minimax-m4-uncatalogued", {"reasoning_split": True}),
("step-3.5-flash-uncatalogued", {"enable_thinking": True}),
Expand Down