Coverage for integrations / coding_agent / aider_core / hart_model_adapter.py: 44.4%
54 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2HARTOS Model Adapter — Bridges HARTOS LLM infrastructure to Aider's Model interface.
4Aider's repomap.py calls self.main_model.token_count(text) for sizing the repo map.
5This adapter provides that method using tiktoken (already a HARTOS dependency),
6without requiring LiteLLM.
8For actual LLM completions (used by the HartCoder), this adapter routes through
9HARTOS's existing OpenAI client and budget gate.
10"""
11import logging
12import os
13from typing import Dict, List, Optional
15logger = logging.getLogger('hevolve.coding_agent.aider_core')
17# Lazy tiktoken import — available in HARTOS venv
18_encoder = None
21def _get_encoder():
22 """Get or create a tiktoken encoder (cached)."""
23 global _encoder
24 if _encoder is None:
25 try:
26 import tiktoken
27 _encoder = tiktoken.encoding_for_model('gpt-4')
28 except ImportError:
29 logger.warning("tiktoken not available, using approximate token counting")
30 _encoder = _ApproximateEncoder()
31 except Exception:
32 _encoder = _ApproximateEncoder()
33 return _encoder
36class _ApproximateEncoder:
37 """Fallback token counter when tiktoken isn't available (~4 chars per token)."""
39 def encode(self, text):
40 return [0] * (len(text) // 4)
43class HartModelAdapter:
44 """Minimal model adapter that satisfies Aider's Model interface for repomap.
46 Only implements what repomap.py actually calls:
47 - token_count(text) -> int
48 """
50 def __init__(self, model_name: str = 'gpt-4', max_context_window: int = 128000):
51 self.name = model_name
52 self.max_context_window = max_context_window
54 def token_count(self, text: str) -> int:
55 """Count tokens in text using tiktoken."""
56 if not text:
57 return 0
58 encoder = _get_encoder()
59 return len(encoder.encode(text))
61 @classmethod
62 def from_hartos_config(cls) -> 'HartModelAdapter':
63 """Create adapter from HARTOS configuration.
65 Uses model_registry if available, falls back to env/defaults.
66 """
67 model_name = os.environ.get('HEVOLVE_CODING_MODEL', 'gpt-4')
68 max_ctx = 128000
70 try:
71 from integrations.agent_engine.model_registry import get_model_by_policy
72 model_info = get_model_by_policy()
73 if model_info:
74 model_name = model_info.get('model_id', model_name)
75 max_ctx = model_info.get('context_window', max_ctx)
76 except ImportError:
77 pass
79 return cls(model_name=model_name, max_context_window=max_ctx)
82def send_completion(
83 messages: List[Dict],
84 model: str = '',
85 temperature: float = 0.0,
86 max_tokens: int = 4096,
87 user_id: str = '',
88 prompt_id: str = '',
89) -> Optional[str]:
90 """Send a completion request through HARTOS's LLM infrastructure.
92 Routes through budget gate for cost tracking. Used by HartCoder
93 for code editing tasks.
95 Returns:
96 Response text or None on failure.
97 """
98 if not model:
99 model = os.environ.get('HEVOLVE_CODING_MODEL', 'gpt-4')
101 try:
102 import openai
103 from helper import get_openai_config
105 config = get_openai_config()
106 client_kwargs = {}
107 if config.get('api_key'):
108 client_kwargs['api_key'] = config['api_key']
109 if config.get('api_base'):
110 client_kwargs['base_url'] = config['api_base']
112 client = openai.OpenAI(**client_kwargs)
113 response = client.chat.completions.create(
114 model=model,
115 messages=messages,
116 temperature=temperature,
117 max_tokens=max_tokens,
118 )
120 result_text = response.choices[0].message.content
122 # Record metered usage if budget gate available
123 try:
124 from integrations.agent_engine.budget_gate import record_metered_usage
125 usage = response.usage
126 record_metered_usage(
127 user_id=user_id or 'coding_agent',
128 model=model,
129 prompt_tokens=usage.prompt_tokens if usage else 0,
130 completion_tokens=usage.completion_tokens if usage else 0,
131 source='aider_native',
132 )
133 except ImportError:
134 pass
136 return result_text
138 except Exception as e:
139 logger.error(f"Completion failed: {e}")
140 return None