dominusnode-openai-functions 2.2.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.
- dominusnode_openai_functions/__init__.py +67 -0
- dominusnode_openai_functions/_core.py +351 -0
- dominusnode_openai_functions-2.2.0.dist-info/METADATA +137 -0
- dominusnode_openai_functions-2.2.0.dist-info/RECORD +7 -0
- dominusnode_openai_functions-2.2.0.dist-info/WHEEL +5 -0
- dominusnode_openai_functions-2.2.0.dist-info/licenses/LICENSE +21 -0
- dominusnode_openai_functions-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""DominusNode v2 OpenAI-compatible function definitions and handler (Python).
|
|
2
|
+
|
|
3
|
+
With the ``openai`` SDK (Chat Completions)::
|
|
4
|
+
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
from dominusnode_openai_functions import chat_completion_tools, create_dominusnode_function_handler
|
|
7
|
+
|
|
8
|
+
handle = create_dominusnode_function_handler() # key from DOMINUSNODE_API_KEY
|
|
9
|
+
resp = OpenAI().chat.completions.create(model="gpt-4.1", messages=messages, tools=chat_completion_tools())
|
|
10
|
+
for call in resp.choices[0].message.tool_calls or []:
|
|
11
|
+
content = handle(call.function.name, call.function.arguments) # raw JSON string is accepted
|
|
12
|
+
messages.append({"role": "tool", "tool_call_id": call.id, "content": content})
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
import json
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
20
|
+
|
|
21
|
+
from dominusnode_openai_functions._core import PREFIX, TOOL_NAMES, TOOL_SPECS, DominusNodeTools
|
|
22
|
+
|
|
23
|
+
_DANGEROUS = ("__proto__", "constructor", "prototype")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def function_definitions() -> List[Dict[str, Any]]:
|
|
27
|
+
"""The 19 tools as ``{name, description, parameters}`` JSON Schema definitions (same as functions.json)."""
|
|
28
|
+
return [{"name": PREFIX + s["name"], "description": s["description"], "parameters": copy.deepcopy(s["parameters"])}
|
|
29
|
+
for s in TOOL_SPECS]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def chat_completion_tools() -> List[Dict[str, Any]]:
|
|
33
|
+
"""Chat Completions ``tools=`` list."""
|
|
34
|
+
return [{"type": "function", "function": f} for f in function_definitions()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def responses_tools() -> List[Dict[str, Any]]:
|
|
38
|
+
"""Responses API ``tools=`` list."""
|
|
39
|
+
return [{"type": "function", **f} for f in function_definitions()]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_dominusnode_function_handler(
|
|
43
|
+
api_key: Optional[str] = None, base_url: Optional[str] = None, proxy_host: Optional[str] = None,
|
|
44
|
+
client: Optional[Any] = None,
|
|
45
|
+
) -> Callable[..., str]:
|
|
46
|
+
"""``handler(name, args) -> JSON string``; ``args`` may be a dict or the model's raw JSON string."""
|
|
47
|
+
tools = DominusNodeTools(api_key=api_key, base_url=base_url, proxy_host=proxy_host, client=client)
|
|
48
|
+
|
|
49
|
+
def handler(name: str, args: Union[Dict[str, Any], str, None] = None) -> str:
|
|
50
|
+
if isinstance(args, str):
|
|
51
|
+
try:
|
|
52
|
+
parsed = json.loads(args) if args.strip() else {}
|
|
53
|
+
except ValueError:
|
|
54
|
+
return json.dumps({"error": "arguments must be a JSON object"})
|
|
55
|
+
else:
|
|
56
|
+
parsed = args or {}
|
|
57
|
+
if not isinstance(parsed, dict):
|
|
58
|
+
return json.dumps({"error": "arguments must be a JSON object"})
|
|
59
|
+
parsed = {k: v for k, v in parsed.items() if k not in _DANGEROUS}
|
|
60
|
+
return tools.call(name, parsed)
|
|
61
|
+
|
|
62
|
+
return handler
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__version__ = "2.2.0"
|
|
66
|
+
__all__ = ["create_dominusnode_function_handler", "function_definitions", "chat_completion_tools", "responses_tools",
|
|
67
|
+
"DominusNodeTools", "TOOL_NAMES", "__version__"]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""DominusNode v2 standard tool set (19 tools), built on the ``dominusnode`` Python SDK.
|
|
2
|
+
|
|
3
|
+
Framework-neutral: each tool has a name, an LLM-facing description and a JSON
|
|
4
|
+
Schema for its arguments. ``DominusNodeTools.call(name, args)`` dispatches to
|
|
5
|
+
the SDK and always returns a JSON string with credentials scrubbed. HTTP,
|
|
6
|
+
SSRF/DNS-rebinding checks, OFAC refusal and signing all live in the SDK.
|
|
7
|
+
|
|
8
|
+
The API key is read only from ``DOMINUSNODE_API_KEY`` (or passed by the host
|
|
9
|
+
application as configuration); it is never a tool argument or tool output.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from dominusnode import DominusNodeClient, DominusNodeError, scrub_credentials, json_default
|
|
20
|
+
|
|
21
|
+
PREFIX = "dominusnode_"
|
|
22
|
+
MAX_BODY_CHARS = 20_000
|
|
23
|
+
_KEY_PLACEHOLDER = "${DOMINUSNODE_API_KEY}"
|
|
24
|
+
|
|
25
|
+
_COUNTRY = {"type": "string", "description": "ISO 3166-1 alpha-2 exit country, e.g. US. Sanctioned countries are refused."}
|
|
26
|
+
_IP_CLASS = {"type": "string", "enum": ["residential", "dc", "mobile", "masked"],
|
|
27
|
+
"description": "Exit IP class. Each class has its own per-GB price (see network_pricing); masked = cheapest, VPN/anonymizer exits, opt-in."}
|
|
28
|
+
_SESSION = {"type": "string", "description": "Sticky session id (1-64 alphanumeric) to keep the same exit IP."}
|
|
29
|
+
_ALLOW_HTTP = {"type": "boolean",
|
|
30
|
+
"description": "Opt in to plaintext http:// targets. Plaintext is visible to the exit node; prefer https."}
|
|
31
|
+
_LIMIT = {"type": "integer", "minimum": 1, "description": "Maximum number of items."}
|
|
32
|
+
_OFFSET = {"type": "integer", "minimum": 0, "description": "Items to skip."}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#: credits_convert needs a wallet signature; signing flows are not exposed as LLM tools.
|
|
36
|
+
CREDITS_CONVERT_SIGNING_MESSAGE = (
|
|
37
|
+
"Converting credits needs a fresh signature from the wallet linked to this account, so this tool does not convert. Use the DominusNode MCP server (tool dominusnode_credits_convert, with DOMINUSNODE_WALLET_KEY_FILE pointing at a chmod-600 wallet key file) or a DominusNode SDK's credits.convert with a signer key file. Check available credit with credits_balance."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _obj(props: Optional[Dict[str, Any]] = None, required: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
42
|
+
return {"type": "object", "properties": props or {}, "required": required or [], "additionalProperties": False}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
TOOL_SPECS: List[Dict[str, Any]] = [
|
|
46
|
+
{"name": "fetch", "description": (
|
|
47
|
+
"Fetch a public URL through the DominusNode relay network (GET or HEAD only, ports 80/443, no redirects "
|
|
48
|
+
"followed, 10 MB cap, private/internal targets refused). Billed per GB at the live price for the exit IP "
|
|
49
|
+
"class (see network_pricing). http:// targets need allow_http=true and are visible to the exit node."),
|
|
50
|
+
"parameters": _obj({"url": {"type": "string", "description": "https:// URL (or http:// with allow_http)."},
|
|
51
|
+
"method": {"type": "string", "enum": ["GET", "HEAD"], "description": "HTTP method."},
|
|
52
|
+
"country": _COUNTRY, "ip_class": _IP_CLASS, "session_id": _SESSION,
|
|
53
|
+
"allow_http": _ALLOW_HTTP}, ["url"])},
|
|
54
|
+
{"name": "proxy_url", "description": (
|
|
55
|
+
"Build a relay gateway proxy URL for use in other HTTP clients. Routing flags go in the username "
|
|
56
|
+
"(country-XX, class-..., session-..., allowhttp-1). The password is shown as a ${DOMINUSNODE_API_KEY} "
|
|
57
|
+
"placeholder: substitute the key from the environment, never paste it into chat."),
|
|
58
|
+
"parameters": _obj({"country": _COUNTRY, "ip_class": _IP_CLASS, "session_id": _SESSION, "allow_http": _ALLOW_HTTP,
|
|
59
|
+
"protocol": {"type": "string", "enum": ["https", "socks5", "http"],
|
|
60
|
+
"description": "https (default: TLS to the gateway), socks5, or http (plaintext gateway; local dev only)."}})},
|
|
61
|
+
{"name": "network_policy", "description": (
|
|
62
|
+
"Relay network policy: allowed ports, plaintext (allowhttp) rules, blocked ranges, sanctioned countries "
|
|
63
|
+
"and node attestation terms."), "parameters": _obj()},
|
|
64
|
+
{"name": "network_pricing", "description": (
|
|
65
|
+
"Live per-GB prices by exit IP class (residential, dc, mobile, masked) and node tier revenue shares. Always use "
|
|
66
|
+
"this for prices; never assume them."), "parameters": _obj()},
|
|
67
|
+
{"name": "network_stats", "description": "Relay network statistics: online nodes, exit countries and IP classes.",
|
|
68
|
+
"parameters": _obj()},
|
|
69
|
+
{"name": "balance", "description": "Prepaid requester wallet balance (cents) used to pay for relayed traffic.",
|
|
70
|
+
"parameters": _obj()},
|
|
71
|
+
{"name": "transactions", "description": "Requester wallet transaction history (top-ups, usage debits, credit conversions).",
|
|
72
|
+
"parameters": _obj({"limit": _LIMIT, "offset": _OFFSET})},
|
|
73
|
+
{"name": "topup_crypto", "description": (
|
|
74
|
+
"Create a crypto invoice to top up the requester wallet ($10-$1,000). Returns a payment address/URL; "
|
|
75
|
+
"the wallet is credited after on-chain confirmation (check with check_payment)."),
|
|
76
|
+
"parameters": _obj({"amount_cents": {"type": "integer", "minimum": 1000, "maximum": 100000,
|
|
77
|
+
"description": "Amount in US cents (1000 = $10)."},
|
|
78
|
+
"currency": {"type": "string", "enum": ["btc", "eth", "ltc", "xmr", "zec", "usdc", "sol",
|
|
79
|
+
"usdt", "dai", "bnb", "link"],
|
|
80
|
+
"description": "Cryptocurrency to pay with."}}, ["amount_cents", "currency"])},
|
|
81
|
+
{"name": "check_payment", "description": "Status of a crypto top-up invoice created by topup_crypto.",
|
|
82
|
+
"parameters": _obj({"invoice_id": {"type": "string", "description": "Invoice UUID."}}, ["invoice_id"])},
|
|
83
|
+
{"name": "usage", "description": "Relayed traffic usage summary (bytes and cost) for an optional ISO 8601 date range.",
|
|
84
|
+
"parameters": _obj({"from": {"type": "string", "description": "ISO 8601 start."},
|
|
85
|
+
"to": {"type": "string", "description": "ISO 8601 end."}})},
|
|
86
|
+
{"name": "active_sessions", "description": "Your currently active relay sessions (id, start time, status).",
|
|
87
|
+
"parameters": _obj()},
|
|
88
|
+
{"name": "report_session", "description": (
|
|
89
|
+
"Report a burned or bad exit for one of your relay sessions so the network can rotate it."),
|
|
90
|
+
"parameters": _obj({"session_id": {"type": "string", "description": "Session UUID from active_sessions."},
|
|
91
|
+
"reason": {"type": "string", "enum": ["blocked", "captcha", "rate_limited", "slow",
|
|
92
|
+
"bad_content", "other"],
|
|
93
|
+
"description": "Why the exit is bad."}}, ["session_id", "reason"])},
|
|
94
|
+
{"name": "node_list", "description": (
|
|
95
|
+
"Relay nodes you contribute (status, observed IP class, last seen). Contributing IPs earns credit."),
|
|
96
|
+
"parameters": _obj({"limit": _LIMIT, "offset": _OFFSET})},
|
|
97
|
+
{"name": "node_tier", "description": (
|
|
98
|
+
"Your node-operator tier, revenue share and progress toward the next tier (tenure, uptime, active IPs, reputation)."),
|
|
99
|
+
"parameters": _obj()},
|
|
100
|
+
{"name": "node_setup_guide", "description": (
|
|
101
|
+
"Step-by-step instructions to earn by contributing an IP: run the DominusNode node agent, register a node, "
|
|
102
|
+
"and how tiers and the self-dealing rule work."), "parameters": _obj()},
|
|
103
|
+
{"name": "credits_balance", "description": "Node-operator earnings: pending and available credit (µUSD).",
|
|
104
|
+
"parameters": _obj()},
|
|
105
|
+
{"name": "credits_convert", "description": (
|
|
106
|
+
"Explains how to convert node credits into wallet balance. Conversion needs a wallet signature, so it runs through the DominusNode MCP server or SDK with a signer key file, not through this tool."),
|
|
107
|
+
"parameters": _obj({"amount_musd": {"type": "string",
|
|
108
|
+
"description": "Amount in micro-USD as a digit string (min 10000 = $0.01)."}},
|
|
109
|
+
["amount_musd"])},
|
|
110
|
+
{"name": "payouts_list", "description": "Your USDC payouts of node earnings with status and transaction hash.",
|
|
111
|
+
"parameters": _obj({"limit": _LIMIT, "offset": _OFFSET})},
|
|
112
|
+
{"name": "x402_info", "description": "x402 micropayment info: facilitators, networks and server-reported prices.",
|
|
113
|
+
"parameters": _obj()},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
TOOL_NAMES: List[str] = [t["name"] for t in TOOL_SPECS]
|
|
117
|
+
SPECS_BY_NAME: Dict[str, Dict[str, Any]] = {t["name"]: t for t in TOOL_SPECS}
|
|
118
|
+
|
|
119
|
+
NODE_SETUP_GUIDE = "\n".join([
|
|
120
|
+
"DominusNode relay node setup (earn by contributing an IP)",
|
|
121
|
+
"",
|
|
122
|
+
"How earning works",
|
|
123
|
+
" - Your node dials OUT to the gateway (no inbound ports) and relays other agents' traffic.",
|
|
124
|
+
" - You earn your tier's share of each paid GB relayed; live prices and shares: network_pricing.",
|
|
125
|
+
" - Self-dealing earns nothing: your own, your wallet's, team's or /24's traffic never routes to your node.",
|
|
126
|
+
" - Tiers rise with paid tenure, uptime, active IPs and reputation (node_tier).",
|
|
127
|
+
" - Nodes only dial ports 80/443, never private/metadata targets or SMTP/SSH/RDP (network_policy).",
|
|
128
|
+
"",
|
|
129
|
+
"Steps",
|
|
130
|
+
" 1. Store your owner wallet key in a private file (never pass it to a tool or chat):",
|
|
131
|
+
" umask 077 && printf '%s' \"$WALLET_PRIVATE_KEY_HEX\" > ~/.dominusnode/wallet.key",
|
|
132
|
+
" 2. On the machine whose IP you contribute (Docker), register with the published image",
|
|
133
|
+
" (official coordinator pins baked in; a plain source build has none and refuses to start):",
|
|
134
|
+
" sudo install -d -o 1001 -g 1001 -m 700 /etc/dominusnode",
|
|
135
|
+
" sudo install -o 1001 -g 1001 -m 600 ~/.dominusnode/wallet.key /etc/dominusnode/wallet.key",
|
|
136
|
+
" export DN_API_KEY # your dn_live_ key, from the environment only",
|
|
137
|
+
" docker run --rm -e DN_API_KEY -e DN_WALLET_KEY_FILE=/run/dominusnode/wallet.key \\",
|
|
138
|
+
" -v /etc/dominusnode:/run/dominusnode ghcr.io/0xcircuitbreaker/dominusnode-node register",
|
|
139
|
+
" sudo rm /etc/dominusnode/wallet.key # not needed after registration",
|
|
140
|
+
" 3. Run it and keep it running - uptime drives your tier:",
|
|
141
|
+
" docker run -d --name dominusnode-node --restart unless-stopped \\",
|
|
142
|
+
" -v /etc/dominusnode/node.json:/run/dominusnode/node.json:ro ghcr.io/0xcircuitbreaker/dominusnode-node",
|
|
143
|
+
" 4. Verify with node_list and node_tier; earnings appear in credits_balance.",
|
|
144
|
+
"",
|
|
145
|
+
"Signing flows (node register, withdraw, payout wallet) are not available as tools here; use the",
|
|
146
|
+
"node agent CLI, the DominusNode MCP server or the SDK with a signer key file.",
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
#: Gateway TLS (HTTPS proxy) port: production 8443; self-hosted coordinators use 8443 too.
|
|
151
|
+
DEFAULT_TLS_PROXY_PORT = 8443
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolve_gateway(
|
|
155
|
+
proxy_host: Optional[str] = None,
|
|
156
|
+
tls_port: Optional[int] = None,
|
|
157
|
+
http_port: Optional[int] = None,
|
|
158
|
+
insecure: Optional[bool] = None,
|
|
159
|
+
env: Optional[Any] = None,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Where proxy traffic goes. TLS to the gateway's HTTPS proxy is the default: production
|
|
162
|
+
gateways refuse credentials on plaintext 8080/1080. ``DOMINUSNODE_PROXY_URL=https://host[:port]``
|
|
163
|
+
sets host and TLS port (same variable as the MCP server), ``DOMINUSNODE_PROXY_TLS_PORT`` the TLS
|
|
164
|
+
port, and ``DOMINUSNODE_PROXY_INSECURE=1`` selects plaintext (local development only).
|
|
165
|
+
|
|
166
|
+
Returns ``{"proxy_host", "http_port", "gateway_tls"}`` where ``gateway_tls`` is ``False`` or
|
|
167
|
+
``{"port": n}``, ready for ``DominusNodeClient(gateway_tls=...)``.
|
|
168
|
+
"""
|
|
169
|
+
from urllib.parse import urlsplit
|
|
170
|
+
|
|
171
|
+
e = os.environ if env is None else env
|
|
172
|
+
|
|
173
|
+
def _port(v: Any) -> Optional[int]:
|
|
174
|
+
try:
|
|
175
|
+
n = int(v)
|
|
176
|
+
except (TypeError, ValueError):
|
|
177
|
+
return None
|
|
178
|
+
return n if 1 <= n <= 65535 else None
|
|
179
|
+
|
|
180
|
+
url_host: Optional[str] = None
|
|
181
|
+
url_port: Optional[int] = None
|
|
182
|
+
raw = (e.get("DOMINUSNODE_PROXY_URL") or "").strip()
|
|
183
|
+
if raw:
|
|
184
|
+
u = urlsplit(raw)
|
|
185
|
+
if u.scheme != "https" or not u.hostname:
|
|
186
|
+
raise ValueError("DOMINUSNODE_PROXY_URL must be https://host[:port] (TLS to the gateway)")
|
|
187
|
+
if u.username or u.password:
|
|
188
|
+
raise ValueError("DOMINUSNODE_PROXY_URL must not contain credentials")
|
|
189
|
+
url_host = u.hostname.lower()
|
|
190
|
+
try:
|
|
191
|
+
url_port = _port(u.port) if u.port else DEFAULT_TLS_PROXY_PORT
|
|
192
|
+
except ValueError:
|
|
193
|
+
raise ValueError("DOMINUSNODE_PROXY_URL has an invalid port") from None
|
|
194
|
+
if insecure is None:
|
|
195
|
+
insecure = (e.get("DOMINUSNODE_PROXY_INSECURE") or "").strip() == "1"
|
|
196
|
+
if insecure and raw:
|
|
197
|
+
raise ValueError("DOMINUSNODE_PROXY_URL (TLS) and DOMINUSNODE_PROXY_INSECURE=1 are mutually exclusive")
|
|
198
|
+
tls = _port(tls_port) or url_port or _port(e.get("DOMINUSNODE_PROXY_TLS_PORT")) or DEFAULT_TLS_PROXY_PORT
|
|
199
|
+
return {
|
|
200
|
+
"proxy_host": proxy_host or url_host or (e.get("DOMINUSNODE_PROXY_HOST") or "").strip() or None,
|
|
201
|
+
"http_port": _port(http_port) or _port(e.get("DOMINUSNODE_PROXY_PORT")),
|
|
202
|
+
"gateway_tls": False if insecure else {"port": tls},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _scrub(text: str, key: Optional[str]) -> str:
|
|
207
|
+
out = scrub_credentials(text)
|
|
208
|
+
if key:
|
|
209
|
+
out = out.replace(key, "[REDACTED]")
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _opt_int(args: Dict[str, Any], name: str) -> Optional[int]:
|
|
214
|
+
v = args.get(name)
|
|
215
|
+
if v is None:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(v, bool):
|
|
218
|
+
raise ValueError(f"{name} must be an integer")
|
|
219
|
+
return int(v)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class DominusNodeTools:
|
|
223
|
+
"""Dispatches the 19 standard tools to a connected ``DominusNodeClient``."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
|
|
226
|
+
proxy_host: Optional[str] = None, client: Optional[Any] = None,
|
|
227
|
+
tls_proxy_port: Optional[int] = None, insecure_proxy: Optional[bool] = None) -> None:
|
|
228
|
+
self._api_key = api_key or os.environ.get("DOMINUSNODE_API_KEY") or None
|
|
229
|
+
self._base_url = base_url or os.environ.get("DOMINUSNODE_BASE_URL") or None
|
|
230
|
+
self._gateway = resolve_gateway(proxy_host, tls_proxy_port, None, insecure_proxy)
|
|
231
|
+
self._proxy_host = self._gateway["proxy_host"]
|
|
232
|
+
self._client = client
|
|
233
|
+
self._connected = client is not None
|
|
234
|
+
self._lock = threading.Lock()
|
|
235
|
+
|
|
236
|
+
def __repr__(self) -> str:
|
|
237
|
+
return "DominusNodeTools(api_key=[REDACTED])"
|
|
238
|
+
|
|
239
|
+
__str__ = __repr__
|
|
240
|
+
|
|
241
|
+
def _sdk(self) -> Any:
|
|
242
|
+
with self._lock:
|
|
243
|
+
if self._client is None:
|
|
244
|
+
kw: Dict[str, Any] = {}
|
|
245
|
+
if self._base_url:
|
|
246
|
+
kw["base_url"] = self._base_url
|
|
247
|
+
if self._proxy_host:
|
|
248
|
+
kw["proxy_host"] = self._proxy_host
|
|
249
|
+
if self._gateway["http_port"]:
|
|
250
|
+
kw["http_proxy_port"] = self._gateway["http_port"]
|
|
251
|
+
kw["gateway_tls"] = self._gateway["gateway_tls"]
|
|
252
|
+
self._client = DominusNodeClient(**kw)
|
|
253
|
+
if not self._connected:
|
|
254
|
+
if not self._api_key:
|
|
255
|
+
raise DominusNodeError("DOMINUSNODE_API_KEY is not set")
|
|
256
|
+
self._client.connect_with_key(self._api_key)
|
|
257
|
+
self._connected = True
|
|
258
|
+
return self._client
|
|
259
|
+
|
|
260
|
+
# ---- tool implementations -------------------------------------------------
|
|
261
|
+
def _fetch(self, a: Dict[str, Any]) -> Any:
|
|
262
|
+
r = self._sdk().proxy.fetch(
|
|
263
|
+
str(a["url"]), method=str(a.get("method") or "GET").upper(), country=a.get("country"),
|
|
264
|
+
ip_class=a.get("ip_class"), session_id=a.get("session_id"), allow_http=bool(a.get("allow_http", False)))
|
|
265
|
+
body = r.text()
|
|
266
|
+
cut = len(body) > MAX_BODY_CHARS
|
|
267
|
+
return {"status": r.status, "statusText": r.status_text,
|
|
268
|
+
"contentType": (r.headers or {}).get("content-type"),
|
|
269
|
+
"location": (r.headers or {}).get("location"),
|
|
270
|
+
"truncated": bool(r.truncated or cut), "body": body[:MAX_BODY_CHARS]}
|
|
271
|
+
|
|
272
|
+
def _proxy_url(self, a: Dict[str, Any]) -> Any:
|
|
273
|
+
c = self._sdk()
|
|
274
|
+
url = c.proxy.build_url(country=a.get("country"), ip_class=a.get("ip_class"), session_id=a.get("session_id"),
|
|
275
|
+
allow_http=bool(a.get("allow_http", False)), protocol=a.get("protocol") or None)
|
|
276
|
+
user = url.split("://", 1)[1].split(":", 1)[0]
|
|
277
|
+
prefix, rest = url.split("@", 1)[0], url.split("@", 1)[1]
|
|
278
|
+
scheme = prefix.split("://", 1)[0]
|
|
279
|
+
return {"proxyUrl": f"{scheme}://{user}:{_KEY_PLACEHOLDER}@{rest}", "username": user,
|
|
280
|
+
"note": "Replace the placeholder with DOMINUSNODE_API_KEY from your environment."}
|
|
281
|
+
|
|
282
|
+
def _dispatch(self, name: str, a: Dict[str, Any]) -> Any:
|
|
283
|
+
if name == "fetch":
|
|
284
|
+
return self._fetch(a)
|
|
285
|
+
if name == "proxy_url":
|
|
286
|
+
return self._proxy_url(a)
|
|
287
|
+
if name == "node_setup_guide":
|
|
288
|
+
return {"guide": NODE_SETUP_GUIDE}
|
|
289
|
+
if name == "credits_convert":
|
|
290
|
+
return {"status": "signing_required", "message": CREDITS_CONVERT_SIGNING_MESSAGE}
|
|
291
|
+
c = self._sdk()
|
|
292
|
+
if name == "network_policy":
|
|
293
|
+
return c.network.policy()
|
|
294
|
+
if name == "network_pricing":
|
|
295
|
+
return c.network.pricing()
|
|
296
|
+
if name == "network_stats":
|
|
297
|
+
return c.network.stats()
|
|
298
|
+
if name == "balance":
|
|
299
|
+
return c.wallet.get_balance()
|
|
300
|
+
if name == "transactions":
|
|
301
|
+
return c.wallet.get_transactions(limit=_opt_int(a, "limit"), offset=_opt_int(a, "offset"))
|
|
302
|
+
if name == "topup_crypto":
|
|
303
|
+
return c.wallet.top_up_crypto(int(a["amount_cents"]), str(a["currency"]))
|
|
304
|
+
if name == "check_payment":
|
|
305
|
+
return c.wallet.get_crypto_status(str(a["invoice_id"]))
|
|
306
|
+
if name == "usage":
|
|
307
|
+
return c.usage.get(from_=a.get("from"), to=a.get("to"))
|
|
308
|
+
if name == "active_sessions":
|
|
309
|
+
return c.sessions.get_active()
|
|
310
|
+
if name == "report_session":
|
|
311
|
+
return c.sessions.report(str(a["session_id"]), str(a["reason"]))
|
|
312
|
+
if name == "node_list":
|
|
313
|
+
return c.nodes.list(limit=_opt_int(a, "limit"), offset=_opt_int(a, "offset"))
|
|
314
|
+
if name == "node_tier":
|
|
315
|
+
return c.nodes.tier()
|
|
316
|
+
if name == "credits_balance":
|
|
317
|
+
return c.credits.balance()
|
|
318
|
+
if name == "payouts_list":
|
|
319
|
+
return c.payouts.list(limit=_opt_int(a, "limit"), offset=_opt_int(a, "offset"))
|
|
320
|
+
if name == "x402_info":
|
|
321
|
+
return c.x402.get_info()
|
|
322
|
+
raise ValueError(f"unknown tool: {name}")
|
|
323
|
+
|
|
324
|
+
def call(self, name: str, args: Optional[Dict[str, Any]] = None) -> str:
|
|
325
|
+
"""Run a tool by its standard name (prefix optional). Always returns a scrubbed JSON string."""
|
|
326
|
+
if name.startswith(PREFIX):
|
|
327
|
+
name = name[len(PREFIX):]
|
|
328
|
+
args = {k: v for k, v in (args or {}).items() if v is not None}
|
|
329
|
+
try:
|
|
330
|
+
if name not in SPECS_BY_NAME:
|
|
331
|
+
raise ValueError(f"unknown tool: {name}")
|
|
332
|
+
allowed = SPECS_BY_NAME[name]["parameters"]["properties"]
|
|
333
|
+
unknown = [k for k in args if k not in allowed]
|
|
334
|
+
if unknown:
|
|
335
|
+
raise ValueError(f"unexpected argument(s): {', '.join(sorted(unknown))}")
|
|
336
|
+
out = json.dumps(self._dispatch(name, args), default=json_default)
|
|
337
|
+
except Exception as err: # noqa: BLE001 - every error is returned to the model, scrubbed
|
|
338
|
+
msg = f"{type(err).__name__}: {err}" if not isinstance(err, DominusNodeError) else str(err)
|
|
339
|
+
out = json.dumps({"error": msg})
|
|
340
|
+
return _scrub(out, self._api_key)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def make_tool_callables(tools: DominusNodeTools) -> Dict[str, Callable[..., str]]:
|
|
344
|
+
"""``{prefixed_name: fn(**kwargs) -> str}`` for frameworks that take plain functions."""
|
|
345
|
+
def mk(n: str) -> Callable[..., str]:
|
|
346
|
+
def fn(**kwargs: Any) -> str:
|
|
347
|
+
return tools.call(n, kwargs)
|
|
348
|
+
fn.__name__ = PREFIX + n
|
|
349
|
+
fn.__doc__ = SPECS_BY_NAME[n]["description"]
|
|
350
|
+
return fn
|
|
351
|
+
return {PREFIX + n: mk(n) for n in TOOL_NAMES}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dominusnode-openai-functions
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: DominusNode v2 OpenAI-compatible function definitions and handler, built on the dominusnode SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/openai-functions
|
|
7
|
+
Project-URL: Repository, https://github.com/0xcircuitbreaker/DomiNode
|
|
8
|
+
Project-URL: Issues, https://github.com/0xcircuitbreaker/DomiNode/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/0xcircuitbreaker/DomiNode/blob/master/integrations/openai-functions/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: dominusnode<3,>=2.2.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# DominusNode for OpenAI-compatible function calling
|
|
19
|
+
|
|
20
|
+
Gives agents the DominusNode AI-agent relay network: fetch public web pages through exit IPs contributed by
|
|
21
|
+
other agents' nodes, pay per GB from a prepaid crypto wallet, and earn by contributing your own IP with the
|
|
22
|
+
node agent. Built on the official DominusNode SDK v2; HTTP, SSRF/DNS-rebinding protection, OFAC refusal and
|
|
23
|
+
credential scrubbing all come from the SDK.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @dominusnode/openai-functions # TypeScript
|
|
29
|
+
pip install dominusnode-openai-functions # Python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
| Variable | Required | Purpose |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `DOMINUSNODE_API_KEY` | yes | Your `dn_live_` API key. Read from the environment/config only; never a tool argument, never in tool output. |
|
|
37
|
+
| `DOMINUSNODE_BASE_URL` | no | API base URL (default `https://api.dominusnode.com`). |
|
|
38
|
+
| `DOMINUSNODE_PROXY_HOST` | no | Gateway host (default `proxy.dominusnode.com`). |
|
|
39
|
+
| `DOMINUSNODE_PROXY_URL` | no | `https://host[:port]` of the gateway's TLS proxy (default port 8443; self-hosted coordinators use 8443 too). |
|
|
40
|
+
| `DOMINUSNODE_PROXY_INSECURE` | no | `1` = plaintext gateway on 8080 (local development only; production refuses it). |
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
This package has a TypeScript part (`@dominusnode/openai-functions`, on `@dominusnode/sdk` v2) and a Python part
|
|
45
|
+
(`dominusnode-openai-functions`, on the `dominusnode` SDK v2). Both expose the same 19 JSON Schema function
|
|
46
|
+
definitions, kept identical to `functions.json` by tests in each language. The definitions work with any
|
|
47
|
+
OpenAI-compatible tool-calling API.
|
|
48
|
+
|
|
49
|
+
### TypeScript (openai, Chat Completions)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import OpenAI from "openai";
|
|
53
|
+
import { chatCompletionTools, createDominusNodeFunctionHandler } from "@dominusnode/openai-functions";
|
|
54
|
+
|
|
55
|
+
const openai = new OpenAI();
|
|
56
|
+
const handle = createDominusNodeFunctionHandler(); // key from DOMINUSNODE_API_KEY
|
|
57
|
+
const completion = await openai.chat.completions.create({ model: "gpt-4.1", messages, tools: chatCompletionTools });
|
|
58
|
+
for (const call of completion.choices[0].message.tool_calls ?? []) {
|
|
59
|
+
const content = await handle(call.function.name, call.function.arguments); // raw JSON string accepted
|
|
60
|
+
messages.push({ role: "tool", tool_call_id: call.id, content });
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For the Responses API use `responsesTools`; the plain definitions are `functionDefinitions` / `functions.json`.
|
|
65
|
+
|
|
66
|
+
### Python (openai)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from openai import OpenAI
|
|
70
|
+
from dominusnode_openai_functions import chat_completion_tools, create_dominusnode_function_handler
|
|
71
|
+
|
|
72
|
+
handle = create_dominusnode_function_handler() # key from DOMINUSNODE_API_KEY
|
|
73
|
+
resp = OpenAI().chat.completions.create(model="gpt-4.1", messages=messages, tools=chat_completion_tools())
|
|
74
|
+
for call in resp.choices[0].message.tool_calls or []:
|
|
75
|
+
messages.append({"role": "tool", "tool_call_id": call.id,
|
|
76
|
+
"content": handle(call.function.name, call.function.arguments)})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Handlers return JSON strings; errors are `{"error": "..."}` with credentials scrubbed.
|
|
80
|
+
|
|
81
|
+
## Tools (v2 standard set, 19)
|
|
82
|
+
|
|
83
|
+
| # | Tool | What it does |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| 1 | `dominusnode_fetch` | Fetch a public URL through the DominusNode relay network (GET or HEAD only, ports 80/443, no redirects followed, 10 MB cap, private/internal targets refused). |
|
|
86
|
+
| 2 | `dominusnode_proxy_url` | Build a relay gateway proxy URL for use in other HTTP clients. |
|
|
87
|
+
| 3 | `dominusnode_network_policy` | Relay network policy: allowed ports, plaintext (allowhttp) rules, blocked ranges, sanctioned countries and node attestation terms. |
|
|
88
|
+
| 4 | `dominusnode_network_pricing` | Live per-GB prices by exit IP class (residential, dc, mobile) and node tier revenue shares. |
|
|
89
|
+
| 5 | `dominusnode_network_stats` | Relay network statistics: online nodes, exit countries and IP classes. |
|
|
90
|
+
| 6 | `dominusnode_balance` | Prepaid requester wallet balance (cents) used to pay for relayed traffic. |
|
|
91
|
+
| 7 | `dominusnode_transactions` | Requester wallet transaction history (top-ups, usage debits, credit conversions). |
|
|
92
|
+
| 8 | `dominusnode_topup_crypto` | Create a crypto invoice to top up the requester wallet ($10-$1,000). |
|
|
93
|
+
| 9 | `dominusnode_check_payment` | Status of a crypto top-up invoice created by topup_crypto. |
|
|
94
|
+
| 10 | `dominusnode_usage` | Relayed traffic usage summary (bytes and cost) for an optional ISO 8601 date range. |
|
|
95
|
+
| 11 | `dominusnode_active_sessions` | Your currently active relay sessions (id, start time, status). |
|
|
96
|
+
| 12 | `dominusnode_report_session` | Report a burned or bad exit for one of your relay sessions so the network can rotate it. |
|
|
97
|
+
| 13 | `dominusnode_node_list` | Relay nodes you contribute (status, observed IP class, last seen). |
|
|
98
|
+
| 14 | `dominusnode_node_tier` | Your node-operator tier, revenue share and progress toward the next tier (tenure, uptime, active IPs, reputation). |
|
|
99
|
+
| 15 | `dominusnode_node_setup_guide` | Step-by-step instructions to earn by contributing an IP: run the DominusNode node agent, register a node, and how tiers and the self-dealing rule work. |
|
|
100
|
+
| 16 | `dominusnode_credits_balance` | Node-operator earnings: pending and available credit (µUSD). |
|
|
101
|
+
| 17 | `dominusnode_credits_convert` | Explains how to convert node credits into wallet balance. Conversion needs a wallet signature, so it runs through the DominusNode MCP server or SDK with a signer key file, not through this tool. |
|
|
102
|
+
| 18 | `dominusnode_payouts_list` | Your USDC payouts of node earnings with status and transaction hash. |
|
|
103
|
+
| 19 | `dominusnode_x402_info` | x402 micropayment info: facilitators, networks and server-reported prices. |
|
|
104
|
+
|
|
105
|
+
Notes for agents:
|
|
106
|
+
|
|
107
|
+
- Prices are never hardcoded: call `network_pricing` for the live per-GB price by IP class (residential, dc, mobile).
|
|
108
|
+
- `fetch` is GET/HEAD only on ports 80/443, follows no redirects and caps bodies at 10 MB. Plaintext `http://`
|
|
109
|
+
needs `allow_http` and is visible to the exit node.
|
|
110
|
+
- Routing: `country` (ISO code; sanctioned countries refused), `ip_class`, `session_id` (sticky exit).
|
|
111
|
+
- Signing flows (node register, withdraw, payout wallet) are deliberately not tools here; use the node agent CLI,
|
|
112
|
+
the DominusNode MCP server, or the SDK with a signer key file.
|
|
113
|
+
|
|
114
|
+
## Development (JS part)
|
|
115
|
+
|
|
116
|
+
`@dominusnode/sdk` 2.x may not be on npm yet; link the in-repo SDK for local builds and tests:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
(cd ../../sdks/node && npm ci && npm run build)
|
|
120
|
+
npm install && npm run link-sdk
|
|
121
|
+
npx tsc && npx vitest run
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`functions.json` is generated from `core.ts`; after changing a spec rebuild and regenerate it
|
|
125
|
+
(`node -e 'import("./dist/handler.js").then(m=>require("fs").writeFileSync("functions.json", JSON.stringify(m.functionDefinitions,null,2)+"\n"))'`).
|
|
126
|
+
Tests in both languages fail if it is stale.
|
|
127
|
+
|
|
128
|
+
## Testing
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install -e ../../sdks/python -e '.[dev]' # or npm install for the JS part
|
|
132
|
+
python -m pytest -q python_tests
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dominusnode_openai_functions/__init__.py,sha256=dc4FvoiT6Zd2x-2fTiYQl427PKgHDxvWwcz2kniyzlI,2867
|
|
2
|
+
dominusnode_openai_functions/_core.py,sha256=HrGhV0kcDUJIDDDxjdCRUte8ve4RU9GSdWR0n5Z7-FA,20101
|
|
3
|
+
dominusnode_openai_functions-2.2.0.dist-info/licenses/LICENSE,sha256=Mm2J1HyFPxXtVvOQ0qhlZpESwW2Ct-oKRW0XMcISMi8,1069
|
|
4
|
+
dominusnode_openai_functions-2.2.0.dist-info/METADATA,sha256=ve2nvyyMR_ZzmOoE6napjeEZ4WSHvaFioxcReufUppo,7609
|
|
5
|
+
dominusnode_openai_functions-2.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
dominusnode_openai_functions-2.2.0.dist-info/top_level.txt,sha256=OPGc6oG1ovDW6Bzy4xqGObHhixJgilsr1f_tfTrV_L8,29
|
|
7
|
+
dominusnode_openai_functions-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dominus Node
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dominusnode_openai_functions
|