mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-14 17:56:03 +00:00
Compare commits
3 Commits
fix/litell
...
v4.10.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7978317c | ||
|
|
b7d8332cb0 | ||
|
|
7fe3eedeea |
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.10.1"
|
version = "4.10.2"
|
||||||
description = "Production-grade platform for building agentic IM bots"
|
description = "Production-grade platform for building agentic IM bots"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Production-grade platform for building agentic IM bots"""
|
"""LangBot - Production-grade platform for building agentic IM bots"""
|
||||||
|
|
||||||
__version__ = '4.10.1'
|
__version__ = '4.10.2'
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ class LoadConfigStage(stage.BootingStage):
|
|||||||
constants.instance_id = new_id
|
constants.instance_id = new_id
|
||||||
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
||||||
|
|
||||||
|
# Instance creation timestamp: sourced from data/labels/instance_id.json.
|
||||||
|
# Instances created before this field existed (or supplied via
|
||||||
|
# system.instance_id) won't have it, so backfill with the current time
|
||||||
|
# and persist it via the dump below — from then on it stays stable.
|
||||||
|
instance_create_ts = ap.instance_id.data.get('instance_create_ts', 0)
|
||||||
|
if not isinstance(instance_create_ts, int) or instance_create_ts <= 0:
|
||||||
|
instance_create_ts = int(time.time())
|
||||||
|
ap.instance_id.data['instance_create_ts'] = instance_create_ts
|
||||||
|
constants.instance_create_ts = instance_create_ts
|
||||||
|
|
||||||
print(f'LangBot instance id: {constants.instance_id}')
|
print(f'LangBot instance id: {constants.instance_id}')
|
||||||
print(f'LangBot edition: {constants.edition}')
|
print(f'LangBot edition: {constants.edition}')
|
||||||
|
|
||||||
|
|||||||
@@ -75,22 +75,33 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
continue
|
continue
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _positive_int(value: typing.Any) -> int | None:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return None
|
||||||
|
if isinstance(value, int) and value > 0:
|
||||||
|
return value
|
||||||
|
if isinstance(value, str) and value.isdigit():
|
||||||
|
parsed_value = int(value)
|
||||||
|
if parsed_value > 0:
|
||||||
|
return parsed_value
|
||||||
|
return None
|
||||||
|
|
||||||
def _context_length_from_scan_payload(self, model_payload: dict[str, typing.Any] | None) -> int | None:
|
def _context_length_from_scan_payload(self, model_payload: dict[str, typing.Any] | None) -> int | None:
|
||||||
if not model_payload:
|
if not model_payload:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for field_name in ('context_length', 'context_window', 'max_context_length'):
|
for field_name in ('context_length', 'context_window', 'max_context_length'):
|
||||||
value = model_payload.get(field_name)
|
context_length = self._positive_int(model_payload.get(field_name))
|
||||||
if isinstance(value, bool):
|
if context_length is not None:
|
||||||
continue
|
return context_length
|
||||||
if isinstance(value, int) and value > 0:
|
|
||||||
return value
|
|
||||||
if isinstance(value, str) and value.isdigit():
|
|
||||||
parsed_value = int(value)
|
|
||||||
if parsed_value > 0:
|
|
||||||
return parsed_value
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _context_length_from_litellm_model_info(self, model_info: typing.Any) -> int | None:
|
||||||
|
if isinstance(model_info, dict):
|
||||||
|
return self._positive_int(model_info.get('max_input_tokens'))
|
||||||
|
return self._positive_int(getattr(model_info, 'max_input_tokens', None))
|
||||||
|
|
||||||
def _metadata_provider_candidates(self, model_name: str) -> list[str]:
|
def _metadata_provider_candidates(self, model_name: str) -> list[str]:
|
||||||
normalized_model_name = (model_name or '').lower()
|
normalized_model_name = (model_name or '').lower()
|
||||||
candidates = []
|
candidates = []
|
||||||
@@ -126,7 +137,7 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _safe_context_length(self, model_name: str) -> int | None:
|
def _safe_context_length(self, model_name: str) -> int | None:
|
||||||
helper = getattr(litellm, 'get_max_tokens', None)
|
helper = getattr(litellm, 'get_model_info', None)
|
||||||
if not callable(helper):
|
if not callable(helper):
|
||||||
return self._known_context_length_fallback(model_name)
|
return self._known_context_length_fallback(model_name)
|
||||||
|
|
||||||
@@ -143,11 +154,12 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
continue
|
continue
|
||||||
tried_candidates.append(candidate)
|
tried_candidates.append(candidate)
|
||||||
try:
|
try:
|
||||||
max_tokens = helper(candidate)
|
model_info = helper(candidate)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if isinstance(max_tokens, int) and max_tokens > 0:
|
context_length = self._context_length_from_litellm_model_info(model_info)
|
||||||
return max_tokens
|
if context_length is not None:
|
||||||
|
return context_length
|
||||||
return self._known_context_length_fallback(model_name)
|
return self._known_context_length_fallback(model_name)
|
||||||
|
|
||||||
def _supports_function_calling(self, model_name: str) -> bool:
|
def _supports_function_calling(self, model_name: str) -> bool:
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ async def build_heartbeat_payload(ap: core_app.Application) -> dict:
|
|||||||
'query_id': '',
|
'query_id': '',
|
||||||
'version': constants.semantic_version,
|
'version': constants.semantic_version,
|
||||||
'instance_id': constants.instance_id,
|
'instance_id': constants.instance_id,
|
||||||
|
'instance_create_ts': constants.instance_create_ts,
|
||||||
'edition': constants.edition,
|
'edition': constants.edition,
|
||||||
'features': features,
|
'features': features,
|
||||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||||
|
|||||||
@@ -16,3 +16,11 @@ debug_mode = False
|
|||||||
edition = 'community'
|
edition = 'community'
|
||||||
|
|
||||||
instance_id = ''
|
instance_id = ''
|
||||||
|
|
||||||
|
instance_create_ts = 0
|
||||||
|
"""Unix timestamp (seconds) of when this instance was first created.
|
||||||
|
|
||||||
|
Sourced from ``data/labels/instance_id.json``. Backfilled to the current
|
||||||
|
time for instances created before this field existed, so it is always a
|
||||||
|
positive value once load_config has run.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1034,11 +1034,28 @@ class TestScanModels:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(litellmchat.litellm, 'get_max_tokens') as mock_get_max_tokens:
|
with patch.object(litellmchat.litellm, 'get_model_info') as mock_get_model_info:
|
||||||
mock_get_max_tokens.side_effect = lambda model: 131072 if model == 'moonshot/moonshot-v1-128k' else None
|
mock_get_model_info.side_effect = (
|
||||||
|
lambda model: {'max_input_tokens': 131072}
|
||||||
|
if model == 'moonshot/moonshot-v1-128k'
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
assert requester._safe_context_length('moonshot-v1-128k') == 131072
|
assert requester._safe_context_length('moonshot-v1-128k') == 131072
|
||||||
|
|
||||||
|
def test_safe_context_length_uses_litellm_max_input_tokens(self):
|
||||||
|
"""LiteLLM max_output_tokens must not be treated as the context window."""
|
||||||
|
requester = litellmchat.LiteLLMRequester(ap=Mock(), config={})
|
||||||
|
|
||||||
|
with patch.object(litellmchat.litellm, 'get_model_info') as mock_get_model_info:
|
||||||
|
mock_get_model_info.return_value = {
|
||||||
|
'max_input_tokens': 128000,
|
||||||
|
'max_output_tokens': 16384,
|
||||||
|
'max_tokens': 16384,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert requester._safe_context_length('gpt-4o') == 128000
|
||||||
|
|
||||||
def test_litellm_bool_helper_tries_moonshot_metadata_alias(self):
|
def test_litellm_bool_helper_tries_moonshot_metadata_alias(self):
|
||||||
"""OpenAI-compatible Moonshot endpoints still use Moonshot metadata for abilities."""
|
"""OpenAI-compatible Moonshot endpoints still use Moonshot metadata for abilities."""
|
||||||
requester = litellmchat.LiteLLMRequester(
|
requester = litellmchat.LiteLLMRequester(
|
||||||
@@ -1102,7 +1119,7 @@ class TestScanModels:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(litellmchat.litellm, 'get_max_tokens', side_effect=Exception('not mapped')):
|
with patch.object(litellmchat.litellm, 'get_model_info', side_effect=Exception('not mapped')):
|
||||||
assert requester._safe_context_length('deepseek-v4-pro') == 1_000_000
|
assert requester._safe_context_length('deepseek-v4-pro') == 1_000_000
|
||||||
assert requester._safe_context_length('deepseek-v4-flash') == 1_000_000
|
assert requester._safe_context_length('deepseek-v4-flash') == 1_000_000
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class TestBuildHeartbeatPayload:
|
|||||||
|
|
||||||
assert payload['event_type'] == 'instance_heartbeat'
|
assert payload['event_type'] == 'instance_heartbeat'
|
||||||
assert payload['query_id'] == ''
|
assert payload['query_id'] == ''
|
||||||
|
assert 'instance_create_ts' in payload
|
||||||
assert 'timestamp' in payload
|
assert 'timestamp' in payload
|
||||||
f = payload['features']
|
f = payload['features']
|
||||||
assert f['database'] == 'postgresql'
|
assert f['database'] == 'postgresql'
|
||||||
|
|||||||
Reference in New Issue
Block a user