mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-04 12:56:02 +00:00
341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""Tests for requester pure utility functions.
|
|
|
|
Tests the helper methods in OpenAIChatCompletions that don't require network calls.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from tests.utils.import_isolation import isolated_sys_modules
|
|
|
|
|
|
class TestMaskApiKey:
|
|
"""Tests for _mask_api_key method."""
|
|
|
|
def _create_requester_with_mocks(self):
|
|
"""Create requester instance with mocked dependencies."""
|
|
mocks = {
|
|
'langbot.pkg.core.app': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.resource.tool': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.pipeline.query': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.provider.message': MagicMock(),
|
|
'langbot.pkg.provider.modelmgr.errors': MagicMock(),
|
|
}
|
|
|
|
with isolated_sys_modules(mocks):
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
mock_app = MagicMock()
|
|
requester = OpenAIChatCompletions(mock_app, {})
|
|
return requester
|
|
|
|
def test_mask_api_key_full(self):
|
|
"""Mask a full API key."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._mask_api_key('sk-1234567890abcdef')
|
|
assert result == 'sk-1...cdef'
|
|
|
|
def test_mask_api_key_short(self):
|
|
"""Mask a short API key (<=8 chars)."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._mask_api_key('short')
|
|
assert result == '****'
|
|
|
|
def test_mask_api_key_empty(self):
|
|
"""Empty API key returns empty string."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._mask_api_key('')
|
|
assert result == ''
|
|
|
|
def test_mask_api_key_none(self):
|
|
"""None API key returns empty string."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._mask_api_key(None)
|
|
assert result == ''
|
|
|
|
def test_mask_api_key_exact_8_chars(self):
|
|
"""API key with exactly 8 chars is masked as **** (<=8 threshold)."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._mask_api_key('12345678')
|
|
assert result == '****' # <= 8 chars gets masked
|
|
|
|
|
|
class TestInferModelType:
|
|
"""Tests for _infer_model_type method."""
|
|
|
|
def _create_requester_with_mocks(self):
|
|
mocks = {
|
|
'langbot.pkg.core.app': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.resource.tool': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.pipeline.query': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.provider.message': MagicMock(),
|
|
'langbot.pkg.provider.modelmgr.errors': MagicMock(),
|
|
}
|
|
|
|
with isolated_sys_modules(mocks):
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
mock_app = MagicMock()
|
|
requester = OpenAIChatCompletions(mock_app, {})
|
|
return requester
|
|
|
|
def test_infer_embedding_from_name(self):
|
|
"""Infer embedding type from model name."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
assert requester._infer_model_type('text-embedding-ada-002') == 'embedding'
|
|
assert requester._infer_model_type('bge-large-en') == 'embedding'
|
|
assert requester._infer_model_type('e5-base') == 'embedding'
|
|
assert requester._infer_model_type('m3e-base') == 'embedding'
|
|
|
|
def test_infer_llm_from_name(self):
|
|
"""Infer LLM type from model name."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
assert requester._infer_model_type('gpt-4') == 'llm'
|
|
assert requester._infer_model_type('claude-3-opus') == 'llm'
|
|
assert requester._infer_model_type('llama-2-70b') == 'llm'
|
|
|
|
def test_infer_model_type_none_id(self):
|
|
"""Handle None model_id."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._infer_model_type(None)
|
|
assert result == 'llm' # Default
|
|
|
|
def test_infer_model_type_empty_id(self):
|
|
"""Handle empty model_id."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._infer_model_type('')
|
|
assert result == 'llm' # Default
|
|
|
|
|
|
class TestNormalizeModalities:
|
|
"""Tests for _normalize_modalities method."""
|
|
|
|
def _create_requester_with_mocks(self):
|
|
mocks = {
|
|
'langbot.pkg.core.app': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.resource.tool': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.pipeline.query': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.provider.message': MagicMock(),
|
|
'langbot.pkg.provider.modelmgr.errors': MagicMock(),
|
|
}
|
|
|
|
with isolated_sys_modules(mocks):
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
mock_app = MagicMock()
|
|
requester = OpenAIChatCompletions(mock_app, {})
|
|
return requester
|
|
|
|
def test_normalize_string_modality(self):
|
|
"""Normalize single string modality."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._normalize_modalities('text,image')
|
|
assert result == ['text', 'image']
|
|
|
|
def test_normalize_list_modalities(self):
|
|
"""Normalize list of modalities."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._normalize_modalities(['text', 'image', 'audio'])
|
|
assert result == ['text', 'image', 'audio']
|
|
|
|
def test_normalize_dict_modalities(self):
|
|
"""Normalize dict with nested modalities."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._normalize_modalities({'input': ['text'], 'output': ['text', 'image']})
|
|
assert result == ['text', 'image']
|
|
|
|
def test_normalize_none(self):
|
|
"""Handle None input."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._normalize_modalities(None)
|
|
assert result == []
|
|
|
|
def test_normalize_arrow_separator(self):
|
|
"""Handle arrow separator in modality string."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
result = requester._normalize_modalities('text->image')
|
|
assert result == ['text', 'image']
|
|
|
|
|
|
class TestParseRerankResponse:
|
|
"""Tests for _parse_rerank_response static method."""
|
|
|
|
def test_parse_cohere_jina_format(self):
|
|
"""Parse Cohere/Jina/SiliconFlow format."""
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
data = {
|
|
'results': [
|
|
{'index': 0, 'relevance_score': 0.95},
|
|
{'index': 1, 'relevance_score': 0.80},
|
|
]
|
|
}
|
|
|
|
result = OpenAIChatCompletions._parse_rerank_response(data)
|
|
assert result == [
|
|
{'index': 0, 'relevance_score': 0.95},
|
|
{'index': 1, 'relevance_score': 0.80},
|
|
]
|
|
|
|
def test_parse_voyage_format(self):
|
|
"""Parse Voyage AI format."""
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
data = {
|
|
'data': [
|
|
{'index': 0, 'relevance_score': 0.90},
|
|
{'index': 2, 'relevance_score': 0.75},
|
|
]
|
|
}
|
|
|
|
result = OpenAIChatCompletions._parse_rerank_response(data)
|
|
assert result == [
|
|
{'index': 0, 'relevance_score': 0.90},
|
|
{'index': 2, 'relevance_score': 0.75},
|
|
]
|
|
|
|
def test_parse_dashscope_format(self):
|
|
"""Parse DashScope format."""
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
data = {
|
|
'output': {
|
|
'results': [
|
|
{'index': 0, 'relevance_score': 0.85},
|
|
]
|
|
}
|
|
}
|
|
|
|
result = OpenAIChatCompletions._parse_rerank_response(data)
|
|
assert result == [{'index': 0, 'relevance_score': 0.85}]
|
|
|
|
def test_parse_unknown_format(self):
|
|
"""Handle unknown format returns empty list."""
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
data = {'unknown_key': 'value'}
|
|
|
|
result = OpenAIChatCompletions._parse_rerank_response(data)
|
|
assert result == []
|
|
|
|
def test_parse_empty_results(self):
|
|
"""Handle empty results."""
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
data = {'results': []}
|
|
|
|
result = OpenAIChatCompletions._parse_rerank_response(data)
|
|
assert result == []
|
|
|
|
|
|
class TestExtractScanMetadata:
|
|
"""Tests for _extract_scan_metadata method."""
|
|
|
|
def _create_requester_with_mocks(self):
|
|
mocks = {
|
|
'langbot.pkg.core.app': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.resource.tool': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.pipeline.query': MagicMock(),
|
|
'langbot_plugin.api.entities.builtin.provider.message': MagicMock(),
|
|
'langbot.pkg.provider.modelmgr.errors': MagicMock(),
|
|
}
|
|
|
|
with isolated_sys_modules(mocks):
|
|
from langbot.pkg.provider.modelmgr.requesters.chatcmpl import OpenAIChatCompletions
|
|
|
|
mock_app = MagicMock()
|
|
requester = OpenAIChatCompletions(mock_app, {})
|
|
return requester
|
|
|
|
def test_extract_basic_metadata(self):
|
|
"""Extract basic model metadata."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
item = {
|
|
'id': 'gpt-4',
|
|
'name': 'GPT-4 Turbo',
|
|
'description': 'Most capable GPT-4 model',
|
|
'context_length': 128000,
|
|
'owned_by': 'openai',
|
|
}
|
|
|
|
result = requester._extract_scan_metadata(item, 'gpt-4')
|
|
|
|
assert result['display_name'] == 'GPT-4 Turbo'
|
|
assert result['description'] == 'Most capable GPT-4 model'
|
|
assert result['context_length'] == 128000
|
|
assert result['owned_by'] == 'openai'
|
|
|
|
def test_extract_metadata_missing_fields(self):
|
|
"""Handle missing metadata fields."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
item = {'id': 'unknown-model'}
|
|
|
|
result = requester._extract_scan_metadata(item, 'unknown-model')
|
|
|
|
assert result['display_name'] is None
|
|
assert result['description'] is None
|
|
assert result['context_length'] is None
|
|
assert result['owned_by'] is None
|
|
|
|
def test_extract_metadata_top_provider_context(self):
|
|
"""Extract context_length from top_provider."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
item = {
|
|
'id': 'model',
|
|
'top_provider': {
|
|
'context_length': 4096,
|
|
},
|
|
}
|
|
|
|
result = requester._extract_scan_metadata(item, 'model')
|
|
|
|
assert result['context_length'] == 4096
|
|
|
|
def test_extract_metadata_empty_strings(self):
|
|
"""Handle empty string values."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
item = {
|
|
'id': 'model',
|
|
'name': '', # Empty name
|
|
'description': ' ', # Whitespace only
|
|
'owned_by': '',
|
|
}
|
|
|
|
result = requester._extract_scan_metadata(item, 'model')
|
|
|
|
assert result['display_name'] is None
|
|
assert result['description'] is None
|
|
assert result['owned_by'] is None
|
|
|
|
def test_extract_metadata_name_matches_id(self):
|
|
"""When name equals id, display_name is None."""
|
|
requester = self._create_requester_with_mocks()
|
|
|
|
item = {
|
|
'id': 'gpt-4',
|
|
'name': 'gpt-4', # Same as id
|
|
}
|
|
|
|
result = requester._extract_scan_metadata(item, 'gpt-4')
|
|
|
|
assert result['display_name'] is None
|