macore-client-dev 0.1.52__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.
- macore_client_dev-0.1.52/.gitignore +49 -0
- macore_client_dev-0.1.52/PKG-INFO +6 -0
- macore_client_dev-0.1.52/macore_client/__about__.py +3 -0
- macore_client_dev-0.1.52/macore_client/__init__.py +9 -0
- macore_client_dev-0.1.52/macore_client/cli.py +77 -0
- macore_client_dev-0.1.52/macore_client/client.py +201 -0
- macore_client_dev-0.1.52/macore_client/live_check.py +226 -0
- macore_client_dev-0.1.52/pyproject.toml +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.hypothesis/
|
|
9
|
+
htmlcov/
|
|
10
|
+
.tox/
|
|
11
|
+
.nox/
|
|
12
|
+
.coverage
|
|
13
|
+
.coverage.*
|
|
14
|
+
|
|
15
|
+
# Virtualenv
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
|
|
19
|
+
# Logs
|
|
20
|
+
logs/
|
|
21
|
+
*.log
|
|
22
|
+
|
|
23
|
+
# macmgr state
|
|
24
|
+
.mac/
|
|
25
|
+
|
|
26
|
+
# Secrets / env
|
|
27
|
+
.env
|
|
28
|
+
.env.*
|
|
29
|
+
!.env.example
|
|
30
|
+
|
|
31
|
+
# Build
|
|
32
|
+
dist/
|
|
33
|
+
build/
|
|
34
|
+
docker/dist/
|
|
35
|
+
*.egg-info/
|
|
36
|
+
|
|
37
|
+
# IDE
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/
|
|
40
|
+
*.swp
|
|
41
|
+
|
|
42
|
+
# OS
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
45
|
+
.cst/
|
|
46
|
+
.tree/
|
|
47
|
+
projectid
|
|
48
|
+
|
|
49
|
+
docker/dist/*.deb
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# client/macore_client/__init__.py
|
|
2
|
+
# Author: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
3
|
+
# Project: model_access_core (71b1e006-b777-4edb-854e-f0a47120f793)
|
|
4
|
+
"""Client package for the Model Access Core (MAC) server."""
|
|
5
|
+
|
|
6
|
+
from macore_client.client import MacClient
|
|
7
|
+
from macore_client.__about__ import __version__
|
|
8
|
+
|
|
9
|
+
__all__ = ["MacClient", "__version__"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# client/macore_client/cli.py
|
|
2
|
+
# Author: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
3
|
+
# Project: model_access_core (71b1e006-b777-4edb-854e-f0a47120f793)
|
|
4
|
+
"""``macore-client`` console script: one subcommand per MAC command."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from macore_client.client import COMMANDS, MacClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_base_url() -> str:
|
|
19
|
+
"""Resolve the server base URL from the environment."""
|
|
20
|
+
return os.environ.get("MAC_VERIFY_BASE_URL", "http://127.0.0.1:8010")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
"""Build the CLI parser with one subcommand per client command method."""
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="macore-client",
|
|
27
|
+
description="Call one Model Access Core server command over JSON-RPC.",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--base-url",
|
|
31
|
+
default=None,
|
|
32
|
+
help="Adapter base URL (default: env MAC_VERIFY_BASE_URL or http://127.0.0.1:8010)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("--timeout", type=float, default=None, help="HTTP timeout seconds")
|
|
35
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
36
|
+
for command in COMMANDS:
|
|
37
|
+
sub = subparsers.add_parser(command.replace("_", "-"), help=f"Run {command}")
|
|
38
|
+
sub.add_argument(
|
|
39
|
+
"--params",
|
|
40
|
+
default="{}",
|
|
41
|
+
help="Command parameters as a JSON object string",
|
|
42
|
+
)
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _run(base_url: str, timeout: float | None, command: str, params: dict[str, Any]) -> Any:
|
|
47
|
+
client = MacClient(base_url, timeout=timeout)
|
|
48
|
+
try:
|
|
49
|
+
return await client.call(command, params)
|
|
50
|
+
finally:
|
|
51
|
+
await client.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> int:
|
|
55
|
+
"""Entry point of the ``macore-client`` command."""
|
|
56
|
+
args = build_parser().parse_args(argv)
|
|
57
|
+
command = str(args.command).replace("-", "_")
|
|
58
|
+
try:
|
|
59
|
+
params = json.loads(args.params)
|
|
60
|
+
except json.JSONDecodeError as exc:
|
|
61
|
+
print(f"macore-client: --params is not valid JSON: {exc}", file=sys.stderr)
|
|
62
|
+
return 2
|
|
63
|
+
if not isinstance(params, dict):
|
|
64
|
+
print("macore-client: --params must be a JSON object", file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
base_url = args.base_url or default_base_url()
|
|
67
|
+
try:
|
|
68
|
+
result = asyncio.run(_run(base_url, args.timeout, command, params))
|
|
69
|
+
except Exception as exc: # surface transport/command failure as text, not traceback
|
|
70
|
+
print(f"macore-client: {command} failed: {exc}", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# client/macore_client/client.py
|
|
2
|
+
# Author: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
3
|
+
# Project: model_access_core (71b1e006-b777-4edb-854e-f0a47120f793)
|
|
4
|
+
"""The MAC client: one method per public MAC command.
|
|
5
|
+
|
|
6
|
+
Wraps the ``mcp_proxy_adapter`` framework JSON-RPC client. No method builds a
|
|
7
|
+
URL, a JSON-RPC envelope or a raw transport payload; transport, protocol
|
|
8
|
+
selection, serialization and queued-job handling all belong to the framework
|
|
9
|
+
client. Every method mirrors a command the MAC server registers, with the same
|
|
10
|
+
name, so client and server read against each other without a translation table.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Any
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _import_jsonrpc_client() -> Any:
|
|
22
|
+
"""Import the framework client with its import-time banner kept off stdout."""
|
|
23
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
24
|
+
from mcp_proxy_adapter.client.jsonrpc_client.client import JsonRpcClient
|
|
25
|
+
return JsonRpcClient
|
|
26
|
+
|
|
27
|
+
# Public MAC commands, one client method each. model_selftest is queued on the
|
|
28
|
+
# server; the rest answer inline.
|
|
29
|
+
COMMANDS: tuple[str, ...] = (
|
|
30
|
+
"info",
|
|
31
|
+
"list_vectorization_models",
|
|
32
|
+
"count_vectorization_tokens",
|
|
33
|
+
"vectorize",
|
|
34
|
+
"openai_models",
|
|
35
|
+
"health",
|
|
36
|
+
"model_health",
|
|
37
|
+
"model_providers",
|
|
38
|
+
"model_provider_status",
|
|
39
|
+
"model_vram_status",
|
|
40
|
+
"model_queue_status",
|
|
41
|
+
"model_session_create",
|
|
42
|
+
"model_session_get",
|
|
43
|
+
"model_session_delete",
|
|
44
|
+
"model_chat",
|
|
45
|
+
"model_count_tokens",
|
|
46
|
+
"model_estimate_request_capacity",
|
|
47
|
+
"model_request_status",
|
|
48
|
+
"model_request_cancel",
|
|
49
|
+
"model_cache_create",
|
|
50
|
+
"model_cache_set_active",
|
|
51
|
+
"model_cache_list",
|
|
52
|
+
"model_cache_delete",
|
|
53
|
+
"model_selftest",
|
|
54
|
+
"hardware_calibration",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_QUEUED_COMMANDS: frozenset[str] = frozenset({"model_selftest"})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def command_methods() -> tuple[str, ...]:
|
|
61
|
+
"""Return the client's public command-method names (for surface checks)."""
|
|
62
|
+
return COMMANDS
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _unwrap(result: Any) -> Any:
|
|
66
|
+
"""Unwrap the unified-execution envelope down to the command result."""
|
|
67
|
+
seen = 0
|
|
68
|
+
while isinstance(result, dict) and seen < 8:
|
|
69
|
+
seen += 1
|
|
70
|
+
if result.get("mode") in {"immediate", "queued"} and "result" in result:
|
|
71
|
+
result = result["result"]
|
|
72
|
+
continue
|
|
73
|
+
if "result" in result and set(result.keys()) <= {"result", "job_id", "status", "queued", "command", "raw_status", "mode"}:
|
|
74
|
+
result = result["result"]
|
|
75
|
+
continue
|
|
76
|
+
break
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MacClient:
|
|
81
|
+
"""Full-surface client for one Model Access Core server.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
transport: The framework JSON-RPC client carrying every request.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
base_url: str = "http://127.0.0.1:8010",
|
|
90
|
+
*,
|
|
91
|
+
token: str | None = None,
|
|
92
|
+
token_header: str | None = None,
|
|
93
|
+
cert: str | None = None,
|
|
94
|
+
key: str | None = None,
|
|
95
|
+
ca: str | None = None,
|
|
96
|
+
check_hostname: bool = False,
|
|
97
|
+
timeout: float | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
parsed = urlparse(base_url)
|
|
100
|
+
protocol = parsed.scheme or "http"
|
|
101
|
+
host = parsed.hostname or "127.0.0.1"
|
|
102
|
+
port = parsed.port or (443 if protocol == "https" else 80)
|
|
103
|
+
json_rpc_client = _import_jsonrpc_client()
|
|
104
|
+
self.transport = json_rpc_client(
|
|
105
|
+
protocol=protocol,
|
|
106
|
+
host=host,
|
|
107
|
+
port=port,
|
|
108
|
+
token=token,
|
|
109
|
+
token_header=token_header,
|
|
110
|
+
cert=cert,
|
|
111
|
+
key=key,
|
|
112
|
+
ca=ca,
|
|
113
|
+
check_hostname=check_hostname,
|
|
114
|
+
timeout=timeout,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def call(self, command: str, params: dict[str, Any] | None = None) -> Any:
|
|
118
|
+
"""Execute one MAC command and return its unwrapped result."""
|
|
119
|
+
if command in _QUEUED_COMMANDS:
|
|
120
|
+
raw = await self.transport.execute_command_unified(
|
|
121
|
+
command,
|
|
122
|
+
params or {},
|
|
123
|
+
auto_poll=True,
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
raw = await self.transport.execute_command_unified(
|
|
127
|
+
command,
|
|
128
|
+
params or {},
|
|
129
|
+
expect_queue=False,
|
|
130
|
+
)
|
|
131
|
+
return _unwrap(raw)
|
|
132
|
+
|
|
133
|
+
async def close(self) -> None:
|
|
134
|
+
"""Close the underlying transport."""
|
|
135
|
+
close = getattr(self.transport, "close", None)
|
|
136
|
+
if close is not None:
|
|
137
|
+
await close()
|
|
138
|
+
|
|
139
|
+
async def info(self, section: str, *, page: int = 1, page_size: int = 1000) -> Any:
|
|
140
|
+
"""Return one bounded page of embedded MAC reference documentation."""
|
|
141
|
+
return await self.call("info", {"section": section, "page": page, "page_size": page_size})
|
|
142
|
+
|
|
143
|
+
async def list_vectorization_models(self) -> Any:
|
|
144
|
+
"""Return the ordered vectorization model inventory."""
|
|
145
|
+
return await self.call("list_vectorization_models")
|
|
146
|
+
|
|
147
|
+
async def openai_models(self) -> Any:
|
|
148
|
+
"""Return the raw OpenAI model list with the added capability marker (no parameters)."""
|
|
149
|
+
return await self.call("openai_models")
|
|
150
|
+
|
|
151
|
+
async def vectorize(
|
|
152
|
+
self,
|
|
153
|
+
text: str | list[str],
|
|
154
|
+
provider_id: str,
|
|
155
|
+
model_id: str,
|
|
156
|
+
dimension: int | None = None,
|
|
157
|
+
) -> Any:
|
|
158
|
+
"""Generate vectors through the selected provider and model."""
|
|
159
|
+
params: dict[str, Any] = {
|
|
160
|
+
"text": text,
|
|
161
|
+
"provider_id": provider_id,
|
|
162
|
+
"model_id": model_id,
|
|
163
|
+
}
|
|
164
|
+
if dimension is not None:
|
|
165
|
+
params["dimension"] = dimension
|
|
166
|
+
return await self.call("vectorize", params)
|
|
167
|
+
|
|
168
|
+
async def count_vectorization_tokens(
|
|
169
|
+
self,
|
|
170
|
+
text: str | list[str],
|
|
171
|
+
provider_id: str,
|
|
172
|
+
model_id: str,
|
|
173
|
+
) -> Any:
|
|
174
|
+
"""Count vectorization input tokens without generating embeddings."""
|
|
175
|
+
return await self.call("count_vectorization_tokens", {
|
|
176
|
+
"text": text,
|
|
177
|
+
"provider_id": provider_id,
|
|
178
|
+
"model_id": model_id,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _make_method(command: str) -> Any:
|
|
183
|
+
async def method(self: MacClient, params: dict[str, Any] | None = None) -> Any:
|
|
184
|
+
return await self.call(command, params)
|
|
185
|
+
|
|
186
|
+
method.__name__ = command
|
|
187
|
+
method.__qualname__ = f"MacClient.{command}"
|
|
188
|
+
method.__doc__ = f"Execute the MAC ``{command}`` command."
|
|
189
|
+
return method
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
for _command in COMMANDS:
|
|
193
|
+
if _command in {
|
|
194
|
+
"info",
|
|
195
|
+
"list_vectorization_models",
|
|
196
|
+
"vectorize",
|
|
197
|
+
"count_vectorization_tokens",
|
|
198
|
+
"openai_models",
|
|
199
|
+
}:
|
|
200
|
+
continue
|
|
201
|
+
setattr(MacClient, _command, _make_method(_command))
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# client/macore_client/live_check.py
|
|
2
|
+
# Author: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
3
|
+
# Project: model_access_core (71b1e006-b777-4edb-854e-f0a47120f793)
|
|
4
|
+
"""Client-driven live acceptance helpers for a deployed MAC server."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from macore_client.client import MacClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _base_url() -> str:
|
|
16
|
+
return os.environ.get("MAC_VERIFY_BASE_URL", "http://127.0.0.1:8010")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _timeout() -> float:
|
|
20
|
+
raw = os.environ.get("MAC_VERIFY_TIMEOUT", "300")
|
|
21
|
+
try:
|
|
22
|
+
return float(raw)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return 300.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _with_client(coro: Any) -> int:
|
|
28
|
+
client = MacClient(
|
|
29
|
+
_base_url(),
|
|
30
|
+
ca=os.environ.get("MAC_VERIFY_CA"),
|
|
31
|
+
check_hostname=True,
|
|
32
|
+
timeout=_timeout(),
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
return await coro(client)
|
|
36
|
+
finally:
|
|
37
|
+
await client.close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def select_catalog_chat_model(catalog: Any, provider_id: str) -> dict[str, int | str] | None:
|
|
41
|
+
"""Return one catalog-backed chat identity for ``provider_id``.
|
|
42
|
+
|
|
43
|
+
The returned mapping is deliberately small and stable for pipeline callers:
|
|
44
|
+
``provider_id``, ``model_id``, ``context_window``, and
|
|
45
|
+
``max_output_tokens``. It contains only positive integer limits and the
|
|
46
|
+
exact model ID advertised by MAC; it never translates provider aliases.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
data = catalog.get("data") if isinstance(catalog, dict) else None
|
|
50
|
+
providers = data if isinstance(data, list) else None
|
|
51
|
+
if providers is None and isinstance(data, dict):
|
|
52
|
+
candidate = data.get("providers")
|
|
53
|
+
providers = candidate if isinstance(candidate, list) else None
|
|
54
|
+
if not isinstance(providers, list):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
provider = next(
|
|
58
|
+
(row for row in providers if isinstance(row, dict) and row.get("provider_id") == provider_id),
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
summaries = provider.get("capability_summary") if isinstance(provider, dict) else None
|
|
62
|
+
if not isinstance(summaries, list):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
selections: list[dict[str, int | str]] = []
|
|
66
|
+
for summary in summaries:
|
|
67
|
+
if not isinstance(summary, dict):
|
|
68
|
+
continue
|
|
69
|
+
model_id = summary.get("model_id")
|
|
70
|
+
context_window = summary.get("context_window_tokens")
|
|
71
|
+
max_output_tokens = summary.get("max_output_tokens")
|
|
72
|
+
if (
|
|
73
|
+
not isinstance(model_id, str)
|
|
74
|
+
or not model_id
|
|
75
|
+
or not isinstance(context_window, int)
|
|
76
|
+
or isinstance(context_window, bool)
|
|
77
|
+
or context_window <= 0
|
|
78
|
+
or not isinstance(max_output_tokens, int)
|
|
79
|
+
or isinstance(max_output_tokens, bool)
|
|
80
|
+
or max_output_tokens <= 0
|
|
81
|
+
):
|
|
82
|
+
continue
|
|
83
|
+
selections.append(
|
|
84
|
+
{
|
|
85
|
+
"provider_id": provider_id,
|
|
86
|
+
"model_id": model_id,
|
|
87
|
+
"context_window": context_window,
|
|
88
|
+
"max_output_tokens": max_output_tokens,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return min(selections, key=lambda selection: str(selection["model_id"])) if selections else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _run_catalog_chat_live(client: MacClient, provider_id: str) -> int:
|
|
96
|
+
catalog = await client.model_providers()
|
|
97
|
+
selection = select_catalog_chat_model(catalog, provider_id)
|
|
98
|
+
if selection is None:
|
|
99
|
+
print(f"FAIL {provider_id}-live: no chat model with positive limits in catalog")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
result = await client.model_chat(
|
|
103
|
+
{
|
|
104
|
+
"provider_id": selection["provider_id"],
|
|
105
|
+
"provider_key_id": "noauth:default",
|
|
106
|
+
"model_id": selection["model_id"],
|
|
107
|
+
"messages": [
|
|
108
|
+
{"role": "user", "content": [{"type": "text", "text": "Reply OK."}]}
|
|
109
|
+
],
|
|
110
|
+
"reserved_output_tokens": min(32, int(selection["max_output_tokens"])),
|
|
111
|
+
"stream": False,
|
|
112
|
+
"generation_options": {},
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
data = result.get("data") if isinstance(result, dict) else None
|
|
116
|
+
message = data.get("message") if isinstance(data, dict) else None
|
|
117
|
+
blocks = message.get("content") if isinstance(message, dict) else None
|
|
118
|
+
text_blocks = [
|
|
119
|
+
block.get("text")
|
|
120
|
+
for block in blocks
|
|
121
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
122
|
+
] if isinstance(blocks, list) else []
|
|
123
|
+
usage = data.get("usage") if isinstance(data, dict) else None
|
|
124
|
+
total_tokens = usage.get("total_tokens") if isinstance(usage, dict) else None
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(result, dict)
|
|
127
|
+
or result.get("success") is not True
|
|
128
|
+
or not isinstance(data, dict)
|
|
129
|
+
or data.get("finish_reason") not in {"stop", "length"}
|
|
130
|
+
or not any(isinstance(text, str) and text.strip() for text in text_blocks)
|
|
131
|
+
or not isinstance(total_tokens, int)
|
|
132
|
+
or total_tokens <= 0
|
|
133
|
+
):
|
|
134
|
+
print(f"FAIL {provider_id}-live: {selection}: {str(result)[:240]}")
|
|
135
|
+
return 1
|
|
136
|
+
print(f"PASS {provider_id}-live: {selection}: tokens={total_tokens}")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def run_health_live() -> int:
|
|
141
|
+
"""The deployed server answers health and model_health."""
|
|
142
|
+
|
|
143
|
+
async def check(client: MacClient) -> int:
|
|
144
|
+
health = await client.call("health")
|
|
145
|
+
model_health = await client.call("model_health")
|
|
146
|
+
data = model_health.get("data") if isinstance(model_health, dict) else None
|
|
147
|
+
adapter = (data or {}).get("adapter") if isinstance(data, dict) else None
|
|
148
|
+
registry = (data or {}).get("registry") if isinstance(data, dict) else None
|
|
149
|
+
providers = (registry or {}).get("providers") if isinstance(registry, dict) else None
|
|
150
|
+
if not isinstance(health, dict):
|
|
151
|
+
print("FAIL health-live: health returned no object")
|
|
152
|
+
return 1
|
|
153
|
+
if adapter != "ok" or not isinstance(providers, int) or providers < 1:
|
|
154
|
+
print(f"FAIL health-live: adapter={adapter!r} providers={providers!r}")
|
|
155
|
+
return 1
|
|
156
|
+
print(f"PASS health-live: adapter=ok providers={providers}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
return asyncio.run(_with_client(check))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def run_providers_live() -> int:
|
|
163
|
+
"""The deployed server lists at least one provider with models."""
|
|
164
|
+
|
|
165
|
+
async def check(client: MacClient) -> int:
|
|
166
|
+
result = await client.call("model_providers")
|
|
167
|
+
data = result.get("data") if isinstance(result, dict) else None
|
|
168
|
+
providers = data if isinstance(data, list) else None
|
|
169
|
+
if providers is None and isinstance(data, dict):
|
|
170
|
+
providers = data.get("providers") if isinstance(data.get("providers"), list) else None
|
|
171
|
+
if not isinstance(providers, list) or not providers:
|
|
172
|
+
print(f"FAIL providers-live: no providers in {str(result)[:200]}")
|
|
173
|
+
return 1
|
|
174
|
+
names = [p.get("provider_id") for p in providers if isinstance(p, dict)]
|
|
175
|
+
print(f"PASS providers-live: {len(providers)} providers: {', '.join(map(str, names))}")
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
return asyncio.run(_with_client(check))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_openrouter_qwen_live() -> int:
|
|
182
|
+
"""One current OpenRouter chat model answers using its catalog identity."""
|
|
183
|
+
|
|
184
|
+
async def check(client: MacClient) -> int:
|
|
185
|
+
return await _run_catalog_chat_live(client, "openrouter-qwen")
|
|
186
|
+
|
|
187
|
+
return asyncio.run(_with_client(check))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def run_lmrs_live() -> int:
|
|
191
|
+
"""One current LMRS chat model answers using its catalog identity."""
|
|
192
|
+
|
|
193
|
+
async def check(client: MacClient) -> int:
|
|
194
|
+
return await _run_catalog_chat_live(client, "lmrs")
|
|
195
|
+
|
|
196
|
+
return asyncio.run(_with_client(check))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run_selftest_live() -> int:
|
|
200
|
+
"""Drive the server's own full live regression (model_selftest)."""
|
|
201
|
+
|
|
202
|
+
async def check(client: MacClient) -> int:
|
|
203
|
+
result = await client.call("model_selftest", {})
|
|
204
|
+
node: Any = result
|
|
205
|
+
for _ in range(6):
|
|
206
|
+
if isinstance(node, dict) and isinstance(node.get("data"), dict):
|
|
207
|
+
node = node["data"]
|
|
208
|
+
continue
|
|
209
|
+
break
|
|
210
|
+
if not isinstance(node, dict) or "exit_code" not in node:
|
|
211
|
+
print(f"FAIL selftest-live: unrecognized selftest result: {str(result)[:300]}")
|
|
212
|
+
return 1
|
|
213
|
+
ok = bool(node.get("ok"))
|
|
214
|
+
exit_code = int(node.get("exit_code") or 0)
|
|
215
|
+
passed = node.get("passed")
|
|
216
|
+
failed = node.get("failed")
|
|
217
|
+
n_pass = len(passed) if isinstance(passed, list) else 0
|
|
218
|
+
n_fail = len(failed) if isinstance(failed, list) else 0
|
|
219
|
+
line = f"selftest-live: ok={ok} exit_code={exit_code} passed={n_pass} failed={n_fail}"
|
|
220
|
+
if not ok or exit_code != 0 or n_pass == 0:
|
|
221
|
+
print(f"FAIL {line}")
|
|
222
|
+
return 1
|
|
223
|
+
print(f"PASS {line}")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
return asyncio.run(_with_client(check))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "macore-client-dev"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Client for the Model Access Core (MAC) server"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp_proxy_adapter>=8.11.11,<9",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
macore-client = "macore_client.cli:main"
|
|
16
|
+
|
|
17
|
+
[tool.hatch.version]
|
|
18
|
+
path = "macore_client/__about__.py"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["macore_client"]
|