agentprov-capture 0.1.1__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.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentprov-capture
3
+ Version: 0.1.1
4
+ Summary: AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer).
5
+ Author: Nicholas Jones
6
+ License-Expression: MIT
7
+ Requires-Dist: rfc8785>=0.1.4
8
+ Requires-Python: >=3.12
9
+ Project-URL: Homepage, https://w3id.org/agentprov/
10
+ Project-URL: Documentation, https://w3id.org/agentprov/docs/
11
+ Project-URL: Source, https://github.com/nickjones39/AgentPROV
12
+ Description-Content-Type: text/markdown
13
+
14
+ # agentprov-capture
15
+
16
+ The `capture/0.1` raw-evidence event log of [AgentPROV](https://w3id.org/agentprov/): its model, the writer that live
17
+ backends use to record an agent run, the hash-chain verifier and the redaction of credentials. Standard library plus
18
+ `rfc8785` only, so it installs in the isolated backend environments (Python 3.12 and later) as well as in the core.
19
+
20
+ You normally get it as a dependency of [`agentprov`](https://pypi.org/project/agentprov/). The format is specified in
21
+ the AgentPROV repository (`docs/architecture/`, <https://github.com/nickjones39/AgentPROV>), and the user guide is at
22
+ <https://w3id.org/agentprov/docs/>.
23
+
24
+ MIT licence.
@@ -0,0 +1,11 @@
1
+ # agentprov-capture
2
+
3
+ The `capture/0.1` raw-evidence event log of [AgentPROV](https://w3id.org/agentprov/): its model, the writer that live
4
+ backends use to record an agent run, the hash-chain verifier and the redaction of credentials. Standard library plus
5
+ `rfc8785` only, so it installs in the isolated backend environments (Python 3.12 and later) as well as in the core.
6
+
7
+ You normally get it as a dependency of [`agentprov`](https://pypi.org/project/agentprov/). The format is specified in
8
+ the AgentPROV repository (`docs/architecture/`, <https://github.com/nickjones39/AgentPROV>), and the user guide is at
9
+ <https://w3id.org/agentprov/docs/>.
10
+
11
+ MIT licence.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "agentprov-capture"
3
+ version = "0.1.1"
4
+ description = "AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ dependencies = ["rfc8785>=0.1.4"]
9
+
10
+ [[project.authors]]
11
+ name = "Nicholas Jones"
12
+
13
+ [project.urls]
14
+ Homepage = "https://w3id.org/agentprov/"
15
+ Documentation = "https://w3id.org/agentprov/docs/"
16
+ Source = "https://github.com/nickjones39/AgentPROV"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.12.18,<0.13"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "agentprov-capture"
3
+ version = "0.1.1"
4
+ description = "AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12" # live backends run Python 3.12 (AgentDojo 0.1.35 baseline parity)
7
+ license = "MIT"
8
+ authors = [{ name = "Nicholas Jones" }]
9
+ dependencies = ["rfc8785>=0.1.4"]
10
+
11
+ [project.urls]
12
+ Homepage = "https://w3id.org/agentprov/"
13
+ Documentation = "https://w3id.org/agentprov/docs/"
14
+ Source = "https://github.com/nickjones39/AgentPROV"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.12.18,<0.13"]
18
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ """agentprov_capture: the capture/0.1 raw-evidence event log shared by the AgentPROV core (reader and
2
+ verifier) and the isolated live backends (writer). Stdlib + rfc8785 only, Python >= 3.12."""
3
+
4
+ FORMAT = "capture/0.1"
5
+ __version__ = "0.1.1" # equals capture/pyproject.toml (tests/unit/test_versions_source.py)
@@ -0,0 +1,252 @@
1
+ """The endpoint registry: ``endpoints.toml`` as validated data (MN M.1.10 item 2; N.2.1, N.2.1a; G.9.2).
2
+
3
+ Stdlib only (``tomllib``; Python >= 3.12). The registry names environment variables, never values. Rules enforced here:
4
+
5
+ * a base URL is https, except loopback (``127.0.0.1``, ``[::1]``, ``localhost``);
6
+ * a base URL has no userinfo, query or fragment (a key could hide in them; G.6.4);
7
+ * a ``local`` entry is loopback and names its ``server`` kind (D-690). Its ``api_key_env`` is optional: a server set up
8
+ with an API key (oMLX's, D-827) gets it as a bearer header when the key is set, and none otherwise. A non-local entry
9
+ names ``api_key_env`` and has no ``server``;
10
+ * a custom base URL's host must equal the registered host unless the caller passes ``allow_custom=True``
11
+ (the CLI's ``--allow-custom-endpoint``, recorded in the manifest);
12
+ * ``api`` names the wire format the endpoint speaks: ``openai_chat`` (the default; OpenAI-compatible chat completions)
13
+ or ``anthropic_messages`` (the Anthropic Messages API; D-656).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ipaddress
19
+ import re
20
+ import tomllib
21
+ from dataclasses import dataclass
22
+ from importlib import resources
23
+ from typing import Any, Final
24
+ from urllib.parse import urlsplit
25
+
26
+ __all__ = [
27
+ "API_KINDS",
28
+ "LOCAL_SERVERS",
29
+ "LOOPBACK_HOSTS",
30
+ "Endpoint",
31
+ "EndpointError",
32
+ "check_base_url",
33
+ "get_endpoint",
34
+ "is_loopback_host",
35
+ "is_private_lan_host",
36
+ "load_endpoints",
37
+ "parse_registry",
38
+ "validate_base_url",
39
+ ]
40
+
41
+ #: The loopback hosts of G.9.2, as ``urllib.parse`` reports them (brackets removed from the IPv6 literal).
42
+ LOOPBACK_HOSTS: Final = frozenset({"127.0.0.1", "::1", "localhost"})
43
+
44
+ #: The wire formats an endpoint may speak (``api``); the first is the default.
45
+ API_KINDS: Final = ("openai_chat", "anthropic_messages")
46
+ #: The local server kinds of D-690. ``server`` selects the discovery and fingerprint probes of ``agentprov models``.
47
+ LOCAL_SERVERS: Final = frozenset({"ollama", "omlx", "lmstudio", "mlx_lm", "llamacpp", "vllm"})
48
+
49
+ _ENV_NAME: Final = re.compile(r"[A-Z][A-Z0-9_]{0,63}")
50
+ _ENDPOINT_NAME: Final = re.compile(r"[a-z][a-z0-9_-]{0,31}")
51
+ _HEADER_NAME: Final = re.compile(r"[a-z0-9][a-z0-9-]{0,63}")
52
+ _ENTRY_KEYS: Final = frozenset(
53
+ {
54
+ "base_url",
55
+ "base_url_env",
56
+ "api_key_env",
57
+ "model_env",
58
+ "local",
59
+ "server_max_concurrency",
60
+ "request_headers",
61
+ "response_headers",
62
+ "api",
63
+ "server",
64
+ }
65
+ )
66
+
67
+
68
+ class EndpointError(ValueError):
69
+ """A registry or base-URL violation. ``code`` is stable; messages never carry values from the environment."""
70
+
71
+ def __init__(self, code: str, message: str) -> None:
72
+ super().__init__(f"{code}: {message}")
73
+ self.code = code
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class Endpoint:
78
+ """One registry entry. ``*_env`` fields are environment-variable NAMES (or None), never values."""
79
+
80
+ name: str
81
+ base_url: str
82
+ host: str
83
+ base_url_env: str | None
84
+ api_key_env: str | None
85
+ model_env: str | None
86
+ local: bool
87
+ server_max_concurrency: int | None
88
+ #: header NAMES (lower case) whose values wire-capture redaction may record besides the global allowlist
89
+ #: (``redact_allowlist.toml``): for example an API-version header (request) or a request-id header (response).
90
+ #: The hard denylist of G.6.4 wins over them.
91
+ request_headers: tuple[str, ...] = ()
92
+ response_headers: tuple[str, ...] = ()
93
+ #: the wire format (one of :data:`API_KINDS`): which LLM element and recorder the harness uses (D-656)
94
+ api: str = API_KINDS[0]
95
+ #: the local server kind (one of :data:`LOCAL_SERVERS`) for a ``local`` entry; ``None`` for a remote one (D-690)
96
+ server: str | None = None
97
+
98
+
99
+ def is_loopback_host(host: str) -> bool:
100
+ return host.lower() in LOOPBACK_HOSTS
101
+
102
+
103
+ #: The private ranges a LAN endpoint may use (D-698): RFC 1918, the shared address space of RFC 6598 (CGNAT, used by
104
+ #: Tailscale-style overlays), and IPv6 unique local addresses. Link-local, loopback and every public range are not.
105
+ LAN_NETWORKS: Final = tuple(ipaddress.ip_network(n) for n in (
106
+ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fc00::/7"))
107
+
108
+
109
+ def is_private_lan_host(host: str) -> bool:
110
+ """A literal IP address in :data:`LAN_NETWORKS` (never a name: a name can be made to resolve anywhere), without an
111
+ IPv6 zone id (``%en0`` names an interface of this host, not a LAN peer)."""
112
+ if "%" in host:
113
+ return False
114
+ try:
115
+ address = ipaddress.ip_address(host)
116
+ except ValueError:
117
+ return False
118
+ return any(address in net for net in LAN_NETWORKS if net.version == address.version)
119
+
120
+
121
+ def validate_base_url(base_url: str, *, allow_lan: bool = False) -> str:
122
+ """Check the G.9.2 / G.6.4 rules on ``base_url`` and return its host (lower case).
123
+
124
+ http is allowed on a loopback host, and, with ``allow_lan`` (the CLI's ``--allow-lan-endpoint``, D-698), on a
125
+ literal private address. Raises :class:`EndpointError` (``endpoint.url.*``). Nothing is fetched or resolved.
126
+ """
127
+ if not isinstance(base_url, str) or not base_url or any(ord(c) < 0x21 or ord(c) == 0x7F for c in base_url):
128
+ raise EndpointError("endpoint.url.malformed", "base URL is empty or contains whitespace or control characters")
129
+ try:
130
+ parts = urlsplit(base_url)
131
+ host = parts.hostname
132
+ parts.port # noqa: B018 - raises ValueError on a bad port
133
+ except ValueError:
134
+ raise EndpointError("endpoint.url.malformed", "base URL cannot be parsed") from None
135
+ if parts.scheme not in ("http", "https") or not host:
136
+ raise EndpointError("endpoint.url.malformed", "base URL needs an http(s) scheme and a host")
137
+ if parts.username is not None or parts.password is not None or "@" in parts.netloc:
138
+ raise EndpointError("endpoint.url.userinfo", "base URL must not carry userinfo")
139
+ if parts.query or "?" in base_url:
140
+ raise EndpointError("endpoint.url.query", "base URL must not carry a query string")
141
+ if parts.fragment or "#" in base_url:
142
+ raise EndpointError("endpoint.url.fragment", "base URL must not carry a fragment")
143
+ host = host.lower()
144
+ if parts.scheme != "https" and not is_loopback_host(host) and not (allow_lan and is_private_lan_host(host)):
145
+ raise EndpointError("endpoint.url.not_https", "base URL must be https unless the host is loopback")
146
+ return host
147
+
148
+
149
+ def check_base_url(endpoint: Endpoint, base_url: str, *, allow_custom: bool = False) -> bool:
150
+ """Validate a base URL given for ``endpoint`` (e.g. from its ``base_url_env``). Returns True when it is a custom
151
+ endpoint (its host differs from the registered one), which the caller must record in the manifest.
152
+
153
+ The https/loopback/userinfo/query rules always apply. A host mismatch raises ``endpoint.url.host_mismatch``
154
+ unless ``allow_custom`` (the CLI's ``--allow-custom-endpoint``, R-39).
155
+ """
156
+ host = validate_base_url(base_url)
157
+ if host == endpoint.host:
158
+ return False
159
+ if not allow_custom:
160
+ raise EndpointError("endpoint.url.host_mismatch", f"host does not match the registered host of {endpoint.name!r}")
161
+ return True
162
+
163
+
164
+ def _opt_env(entry: dict[str, Any], key: str, name: str) -> str | None:
165
+ value = entry.get(key)
166
+ if value is None:
167
+ return None
168
+ if not isinstance(value, str) or not _ENV_NAME.fullmatch(value):
169
+ raise EndpointError("endpoint.registry.env_name", f"endpoint {name!r}: {key} must be an upper-case variable name")
170
+ return value
171
+
172
+
173
+ def _opt_headers(entry: dict[str, Any], key: str, name: str) -> tuple[str, ...]:
174
+ value = entry.get(key, [])
175
+ if not isinstance(value, list) or not all(isinstance(h, str) and _HEADER_NAME.fullmatch(h) for h in value):
176
+ raise EndpointError("endpoint.registry.header", f"endpoint {name!r}: {key} must be a list of lower-case header names")
177
+ return tuple(sorted(set(value)))
178
+
179
+
180
+ def parse_registry(data: dict[str, Any]) -> dict[str, Endpoint]:
181
+ """Validate a parsed ``endpoints.toml`` document and return the entries by name, sorted."""
182
+ if data.get("version") != 1:
183
+ raise EndpointError("endpoint.registry.version", "unsupported registry version")
184
+ entries = data.get("endpoints")
185
+ if not isinstance(entries, dict) or not entries:
186
+ raise EndpointError("endpoint.registry.empty", "registry has no [endpoints.*] tables")
187
+ out: dict[str, Endpoint] = {}
188
+ for name in sorted(entries):
189
+ entry = entries[name]
190
+ if not _ENDPOINT_NAME.fullmatch(name) or not isinstance(entry, dict):
191
+ raise EndpointError("endpoint.registry.name", "bad endpoint name or entry")
192
+ unknown = set(entry) - _ENTRY_KEYS
193
+ if unknown:
194
+ raise EndpointError("endpoint.registry.key", f"endpoint {name!r}: unknown key {sorted(unknown)[0]!r}")
195
+ base_url = entry.get("base_url")
196
+ if not isinstance(base_url, str):
197
+ raise EndpointError("endpoint.registry.base_url", f"endpoint {name!r}: base_url is required")
198
+ host = validate_base_url(base_url)
199
+ local = entry.get("local", False)
200
+ if not isinstance(local, bool):
201
+ raise EndpointError("endpoint.registry.local", f"endpoint {name!r}: local must be a boolean")
202
+ if local and not is_loopback_host(host):
203
+ raise EndpointError("endpoint.registry.local", f"endpoint {name!r}: a local entry must be loopback")
204
+ api_key_env = _opt_env(entry, "api_key_env", name)
205
+ if not local and api_key_env is None:
206
+ raise EndpointError("endpoint.registry.api_key_env", f"endpoint {name!r}: api_key_env is required")
207
+ server = entry.get("server")
208
+ if local and (not isinstance(server, str) or server not in LOCAL_SERVERS):
209
+ raise EndpointError("endpoint.registry.server", f"endpoint {name!r}: a local entry needs server, one of "
210
+ f"{sorted(LOCAL_SERVERS)}")
211
+ if not local and server is not None:
212
+ raise EndpointError("endpoint.registry.server", f"endpoint {name!r}: server belongs to local entries")
213
+ conc = entry.get("server_max_concurrency")
214
+ if conc is not None and (isinstance(conc, bool) or not isinstance(conc, int) or conc < 1):
215
+ raise EndpointError(
216
+ "endpoint.registry.concurrency", f"endpoint {name!r}: server_max_concurrency must be an integer >= 1"
217
+ )
218
+ api = entry.get("api", API_KINDS[0])
219
+ if api not in API_KINDS:
220
+ raise EndpointError("endpoint.registry.api", f"endpoint {name!r}: api must be one of {list(API_KINDS)}")
221
+ if local and api != API_KINDS[0]: # every local server kind of D-690 speaks the OpenAI chat wire
222
+ raise EndpointError("endpoint.registry.local_api", f"endpoint {name!r}: a local entry speaks {API_KINDS[0]}")
223
+ out[name] = Endpoint(
224
+ name=name,
225
+ base_url=base_url,
226
+ host=host,
227
+ base_url_env=_opt_env(entry, "base_url_env", name),
228
+ api_key_env=api_key_env,
229
+ model_env=_opt_env(entry, "model_env", name),
230
+ local=local,
231
+ server_max_concurrency=conc,
232
+ request_headers=_opt_headers(entry, "request_headers", name),
233
+ response_headers=_opt_headers(entry, "response_headers", name),
234
+ api=api,
235
+ server=server,
236
+ )
237
+ return out
238
+
239
+
240
+ def load_endpoints() -> dict[str, Endpoint]:
241
+ """Load and validate the packaged ``endpoints.toml`` (read through ``importlib.resources``)."""
242
+ raw = resources.files("agentprov_capture").joinpath("endpoints.toml").read_bytes()
243
+ return parse_registry(tomllib.loads(raw.decode("utf-8")))
244
+
245
+
246
+ def get_endpoint(name: str) -> Endpoint:
247
+ """The registry entry ``name`` (``EndpointError("endpoint.unknown")`` if absent)."""
248
+ registry = load_endpoints()
249
+ try:
250
+ return registry[name]
251
+ except KeyError:
252
+ raise EndpointError("endpoint.unknown", f"unknown endpoint shorthand; known: {sorted(registry)}") from None
@@ -0,0 +1,156 @@
1
+ # AgentPROV endpoint registry (MN M.1.10 item 2; N.2.1 `--model`; N.2.1a; G.9.2).
2
+ # Data, not code: read through importlib.resources by agentprov_capture.endpoints (no __init__.py needed here).
3
+ #
4
+ # Names only: `base_url_env`, `api_key_env` and `model_env` name environment variables (the .env keys); no value is
5
+ # ever stored here. There are no default model ids (Q5, closed): a model id comes from `--model-id` or `model_env`.
6
+ # `base_url` is the public default. Its host is the registered host that any other base URL must match unless
7
+ # `--allow-custom-endpoint` is given (R-39). A base URL MUST be https, except loopback (127.0.0.1, [::1], localhost),
8
+ # and MUST carry no userinfo, query or fragment (G.6.4).
9
+ #
10
+ # `request_headers` / `response_headers` (optional, lower-case header NAMES) extend the global header allowlist of
11
+ # redact_allowlist.toml for that endpoint (G.6.4: "per-endpoint names declared in endpoints.toml"); the hard denylist
12
+ # wins over them. Only request-id style headers are declared; add a name here, with a test, when a provider needs it.
13
+ #
14
+ # `api` (optional) is the wire format: `openai_chat` (default) or `anthropic_messages` (D-656). It selects the LLM
15
+ # element and the wire recorder's decoding; the CLI sets the agent's provider from it.
16
+ #
17
+ # Baseline set from AgentDojo-PROV `runtime.py` (build_* functions and _LLM_BUILDERS); `omlx` is new (D-10); `lmstudio`,
18
+ # `mlx_lm`, `llamacpp` and `vllm` are new (D-690).
19
+
20
+ version = 1
21
+
22
+ [endpoints.deepseek]
23
+ base_url = "https://api.deepseek.com/v1"
24
+ base_url_env = "DEEPSEEK_BASE_URL"
25
+ api_key_env = "DEEPSEEK_API_KEY"
26
+ model_env = "DEEPSEEK_MODEL"
27
+ local = false
28
+
29
+ [endpoints.openai]
30
+ base_url = "https://api.openai.com/v1"
31
+ base_url_env = "OPENAI_BASE_URL"
32
+ api_key_env = "OPENAI_API_KEY"
33
+ model_env = "OPENAI_MODEL"
34
+ local = false
35
+ response_headers = ["x-request-id", "openai-version"]
36
+
37
+ # One OpenRouter key and base URL serve openrouter, gemini, llama, qwen and glm (baseline `_build_openrouter_llm`).
38
+ [endpoints.openrouter]
39
+ base_url = "https://openrouter.ai/api/v1"
40
+ base_url_env = "OPENROUTER_BASE_URL"
41
+ api_key_env = "OPENROUTER_API_KEY"
42
+ model_env = "OPENROUTER_MODEL"
43
+ local = false
44
+ response_headers = ["x-request-id"]
45
+
46
+ [endpoints.gemini]
47
+ base_url = "https://openrouter.ai/api/v1"
48
+ base_url_env = "OPENROUTER_BASE_URL"
49
+ api_key_env = "OPENROUTER_API_KEY"
50
+ model_env = "GEMINI_MODEL"
51
+ local = false
52
+ response_headers = ["x-request-id"]
53
+
54
+ [endpoints.llama]
55
+ base_url = "https://openrouter.ai/api/v1"
56
+ base_url_env = "OPENROUTER_BASE_URL"
57
+ api_key_env = "OPENROUTER_API_KEY"
58
+ model_env = "LLAMA_MODEL"
59
+ local = false
60
+ response_headers = ["x-request-id"]
61
+
62
+ [endpoints.qwen]
63
+ base_url = "https://openrouter.ai/api/v1"
64
+ base_url_env = "OPENROUTER_BASE_URL"
65
+ api_key_env = "OPENROUTER_API_KEY"
66
+ model_env = "QWEN_MODEL"
67
+ local = false
68
+ response_headers = ["x-request-id"]
69
+
70
+ [endpoints.glm]
71
+ base_url = "https://openrouter.ai/api/v1"
72
+ base_url_env = "OPENROUTER_BASE_URL"
73
+ api_key_env = "OPENROUTER_API_KEY"
74
+ model_env = "GLM_MODEL"
75
+ local = false
76
+ response_headers = ["x-request-id"]
77
+
78
+ # The Anthropic Messages API, direct (D-656). The SDK appends /v1/messages to the base URL. Model ids are the bare API
79
+ # ids (claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5, ...), never date-suffixed. Recorded header values: the
80
+ # API version and beta flags sent, and the request id and the two retry headers the SDK's retry decision reads. The
81
+ # organisation id and the rate-limit headers keep their names only; x-api-key is on the hard denylist.
82
+ [endpoints.anthropic]
83
+ base_url = "https://api.anthropic.com"
84
+ base_url_env = "ANTHROPIC_BASE_URL"
85
+ api_key_env = "ANTHROPIC_API_KEY"
86
+ model_env = "ANTHROPIC_MODEL"
87
+ local = false
88
+ api = "anthropic_messages"
89
+ request_headers = ["anthropic-beta", "anthropic-version"]
90
+ response_headers = ["request-id", "retry-after", "x-should-retry"]
91
+
92
+ # Local OpenAI-compatible servers (N.2.1a; D-690): http on loopback is allowed and no key is needed (an optional
93
+ # `api_key_env` sends one when it is set, D-827). `server` names the server kind; `agentprov models` uses it to
94
+ # fingerprint the server that answers (two kinds share a default port: 8000 omlx/vllm, 8080 mlx_lm/llamacpp) and to
95
+ # read the model identity it exposes. Base URLs are pinned to 127.0.0.1, not `localhost`, which may resolve to ::1.
96
+ # Tool calls: whether a model's replies carry structured `tool_calls` depends on the server, its flags and the model's
97
+ # template; the preflight probe decides (D.10.9 A4), and text-encoded calls need
98
+ # `--allow-text-tool-calls`. `server_max_concurrency` is set only where the server's default is fixed; LM Studio and
99
+ # llama.cpp report their own value, and `--server-max-concurrency` overrides.
100
+
101
+ # Ollama. Structured tool calls for models whose capabilities include "tools"; others are refused by the server (400).
102
+ # OLLAMA_NUM_PARALLEL depends on the version and memory, and is not reported, so no concurrency default is set.
103
+ [endpoints.ollama]
104
+ base_url = "http://127.0.0.1:11434/v1"
105
+ base_url_env = "LOCAL_LLM_BASE_URL"
106
+ model_env = "LOCAL_MODEL_ID"
107
+ local = true
108
+ server = "ollama"
109
+
110
+ # oMLX (MLX inference server, Apple Silicon). The model id is the oMLX model directory name or alias (`--model-id`).
111
+ # `server_max_concurrency` is oMLX's `--max-concurrent-requests` default. Structured tool calls through oMLX's parser.
112
+ # `api_key_env` is optional on a local entry (D-827): oMLX can be set up with an API key. When `OMLX_API_KEY` is set
113
+ # (keychain first, then .env), it is sent as a bearer header to the loopback server, and never to a LAN address; when
114
+ # it is not set, no key is sent, as for every other local server. `model_env` (D-827 addendum): `OMLX_MODEL` in .env
115
+ # names the served model id (oMLX's own id, e.g. its folder name, not a Hugging Face repo id) when `--model-id` is not
116
+ # given; without either, a terminal offers a pick.
117
+ [endpoints.omlx]
118
+ base_url = "http://127.0.0.1:8000/v1"
119
+ base_url_env = "OMLX_BASE_URL"
120
+ api_key_env = "OMLX_API_KEY"
121
+ model_env = "OMLX_MODEL"
122
+ local = true
123
+ server = "omlx"
124
+ server_max_concurrency = 8
125
+
126
+ # LM Studio's server (`lms server start`). Native tool calls for models with `trained_for_tool_use`; for other models LM
127
+ # Studio adds its own tool-use system prompt on the server (invisible to the wire capture), which `run` refuses unless
128
+ # `--allow-text-tool-calls`. The parallel-request limit is read from the loaded instance (`/api/v1/models`).
129
+ [endpoints.lmstudio]
130
+ base_url = "http://127.0.0.1:1234/v1"
131
+ base_url_env = "LMSTUDIO_BASE_URL"
132
+ local = true
133
+ server = "lmstudio"
134
+
135
+ # mlx_lm.server (MLX LM). The model id is a Hugging Face repo id or a local path. Requests are served one at a time by
136
+ # default. Tool calls depend on the mlx-lm version and the model's chat template; text-encoded calls are common.
137
+ [endpoints.mlx_lm]
138
+ base_url = "http://127.0.0.1:8080/v1"
139
+ base_url_env = "MLX_LM_BASE_URL"
140
+ local = true
141
+ server = "mlx_lm"
142
+ server_max_concurrency = 1
143
+
144
+ # llama.cpp `llama-server`. Structured tool calls only when started with `--jinja`; the slot count is read from /props.
145
+ [endpoints.llamacpp]
146
+ base_url = "http://127.0.0.1:8080/v1"
147
+ base_url_env = "LLAMACPP_BASE_URL"
148
+ local = true
149
+ server = "llamacpp"
150
+
151
+ # vLLM's OpenAI-compatible server. Structured tool calls only with `--enable-auto-tool-choice --tool-call-parser <p>`.
152
+ [endpoints.vllm]
153
+ base_url = "http://127.0.0.1:8000/v1"
154
+ base_url_env = "VLLM_BASE_URL"
155
+ local = true
156
+ server = "vllm"