mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-27 20:06:39 +08:00
fix(reasoning): apply explicit per-call levels without runner config caching
This commit is contained in:
@@ -1,129 +1,26 @@
|
||||
"""Host-only, descriptor-driven Runner reasoning policy and durable snapshot tests."""
|
||||
"""Explicit model call options preserve legacy defaults and shared state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.provider.modelmgr import errors, reasoning
|
||||
from tests.unit_tests.provider.test_reasoning_control import _requester, _runtime_model
|
||||
|
||||
|
||||
PRIMARY = '00000000-0000-4000-8000-000000000011'
|
||||
FALLBACK = '00000000-0000-4000-8000-000000000012'
|
||||
OTHER = '00000000-0000-4000-8000-000000000013'
|
||||
|
||||
|
||||
def policy():
|
||||
return importlib.import_module('langbot.pkg.agent.runner.model_reasoning')
|
||||
|
||||
|
||||
def descriptor(*names):
|
||||
return SimpleNamespace(config_schema=[{'name': name, 'type': 'model-fallback-selector'} for name in names])
|
||||
|
||||
|
||||
def resources(*ids):
|
||||
return {'models': [{'model_id': model_id} for model_id in ids]}
|
||||
|
||||
|
||||
def selection(level='high'):
|
||||
return {'primary': PRIMARY, 'fallbacks': [FALLBACK], 'reasoning': {PRIMARY: level, FALLBACK: 'low'}}
|
||||
|
||||
|
||||
def test_generic_descriptor_extracts_only_selected_authorized_models():
|
||||
value = selection()
|
||||
value['reasoning'][OTHER] = 'max'
|
||||
result = policy().extract_model_reasoning_overrides(
|
||||
descriptor('arbitrary'), {'arbitrary': value}, resources(PRIMARY, FALLBACK, OTHER)
|
||||
)
|
||||
assert result == {PRIMARY: {'level': 'high'}, FALLBACK: {'level': 'low'}}
|
||||
assert policy().extract_model_reasoning_overrides(
|
||||
descriptor('arbitrary'), {'arbitrary': value}, resources(FALLBACK)
|
||||
) == {FALLBACK: {'level': 'low'}}
|
||||
value['reasoning'][PRIMARY] = 'disabled'
|
||||
assert result[PRIMARY] == {'level': 'high'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('level', reasoning.REASONING_LEVELS)
|
||||
def test_all_canonical_levels_use_core_normalization(level):
|
||||
result = policy().extract_model_reasoning_overrides(
|
||||
descriptor('models'), {'models': selection(level)}, resources(PRIMARY)
|
||||
)
|
||||
assert result == {PRIMARY: reasoning.normalize_reasoning_config({'level': level})}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', ['plain-model', {}, {'primary': PRIMARY}, {'primary': PRIMARY, 'reasoning': {}}])
|
||||
def test_absent_map_does_not_create_default_override(value):
|
||||
assert policy().extract_model_reasoning_overrides(descriptor('models'), {'models': value}, resources(PRIMARY)) == {}
|
||||
|
||||
|
||||
def test_undeclared_fields_and_descriptor_defaults_do_not_supply_overrides():
|
||||
desc = descriptor('declared')
|
||||
desc.config_schema[0]['default'] = selection()
|
||||
assert policy().extract_model_reasoning_overrides(desc, {'model': selection()}, resources(PRIMARY)) == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value', [None, [], 'high', {PRIMARY: None}, {PRIMARY: {}}, {PRIMARY: 'secret-invalid-level'}, {PRIMARY: ['high']}]
|
||||
)
|
||||
def test_invalid_explicit_maps_fail_with_safe_error(value):
|
||||
with pytest.raises(ValueError, match='Invalid runner model reasoning configuration') as exc:
|
||||
policy().extract_model_reasoning_overrides(
|
||||
descriptor('models'), {'models': {'primary': PRIMARY, 'reasoning': value}}, resources(PRIMARY)
|
||||
)
|
||||
assert 'secret-invalid-level' not in str(exc.value)
|
||||
assert PRIMARY not in str(exc.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reverse', [False, True])
|
||||
def test_multiple_selectors_reject_conflicting_overrides_deterministically(reverse):
|
||||
names = ['one', 'two']
|
||||
if reverse:
|
||||
names.reverse()
|
||||
config = {'one': selection('high'), 'two': selection('provider_default')}
|
||||
with pytest.raises(ValueError, match='Conflicting runner model reasoning overrides'):
|
||||
policy().extract_model_reasoning_overrides(descriptor(*names), config, resources(PRIMARY))
|
||||
config['two'] = selection('high')
|
||||
assert policy().extract_model_reasoning_overrides(descriptor(*names), config, resources(PRIMARY)) == {
|
||||
PRIMARY: {'level': 'high'}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_deepcopies_reasoning_without_granting_models():
|
||||
registry = AgentRunSessionRegistry()
|
||||
overrides = {PRIMARY: {'level': 'high'}, OTHER: {'level': 'max'}}
|
||||
await registry.register(
|
||||
run_id='frozen',
|
||||
runner_id='plugin:test/runner/main',
|
||||
query_id=None,
|
||||
plugin_identity='test/runner',
|
||||
resources=resources(PRIMARY),
|
||||
model_reasoning_overrides=overrides,
|
||||
)
|
||||
overrides[PRIMARY]['level'] = 'low'
|
||||
session = await registry.get('frozen')
|
||||
assert session['authorization']['model_reasoning_overrides'][PRIMARY] == {'level': 'high'}
|
||||
assert not registry.is_resource_allowed(session, 'model', OTHER, 'invoke')
|
||||
|
||||
|
||||
def test_request_local_clone_preserves_shared_model_and_absent_default():
|
||||
model = _runtime_model(_requester('openai'), 'high', name='gpt-5')
|
||||
assert policy().model_with_reasoning_override(model, PRIMARY, None) is model
|
||||
assert policy().model_with_reasoning_override(model, PRIMARY, {'authorization': {}}) is model
|
||||
overrides = {PRIMARY: {'level': 'provider_default'}}
|
||||
clone = policy().model_with_reasoning_override(
|
||||
model, PRIMARY, {'authorization': {'model_reasoning_overrides': overrides}}
|
||||
)
|
||||
assert reasoning.model_with_reasoning_level(model, None) is model
|
||||
assert reasoning.model_with_reasoning_level(model, None) is model
|
||||
clone = reasoning.model_with_reasoning_level(model, 'provider_default')
|
||||
assert clone is not model
|
||||
assert clone.provider is model.provider
|
||||
assert clone.model_entity is model.model_entity
|
||||
assert clone.reasoning_config_override == {'level': 'provider_default'}
|
||||
clone.reasoning_config_override['level'] = 'disabled'
|
||||
assert overrides[PRIMARY]['level'] == 'provider_default'
|
||||
assert model.reasoning_config_override is None
|
||||
assert model.model_entity.reasoning_config == {'level': 'high'}
|
||||
|
||||
@@ -141,9 +38,7 @@ def test_real_requester_reasoning_boundary(provider, name, expected, monkeypatch
|
||||
monkeypatch.setattr(request, '_supports_reasoning', lambda _: True)
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, 'low', name=name)
|
||||
clone = policy().model_with_reasoning_override(
|
||||
model, PRIMARY, {'authorization': {'model_reasoning_overrides': {PRIMARY: {'level': 'high'}}}}
|
||||
)
|
||||
clone = reasoning.model_with_reasoning_level(model, 'high')
|
||||
assert request._build_reasoning_args(clone) == expected
|
||||
clone.reasoning_config_override = {'level': 'provider_default'}
|
||||
assert request._build_reasoning_args(clone) == {}
|
||||
@@ -158,58 +53,15 @@ def test_real_requester_still_rejects_ability_and_capability_mismatches(name, ab
|
||||
monkeypatch.setattr(request, '_supports_reasoning', lambda _: True)
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, name=name, abilities=abilities)
|
||||
session = {'authorization': {'model_reasoning_overrides': {PRIMARY: {'level': level}}}}
|
||||
if not abilities:
|
||||
with pytest.raises(ValueError, match='reasoning ability'):
|
||||
policy().model_with_reasoning_override(model, PRIMARY, session)
|
||||
reasoning.model_with_reasoning_level(model, level)
|
||||
else:
|
||||
clone = policy().model_with_reasoning_override(model, PRIMARY, session)
|
||||
clone = reasoning.model_with_reasoning_level(model, level)
|
||||
with pytest.raises(errors.RequesterError):
|
||||
request._build_reasoning_args(clone)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrator_freezes_host_policy_and_persistent_reload(tmp_path):
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from langbot.pkg.agent.runner.orchestrator import AgentRunOrchestrator
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.plugin.agent_run_support import _load_persistent_agent_run_session
|
||||
from tests.unit_tests.agent.test_orchestrator_integration import (
|
||||
FakeApplication,
|
||||
FakePluginConnector,
|
||||
FakeRegistry,
|
||||
make_descriptor,
|
||||
make_query,
|
||||
)
|
||||
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "reasoning.db"}')
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
connector = FakePluginConnector(results=[{'type': 'run.completed', 'data': {}}])
|
||||
ap = FakeApplication(connector, engine)
|
||||
desc = make_descriptor()
|
||||
query = make_query()
|
||||
query.pipeline_config['ai']['runner_config'][desc.id]['model']['reasoning'] = {
|
||||
'model_primary': 'high',
|
||||
'model_fallback': 'low',
|
||||
}
|
||||
expected = {'model_primary': {'level': 'high'}, 'model_fallback': {'level': 'low'}}
|
||||
try:
|
||||
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(desc))
|
||||
_ = [value async for value in orchestrator.run_from_query(query)]
|
||||
session = connector.sessions_during_run[0]
|
||||
assert session['authorization']['model_reasoning_overrides'] == expected
|
||||
wire = connector.contexts[0]
|
||||
assert 'model_reasoning_overrides' not in wire
|
||||
assert 'model_reasoning_overrides' not in wire['resources']
|
||||
query.pipeline_config['ai']['runner_config'][desc.id]['model']['reasoning']['model_primary'] = 'disabled'
|
||||
assert session['authorization']['model_reasoning_overrides'] == expected
|
||||
restored = await _load_persistent_agent_run_session(wire['run_id'], ap, 'test')
|
||||
assert restored['authorization']['model_reasoning_overrides'] == expected
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('provider', 'name', 'level', 'expected'),
|
||||
@@ -229,9 +81,7 @@ async def test_real_completion_and_count_tokens_build_boundary(provider, name, l
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, name=name)
|
||||
model.provider.token_mgr.get_token = lambda: 'test-only-not-a-secret'
|
||||
clone = policy().model_with_reasoning_override(
|
||||
model, PRIMARY, {'authorization': {'model_reasoning_overrides': {PRIMARY: {'level': level}}}}
|
||||
)
|
||||
clone = reasoning.model_with_reasoning_level(model, level)
|
||||
messages = [provider_message.Message(role='user', content='hello')]
|
||||
for stream in (False, True):
|
||||
built = await request._build_completion_args(clone, messages, extra_args={'temperature': 0.7}, stream=stream)
|
||||
@@ -259,10 +109,46 @@ async def test_real_requester_rejects_caller_reasoning_conflicts(monkeypatch):
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, name='gpt-5')
|
||||
model.provider.token_mgr.get_token = lambda: 'test-only-not-a-secret'
|
||||
clone = policy().model_with_reasoning_override(
|
||||
model, PRIMARY, {'authorization': {'model_reasoning_overrides': {PRIMARY: {'level': 'high'}}}}
|
||||
)
|
||||
clone = reasoning.model_with_reasoning_level(model, 'high')
|
||||
with pytest.raises(errors.RequesterError, match='conflicts with advanced parameters'):
|
||||
await request._build_completion_args(
|
||||
clone, [provider_message.Message(role='user', content='hello')], extra_args={'reasoning_effort': 'low'}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('stream', [False, True])
|
||||
async def test_space_detected_reasoning_without_catalog_flag_reaches_completion(stream, monkeypatch):
|
||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
||||
|
||||
request = _requester('openai', 'space-chat-completions')
|
||||
monkeypatch.setattr(request, '_supports_reasoning', lambda _: True)
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, name='gpt-5.6-sol', abilities=['vision', 'func_call'])
|
||||
model.provider.token_mgr.get_token = lambda: 'test-only-not-a-secret'
|
||||
scoped = reasoning.model_with_reasoning_level(model, 'medium')
|
||||
built = await request._build_completion_args(
|
||||
scoped, [provider_message.Message(role='user', content='hello')], stream=stream
|
||||
)
|
||||
assert built['reasoning_effort'] == 'medium'
|
||||
assert built.get('stream', False) is stream
|
||||
assert model.model_entity.abilities == ['vision', 'func_call']
|
||||
assert model.reasoning_config_override is None
|
||||
|
||||
|
||||
def test_space_unknown_model_does_not_gain_reasoning(monkeypatch):
|
||||
request = _requester('openai', 'space-chat-completions')
|
||||
monkeypatch.setattr(request, '_supports_reasoning', lambda _: False)
|
||||
monkeypatch.setattr(request, '_safe_model_info', lambda _: {})
|
||||
model = _runtime_model(request, name='unknown-model', abilities=[])
|
||||
with pytest.raises(ValueError, match='reasoning ability'):
|
||||
reasoning.model_with_reasoning_level(model, 'medium')
|
||||
|
||||
|
||||
def test_space_detected_reasoning_still_validates_conflicting_parameters(monkeypatch):
|
||||
request = _requester('openai', 'space-chat-completions')
|
||||
monkeypatch.setattr(request, '_supports_reasoning', lambda _: True)
|
||||
model = _runtime_model(request, name='gpt-5.6-sol', abilities=[])
|
||||
model.model_entity.extra_args = {'reasoning_effort': 'low'}
|
||||
with pytest.raises(ValueError, match='conflicts with advanced parameters'):
|
||||
reasoning.model_with_reasoning_level(model, 'medium')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Actual secured Host actions consume frozen policy, not plugin payload hints."""
|
||||
"""Secured Host actions accept explicit per-call options after authorization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,6 +29,7 @@ class RecordingProvider:
|
||||
|
||||
def __init__(self, request):
|
||||
self.requester = request
|
||||
self.provider_entity = SimpleNamespace(requester='openai')
|
||||
self.calls = []
|
||||
|
||||
async def record(self, kwargs):
|
||||
@@ -74,9 +75,7 @@ async def host(monkeypatch):
|
||||
)
|
||||
runtime = make_handler(ap)
|
||||
|
||||
async def register(
|
||||
run_id='run', overrides=None, workspace='workspace-a', plugin='test-author/test-plugin', operations=None
|
||||
):
|
||||
async def register(run_id='run', workspace='workspace-a', plugin='test-author/test-plugin', operations=None):
|
||||
await registry.register(
|
||||
run_id=run_id,
|
||||
runner_id='plugin:test-author/test-plugin/arbitrary',
|
||||
@@ -89,7 +88,6 @@ async def host(monkeypatch):
|
||||
for model_id in (PRIMARY, FALLBACK)
|
||||
]
|
||||
},
|
||||
model_reasoning_overrides=overrides,
|
||||
)
|
||||
return await registry.get(run_id)
|
||||
|
||||
@@ -114,10 +112,10 @@ async def call(host, action, model_id=PRIMARY, run_id='run', **extra):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_primary_fallback_and_repeated_tool_round_use_frozen_per_model_policy(host, action):
|
||||
await host.register(overrides={PRIMARY: {'level': 'high'}, FALLBACK: {'level': 'low'}})
|
||||
async def test_primary_fallback_and_repeated_tool_round_use_explicit_options(host, action):
|
||||
await host.register()
|
||||
for model_id, level in [(PRIMARY, 'high'), (FALLBACK, 'low'), (FALLBACK, 'low')]:
|
||||
responses = await call(host, action, model_id)
|
||||
responses = await call(host, action, model_id, reasoning_level=level)
|
||||
assert all(response.code == 0 for response in responses)
|
||||
kwargs, built = host.provider.calls[-1]
|
||||
assert built == {'reasoning_effort': level}
|
||||
@@ -132,8 +130,8 @@ async def test_primary_fallback_and_repeated_tool_round_use_frozen_per_model_pol
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
@pytest.mark.parametrize('level', [None, 'provider_default'])
|
||||
async def test_absent_and_explicit_provider_default_are_distinct(host, action, level):
|
||||
await host.register(overrides={PRIMARY: {'level': level}} if level else None)
|
||||
assert all(response.code == 0 for response in await call(host, action))
|
||||
await host.register()
|
||||
assert all(response.code == 0 for response in await call(host, action, reasoning_level=level))
|
||||
kwargs, built = host.provider.calls[-1]
|
||||
assert built == ({} if level else {'reasoning_effort': 'medium'})
|
||||
assert (kwargs['model'] is host.models[PRIMARY]) is (level is None)
|
||||
@@ -158,8 +156,8 @@ async def test_regular_plugin_without_run_keeps_model_defaults_and_ignores_forge
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_plugin_cannot_replace_host_map(host, action):
|
||||
await host.register(overrides={PRIMARY: {'level': 'high'}})
|
||||
async def test_obsolete_hidden_policy_fields_do_not_override_explicit_api(host, action):
|
||||
await host.register()
|
||||
responses = await call(
|
||||
host,
|
||||
action,
|
||||
@@ -167,7 +165,7 @@ async def test_plugin_cannot_replace_host_map(host, action):
|
||||
reasoning_config_override={'level': 'disabled'},
|
||||
)
|
||||
assert all(response.code == 0 for response in responses)
|
||||
assert host.provider.calls[-1][1] == {'reasoning_effort': 'high'}
|
||||
assert host.provider.calls[-1][1] == {'reasoning_effort': 'medium'}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -175,7 +173,6 @@ async def test_plugin_cannot_replace_host_map(host, action):
|
||||
@pytest.mark.parametrize('denial', ['workspace', 'plugin', 'unselected', 'operation', 'expired'])
|
||||
async def test_authorization_denial_happens_before_model_access(host, action, denial):
|
||||
await host.register(
|
||||
overrides={PRIMARY: {'level': 'high'}, OTHER: {'level': 'max'}},
|
||||
workspace='workspace-b' if denial == 'workspace' else 'workspace-a',
|
||||
plugin='other/plugin' if denial == 'plugin' else 'test-author/test-plugin',
|
||||
operations=['rerank'] if denial == 'operation' else None,
|
||||
@@ -191,9 +188,14 @@ async def test_authorization_denial_happens_before_model_access(host, action, de
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_concurrent_runs_share_model_without_cross_run_or_round_leakage(host, action):
|
||||
await host.register('high-run', {PRIMARY: {'level': 'high'}})
|
||||
await host.register('low-run', {PRIMARY: {'level': 'low'}})
|
||||
results = await asyncio.gather(*(call(host, action, run_id=run_id) for run_id in ['high-run', 'low-run'] * 3))
|
||||
await host.register('high-run')
|
||||
await host.register('low-run')
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
call(host, action, run_id=run_id, reasoning_level=run_id.split('-')[0])
|
||||
for run_id in ['high-run', 'low-run'] * 3
|
||||
)
|
||||
)
|
||||
assert all(response.code == 0 for result in results for response in result)
|
||||
assert sorted(built['reasoning_effort'] for _, built in host.provider.calls) == ['high'] * 3 + ['low'] * 3
|
||||
assert len({id(kwargs['model']) for kwargs, _ in host.provider.calls}) == 6
|
||||
@@ -203,7 +205,7 @@ async def test_concurrent_runs_share_model_without_cross_run_or_round_leakage(ho
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_model_runtime_workspace_mismatch_denies(host, action):
|
||||
await host.register(overrides={PRIMARY: {'level': 'high'}})
|
||||
await host.register()
|
||||
host.models[PRIMARY].model_entity.workspace_uuid = 'workspace-b'
|
||||
responses = await call(host, action)
|
||||
assert all(response.code != 0 for response in responses)
|
||||
@@ -213,33 +215,16 @@ async def test_model_runtime_workspace_mismatch_denies(host, action):
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_host_reuses_core_ability_validation_before_provider_call(host, action):
|
||||
await host.register(overrides={PRIMARY: {'level': 'high'}})
|
||||
await host.register()
|
||||
host.models[PRIMARY].model_entity.abilities = []
|
||||
with pytest.raises(ValueError, match='reasoning ability'):
|
||||
await call(host, action)
|
||||
await call(host, action, reasoning_level='high')
|
||||
assert not host.provider.calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('action', ACTIONS)
|
||||
async def test_snapshot_survives_configuration_edits_and_tool_followup(host, action):
|
||||
config = {PRIMARY: {'level': 'high'}, FALLBACK: {'level': 'low'}}
|
||||
await host.register(overrides=config)
|
||||
config[PRIMARY]['level'] = 'disabled'
|
||||
config[FALLBACK]['level'] = 'max'
|
||||
responses = await call(
|
||||
host,
|
||||
action,
|
||||
FALLBACK,
|
||||
messages=[
|
||||
{'role': 'user', 'content': 'search'},
|
||||
{
|
||||
'role': 'assistant',
|
||||
'content': '',
|
||||
'tool_calls': [{'id': 'call-1', 'type': 'function', 'function': {'name': 'search', 'arguments': '{}'}}],
|
||||
},
|
||||
{'role': 'tool', 'content': 'search result', 'tool_call_id': 'call-1'},
|
||||
],
|
||||
)
|
||||
async def test_regular_plugins_can_explicitly_set_level_without_runner_session(host, action):
|
||||
responses = await call(host, action, run_id=None, reasoning_level='low')
|
||||
assert all(response.code == 0 for response in responses)
|
||||
assert host.provider.calls[-1][1] == {'reasoning_effort': 'low'}
|
||||
|
||||
Reference in New Issue
Block a user