voidaccess 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analysis/__init__.py +49 -0
- analysis/opsec.py +454 -0
- analysis/patterns.py +202 -0
- analysis/temporal.py +201 -0
- api/__init__.py +1 -0
- api/auth.py +163 -0
- api/main.py +509 -0
- api/routes/__init__.py +1 -0
- api/routes/admin.py +214 -0
- api/routes/auth.py +157 -0
- api/routes/entities.py +871 -0
- api/routes/export.py +359 -0
- api/routes/investigations.py +2567 -0
- api/routes/monitors.py +405 -0
- api/routes/search.py +157 -0
- api/routes/settings.py +851 -0
- auth/__init__.py +1 -0
- auth/token_blacklist.py +108 -0
- cli/__init__.py +3 -0
- cli/adapters/__init__.py +1 -0
- cli/adapters/sqlite.py +273 -0
- cli/browser.py +376 -0
- cli/commands/__init__.py +1 -0
- cli/commands/configure.py +185 -0
- cli/commands/enrich.py +154 -0
- cli/commands/export.py +158 -0
- cli/commands/investigate.py +601 -0
- cli/commands/show.py +87 -0
- cli/config.py +180 -0
- cli/display.py +212 -0
- cli/main.py +154 -0
- cli/tor_detect.py +71 -0
- config.py +180 -0
- crawler/__init__.py +28 -0
- crawler/dedup.py +97 -0
- crawler/frontier.py +115 -0
- crawler/spider.py +462 -0
- crawler/utils.py +122 -0
- db/__init__.py +47 -0
- db/migrations/__init__.py +0 -0
- db/migrations/env.py +80 -0
- db/migrations/versions/0001_initial_schema.py +270 -0
- db/migrations/versions/0002_add_investigation_status_column.py +27 -0
- db/migrations/versions/0002_add_missing_tables.py +33 -0
- db/migrations/versions/0003_add_canonical_value_and_entity_links.py +61 -0
- db/migrations/versions/0004_add_page_posted_at.py +41 -0
- db/migrations/versions/0005_add_extraction_method.py +32 -0
- db/migrations/versions/0006_add_monitor_alerts.py +26 -0
- db/migrations/versions/0007_add_actor_style_profiles.py +23 -0
- db/migrations/versions/0008_add_users_table.py +47 -0
- db/migrations/versions/0009_add_investigation_id_to_relationships.py +29 -0
- db/migrations/versions/0010_add_composite_index_entity_relationships.py +22 -0
- db/migrations/versions/0011_add_page_extraction_cache.py +52 -0
- db/migrations/versions/0013_add_graph_status.py +31 -0
- db/migrations/versions/0015_add_progress_fields.py +41 -0
- db/migrations/versions/0016_backfill_graph_status.py +33 -0
- db/migrations/versions/0017_add_user_api_keys.py +44 -0
- db/migrations/versions/0018_add_user_id_to_investigations.py +33 -0
- db/migrations/versions/0019_add_content_safety_log.py +46 -0
- db/migrations/versions/0020_add_entity_source_tracking.py +50 -0
- db/models.py +618 -0
- db/queries.py +841 -0
- db/session.py +270 -0
- export/__init__.py +34 -0
- export/misp.py +257 -0
- export/sigma.py +342 -0
- export/stix.py +418 -0
- extractor/__init__.py +21 -0
- extractor/llm_extract.py +372 -0
- extractor/ner.py +512 -0
- extractor/normalizer.py +638 -0
- extractor/pipeline.py +401 -0
- extractor/regex_patterns.py +325 -0
- fingerprint/__init__.py +33 -0
- fingerprint/profiler.py +240 -0
- fingerprint/stylometry.py +249 -0
- graph/__init__.py +73 -0
- graph/builder.py +894 -0
- graph/export.py +225 -0
- graph/model.py +83 -0
- graph/queries.py +297 -0
- graph/visualize.py +178 -0
- i18n/__init__.py +24 -0
- i18n/detect.py +76 -0
- i18n/query_expand.py +72 -0
- i18n/translate.py +210 -0
- monitor/__init__.py +27 -0
- monitor/_db.py +74 -0
- monitor/alerts.py +345 -0
- monitor/config.py +118 -0
- monitor/diff.py +75 -0
- monitor/jobs.py +247 -0
- monitor/scheduler.py +184 -0
- scraper/__init__.py +0 -0
- scraper/scrape.py +857 -0
- scraper/scrape_js.py +272 -0
- search/__init__.py +318 -0
- search/circuit_breaker.py +240 -0
- search/search.py +334 -0
- sources/__init__.py +96 -0
- sources/blockchain.py +444 -0
- sources/cache.py +93 -0
- sources/cisa.py +108 -0
- sources/dns_enrichment.py +557 -0
- sources/domain_reputation.py +643 -0
- sources/email_reputation.py +635 -0
- sources/engines.py +244 -0
- sources/enrichment.py +1244 -0
- sources/github_scraper.py +589 -0
- sources/gitlab_scraper.py +624 -0
- sources/hash_reputation.py +856 -0
- sources/historical_intel.py +253 -0
- sources/ip_reputation.py +521 -0
- sources/paste_scraper.py +484 -0
- sources/pastes.py +278 -0
- sources/rss_scraper.py +576 -0
- sources/seed_manager.py +373 -0
- sources/seeds.py +368 -0
- sources/shodan.py +103 -0
- sources/telegram.py +199 -0
- sources/virustotal.py +113 -0
- utils/__init__.py +0 -0
- utils/async_utils.py +89 -0
- utils/content_safety.py +193 -0
- utils/defang.py +94 -0
- utils/encryption.py +34 -0
- utils/ioc_freshness.py +124 -0
- utils/user_keys.py +33 -0
- vector/__init__.py +39 -0
- vector/embedder.py +100 -0
- vector/model_singleton.py +49 -0
- vector/search.py +87 -0
- vector/store.py +514 -0
- voidaccess/__init__.py +0 -0
- voidaccess/llm.py +717 -0
- voidaccess/llm_utils.py +696 -0
- voidaccess-1.3.0.dist-info/METADATA +395 -0
- voidaccess-1.3.0.dist-info/RECORD +142 -0
- voidaccess-1.3.0.dist-info/WHEEL +5 -0
- voidaccess-1.3.0.dist-info/entry_points.txt +2 -0
- voidaccess-1.3.0.dist-info/licenses/LICENSE +21 -0
- voidaccess-1.3.0.dist-info/top_level.txt +19 -0
voidaccess/llm_utils.py
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
from langchain_openai import ChatOpenAI
|
|
5
|
+
from langchain_ollama import ChatOllama
|
|
6
|
+
from typing import Callable, Optional, List
|
|
7
|
+
from langchain_anthropic import ChatAnthropic
|
|
8
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
9
|
+
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
10
|
+
import os
|
|
11
|
+
from config import (
|
|
12
|
+
OLLAMA_BASE_URL,
|
|
13
|
+
OPENROUTER_BASE_URL,
|
|
14
|
+
OPENROUTER_API_KEY,
|
|
15
|
+
GOOGLE_API_KEY,
|
|
16
|
+
OPENAI_API_KEY,
|
|
17
|
+
ANTHROPIC_API_KEY,
|
|
18
|
+
LLAMA_CPP_BASE_URL,
|
|
19
|
+
GROQ_API_KEY,
|
|
20
|
+
DEFAULT_MODELS,
|
|
21
|
+
DEFAULT_MODEL,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BufferedStreamingHandler(BaseCallbackHandler):
|
|
28
|
+
def __init__(self, buffer_limit: int = 60, ui_callback: Optional[Callable[[str], None]] = None):
|
|
29
|
+
self.buffer = ""
|
|
30
|
+
self.buffer_limit = buffer_limit
|
|
31
|
+
self.ui_callback = ui_callback
|
|
32
|
+
|
|
33
|
+
def on_llm_new_token(self, token: str, **kwargs) -> None:
|
|
34
|
+
self.buffer += token
|
|
35
|
+
if "\n" in token or len(self.buffer) >= self.buffer_limit:
|
|
36
|
+
print(self.buffer, end="", flush=True)
|
|
37
|
+
if self.ui_callback:
|
|
38
|
+
self.ui_callback(self.buffer)
|
|
39
|
+
self.buffer = ""
|
|
40
|
+
|
|
41
|
+
def on_llm_end(self, response, **kwargs) -> None:
|
|
42
|
+
if self.buffer:
|
|
43
|
+
print(self.buffer, end="", flush=True)
|
|
44
|
+
if self.ui_callback:
|
|
45
|
+
self.ui_callback(self.buffer)
|
|
46
|
+
self.buffer = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- Configuration Data ---
|
|
50
|
+
# Instantiate common dependencies once
|
|
51
|
+
_common_callbacks = [BufferedStreamingHandler(buffer_limit=60)]
|
|
52
|
+
|
|
53
|
+
# Define common parameters for most LLMs
|
|
54
|
+
_common_llm_params = {
|
|
55
|
+
"temperature": 0,
|
|
56
|
+
"streaming": True,
|
|
57
|
+
"callbacks": _common_callbacks,
|
|
58
|
+
"timeout": 30.0,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
RECOMMENDED_MODELS = [
|
|
62
|
+
{
|
|
63
|
+
"id": "openrouter/deepseek/deepseek-chat",
|
|
64
|
+
"name": "DeepSeek Chat",
|
|
65
|
+
"provider": "OpenRouter",
|
|
66
|
+
"free_tier": False,
|
|
67
|
+
"recommended": True,
|
|
68
|
+
"default": True,
|
|
69
|
+
"note": "Recommended default — fast, cheap, no refusals"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
|
73
|
+
"name": "Llama 3.3 70B (Free)",
|
|
74
|
+
"provider": "OpenRouter",
|
|
75
|
+
"free_tier": True,
|
|
76
|
+
"recommended": True,
|
|
77
|
+
"default": False,
|
|
78
|
+
"note": "Free via OpenRouter — rate limited"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "groq/llama-3.3-70b-versatile",
|
|
82
|
+
"name": "Llama 3.3 70B",
|
|
83
|
+
"provider": "Groq",
|
|
84
|
+
"free_tier": True,
|
|
85
|
+
"recommended": True,
|
|
86
|
+
"default": False,
|
|
87
|
+
"note": "Free via Groq — fastest inference"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "groq/llama-3.1-8b-instant",
|
|
91
|
+
"name": "Llama 3.1 8B Instant",
|
|
92
|
+
"provider": "Groq",
|
|
93
|
+
"free_tier": True,
|
|
94
|
+
"recommended": False,
|
|
95
|
+
"default": False,
|
|
96
|
+
"note": "Free via Groq — fastest, lower quality"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "openrouter/deepseek/deepseek-r1",
|
|
100
|
+
"name": "DeepSeek R1",
|
|
101
|
+
"provider": "OpenRouter",
|
|
102
|
+
"free_tier": False,
|
|
103
|
+
"recommended": False,
|
|
104
|
+
"default": False,
|
|
105
|
+
"note": "Reasoning model — slower but thorough"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "openrouter/google/gemini-2.0-flash-001",
|
|
109
|
+
"name": "Gemini 2.0 Flash",
|
|
110
|
+
"provider": "OpenRouter",
|
|
111
|
+
"free_tier": False,
|
|
112
|
+
"recommended": False,
|
|
113
|
+
"default": False,
|
|
114
|
+
"note": "Fast, large context"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "openrouter/anthropic/claude-haiku-4-5",
|
|
118
|
+
"name": "Claude Haiku",
|
|
119
|
+
"provider": "OpenRouter",
|
|
120
|
+
"free_tier": False,
|
|
121
|
+
"recommended": False,
|
|
122
|
+
"default": False,
|
|
123
|
+
"note": "Fast Anthropic model via OpenRouter"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "gpt-4o-mini",
|
|
127
|
+
"name": "GPT-4o Mini",
|
|
128
|
+
"provider": "OpenAI",
|
|
129
|
+
"free_tier": False,
|
|
130
|
+
"recommended": True,
|
|
131
|
+
"default": False,
|
|
132
|
+
"note": "Best OpenAI price/performance"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "claude-haiku-4-5-20251001",
|
|
136
|
+
"name": "Claude Haiku",
|
|
137
|
+
"provider": "Anthropic",
|
|
138
|
+
"free_tier": False,
|
|
139
|
+
"recommended": True,
|
|
140
|
+
"default": False,
|
|
141
|
+
"note": "Fastest Claude model"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "gemini-1.5-flash",
|
|
145
|
+
"name": "Gemini 1.5 Flash",
|
|
146
|
+
"provider": "Google",
|
|
147
|
+
"free_tier": True,
|
|
148
|
+
"recommended": True,
|
|
149
|
+
"default": False,
|
|
150
|
+
"note": "Free tier via Google AI Studio"
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Map input model choices (lowercased) to their configuration
|
|
155
|
+
# Each config includes the class and any model-specific constructor parameters
|
|
156
|
+
_llm_config_map = {
|
|
157
|
+
'gpt-4.1': {
|
|
158
|
+
'class': ChatOpenAI,
|
|
159
|
+
'constructor_params': {'model_name': 'gpt-4.1'}
|
|
160
|
+
},
|
|
161
|
+
'gpt-5.2': {
|
|
162
|
+
'class': ChatOpenAI,
|
|
163
|
+
'constructor_params': {'model_name': 'gpt-5.2'}
|
|
164
|
+
},
|
|
165
|
+
'gpt-5.1': {
|
|
166
|
+
'class': ChatOpenAI,
|
|
167
|
+
'constructor_params': {'model_name': 'gpt-5.1'}
|
|
168
|
+
},
|
|
169
|
+
'gpt-5-mini': {
|
|
170
|
+
'class': ChatOpenAI,
|
|
171
|
+
'constructor_params': {'model_name': 'gpt-5-mini'}
|
|
172
|
+
},
|
|
173
|
+
'gpt-5-nano': {
|
|
174
|
+
'class': ChatOpenAI,
|
|
175
|
+
'constructor_params': {'model_name': 'gpt-5-nano'}
|
|
176
|
+
},
|
|
177
|
+
'claude-sonnet-4-5': {
|
|
178
|
+
'class': ChatAnthropic,
|
|
179
|
+
'constructor_params': {'model': 'claude-sonnet-4-5'}
|
|
180
|
+
},
|
|
181
|
+
'claude-sonnet-4-0': {
|
|
182
|
+
'class': ChatAnthropic,
|
|
183
|
+
'constructor_params': {'model': 'claude-sonnet-4-0'}
|
|
184
|
+
},
|
|
185
|
+
'gemini-2.5-flash': {
|
|
186
|
+
'class': ChatGoogleGenerativeAI,
|
|
187
|
+
'constructor_params': {'model': 'gemini-2.5-flash', 'google_api_key': GOOGLE_API_KEY }
|
|
188
|
+
},
|
|
189
|
+
'gemini-2.5-flash-lite': {
|
|
190
|
+
'class': ChatGoogleGenerativeAI,
|
|
191
|
+
'constructor_params': {'model': 'gemini-2.5-flash-lite', 'google_api_key': GOOGLE_API_KEY}
|
|
192
|
+
},
|
|
193
|
+
'gemini-2.5-pro': {
|
|
194
|
+
'class': ChatGoogleGenerativeAI,
|
|
195
|
+
'constructor_params': {'model': 'gemini-2.5-pro', 'google_api_key': GOOGLE_API_KEY}
|
|
196
|
+
},
|
|
197
|
+
'deepseek-v3-openrouter': {
|
|
198
|
+
'class': ChatOpenAI,
|
|
199
|
+
'constructor_params': {
|
|
200
|
+
'model_name': 'deepseek/deepseek-chat-v3-0324',
|
|
201
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
202
|
+
'api_key': OPENROUTER_API_KEY,
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
'minimax-m2.5-openrouter': {
|
|
206
|
+
'class': ChatOpenAI,
|
|
207
|
+
'constructor_params': {
|
|
208
|
+
'model_name': 'minimax/minimax-m2.5',
|
|
209
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
210
|
+
'api_key': OPENROUTER_API_KEY,
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
'qwen3-80b-openrouter': {
|
|
214
|
+
'class': ChatOpenAI,
|
|
215
|
+
'constructor_params': {
|
|
216
|
+
'model_name': 'qwen/qwen3-next-80b-a3b-instruct:free',
|
|
217
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
218
|
+
'api_key': OPENROUTER_API_KEY
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
'nemotron-nano-9b-openrouter': {
|
|
222
|
+
'class': ChatOpenAI,
|
|
223
|
+
'constructor_params': {
|
|
224
|
+
'model_name': 'nvidia/nemotron-nano-9b-v2:free',
|
|
225
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
226
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
'gpt-oss-120b-openrouter': {
|
|
230
|
+
'class': ChatOpenAI,
|
|
231
|
+
'constructor_params': {
|
|
232
|
+
'model_name': 'openai/gpt-oss-120b:free',
|
|
233
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
234
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
'gpt-5.1-openrouter': {
|
|
238
|
+
'class': ChatOpenAI,
|
|
239
|
+
'constructor_params': {
|
|
240
|
+
'model_name': 'openai/gpt-5.1',
|
|
241
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
242
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
'gpt-5-mini-openrouter': {
|
|
246
|
+
'class': ChatOpenAI,
|
|
247
|
+
'constructor_params': {
|
|
248
|
+
'model_name': 'openai/gpt-5-mini',
|
|
249
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
250
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
'claude-sonnet-4.5-openrouter': {
|
|
254
|
+
'class': ChatOpenAI,
|
|
255
|
+
'constructor_params': {
|
|
256
|
+
'model_name': 'anthropic/claude-sonnet-4.5',
|
|
257
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
258
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
'grok-4.1-fast-openrouter': {
|
|
262
|
+
'class': ChatOpenAI,
|
|
263
|
+
'constructor_params': {
|
|
264
|
+
'model_name': 'x-ai/grok-4.1-fast',
|
|
265
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
266
|
+
'api_key': OPENROUTER_API_KEY # Use OpenRouter API key
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
'groq/llama-3.3-70b': {
|
|
270
|
+
'class': ChatOpenAI,
|
|
271
|
+
'constructor_params': {
|
|
272
|
+
'model_name': 'llama-3.3-70b-versatile',
|
|
273
|
+
'base_url': 'https://api.groq.com/openai/v1',
|
|
274
|
+
'api_key': GROQ_API_KEY,
|
|
275
|
+
},
|
|
276
|
+
'label': 'Llama 3.3 70B (Groq — Free tier)',
|
|
277
|
+
'recommended': True,
|
|
278
|
+
'free_tier': True,
|
|
279
|
+
},
|
|
280
|
+
'groq/llama-3.1-8b': {
|
|
281
|
+
'class': ChatOpenAI,
|
|
282
|
+
'constructor_params': {
|
|
283
|
+
'model_name': 'llama-3.1-8b-instant',
|
|
284
|
+
'base_url': 'https://api.groq.com/openai/v1',
|
|
285
|
+
'api_key': GROQ_API_KEY,
|
|
286
|
+
},
|
|
287
|
+
'label': 'Llama 3.1 8B Instant (Groq — Free tier, fast)',
|
|
288
|
+
'recommended': False,
|
|
289
|
+
'free_tier': True,
|
|
290
|
+
},
|
|
291
|
+
'minimax-m2.1-openrouter': {
|
|
292
|
+
'class': ChatOpenAI,
|
|
293
|
+
'constructor_params': {
|
|
294
|
+
'model_name': 'minimax/minimax-m2.1',
|
|
295
|
+
'base_url': OPENROUTER_BASE_URL,
|
|
296
|
+
'api_key': OPENROUTER_API_KEY,
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
# 'llama3.2': {
|
|
300
|
+
# 'class': ChatOllama,
|
|
301
|
+
# 'constructor_params': {'model': 'llama3.2:latest', 'base_url': OLLAMA_BASE_URL}
|
|
302
|
+
# },
|
|
303
|
+
# 'llama3.1': {
|
|
304
|
+
# 'class': ChatOllama,
|
|
305
|
+
# 'constructor_params': {'model': 'llama3.1:latest', 'base_url': OLLAMA_BASE_URL}
|
|
306
|
+
# },
|
|
307
|
+
# 'gemma3': {
|
|
308
|
+
# 'class': ChatOllama,
|
|
309
|
+
# 'constructor_params': {'model': 'gemma3:latest', 'base_url': OLLAMA_BASE_URL}
|
|
310
|
+
# },
|
|
311
|
+
# 'deepseek-r1': {
|
|
312
|
+
# 'class': ChatOllama,
|
|
313
|
+
# 'constructor_params': {'model': 'deepseek-r1:latest', 'base_url': OLLAMA_BASE_URL}
|
|
314
|
+
# },
|
|
315
|
+
|
|
316
|
+
# Add more models here easily:
|
|
317
|
+
# 'mistral7b': {
|
|
318
|
+
# 'class': ChatOllama,
|
|
319
|
+
# 'constructor_params': {'model': 'mistral:7b', 'base_url': OLLAMA_BASE_URL}
|
|
320
|
+
# },
|
|
321
|
+
# 'gpt3.5': {
|
|
322
|
+
# 'class': ChatOpenAI,
|
|
323
|
+
# 'constructor_params': {'model_name': 'gpt-3.5-turbo', 'base_url': OLLAMA_BASE_URL}
|
|
324
|
+
# }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _normalize_model_name(name: str) -> str:
|
|
329
|
+
return name.strip().lower()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _get_ollama_base_url() -> Optional[str]:
|
|
333
|
+
if not OLLAMA_BASE_URL:
|
|
334
|
+
return None
|
|
335
|
+
return OLLAMA_BASE_URL.rstrip("/") + "/"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def fetch_ollama_models() -> List[str]:
|
|
339
|
+
"""
|
|
340
|
+
Retrieve the list of locally available Ollama models by querying the Ollama HTTP API.
|
|
341
|
+
Returns an empty list if the API isn't reachable or the base URL is not defined.
|
|
342
|
+
"""
|
|
343
|
+
base_url = _get_ollama_base_url()
|
|
344
|
+
if not base_url:
|
|
345
|
+
return []
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
resp = requests.get(urljoin(base_url, "api/tags"), timeout=3)
|
|
349
|
+
resp.raise_for_status()
|
|
350
|
+
models = resp.json().get("models", [])
|
|
351
|
+
available = []
|
|
352
|
+
for m in models:
|
|
353
|
+
name = m.get("name") or m.get("model")
|
|
354
|
+
if name:
|
|
355
|
+
available.append(name)
|
|
356
|
+
return available
|
|
357
|
+
except (requests.RequestException, ValueError):
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# Added Support for llama.cpp models since they use OpenAI-compatible API
|
|
362
|
+
def fetch_llama_cpp_models() -> List[str]:
|
|
363
|
+
"""
|
|
364
|
+
Retrieve available models from an OpenAI-compatible llama.cpp server.
|
|
365
|
+
Uses /v1/models.
|
|
366
|
+
"""
|
|
367
|
+
if not LLAMA_CPP_BASE_URL:
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
base = LLAMA_CPP_BASE_URL.rstrip("/")
|
|
371
|
+
try:
|
|
372
|
+
resp = requests.get(f"{base}/v1/models", timeout=3)
|
|
373
|
+
resp.raise_for_status()
|
|
374
|
+
data = resp.json().get("data", [])
|
|
375
|
+
return [m["id"] for m in data if "id" in m]
|
|
376
|
+
except (requests.RequestException, ValueError, KeyError):
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _is_set(v: Optional[str]) -> bool:
|
|
382
|
+
return bool(v and str(v).strip() and "your_" not in str(v))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Changed it so the GUI only loaded available models
|
|
386
|
+
def get_model_choices() -> List[str]:
|
|
387
|
+
"""
|
|
388
|
+
Combine configured cloud models with locally available Ollama models.
|
|
389
|
+
Cloud models are shown only if required API keys are present.
|
|
390
|
+
"""
|
|
391
|
+
gated_base_models: List[str] = []
|
|
392
|
+
|
|
393
|
+
openai_ok = _is_set(OPENAI_API_KEY)
|
|
394
|
+
anthropic_ok = _is_set(ANTHROPIC_API_KEY)
|
|
395
|
+
google_ok = _is_set(GOOGLE_API_KEY)
|
|
396
|
+
openrouter_ok = _is_set(OPENROUTER_API_KEY) and _is_set(OPENROUTER_BASE_URL)
|
|
397
|
+
groq_ok = _is_set(GROQ_API_KEY)
|
|
398
|
+
|
|
399
|
+
for k, cfg in _llm_config_map.items():
|
|
400
|
+
cls = cfg.get("class")
|
|
401
|
+
ctor = cfg.get("constructor_params", {}) or {}
|
|
402
|
+
|
|
403
|
+
# OpenRouter models (ChatOpenAI with base_url set to OpenRouter)
|
|
404
|
+
if cls is ChatOpenAI and (ctor.get("base_url") == OPENROUTER_BASE_URL or "openrouter" in k):
|
|
405
|
+
if openrouter_ok:
|
|
406
|
+
gated_base_models.append(k)
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# Groq models (ChatOpenAI with base_url set to Groq)
|
|
410
|
+
if cls is ChatOpenAI and ctor.get("base_url") == "https://api.groq.com/openai/v1":
|
|
411
|
+
if groq_ok:
|
|
412
|
+
gated_base_models.append(k)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Direct OpenAI models
|
|
416
|
+
if cls is ChatOpenAI:
|
|
417
|
+
if openai_ok:
|
|
418
|
+
gated_base_models.append(k)
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
# Anthropic
|
|
422
|
+
if cls is ChatAnthropic:
|
|
423
|
+
if anthropic_ok:
|
|
424
|
+
gated_base_models.append(k)
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Google Gemini
|
|
428
|
+
if cls is ChatGoogleGenerativeAI:
|
|
429
|
+
if google_ok:
|
|
430
|
+
gated_base_models.append(k)
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
# Anything else: keep
|
|
434
|
+
gated_base_models.append(k)
|
|
435
|
+
|
|
436
|
+
# Local Models
|
|
437
|
+
dynamic_models = []
|
|
438
|
+
|
|
439
|
+
# Dynamic local models via Ollama-style API (/api/tags)
|
|
440
|
+
dynamic_models += fetch_ollama_models()
|
|
441
|
+
|
|
442
|
+
# Dynamic local models via llama.cpp which uses OpenAI style API
|
|
443
|
+
dynamic_models += fetch_llama_cpp_models()
|
|
444
|
+
|
|
445
|
+
normalized = {_normalize_model_name(m): m for m in gated_base_models}
|
|
446
|
+
for dm in dynamic_models:
|
|
447
|
+
key = _normalize_model_name(dm)
|
|
448
|
+
if key not in normalized:
|
|
449
|
+
normalized[key] = dm
|
|
450
|
+
|
|
451
|
+
ordered_dynamic = sorted(
|
|
452
|
+
[name for key, name in normalized.items() if name not in gated_base_models],
|
|
453
|
+
key=_normalize_model_name,
|
|
454
|
+
)
|
|
455
|
+
return gated_base_models + ordered_dynamic
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def resolve_model_config(model_choice: str):
|
|
461
|
+
"""
|
|
462
|
+
Resolve a model choice (case-insensitive) to the corresponding configuration.
|
|
463
|
+
Supports both the predefined remote models and any locally installed Ollama models.
|
|
464
|
+
"""
|
|
465
|
+
model_choice_lower = _normalize_model_name(model_choice)
|
|
466
|
+
config = _llm_config_map.get(model_choice_lower)
|
|
467
|
+
if config:
|
|
468
|
+
return config
|
|
469
|
+
|
|
470
|
+
# llama.cpp (OpenAI-compatible)
|
|
471
|
+
for llama_model in fetch_llama_cpp_models():
|
|
472
|
+
if _normalize_model_name(llama_model) == model_choice_lower:
|
|
473
|
+
return {
|
|
474
|
+
"class": ChatOpenAI,
|
|
475
|
+
"constructor_params": {
|
|
476
|
+
"model_name": llama_model,
|
|
477
|
+
"base_url": LLAMA_CPP_BASE_URL,
|
|
478
|
+
"api_key": OPENAI_API_KEY or "sk-local",
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for ollama_model in fetch_ollama_models():
|
|
483
|
+
if _normalize_model_name(ollama_model) == model_choice_lower:
|
|
484
|
+
return {
|
|
485
|
+
"class": ChatOllama,
|
|
486
|
+
"constructor_params": {"model": ollama_model, "base_url": OLLAMA_BASE_URL},
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
# _make_*_llm helpers — used by the model-validate endpoint and as
|
|
494
|
+
# fallback routing inside resolve_model_config() for dynamic model IDs.
|
|
495
|
+
# Each helper raises ValueError with a clear message if the key is absent.
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
def _resolve_key(key_name: str, api_keys: Optional[dict]) -> Optional[str]:
|
|
499
|
+
"""Return user-override key if available, else fall back to server config."""
|
|
500
|
+
if api_keys:
|
|
501
|
+
v = api_keys.get(key_name)
|
|
502
|
+
if v and str(v).strip():
|
|
503
|
+
return str(v).strip()
|
|
504
|
+
return globals().get(key_name) or None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _make_openrouter_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
508
|
+
"""Build a ChatOpenAI instance pointed at OpenRouter for *model_id*."""
|
|
509
|
+
if not model_id:
|
|
510
|
+
model_id = DEFAULT_MODELS["openrouter"]
|
|
511
|
+
key = _resolve_key("OPENROUTER_API_KEY", api_keys)
|
|
512
|
+
if not key:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
"No API key configured for OpenRouter. "
|
|
515
|
+
"Add OPENROUTER_API_KEY in Settings."
|
|
516
|
+
)
|
|
517
|
+
base = (OPENROUTER_BASE_URL or "https://openrouter.ai/api/v1").rstrip("/")
|
|
518
|
+
return ChatOpenAI(
|
|
519
|
+
**{**_common_llm_params, "model_name": model_id, "base_url": base, "api_key": key}
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _make_groq_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
524
|
+
"""Build a ChatOpenAI instance pointed at Groq for *model_id*."""
|
|
525
|
+
if not model_id:
|
|
526
|
+
model_id = DEFAULT_MODELS["groq"]
|
|
527
|
+
key = _resolve_key("GROQ_API_KEY", api_keys)
|
|
528
|
+
if not key:
|
|
529
|
+
raise ValueError(
|
|
530
|
+
"No API key configured for Groq. "
|
|
531
|
+
"Add GROQ_API_KEY in Settings."
|
|
532
|
+
)
|
|
533
|
+
return ChatOpenAI(
|
|
534
|
+
**{**_common_llm_params, "model_name": model_id,
|
|
535
|
+
"base_url": "https://api.groq.com/openai/v1", "api_key": key}
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _make_openai_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
540
|
+
"""Build a ChatOpenAI instance for a native OpenAI model."""
|
|
541
|
+
if not model_id:
|
|
542
|
+
model_id = DEFAULT_MODELS["openai"]
|
|
543
|
+
key = _resolve_key("OPENAI_API_KEY", api_keys)
|
|
544
|
+
if not key:
|
|
545
|
+
raise ValueError(
|
|
546
|
+
"No API key configured for OpenAI. "
|
|
547
|
+
"Add OPENAI_API_KEY in Settings."
|
|
548
|
+
)
|
|
549
|
+
return ChatOpenAI(**{**_common_llm_params, "model_name": model_id, "api_key": key})
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _make_anthropic_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
553
|
+
"""Build a ChatAnthropic instance."""
|
|
554
|
+
if not model_id:
|
|
555
|
+
model_id = DEFAULT_MODELS["anthropic"]
|
|
556
|
+
key = _resolve_key("ANTHROPIC_API_KEY", api_keys)
|
|
557
|
+
if not key:
|
|
558
|
+
raise ValueError(
|
|
559
|
+
"No API key configured for Anthropic. "
|
|
560
|
+
"Add ANTHROPIC_API_KEY in Settings."
|
|
561
|
+
)
|
|
562
|
+
base = {k: v for k, v in _common_llm_params.items() if k != "streaming"}
|
|
563
|
+
return ChatAnthropic(**{**base, "model": model_id, "anthropic_api_key": key})
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _make_google_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
567
|
+
"""Build a ChatGoogleGenerativeAI instance."""
|
|
568
|
+
if not model_id:
|
|
569
|
+
model_id = DEFAULT_MODELS["google"]
|
|
570
|
+
key = _resolve_key("GOOGLE_API_KEY", api_keys)
|
|
571
|
+
if not key:
|
|
572
|
+
raise ValueError(
|
|
573
|
+
"No API key configured for Google Gemini. "
|
|
574
|
+
"Add GOOGLE_API_KEY in Settings."
|
|
575
|
+
)
|
|
576
|
+
base = {k: v for k, v in _common_llm_params.items() if k != "streaming"}
|
|
577
|
+
return ChatGoogleGenerativeAI(**{**base, "model": model_id, "google_api_key": key})
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _make_ollama_llm(model_id: str, api_keys: Optional[dict] = None):
|
|
581
|
+
"""Build a ChatOllama instance for a locally running model."""
|
|
582
|
+
if not model_id:
|
|
583
|
+
model_id = DEFAULT_MODELS["ollama"]
|
|
584
|
+
base_url = OLLAMA_BASE_URL or "http://localhost:11434"
|
|
585
|
+
return ChatOllama(**{**_common_llm_params, "model": model_id, "base_url": base_url})
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ---------------------------------------------------------------------------
|
|
589
|
+
# Dynamic resolve — prefix routing appended as final fallback so that any
|
|
590
|
+
# model ID like "openrouter/x/y", "groq/llama-3.3-70b-versatile",
|
|
591
|
+
# "gpt-4o", "claude-3-5-sonnet-20241022", etc. resolves correctly even if
|
|
592
|
+
# it isn't in _llm_config_map. Returns a synthetic config dict that
|
|
593
|
+
# llm.py's get_llm() can consume via the normal code-path.
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
def _resolve_model_config_by_prefix(model_choice: str) -> Optional[dict]:
|
|
597
|
+
"""
|
|
598
|
+
Detect provider from model ID prefix and return a synthetic config dict.
|
|
599
|
+
|
|
600
|
+
Model ID conventions:
|
|
601
|
+
- "openrouter/..." → OpenRouter (strip the prefix → actual model ID)
|
|
602
|
+
- "groq/..." → Groq (strip the prefix)
|
|
603
|
+
- "gpt-..." → OpenAI direct
|
|
604
|
+
- "claude-..." → Anthropic
|
|
605
|
+
- "gemini-..." → Google
|
|
606
|
+
- "ollama/..." → Ollama (strip the prefix)
|
|
607
|
+
- anything else → OpenRouter fallback (with warning)
|
|
608
|
+
"""
|
|
609
|
+
mc = model_choice.strip()
|
|
610
|
+
|
|
611
|
+
if mc.startswith("openrouter/"):
|
|
612
|
+
actual = mc[len("openrouter/"):]
|
|
613
|
+
base = (OPENROUTER_BASE_URL or "https://openrouter.ai/api/v1").rstrip("/")
|
|
614
|
+
return {
|
|
615
|
+
"class": ChatOpenAI,
|
|
616
|
+
"constructor_params": {
|
|
617
|
+
"model_name": actual,
|
|
618
|
+
"base_url": base,
|
|
619
|
+
"api_key": OPENROUTER_API_KEY,
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if mc.startswith("groq/"):
|
|
624
|
+
actual = mc[len("groq/"):]
|
|
625
|
+
return {
|
|
626
|
+
"class": ChatOpenAI,
|
|
627
|
+
"constructor_params": {
|
|
628
|
+
"model_name": actual,
|
|
629
|
+
"base_url": "https://api.groq.com/openai/v1",
|
|
630
|
+
"api_key": GROQ_API_KEY,
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if mc.startswith("gpt-"):
|
|
635
|
+
return {
|
|
636
|
+
"class": ChatOpenAI,
|
|
637
|
+
"constructor_params": {"model_name": mc, "api_key": OPENAI_API_KEY},
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if mc.startswith("claude-"):
|
|
641
|
+
return {
|
|
642
|
+
"class": ChatAnthropic,
|
|
643
|
+
"constructor_params": {"model": mc, "anthropic_api_key": ANTHROPIC_API_KEY},
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if mc.startswith("gemini-"):
|
|
647
|
+
return {
|
|
648
|
+
"class": ChatGoogleGenerativeAI,
|
|
649
|
+
"constructor_params": {"model": mc, "google_api_key": GOOGLE_API_KEY},
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if mc.startswith("ollama/"):
|
|
653
|
+
actual = mc[len("ollama/"):]
|
|
654
|
+
return {
|
|
655
|
+
"class": ChatOllama,
|
|
656
|
+
"constructor_params": {
|
|
657
|
+
"model": actual,
|
|
658
|
+
"base_url": OLLAMA_BASE_URL or "http://localhost:11434",
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# Unknown prefix — attempt OpenRouter as fallback
|
|
663
|
+
logger.warning(
|
|
664
|
+
"Unknown model prefix for '%s' — attempting OpenRouter fallback", model_choice
|
|
665
|
+
)
|
|
666
|
+
base = (OPENROUTER_BASE_URL or "https://openrouter.ai/api/v1").rstrip("/")
|
|
667
|
+
return {
|
|
668
|
+
"class": ChatOpenAI,
|
|
669
|
+
"constructor_params": {
|
|
670
|
+
"model_name": mc,
|
|
671
|
+
"base_url": base,
|
|
672
|
+
"api_key": OPENROUTER_API_KEY,
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# Patch resolve_model_config to also use prefix routing as its last-resort fallback.
|
|
678
|
+
# We keep the original function body intact and append an extra branch here.
|
|
679
|
+
_original_resolve_model_config = resolve_model_config
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def resolve_model_config(model_choice: str): # type: ignore[redefined-outer-name]
|
|
683
|
+
"""
|
|
684
|
+
Resolve a model choice to the corresponding configuration.
|
|
685
|
+
|
|
686
|
+
Lookup order:
|
|
687
|
+
1. Predefined _llm_config_map (case-insensitive, includes all hardcoded models).
|
|
688
|
+
2. llama.cpp dynamic list (/v1/models).
|
|
689
|
+
3. Ollama dynamic list (/api/tags).
|
|
690
|
+
4. Prefix-based routing for dynamic model IDs (openrouter/, groq/, gpt-*, …).
|
|
691
|
+
"""
|
|
692
|
+
result = _original_resolve_model_config(model_choice)
|
|
693
|
+
if result is not None:
|
|
694
|
+
return result
|
|
695
|
+
# Fall through to prefix-based dynamic routing
|
|
696
|
+
return _resolve_model_config_by_prefix(model_choice)
|