halyn 2.1.1__tar.gz → 2.1.3__tar.gz
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.
- {halyn-2.1.1/src/halyn.egg-info → halyn-2.1.3}/PKG-INFO +79 -13
- {halyn-2.1.1 → halyn-2.1.3}/README.md +78 -12
- {halyn-2.1.1 → halyn-2.1.3}/pyproject.toml +1 -1
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/__init__.py +1 -1
- halyn-2.1.3/src/halyn/llm.py +362 -0
- {halyn-2.1.1 → halyn-2.1.3/src/halyn.egg-info}/PKG-INFO +79 -13
- {halyn-2.1.1 → halyn-2.1.3}/tests/test_halyn.py +4 -1
- halyn-2.1.1/src/halyn/llm.py +0 -180
- {halyn-2.1.1 → halyn-2.1.3}/LICENSE +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/setup.cfg +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/__main__.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/audit.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/auth.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/autonomy.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/cli.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/config.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/consent.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/control_plane.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/dashboard.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/discovery.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/__init__.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/browser.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/dds.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/docker.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/http_auto.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/mqtt.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/opcua.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/ros2.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/serial.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/socket_raw.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/ssh.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/unitree.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/drivers/websocket.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/engine.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/integrations/__init__.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/intent.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/mcp.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/mcp_serve.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/memory/__init__.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/memory/store.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/nrp_bridge.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/py.typed +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/sanitizer.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/__init__.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/audit_guard.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/ebpf_monitor.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/fs_watch.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/process_guard.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/security/proxy.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/server.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/shield.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/types.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn/watchdog.py +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn.egg-info/SOURCES.txt +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn.egg-info/dependency_links.txt +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn.egg-info/entry_points.txt +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn.egg-info/requires.txt +0 -0
- {halyn-2.1.1 → halyn-2.1.3}/src/halyn.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: halyn
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable.
|
|
5
5
|
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
6
|
License: BSL-1.1
|
|
@@ -157,18 +157,84 @@ Deployed via `/etc/opt/chrome/policies/managed/halyn.json` — the agent cannot
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
##
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
## Compatible AI
|
|
161
|
+
|
|
162
|
+
Halyn intercepts at the kernel and proxy level. It does not care which AI is running — it audits all of them equally. No AI is excluded.
|
|
163
|
+
|
|
164
|
+
### How compatibility works
|
|
165
|
+
|
|
166
|
+
Halyn intercepts three things:
|
|
167
|
+
- **API calls** (iptables REDIRECT on port 443/80) — catches any HTTP request to any AI provider
|
|
168
|
+
- **Filesystem access** (inotify/FSEvents/eBPF) — catches any agent touching files, regardless of origin
|
|
169
|
+
- **Process syscalls** (eBPF, Linux ≥5.8) — catches any agent at the kernel level
|
|
170
|
+
|
|
171
|
+
This means: if an AI agent makes an API call or accesses your system, Halyn sees it.
|
|
172
|
+
|
|
173
|
+
### Cloud AI
|
|
174
|
+
|
|
175
|
+
| Provider | Models (March 2026) | API |
|
|
176
|
+
|----------|---------------------|-----|
|
|
177
|
+
| **Anthropic** | Claude Sonnet 4.6, Claude Opus 4.6, Claude Haiku 4.5 | api.anthropic.com |
|
|
178
|
+
| **OpenAI** | GPT-4.1, GPT-4.1 mini, GPT-4.1 nano, o3, o4-mini | api.openai.com |
|
|
179
|
+
| **Google** | Gemini 3.1 Pro, Gemini 3.1 Flash, Gemini 3.1 Flash-Lite | generativelanguage.googleapis.com |
|
|
180
|
+
| **Mistral AI** | Mistral Large 2, Mistral Small 3, Codestral | api.mistral.ai |
|
|
181
|
+
| **xAI** | Grok-3, Grok-3 mini | api.x.ai |
|
|
182
|
+
| **DeepSeek** | DeepSeek-V3, DeepSeek-R1 | api.deepseek.com |
|
|
183
|
+
| **Cohere** | Command R+, Command R, Aya | api.cohere.com |
|
|
184
|
+
| **Perplexity** | Sonar Pro, Sonar, Sonar Reasoning | api.perplexity.ai |
|
|
185
|
+
| **01.AI** | Yi-Large, Yi-Vision | api.01.ai |
|
|
186
|
+
| **Alibaba** | Qwen-Max, Qwen-Plus, Qwen-Turbo | dashscope.aliyuncs.com |
|
|
187
|
+
| **Baidu** | ERNIE 4.5, ERNIE Speed | aip.baidubce.com |
|
|
188
|
+
| **Amazon Bedrock** | Claude, Titan, Llama, Mistral (via AWS) | bedrock.amazonaws.com |
|
|
189
|
+
| **Azure OpenAI** | GPT-4.1, o3 (via Microsoft) | *.openai.azure.com |
|
|
190
|
+
| **NVIDIA NIM** | Llama 3.3, Mistral, DeepSeek-R1 (on NVIDIA cloud) | integrate.api.nvidia.com |
|
|
191
|
+
| **Together AI** | 50+ open models via API | api.together.xyz |
|
|
192
|
+
| **Groq** | Llama, Mixtral, Gemma (ultra-fast inference) | api.groq.com |
|
|
193
|
+
| **Fireworks AI** | Llama, Mixtral, DeepSeek | api.fireworks.ai |
|
|
194
|
+
|
|
195
|
+
### Local AI
|
|
196
|
+
|
|
197
|
+
Any local model is compatible — Halyn intercepts at the process level, not the network level.
|
|
198
|
+
|
|
199
|
+
| Runtime | Models | Notes |
|
|
200
|
+
|---------|--------|-------|
|
|
201
|
+
| **Ollama** | Llama 3.3, Qwen2.5, Mistral, DeepSeek-R1, Phi-4, Gemma 3, ... | OpenAI-compatible API |
|
|
202
|
+
| **LM Studio** | Any GGUF model | OpenAI-compatible server |
|
|
203
|
+
| **Jan.ai** | Any GGUF or ONNX model | Desktop + server mode |
|
|
204
|
+
| **GPT4All** | Llama, Mistral, Phi variants | Offline, no telemetry |
|
|
205
|
+
| **llama.cpp** | Any GGUF model directly | Server mode (`--server`) |
|
|
206
|
+
| **LocalAI** | 100+ models, any GGUF | Drop-in OpenAI replacement |
|
|
207
|
+
| **text-generation-webui** | Any HuggingFace model | Extension ecosystem |
|
|
208
|
+
| **KoboldCpp** | Any GGUF model | Focus on creative writing |
|
|
209
|
+
| **OpenWebUI** | Ollama + OpenAI frontend | Browser-based |
|
|
210
|
+
| **AnythingLLM** | Multi-model workspace | Team-friendly |
|
|
211
|
+
| **Xinference** | HuggingFace + GGUF | Enterprise local inference |
|
|
212
|
+
| **vLLM** | HuggingFace models | High-throughput server |
|
|
213
|
+
| **TGI (HuggingFace)** | HuggingFace models | Production inference |
|
|
214
|
+
|
|
215
|
+
### Agentic frameworks
|
|
216
|
+
|
|
217
|
+
Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
218
|
+
|
|
219
|
+
| Framework | Notes |
|
|
220
|
+
|-----------|-------|
|
|
221
|
+
| **OpenClaw** | Full interceptor — every action audited |
|
|
222
|
+
| **Claude Cowork** | Proxy + filesystem hooks |
|
|
223
|
+
| **Claude Code** | Process-level monitoring |
|
|
224
|
+
| **LangChain** | API calls intercepted automatically |
|
|
225
|
+
| **LlamaIndex** | API calls intercepted automatically |
|
|
226
|
+
| **AutoGen** | API calls intercepted automatically |
|
|
227
|
+
| **CrewAI** | API calls intercepted automatically |
|
|
228
|
+
| **Semantic Kernel** | API calls intercepted automatically |
|
|
229
|
+
| **BeeQ** | Native AAP integration |
|
|
230
|
+
| **Any MCP agent** | MCP server passthrough |
|
|
231
|
+
| **Any A2A agent** | Network-level interception |
|
|
232
|
+
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
|
233
|
+
|
|
234
|
+
### The rule
|
|
235
|
+
|
|
236
|
+
> If an AI touches your machine or calls an API — Halyn sees it.
|
|
237
|
+
> No exception. No exclusion. That's the point.
|
|
172
238
|
|
|
173
239
|
---
|
|
174
240
|
|
|
@@ -124,18 +124,84 @@ Deployed via `/etc/opt/chrome/policies/managed/halyn.json` — the agent cannot
|
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
-
##
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
## Compatible AI
|
|
128
|
+
|
|
129
|
+
Halyn intercepts at the kernel and proxy level. It does not care which AI is running — it audits all of them equally. No AI is excluded.
|
|
130
|
+
|
|
131
|
+
### How compatibility works
|
|
132
|
+
|
|
133
|
+
Halyn intercepts three things:
|
|
134
|
+
- **API calls** (iptables REDIRECT on port 443/80) — catches any HTTP request to any AI provider
|
|
135
|
+
- **Filesystem access** (inotify/FSEvents/eBPF) — catches any agent touching files, regardless of origin
|
|
136
|
+
- **Process syscalls** (eBPF, Linux ≥5.8) — catches any agent at the kernel level
|
|
137
|
+
|
|
138
|
+
This means: if an AI agent makes an API call or accesses your system, Halyn sees it.
|
|
139
|
+
|
|
140
|
+
### Cloud AI
|
|
141
|
+
|
|
142
|
+
| Provider | Models (March 2026) | API |
|
|
143
|
+
|----------|---------------------|-----|
|
|
144
|
+
| **Anthropic** | Claude Sonnet 4.6, Claude Opus 4.6, Claude Haiku 4.5 | api.anthropic.com |
|
|
145
|
+
| **OpenAI** | GPT-4.1, GPT-4.1 mini, GPT-4.1 nano, o3, o4-mini | api.openai.com |
|
|
146
|
+
| **Google** | Gemini 3.1 Pro, Gemini 3.1 Flash, Gemini 3.1 Flash-Lite | generativelanguage.googleapis.com |
|
|
147
|
+
| **Mistral AI** | Mistral Large 2, Mistral Small 3, Codestral | api.mistral.ai |
|
|
148
|
+
| **xAI** | Grok-3, Grok-3 mini | api.x.ai |
|
|
149
|
+
| **DeepSeek** | DeepSeek-V3, DeepSeek-R1 | api.deepseek.com |
|
|
150
|
+
| **Cohere** | Command R+, Command R, Aya | api.cohere.com |
|
|
151
|
+
| **Perplexity** | Sonar Pro, Sonar, Sonar Reasoning | api.perplexity.ai |
|
|
152
|
+
| **01.AI** | Yi-Large, Yi-Vision | api.01.ai |
|
|
153
|
+
| **Alibaba** | Qwen-Max, Qwen-Plus, Qwen-Turbo | dashscope.aliyuncs.com |
|
|
154
|
+
| **Baidu** | ERNIE 4.5, ERNIE Speed | aip.baidubce.com |
|
|
155
|
+
| **Amazon Bedrock** | Claude, Titan, Llama, Mistral (via AWS) | bedrock.amazonaws.com |
|
|
156
|
+
| **Azure OpenAI** | GPT-4.1, o3 (via Microsoft) | *.openai.azure.com |
|
|
157
|
+
| **NVIDIA NIM** | Llama 3.3, Mistral, DeepSeek-R1 (on NVIDIA cloud) | integrate.api.nvidia.com |
|
|
158
|
+
| **Together AI** | 50+ open models via API | api.together.xyz |
|
|
159
|
+
| **Groq** | Llama, Mixtral, Gemma (ultra-fast inference) | api.groq.com |
|
|
160
|
+
| **Fireworks AI** | Llama, Mixtral, DeepSeek | api.fireworks.ai |
|
|
161
|
+
|
|
162
|
+
### Local AI
|
|
163
|
+
|
|
164
|
+
Any local model is compatible — Halyn intercepts at the process level, not the network level.
|
|
165
|
+
|
|
166
|
+
| Runtime | Models | Notes |
|
|
167
|
+
|---------|--------|-------|
|
|
168
|
+
| **Ollama** | Llama 3.3, Qwen2.5, Mistral, DeepSeek-R1, Phi-4, Gemma 3, ... | OpenAI-compatible API |
|
|
169
|
+
| **LM Studio** | Any GGUF model | OpenAI-compatible server |
|
|
170
|
+
| **Jan.ai** | Any GGUF or ONNX model | Desktop + server mode |
|
|
171
|
+
| **GPT4All** | Llama, Mistral, Phi variants | Offline, no telemetry |
|
|
172
|
+
| **llama.cpp** | Any GGUF model directly | Server mode (`--server`) |
|
|
173
|
+
| **LocalAI** | 100+ models, any GGUF | Drop-in OpenAI replacement |
|
|
174
|
+
| **text-generation-webui** | Any HuggingFace model | Extension ecosystem |
|
|
175
|
+
| **KoboldCpp** | Any GGUF model | Focus on creative writing |
|
|
176
|
+
| **OpenWebUI** | Ollama + OpenAI frontend | Browser-based |
|
|
177
|
+
| **AnythingLLM** | Multi-model workspace | Team-friendly |
|
|
178
|
+
| **Xinference** | HuggingFace + GGUF | Enterprise local inference |
|
|
179
|
+
| **vLLM** | HuggingFace models | High-throughput server |
|
|
180
|
+
| **TGI (HuggingFace)** | HuggingFace models | Production inference |
|
|
181
|
+
|
|
182
|
+
### Agentic frameworks
|
|
183
|
+
|
|
184
|
+
Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
185
|
+
|
|
186
|
+
| Framework | Notes |
|
|
187
|
+
|-----------|-------|
|
|
188
|
+
| **OpenClaw** | Full interceptor — every action audited |
|
|
189
|
+
| **Claude Cowork** | Proxy + filesystem hooks |
|
|
190
|
+
| **Claude Code** | Process-level monitoring |
|
|
191
|
+
| **LangChain** | API calls intercepted automatically |
|
|
192
|
+
| **LlamaIndex** | API calls intercepted automatically |
|
|
193
|
+
| **AutoGen** | API calls intercepted automatically |
|
|
194
|
+
| **CrewAI** | API calls intercepted automatically |
|
|
195
|
+
| **Semantic Kernel** | API calls intercepted automatically |
|
|
196
|
+
| **BeeQ** | Native AAP integration |
|
|
197
|
+
| **Any MCP agent** | MCP server passthrough |
|
|
198
|
+
| **Any A2A agent** | Network-level interception |
|
|
199
|
+
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
|
200
|
+
|
|
201
|
+
### The rule
|
|
202
|
+
|
|
203
|
+
> If an AI touches your machine or calls an API — Halyn sees it.
|
|
204
|
+
> No exception. No exclusion. That's the point.
|
|
139
205
|
|
|
140
206
|
---
|
|
141
207
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Copyright (c) 2026 Elmadani SALKA
|
|
2
|
+
# Licensed under BSL-1.1. See LICENSE file.
|
|
3
|
+
# Commercial use requires a license — contact@halyn.dev
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
LLM Connector — Multi-provider abstraction.
|
|
7
|
+
|
|
8
|
+
Halyn is provider-agnostic. It monitors any AI, regardless of origin.
|
|
9
|
+
This module provides connectors for direct LLM integration (optional).
|
|
10
|
+
The proxy layer works independently — without any connector configured.
|
|
11
|
+
|
|
12
|
+
Supported by the proxy (no connector needed):
|
|
13
|
+
Cloud: Anthropic, OpenAI, Google, Mistral, xAI, DeepSeek, Cohere,
|
|
14
|
+
Perplexity, 01.AI, Alibaba, Baidu, Amazon Bedrock, Azure OpenAI,
|
|
15
|
+
NVIDIA NIM, Together AI, Groq, Fireworks AI, and any HTTP API.
|
|
16
|
+
Local: Ollama, LM Studio, Jan.ai, GPT4All, llama.cpp, LocalAI,
|
|
17
|
+
text-generation-webui, KoboldCpp, vLLM, TGI, and any local server.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import urllib.request
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─── Base ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
class LLMResponse:
|
|
35
|
+
def __init__(self, content: str, model: str = "", usage: dict | None = None) -> None:
|
|
36
|
+
self.content = content
|
|
37
|
+
self.model = model
|
|
38
|
+
self.usage = usage or {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LLMConnector(ABC):
|
|
42
|
+
"""Base class for all LLM connectors."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def complete(
|
|
46
|
+
self,
|
|
47
|
+
messages: list[dict],
|
|
48
|
+
system: str = "",
|
|
49
|
+
max_tokens: int = 1024,
|
|
50
|
+
) -> LLMResponse:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ─── Cloud providers ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
class AnthropicConnector(LLMConnector):
|
|
57
|
+
"""Anthropic — Claude Sonnet 4.6, Opus 4.6, Haiku 4.5."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, api_key: str = "", model: str = "claude-sonnet-4-6") -> None:
|
|
60
|
+
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
|
|
61
|
+
self.model = model
|
|
62
|
+
self.endpoint = "https://api.anthropic.com/v1/messages"
|
|
63
|
+
|
|
64
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
65
|
+
body: dict[str, Any] = {"model": self.model, "max_tokens": max_tokens, "messages": messages}
|
|
66
|
+
if system:
|
|
67
|
+
body["system"] = system
|
|
68
|
+
data = self._post(self.endpoint, body, {
|
|
69
|
+
"x-api-key": self.api_key,
|
|
70
|
+
"anthropic-version": "2023-06-01",
|
|
71
|
+
})
|
|
72
|
+
content = data["content"][0]["text"]
|
|
73
|
+
return LLMResponse(content, self.model, data.get("usage"))
|
|
74
|
+
|
|
75
|
+
def _post(self, url: str, body: dict, headers: dict) -> dict:
|
|
76
|
+
payload = json.dumps(body).encode()
|
|
77
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
78
|
+
req.add_header("Content-Type", "application/json")
|
|
79
|
+
for k, v in headers.items():
|
|
80
|
+
req.add_header(k, v)
|
|
81
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
82
|
+
return json.loads(resp.read())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OpenAIConnector(LLMConnector):
|
|
86
|
+
"""OpenAI — GPT-4.1, GPT-4.1 mini, GPT-4.1 nano, o3, o4-mini.
|
|
87
|
+
Also compatible with: Azure OpenAI, NVIDIA NIM, Together AI, Groq,
|
|
88
|
+
Fireworks AI, and any OpenAI-compatible endpoint.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
api_key: str = "",
|
|
94
|
+
model: str = "gpt-4.1",
|
|
95
|
+
endpoint: str = "https://api.openai.com/v1",
|
|
96
|
+
) -> None:
|
|
97
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
|
|
98
|
+
self.model = model
|
|
99
|
+
self.endpoint = endpoint.rstrip("/")
|
|
100
|
+
|
|
101
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
102
|
+
msgs = ([{"role": "system", "content": system}] if system else []) + messages
|
|
103
|
+
body = {"model": self.model, "max_tokens": max_tokens, "messages": msgs}
|
|
104
|
+
data = self._post(f"{self.endpoint}/chat/completions", body)
|
|
105
|
+
content = data["choices"][0]["message"]["content"]
|
|
106
|
+
return LLMResponse(content, self.model, data.get("usage"))
|
|
107
|
+
|
|
108
|
+
def _post(self, url: str, body: dict) -> dict:
|
|
109
|
+
payload = json.dumps(body).encode()
|
|
110
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
111
|
+
req.add_header("Content-Type", "application/json")
|
|
112
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
113
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
114
|
+
return json.loads(resp.read())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class GeminiConnector(LLMConnector):
|
|
118
|
+
"""Google — Gemini 3.1 Pro, Gemini 3.1 Flash, Gemini 3.1 Flash-Lite."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, api_key: str = "", model: str = "gemini-3.1-flash-lite-preview") -> None:
|
|
121
|
+
self.api_key = api_key or os.environ.get("GOOGLE_API_KEY", "")
|
|
122
|
+
self.model = model
|
|
123
|
+
|
|
124
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
125
|
+
url = (
|
|
126
|
+
f"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
127
|
+
f"{self.model}:generateContent?key={self.api_key}"
|
|
128
|
+
)
|
|
129
|
+
contents = [{"role": m["role"].replace("assistant", "model"), "parts": [{"text": m["content"]}]}
|
|
130
|
+
for m in messages]
|
|
131
|
+
body: dict[str, Any] = {
|
|
132
|
+
"contents": contents,
|
|
133
|
+
"generationConfig": {"maxOutputTokens": max_tokens},
|
|
134
|
+
}
|
|
135
|
+
if system:
|
|
136
|
+
body["systemInstruction"] = {"parts": [{"text": system}]}
|
|
137
|
+
payload = json.dumps(body).encode()
|
|
138
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
139
|
+
req.add_header("Content-Type", "application/json")
|
|
140
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
141
|
+
data = json.loads(resp.read())
|
|
142
|
+
content = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
143
|
+
return LLMResponse(content, self.model)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class MistralConnector(LLMConnector):
|
|
147
|
+
"""Mistral AI — Mistral Large 2, Mistral Small 3, Codestral."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, api_key: str = "", model: str = "mistral-small-latest") -> None:
|
|
150
|
+
self.api_key = api_key or os.environ.get("MISTRAL_API_KEY", "")
|
|
151
|
+
self.model = model
|
|
152
|
+
self._oa = OpenAIConnector(api_key, model, "https://api.mistral.ai/v1")
|
|
153
|
+
|
|
154
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
155
|
+
self._oa.api_key = self.api_key
|
|
156
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class XAIConnector(LLMConnector):
|
|
160
|
+
"""xAI — Grok-3, Grok-3 mini."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, api_key: str = "", model: str = "grok-3-mini") -> None:
|
|
163
|
+
self.api_key = api_key or os.environ.get("XAI_API_KEY", "")
|
|
164
|
+
self._oa = OpenAIConnector(api_key, model, "https://api.x.ai/v1")
|
|
165
|
+
|
|
166
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
167
|
+
self._oa.api_key = self.api_key
|
|
168
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DeepSeekConnector(LLMConnector):
|
|
172
|
+
"""DeepSeek — DeepSeek-V3, DeepSeek-R1."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, api_key: str = "", model: str = "deepseek-chat") -> None:
|
|
175
|
+
self.api_key = api_key or os.environ.get("DEEPSEEK_API_KEY", "")
|
|
176
|
+
self._oa = OpenAIConnector(api_key, model, "https://api.deepseek.com/v1")
|
|
177
|
+
|
|
178
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
179
|
+
self._oa.api_key = self.api_key
|
|
180
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class GroqConnector(LLMConnector):
|
|
184
|
+
"""Groq — Llama, Mixtral, Gemma (ultra-fast inference)."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, api_key: str = "", model: str = "llama-3.3-70b-versatile") -> None:
|
|
187
|
+
self.api_key = api_key or os.environ.get("GROQ_API_KEY", "")
|
|
188
|
+
self._oa = OpenAIConnector(api_key, model, "https://api.groq.com/openai/v1")
|
|
189
|
+
|
|
190
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
191
|
+
self._oa.api_key = self.api_key
|
|
192
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PerplexityConnector(LLMConnector):
|
|
196
|
+
"""Perplexity — Sonar Pro, Sonar, Sonar Reasoning."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, api_key: str = "", model: str = "sonar") -> None:
|
|
199
|
+
self.api_key = api_key or os.environ.get("PERPLEXITY_API_KEY", "")
|
|
200
|
+
self._oa = OpenAIConnector(api_key, model, "https://api.perplexity.ai")
|
|
201
|
+
|
|
202
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
203
|
+
self._oa.api_key = self.api_key
|
|
204
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CohereConnector(LLMConnector):
|
|
208
|
+
"""Cohere — Command R+, Command R, Aya."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, api_key: str = "", model: str = "command-r-plus") -> None:
|
|
211
|
+
self.api_key = api_key or os.environ.get("COHERE_API_KEY", "")
|
|
212
|
+
self.model = model
|
|
213
|
+
|
|
214
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
215
|
+
url = "https://api.cohere.com/v2/chat"
|
|
216
|
+
chat_history = [{"role": m["role"], "content": m["content"]} for m in messages[:-1]]
|
|
217
|
+
body: dict[str, Any] = {
|
|
218
|
+
"model": self.model,
|
|
219
|
+
"messages": [{"role": m["role"], "content": m["content"]} for m in messages],
|
|
220
|
+
"max_tokens": max_tokens,
|
|
221
|
+
}
|
|
222
|
+
if system:
|
|
223
|
+
body["system"] = system
|
|
224
|
+
payload = json.dumps(body).encode()
|
|
225
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
226
|
+
req.add_header("Content-Type", "application/json")
|
|
227
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
228
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
229
|
+
data = json.loads(resp.read())
|
|
230
|
+
content = data["message"]["content"][0]["text"]
|
|
231
|
+
return LLMResponse(content, self.model)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ─── Local providers ──────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
class OllamaConnector(LLMConnector):
|
|
237
|
+
"""Ollama — any local model: Llama 3.3, Qwen2.5, Mistral, DeepSeek-R1,
|
|
238
|
+
Phi-4, Gemma 3, and hundreds more. OpenAI-compatible API.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(self, model: str = "llama3.2", host: str = "http://localhost:11434") -> None:
|
|
242
|
+
self.model = model
|
|
243
|
+
self._oa = OpenAIConnector("ollama", model, f"{host}/v1")
|
|
244
|
+
|
|
245
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
246
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class LocalAIConnector(LLMConnector):
|
|
250
|
+
"""LocalAI / LM Studio / Jan.ai / KoboldCpp / text-generation-webui /
|
|
251
|
+
llama.cpp / vLLM / TGI — any OpenAI-compatible local server.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
model: str = "local-model",
|
|
257
|
+
host: str = "http://localhost:8080",
|
|
258
|
+
api_key: str = "local",
|
|
259
|
+
) -> None:
|
|
260
|
+
self.model = model
|
|
261
|
+
self._oa = OpenAIConnector(api_key, model, host)
|
|
262
|
+
|
|
263
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
264
|
+
return self._oa.complete(messages, system, max_tokens)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class HuggingFaceConnector(LLMConnector):
|
|
268
|
+
"""HuggingFace — any model via transformers pipeline (fully local)."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, model: str = "microsoft/Phi-4") -> None:
|
|
271
|
+
self.model_name = model
|
|
272
|
+
self._pipeline: Any = None
|
|
273
|
+
|
|
274
|
+
def _load(self) -> None:
|
|
275
|
+
if self._pipeline is None:
|
|
276
|
+
try:
|
|
277
|
+
from transformers import pipeline # type: ignore
|
|
278
|
+
except ImportError as e:
|
|
279
|
+
raise ImportError("pip install transformers torch") from e
|
|
280
|
+
log.info("llm.loading model=%s", self.model_name)
|
|
281
|
+
self._pipeline = pipeline("text-generation", model=self.model_name, device_map="auto")
|
|
282
|
+
log.info("llm.loaded model=%s", self.model_name)
|
|
283
|
+
|
|
284
|
+
def complete(self, messages: list[dict], system: str = "", max_tokens: int = 1024) -> LLMResponse:
|
|
285
|
+
self._load()
|
|
286
|
+
prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
|
|
287
|
+
if system:
|
|
288
|
+
prompt = f"system: {system}\n{prompt}"
|
|
289
|
+
result = self._pipeline(prompt, max_new_tokens=max_tokens, do_sample=False)
|
|
290
|
+
text = result[0]["generated_text"][len(prompt):].strip()
|
|
291
|
+
return LLMResponse(text, self.model_name)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ─── Factory ──────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
def create_connector(provider: str, **kwargs: Any) -> LLMConnector:
|
|
297
|
+
"""
|
|
298
|
+
Create a connector by provider name.
|
|
299
|
+
|
|
300
|
+
Cloud providers: anthropic, openai, azure, google, gemini, mistral,
|
|
301
|
+
xai, grok, deepseek, cohere, perplexity, groq,
|
|
302
|
+
fireworks, together, nvidia, bedrock
|
|
303
|
+
Local providers: ollama, lmstudio, jan, localai, llamacpp, gpt4all,
|
|
304
|
+
kobold, vllm, tgi, huggingface, openai-compatible
|
|
305
|
+
"""
|
|
306
|
+
# Normalize aliases
|
|
307
|
+
aliases = {
|
|
308
|
+
# Anthropic
|
|
309
|
+
"anthropic": "anthropic", "claude": "anthropic",
|
|
310
|
+
# OpenAI
|
|
311
|
+
"openai": "openai", "gpt": "openai", "azure": "openai",
|
|
312
|
+
"nvidia": "openai", "together": "openai", "fireworks": "openai",
|
|
313
|
+
"bedrock": "openai", # via OpenAI-compat gateway
|
|
314
|
+
# Google
|
|
315
|
+
"google": "google", "gemini": "google",
|
|
316
|
+
# Mistral
|
|
317
|
+
"mistral": "mistral",
|
|
318
|
+
# xAI
|
|
319
|
+
"xai": "xai", "grok": "xai",
|
|
320
|
+
# DeepSeek
|
|
321
|
+
"deepseek": "deepseek",
|
|
322
|
+
# Groq
|
|
323
|
+
"groq": "groq",
|
|
324
|
+
# Perplexity
|
|
325
|
+
"perplexity": "perplexity", "sonar": "perplexity",
|
|
326
|
+
# Cohere
|
|
327
|
+
"cohere": "cohere", "command": "cohere",
|
|
328
|
+
# Local
|
|
329
|
+
"ollama": "ollama",
|
|
330
|
+
"lmstudio": "local", "jan": "local", "janai": "local",
|
|
331
|
+
"localai": "local", "llamacpp": "local", "llama.cpp": "local",
|
|
332
|
+
"kobold": "local", "koboldcpp": "local",
|
|
333
|
+
"vllm": "local", "tgi": "local",
|
|
334
|
+
"gpt4all": "local", "openwebui": "local",
|
|
335
|
+
"local": "local", "openai-compatible": "local",
|
|
336
|
+
# HuggingFace
|
|
337
|
+
"huggingface": "hf", "hf": "hf", "transformers": "hf",
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
connectors = {
|
|
341
|
+
"anthropic": AnthropicConnector,
|
|
342
|
+
"openai": OpenAIConnector,
|
|
343
|
+
"google": GeminiConnector,
|
|
344
|
+
"mistral": MistralConnector,
|
|
345
|
+
"xai": XAIConnector,
|
|
346
|
+
"deepseek": DeepSeekConnector,
|
|
347
|
+
"groq": GroqConnector,
|
|
348
|
+
"perplexity": PerplexityConnector,
|
|
349
|
+
"cohere": CohereConnector,
|
|
350
|
+
"ollama": OllamaConnector,
|
|
351
|
+
"local": LocalAIConnector,
|
|
352
|
+
"hf": HuggingFaceConnector,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
key = aliases.get(provider.lower(), provider.lower())
|
|
356
|
+
cls = connectors.get(key)
|
|
357
|
+
if cls is None:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"Unknown provider '{provider}'. "
|
|
360
|
+
f"Note: the Halyn proxy works with ANY AI regardless of this connector."
|
|
361
|
+
)
|
|
362
|
+
return cls(**kwargs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: halyn
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable.
|
|
5
5
|
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
6
|
License: BSL-1.1
|
|
@@ -157,18 +157,84 @@ Deployed via `/etc/opt/chrome/policies/managed/halyn.json` — the agent cannot
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
##
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
## Compatible AI
|
|
161
|
+
|
|
162
|
+
Halyn intercepts at the kernel and proxy level. It does not care which AI is running — it audits all of them equally. No AI is excluded.
|
|
163
|
+
|
|
164
|
+
### How compatibility works
|
|
165
|
+
|
|
166
|
+
Halyn intercepts three things:
|
|
167
|
+
- **API calls** (iptables REDIRECT on port 443/80) — catches any HTTP request to any AI provider
|
|
168
|
+
- **Filesystem access** (inotify/FSEvents/eBPF) — catches any agent touching files, regardless of origin
|
|
169
|
+
- **Process syscalls** (eBPF, Linux ≥5.8) — catches any agent at the kernel level
|
|
170
|
+
|
|
171
|
+
This means: if an AI agent makes an API call or accesses your system, Halyn sees it.
|
|
172
|
+
|
|
173
|
+
### Cloud AI
|
|
174
|
+
|
|
175
|
+
| Provider | Models (March 2026) | API |
|
|
176
|
+
|----------|---------------------|-----|
|
|
177
|
+
| **Anthropic** | Claude Sonnet 4.6, Claude Opus 4.6, Claude Haiku 4.5 | api.anthropic.com |
|
|
178
|
+
| **OpenAI** | GPT-4.1, GPT-4.1 mini, GPT-4.1 nano, o3, o4-mini | api.openai.com |
|
|
179
|
+
| **Google** | Gemini 3.1 Pro, Gemini 3.1 Flash, Gemini 3.1 Flash-Lite | generativelanguage.googleapis.com |
|
|
180
|
+
| **Mistral AI** | Mistral Large 2, Mistral Small 3, Codestral | api.mistral.ai |
|
|
181
|
+
| **xAI** | Grok-3, Grok-3 mini | api.x.ai |
|
|
182
|
+
| **DeepSeek** | DeepSeek-V3, DeepSeek-R1 | api.deepseek.com |
|
|
183
|
+
| **Cohere** | Command R+, Command R, Aya | api.cohere.com |
|
|
184
|
+
| **Perplexity** | Sonar Pro, Sonar, Sonar Reasoning | api.perplexity.ai |
|
|
185
|
+
| **01.AI** | Yi-Large, Yi-Vision | api.01.ai |
|
|
186
|
+
| **Alibaba** | Qwen-Max, Qwen-Plus, Qwen-Turbo | dashscope.aliyuncs.com |
|
|
187
|
+
| **Baidu** | ERNIE 4.5, ERNIE Speed | aip.baidubce.com |
|
|
188
|
+
| **Amazon Bedrock** | Claude, Titan, Llama, Mistral (via AWS) | bedrock.amazonaws.com |
|
|
189
|
+
| **Azure OpenAI** | GPT-4.1, o3 (via Microsoft) | *.openai.azure.com |
|
|
190
|
+
| **NVIDIA NIM** | Llama 3.3, Mistral, DeepSeek-R1 (on NVIDIA cloud) | integrate.api.nvidia.com |
|
|
191
|
+
| **Together AI** | 50+ open models via API | api.together.xyz |
|
|
192
|
+
| **Groq** | Llama, Mixtral, Gemma (ultra-fast inference) | api.groq.com |
|
|
193
|
+
| **Fireworks AI** | Llama, Mixtral, DeepSeek | api.fireworks.ai |
|
|
194
|
+
|
|
195
|
+
### Local AI
|
|
196
|
+
|
|
197
|
+
Any local model is compatible — Halyn intercepts at the process level, not the network level.
|
|
198
|
+
|
|
199
|
+
| Runtime | Models | Notes |
|
|
200
|
+
|---------|--------|-------|
|
|
201
|
+
| **Ollama** | Llama 3.3, Qwen2.5, Mistral, DeepSeek-R1, Phi-4, Gemma 3, ... | OpenAI-compatible API |
|
|
202
|
+
| **LM Studio** | Any GGUF model | OpenAI-compatible server |
|
|
203
|
+
| **Jan.ai** | Any GGUF or ONNX model | Desktop + server mode |
|
|
204
|
+
| **GPT4All** | Llama, Mistral, Phi variants | Offline, no telemetry |
|
|
205
|
+
| **llama.cpp** | Any GGUF model directly | Server mode (`--server`) |
|
|
206
|
+
| **LocalAI** | 100+ models, any GGUF | Drop-in OpenAI replacement |
|
|
207
|
+
| **text-generation-webui** | Any HuggingFace model | Extension ecosystem |
|
|
208
|
+
| **KoboldCpp** | Any GGUF model | Focus on creative writing |
|
|
209
|
+
| **OpenWebUI** | Ollama + OpenAI frontend | Browser-based |
|
|
210
|
+
| **AnythingLLM** | Multi-model workspace | Team-friendly |
|
|
211
|
+
| **Xinference** | HuggingFace + GGUF | Enterprise local inference |
|
|
212
|
+
| **vLLM** | HuggingFace models | High-throughput server |
|
|
213
|
+
| **TGI (HuggingFace)** | HuggingFace models | Production inference |
|
|
214
|
+
|
|
215
|
+
### Agentic frameworks
|
|
216
|
+
|
|
217
|
+
Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
218
|
+
|
|
219
|
+
| Framework | Notes |
|
|
220
|
+
|-----------|-------|
|
|
221
|
+
| **OpenClaw** | Full interceptor — every action audited |
|
|
222
|
+
| **Claude Cowork** | Proxy + filesystem hooks |
|
|
223
|
+
| **Claude Code** | Process-level monitoring |
|
|
224
|
+
| **LangChain** | API calls intercepted automatically |
|
|
225
|
+
| **LlamaIndex** | API calls intercepted automatically |
|
|
226
|
+
| **AutoGen** | API calls intercepted automatically |
|
|
227
|
+
| **CrewAI** | API calls intercepted automatically |
|
|
228
|
+
| **Semantic Kernel** | API calls intercepted automatically |
|
|
229
|
+
| **BeeQ** | Native AAP integration |
|
|
230
|
+
| **Any MCP agent** | MCP server passthrough |
|
|
231
|
+
| **Any A2A agent** | Network-level interception |
|
|
232
|
+
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
|
233
|
+
|
|
234
|
+
### The rule
|
|
235
|
+
|
|
236
|
+
> If an AI touches your machine or calls an API — Halyn sees it.
|
|
237
|
+
> No exception. No exclusion. That's the point.
|
|
172
238
|
|
|
173
239
|
---
|
|
174
240
|
|
halyn-2.1.1/src/halyn/llm.py
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Elmadani SALKA
|
|
2
|
-
# Licensed under BSL-1.1. See LICENSE file.
|
|
3
|
-
# Commercial use requires a license — contact@halyn.dev
|
|
4
|
-
|
|
5
|
-
"""
|
|
6
|
-
LLM Connector — Multi-provider LLM abstraction.
|
|
7
|
-
|
|
8
|
-
Supports: Claude API, OpenAI API, Ollama (local), HuggingFace (local),
|
|
9
|
-
vLLM (self-hosted), any OpenAI-compatible endpoint.
|
|
10
|
-
|
|
11
|
-
The LLM is NOT in the control plane. It connects FROM OUTSIDE via MCP.
|
|
12
|
-
This module handles outbound LLM calls when Halyn needs reasoning
|
|
13
|
-
(e.g. for autonomous reasoning, incident analysis, summarization).
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import logging
|
|
20
|
-
import os
|
|
21
|
-
from abc import ABC, abstractmethod
|
|
22
|
-
from typing import Any
|
|
23
|
-
|
|
24
|
-
log = logging.getLogger("halyn.llm")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class LLMConnector(ABC):
|
|
28
|
-
"""Base class for LLM connections."""
|
|
29
|
-
|
|
30
|
-
@abstractmethod
|
|
31
|
-
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1000) -> str:
|
|
32
|
-
"""Send prompt, get response."""
|
|
33
|
-
|
|
34
|
-
@abstractmethod
|
|
35
|
-
async def is_available(self) -> bool:
|
|
36
|
-
"""Check if LLM is reachable."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class ClaudeConnector(LLMConnector):
|
|
40
|
-
"""Anthropic Claude API."""
|
|
41
|
-
|
|
42
|
-
def __init__(self, api_key: str = "", model: str = "claude-sonnet-4-6") -> None:
|
|
43
|
-
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
|
|
44
|
-
self.model = model
|
|
45
|
-
self.endpoint = "https://api.anthropic.com/v1/messages"
|
|
46
|
-
|
|
47
|
-
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1000) -> str:
|
|
48
|
-
import aiohttp
|
|
49
|
-
headers = {
|
|
50
|
-
"x-api-key": self.api_key,
|
|
51
|
-
"anthropic-version": "2023-06-01",
|
|
52
|
-
"content-type": "application/json",
|
|
53
|
-
}
|
|
54
|
-
body: dict[str, Any] = {
|
|
55
|
-
"model": self.model,
|
|
56
|
-
"max_tokens": max_tokens,
|
|
57
|
-
"messages": [{"role": "user", "content": prompt}],
|
|
58
|
-
}
|
|
59
|
-
if system:
|
|
60
|
-
body["system"] = system
|
|
61
|
-
async with aiohttp.ClientSession() as session:
|
|
62
|
-
async with session.post(self.endpoint, json=body, headers=headers) as resp:
|
|
63
|
-
data = await resp.json()
|
|
64
|
-
return data.get("content", [{}])[0].get("text", "")
|
|
65
|
-
|
|
66
|
-
async def is_available(self) -> bool:
|
|
67
|
-
return bool(self.api_key)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class OpenAIConnector(LLMConnector):
|
|
71
|
-
"""OpenAI or any OpenAI-compatible API (vLLM, LiteLLM, etc.)."""
|
|
72
|
-
|
|
73
|
-
def __init__(self, api_key: str = "", model: str = "gpt-4.1",
|
|
74
|
-
endpoint: str = "https://api.openai.com/v1") -> None:
|
|
75
|
-
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
|
|
76
|
-
self.model = model
|
|
77
|
-
self.endpoint = endpoint
|
|
78
|
-
|
|
79
|
-
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1000) -> str:
|
|
80
|
-
import aiohttp
|
|
81
|
-
headers = {
|
|
82
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
}
|
|
85
|
-
messages = []
|
|
86
|
-
if system:
|
|
87
|
-
messages.append({"role": "system", "content": system})
|
|
88
|
-
messages.append({"role": "user", "content": prompt})
|
|
89
|
-
body = {"model": self.model, "max_tokens": max_tokens, "messages": messages}
|
|
90
|
-
async with aiohttp.ClientSession() as session:
|
|
91
|
-
async with session.post(f"{self.endpoint}/chat/completions",
|
|
92
|
-
json=body, headers=headers) as resp:
|
|
93
|
-
data = await resp.json()
|
|
94
|
-
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
95
|
-
|
|
96
|
-
async def is_available(self) -> bool:
|
|
97
|
-
return bool(self.api_key)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class OllamaConnector(LLMConnector):
|
|
101
|
-
"""Ollama local inference. Zero cost, zero internet."""
|
|
102
|
-
|
|
103
|
-
def __init__(self, model: str = "llama3.2", host: str = "http://localhost:11434") -> None:
|
|
104
|
-
self.model = model
|
|
105
|
-
self.host = host
|
|
106
|
-
|
|
107
|
-
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1000) -> str:
|
|
108
|
-
import aiohttp
|
|
109
|
-
body: dict[str, Any] = {
|
|
110
|
-
"model": self.model,
|
|
111
|
-
"prompt": prompt,
|
|
112
|
-
"stream": False,
|
|
113
|
-
}
|
|
114
|
-
if system:
|
|
115
|
-
body["system"] = system
|
|
116
|
-
async with aiohttp.ClientSession() as session:
|
|
117
|
-
async with session.post(f"{self.host}/api/generate", json=body) as resp:
|
|
118
|
-
data = await resp.json()
|
|
119
|
-
return data.get("response", "")
|
|
120
|
-
|
|
121
|
-
async def is_available(self) -> bool:
|
|
122
|
-
try:
|
|
123
|
-
import aiohttp
|
|
124
|
-
async with aiohttp.ClientSession() as session:
|
|
125
|
-
async with session.get(f"{self.host}/api/tags", timeout=aiohttp.ClientTimeout(total=3)) as resp:
|
|
126
|
-
return resp.status == 200
|
|
127
|
-
except Exception:
|
|
128
|
-
return False
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
class HuggingFaceConnector(LLMConnector):
|
|
132
|
-
"""Run any HuggingFace model locally. Zero cloud."""
|
|
133
|
-
|
|
134
|
-
def __init__(self, model: str = "mistralai/Mistral-7B-Instruct-v0.3") -> None:
|
|
135
|
-
self.model_name = model
|
|
136
|
-
self._pipeline: Any = None
|
|
137
|
-
|
|
138
|
-
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1000) -> str:
|
|
139
|
-
if self._pipeline is None:
|
|
140
|
-
self._load()
|
|
141
|
-
full_prompt = f"{system}
|
|
142
|
-
|
|
143
|
-
{prompt}" if system else prompt
|
|
144
|
-
result = self._pipeline(full_prompt, max_new_tokens=max_tokens, do_sample=True, temperature=0.7)
|
|
145
|
-
return result[0]["generated_text"][len(full_prompt):]
|
|
146
|
-
|
|
147
|
-
async def is_available(self) -> bool:
|
|
148
|
-
try:
|
|
149
|
-
import transformers # noqa: F401
|
|
150
|
-
return True
|
|
151
|
-
except ImportError:
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
def _load(self) -> None:
|
|
155
|
-
from transformers import pipeline
|
|
156
|
-
log.info("llm.loading model=%s (this may take a while...)", self.model_name)
|
|
157
|
-
self._pipeline = pipeline("text-generation", model=self.model_name, device_map="auto")
|
|
158
|
-
log.info("llm.loaded model=%s", self.model_name)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# ─── Factory ────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
def create_connector(provider: str, **kwargs: Any) -> LLMConnector:
|
|
164
|
-
"""Create an LLM connector by name."""
|
|
165
|
-
connectors: dict[str, type[LLMConnector]] = {
|
|
166
|
-
"claude": ClaudeConnector,
|
|
167
|
-
"anthropic": ClaudeConnector,
|
|
168
|
-
"openai": OpenAIConnector,
|
|
169
|
-
"gpt": OpenAIConnector,
|
|
170
|
-
"ollama": OllamaConnector,
|
|
171
|
-
"huggingface": HuggingFaceConnector,
|
|
172
|
-
"hf": HuggingFaceConnector,
|
|
173
|
-
"vllm": OpenAIConnector, # vLLM is OpenAI-compatible
|
|
174
|
-
"litellm": OpenAIConnector,
|
|
175
|
-
}
|
|
176
|
-
cls = connectors.get(provider.lower())
|
|
177
|
-
if cls is None:
|
|
178
|
-
raise ValueError(f"Unknown LLM provider: {provider}. Available: {list(connectors.keys())}")
|
|
179
|
-
return cls(**kwargs)
|
|
180
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|