feat(fusion_accounting_ai): add LLMProvider contract + configurable openai base_url

Phase 1 prerequisite for local LLM support. Adapters now declare
capability flags (supports_tool_calling, max_context_tokens, etc.) so
the engine can reason about what backend is available.

OpenAI adapter accepts fusion_accounting.openai_base_url config -- point
it at LM Studio (http://host.docker.internal:1234/v1) or Ollama
(http://host.docker.internal:11434/v1) and the existing OpenAI adapter
works unchanged.

Implementation note: existing Odoo AbstractModel adapters
(fusion.accounting.adapter.openai/claude) are preserved untouched to
avoid breaking the chat panel; the new plain-Python OpenAIAdapter and
ClaudeAdapter classes (LLMProvider subclasses) are added alongside them.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 10:05:54 -04:00
parent 78a481f3f4
commit 60bf2adfa8
7 changed files with 219 additions and 1 deletions

View File

@@ -4,6 +4,8 @@ import logging
from odoo import models, api, _
from odoo.exceptions import UserError
from ._base import LLMProvider
_logger = logging.getLogger(__name__)
try:
@@ -12,6 +14,64 @@ except ImportError:
anthropic_sdk = None
class ClaudeAdapter(LLMProvider):
"""Plain-Python LLMProvider implementation for Anthropic Claude.
Preserves all existing functionality (extended thinking, native tool_use
blocks) used by the Odoo AbstractModel-based adapter -- this class is
additive for the Phase 1 LLMProvider contract.
"""
supports_tool_calling = True
supports_streaming = True
max_context_tokens = 200000
supports_embeddings = False
def __init__(self, env):
super().__init__(env)
if anthropic_sdk is None:
raise UserError(_("The 'anthropic' Python package is not installed."))
ICP = env['ir.config_parameter'].sudo()
try:
api_key = env['fusion.api.service'].get_api_key(
provider_type='anthropic',
consumer='fusion_accounting',
feature='chat_with_tools',
)
except Exception:
api_key = ICP.get_param('fusion_accounting.anthropic_api_key', '')
if not api_key:
api_key = 'not-needed'
self.client = anthropic_sdk.Anthropic(api_key=api_key)
self.model = ICP.get_param(
'fusion_accounting.claude_model', 'claude-sonnet-4-6')
def complete(self, *, system, messages, max_tokens=2048, temperature=0.0) -> dict:
api_messages = [
m for m in messages if m.get('role') in ('user', 'assistant')
]
try:
response = self.client.messages.create(
model=self.model,
max_tokens=max_tokens,
temperature=temperature,
system=system,
messages=api_messages,
)
except Exception as e:
_logger.error("Claude complete error: %s", e)
raise UserError(_("Claude API error: %s", str(e)))
text_parts = [b.text for b in response.content if getattr(b, 'type', None) == 'text']
return {
'content': '\n'.join(text_parts),
'tokens_used': (
getattr(response.usage, 'input_tokens', 0)
+ getattr(response.usage, 'output_tokens', 0)
),
'model': self.model,
}
class FusionAccountingAdapterClaude(models.AbstractModel):
_name = 'fusion.accounting.adapter.claude'
_description = 'Claude AI Adapter'