"""Local LLM compat smoke for the commentary generator. Auto-detects an LM Studio (:1234) or Ollama (:11434) server on either `host.docker.internal` or `localhost`. If none is reachable the test self-skips so CI without a local LLM stays green. Tagged 'local_llm' so it's never part of the default run. """ import socket from datetime import date from odoo.tests.common import TransactionCase, tagged def _server_reachable(host, port, timeout=1.0): try: with socket.create_connection((host, port), timeout=timeout): return True except (OSError, socket.timeout): return False def _detect_local_llm(): """Return (base_url, default_model) for the first reachable server, or (None, None) if none of the common dev endpoints respond.""" candidates = [ ('host.docker.internal', 1234, 'local-model'), ('host.docker.internal', 11434, 'llama3.1:8b'), ('localhost', 1234, 'local-model'), ('localhost', 11434, 'llama3.1:8b'), ] for host, port, default_model in candidates: if _server_reachable(host, port, timeout=0.5): return (f'http://{host}:{port}/v1', default_model) return (None, None) @tagged('post_install', '-at_install', 'local_llm') class TestLocalLLMCommentary(TransactionCase): def setUp(self): super().setUp() self.base_url, self.model = _detect_local_llm() if not self.base_url: self.skipTest( "No local LLM server detected " "(LM Studio :1234 / Ollama :11434)" ) def test_commentary_with_local_llm(self): params = self.env['ir.config_parameter'].sudo() keys = [ 'fusion_accounting.openai_base_url', 'fusion_accounting.openai_model', 'fusion_accounting.openai_api_key', 'fusion_accounting.provider.reports_commentary', ] prior = {k: params.get_param(k) for k in keys} params.set_param('fusion_accounting.openai_base_url', self.base_url) params.set_param('fusion_accounting.openai_model', self.model) params.set_param('fusion_accounting.openai_api_key', 'lm-studio') params.set_param( 'fusion_accounting.provider.reports_commentary', 'openai', ) try: from odoo.addons.fusion_accounting_reports.services.commentary_generator import ( generate_commentary, ) from odoo.addons.fusion_accounting_reports.services.date_periods import ( Period, ) period = Period(date(2026, 1, 1), date(2026, 12, 31), '2026') result = self.env['fusion.report.engine'].compute_pnl( period, company_id=self.env.company.id, ) commentary = generate_commentary(self.env, report_result=result) self.assertIn('summary', commentary) # Don't assert specific content - just that it returned a dict finally: for k, v in prior.items(): if v is not None: params.set_param(k, v)