cometapi-cli 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cometapi_cli/__init__.py +1 -1
- cometapi_cli/app.py +4 -0
- cometapi_cli/catalog.py +141 -0
- cometapi_cli/commands/account.py +4 -1
- cometapi_cli/commands/chat.py +12 -3
- cometapi_cli/commands/config_cmd.py +14 -3
- cometapi_cli/commands/doctor.py +4 -1
- cometapi_cli/commands/logs.py +4 -1
- cometapi_cli/commands/model.py +70 -0
- cometapi_cli/commands/models.py +174 -14
- cometapi_cli/commands/run.py +210 -0
- cometapi_cli/commands/stats.py +4 -1
- cometapi_cli/commands/tasks.py +12 -2
- cometapi_cli/commands/tokens.py +4 -1
- cometapi_cli/formatters.py +1 -1
- cometapi_cli/urls.py +20 -0
- cometapi_cli-0.3.0.dist-info/METADATA +311 -0
- cometapi_cli-0.3.0.dist-info/RECORD +31 -0
- cometapi_cli-0.2.2.dist-info/METADATA +0 -246
- cometapi_cli-0.2.2.dist-info/RECORD +0 -27
- {cometapi_cli-0.2.2.dist-info → cometapi_cli-0.3.0.dist-info}/WHEEL +0 -0
- {cometapi_cli-0.2.2.dist-info → cometapi_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {cometapi_cli-0.2.2.dist-info → cometapi_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
cometapi_cli/__init__.py
CHANGED
cometapi_cli/app.py
CHANGED
|
@@ -61,8 +61,10 @@ from cometapi_cli.commands.chat import chat # noqa: E402
|
|
|
61
61
|
from cometapi_cli.commands.config_cmd import config_app, init # noqa: E402
|
|
62
62
|
from cometapi_cli.commands.doctor import doctor # noqa: E402
|
|
63
63
|
from cometapi_cli.commands.logs import logs # noqa: E402
|
|
64
|
+
from cometapi_cli.commands.model import model_app # noqa: E402
|
|
64
65
|
from cometapi_cli.commands.models import models # noqa: E402
|
|
65
66
|
from cometapi_cli.commands.repl import run_repl # noqa: E402
|
|
67
|
+
from cometapi_cli.commands.run import run # noqa: E402
|
|
66
68
|
from cometapi_cli.commands.stats import stats # noqa: E402
|
|
67
69
|
from cometapi_cli.commands.tasks import tasks # noqa: E402
|
|
68
70
|
from cometapi_cli.commands.tokens import tokens # noqa: E402
|
|
@@ -75,9 +77,11 @@ app.command()(stats)
|
|
|
75
77
|
app.command()(tokens)
|
|
76
78
|
app.command()(logs)
|
|
77
79
|
app.command()(tasks)
|
|
80
|
+
app.command()(run)
|
|
78
81
|
app.command()(init)
|
|
79
82
|
app.command()(doctor)
|
|
80
83
|
app.command(name="repl")(run_repl)
|
|
84
|
+
app.add_typer(model_app)
|
|
81
85
|
app.add_typer(config_app)
|
|
82
86
|
|
|
83
87
|
|
cometapi_cli/catalog.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Public CometAPI model catalog helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .urls import get_api_root_url
|
|
14
|
+
|
|
15
|
+
CATALOG_CACHE_FILE = Path.home() / ".cache" / "cometapi" / "models.json"
|
|
16
|
+
CATALOG_TTL_SECONDS = 60 * 60
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _request_json(url: str) -> dict[str, Any]:
|
|
20
|
+
try:
|
|
21
|
+
with urllib.request.urlopen(url, timeout=30) as response:
|
|
22
|
+
return json.loads(response.read().decode("utf-8"))
|
|
23
|
+
except urllib.error.HTTPError as e:
|
|
24
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
25
|
+
raise RuntimeError(f"GET {url} failed: HTTP {e.code} {body[:200]}") from e
|
|
26
|
+
except urllib.error.URLError as e:
|
|
27
|
+
raise RuntimeError(f"GET {url} failed: {e.reason}") from e
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_cache(base_url: str) -> list[dict[str, Any]] | None:
|
|
31
|
+
try:
|
|
32
|
+
raw = json.loads(CATALOG_CACHE_FILE.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
return None
|
|
35
|
+
if raw.get("base_url") != base_url:
|
|
36
|
+
return None
|
|
37
|
+
if time.time() - float(raw.get("fetched_at", 0)) > CATALOG_TTL_SECONDS:
|
|
38
|
+
return None
|
|
39
|
+
data = raw.get("data")
|
|
40
|
+
return data if isinstance(data, list) else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _write_cache(base_url: str, data: list[dict[str, Any]]) -> None:
|
|
44
|
+
try:
|
|
45
|
+
CATALOG_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
CATALOG_CACHE_FILE.write_text(
|
|
47
|
+
json.dumps({"base_url": base_url, "fetched_at": time.time(), "data": data}),
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
except OSError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fetch_catalog_models(*, refresh: bool = False, no_cache: bool = False) -> list[dict[str, Any]]:
|
|
55
|
+
"""Fetch the public model catalog, using a one-hour local cache by default."""
|
|
56
|
+
base_url = get_api_root_url()
|
|
57
|
+
if not refresh and not no_cache:
|
|
58
|
+
cached = _read_cache(base_url)
|
|
59
|
+
if cached is not None:
|
|
60
|
+
return cached
|
|
61
|
+
|
|
62
|
+
payload = _request_json(f"{base_url}/api/models")
|
|
63
|
+
if not payload.get("success", False):
|
|
64
|
+
raise RuntimeError(payload.get("message") or "GET /api/models returned success=false")
|
|
65
|
+
|
|
66
|
+
data = payload.get("data", [])
|
|
67
|
+
if not isinstance(data, list):
|
|
68
|
+
raise RuntimeError("GET /api/models returned an unexpected data shape")
|
|
69
|
+
|
|
70
|
+
if not no_cache:
|
|
71
|
+
_write_cache(base_url, data)
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_jsonish(value: Any) -> Any:
|
|
76
|
+
"""Parse JSON-in-string fields from the catalog when possible."""
|
|
77
|
+
if not isinstance(value, str):
|
|
78
|
+
return value
|
|
79
|
+
stripped = value.strip()
|
|
80
|
+
if not stripped:
|
|
81
|
+
return value
|
|
82
|
+
try:
|
|
83
|
+
return json.loads(stripped)
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def endpoint_terms(model: dict[str, Any]) -> list[str]:
|
|
89
|
+
endpoints = parse_jsonish(model.get("endpoints"))
|
|
90
|
+
if isinstance(endpoints, dict):
|
|
91
|
+
terms: list[str] = []
|
|
92
|
+
for name, detail in endpoints.items():
|
|
93
|
+
terms.append(str(name))
|
|
94
|
+
if isinstance(detail, dict):
|
|
95
|
+
for key in ("path", "method"):
|
|
96
|
+
value = detail.get(key)
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
terms.append(value)
|
|
99
|
+
return terms
|
|
100
|
+
if isinstance(endpoints, list):
|
|
101
|
+
return [str(item) for item in endpoints]
|
|
102
|
+
if endpoints:
|
|
103
|
+
return [str(endpoints)]
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def endpoint_labels(model: dict[str, Any]) -> list[str]:
|
|
108
|
+
"""Return compact endpoint labels for display."""
|
|
109
|
+
endpoints = parse_jsonish(model.get("endpoints"))
|
|
110
|
+
if isinstance(endpoints, dict):
|
|
111
|
+
return [str(name) for name in endpoints]
|
|
112
|
+
if isinstance(endpoints, list):
|
|
113
|
+
return [str(item) for item in endpoints]
|
|
114
|
+
if endpoints:
|
|
115
|
+
return [str(endpoints)]
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_catalog_model(model_id: str, models: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
120
|
+
normalized = model_id.replace(".", "-")
|
|
121
|
+
for model in models:
|
|
122
|
+
if model.get("id") == model_id or model.get("code") == model_id:
|
|
123
|
+
return model
|
|
124
|
+
if model.get("code") == normalized:
|
|
125
|
+
return model
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def fetch_model_detail(model_id: str, *, refresh: bool = False, no_cache: bool = False) -> dict[str, Any]:
|
|
130
|
+
"""Fetch public model detail by model id, resolving catalog code first."""
|
|
131
|
+
models = fetch_catalog_models(refresh=refresh, no_cache=no_cache)
|
|
132
|
+
model = find_catalog_model(model_id, models)
|
|
133
|
+
code = model.get("code") if model else model_id.replace(".", "-")
|
|
134
|
+
url_code = urllib.parse.quote(str(code), safe="/")
|
|
135
|
+
payload = _request_json(f"{get_api_root_url()}/api/model/{url_code}")
|
|
136
|
+
if not payload.get("success", False):
|
|
137
|
+
raise RuntimeError(payload.get("message") or f"model not found: {model_id}")
|
|
138
|
+
data = payload.get("data")
|
|
139
|
+
if not isinstance(data, dict):
|
|
140
|
+
raise RuntimeError("GET /api/model returned an unexpected data shape")
|
|
141
|
+
return data
|
cometapi_cli/commands/account.py
CHANGED
|
@@ -14,7 +14,10 @@ from ..formatters import OutputFormat, output, resolve_format
|
|
|
14
14
|
@handle_errors
|
|
15
15
|
def account(
|
|
16
16
|
ctx: typer.Context,
|
|
17
|
-
output_format: Annotated[
|
|
17
|
+
output_format: Annotated[
|
|
18
|
+
OutputFormat | None,
|
|
19
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
20
|
+
] = None,
|
|
18
21
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
19
22
|
) -> None:
|
|
20
23
|
"""Show your CometAPI account profile (requires access token)."""
|
cometapi_cli/commands/chat.py
CHANGED
|
@@ -17,12 +17,21 @@ from ..formatters import OutputFormat, resolve_format
|
|
|
17
17
|
def chat(
|
|
18
18
|
ctx: typer.Context,
|
|
19
19
|
message: Annotated[str | None, typer.Argument(help="Message to send. Omit to enter interactive REPL.")] = None,
|
|
20
|
-
model: Annotated[
|
|
20
|
+
model: Annotated[
|
|
21
|
+
str | None,
|
|
22
|
+
typer.Option("--model", "-m", help="Model to use (default: from config or gpt-5.4)."),
|
|
23
|
+
] = None,
|
|
21
24
|
system: Annotated[str | None, typer.Option("--system", "-s", help="System prompt.")] = None,
|
|
22
|
-
temperature: Annotated[
|
|
25
|
+
temperature: Annotated[
|
|
26
|
+
float | None,
|
|
27
|
+
typer.Option("--temperature", "-t", help="Sampling temperature (0.0-2.0)."),
|
|
28
|
+
] = None,
|
|
23
29
|
max_tokens: Annotated[int | None, typer.Option("--max-tokens", help="Max tokens in response.")] = None,
|
|
24
30
|
stream: Annotated[bool, typer.Option("--stream/--no-stream", help="Stream output.")] = True,
|
|
25
|
-
output_format: Annotated[
|
|
31
|
+
output_format: Annotated[
|
|
32
|
+
OutputFormat | None,
|
|
33
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
34
|
+
] = None,
|
|
26
35
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
27
36
|
) -> None:
|
|
28
37
|
"""Send a chat message, or start interactive REPL (no args)."""
|
|
@@ -171,7 +171,10 @@ def init(ctx: typer.Context) -> None:
|
|
|
171
171
|
@handle_errors
|
|
172
172
|
def config_show(
|
|
173
173
|
ctx: typer.Context,
|
|
174
|
-
output_format: Annotated[
|
|
174
|
+
output_format: Annotated[
|
|
175
|
+
OutputFormat | None,
|
|
176
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
177
|
+
] = None,
|
|
175
178
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
176
179
|
) -> None:
|
|
177
180
|
"""Display current configuration (secrets are masked)."""
|
|
@@ -195,7 +198,10 @@ def config_show(
|
|
|
195
198
|
@config_app.command("set")
|
|
196
199
|
@handle_errors
|
|
197
200
|
def config_set(
|
|
198
|
-
key: Annotated[
|
|
201
|
+
key: Annotated[
|
|
202
|
+
str,
|
|
203
|
+
typer.Argument(help="Configuration key (api_key, access_token, base_url, default_model, output_format)."),
|
|
204
|
+
],
|
|
199
205
|
value: Annotated[str, typer.Argument(help="Value to set. For output_format: table, json, yaml, csv, markdown.")],
|
|
200
206
|
) -> None:
|
|
201
207
|
"""Set a configuration value.
|
|
@@ -221,7 +227,12 @@ def config_set(
|
|
|
221
227
|
@config_app.command("unset")
|
|
222
228
|
@handle_errors
|
|
223
229
|
def config_unset(
|
|
224
|
-
key: Annotated[
|
|
230
|
+
key: Annotated[
|
|
231
|
+
str,
|
|
232
|
+
typer.Argument(
|
|
233
|
+
help="Configuration key to remove (api_key, access_token, base_url, default_model, output_format)."
|
|
234
|
+
),
|
|
235
|
+
],
|
|
225
236
|
) -> None:
|
|
226
237
|
"""Remove a configuration value."""
|
|
227
238
|
cfg = load_config()
|
cometapi_cli/commands/doctor.py
CHANGED
|
@@ -25,7 +25,10 @@ def _warn_mark() -> str:
|
|
|
25
25
|
@handle_errors
|
|
26
26
|
def doctor(
|
|
27
27
|
ctx: typer.Context,
|
|
28
|
-
output_format: Annotated[
|
|
28
|
+
output_format: Annotated[
|
|
29
|
+
OutputFormat | None,
|
|
30
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
31
|
+
] = None,
|
|
29
32
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
30
33
|
) -> None:
|
|
31
34
|
"""Check CLI configuration and API connectivity."""
|
cometapi_cli/commands/logs.py
CHANGED
|
@@ -176,7 +176,10 @@ def logs(
|
|
|
176
176
|
page: Annotated[int, typer.Option("--page", "-p", help="Page number.")] = 1,
|
|
177
177
|
limit: Annotated[int, typer.Option("--limit", "-l", help="Results per page.")] = 20,
|
|
178
178
|
export: Annotated[bool, typer.Option("--export", help="Export logs as CSV to stdout.")] = False,
|
|
179
|
-
output_format: Annotated[
|
|
179
|
+
output_format: Annotated[
|
|
180
|
+
OutputFormat | None,
|
|
181
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
182
|
+
] = None,
|
|
180
183
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
181
184
|
) -> None:
|
|
182
185
|
"""Show your usage logs (requires access token)."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Model detail commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ..catalog import endpoint_terms, fetch_model_detail
|
|
11
|
+
from ..errors import handle_errors
|
|
12
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
13
|
+
from .models import _pricing_label
|
|
14
|
+
|
|
15
|
+
model_app = typer.Typer(
|
|
16
|
+
name="model",
|
|
17
|
+
help="Inspect CometAPI model metadata.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _strip_markdown(text: str | None, *, limit: int = 360) -> str:
|
|
24
|
+
if not text:
|
|
25
|
+
return "-"
|
|
26
|
+
cleaned = re.sub(r"[`#\\*]", "", text)
|
|
27
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
28
|
+
if len(cleaned) > limit:
|
|
29
|
+
return cleaned[: limit - 3] + "..."
|
|
30
|
+
return cleaned or "-"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _format_detail(detail: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
pricing = detail.get("pricing") if isinstance(detail.get("pricing"), dict) else {}
|
|
35
|
+
features = detail.get("features") if isinstance(detail.get("features"), list) else []
|
|
36
|
+
return {
|
|
37
|
+
"id": detail.get("id", "") or "-",
|
|
38
|
+
"code": detail.get("code", "") or "-",
|
|
39
|
+
"provider": detail.get("provider", "") or "-",
|
|
40
|
+
"model_type": detail.get("model_type", "") or "-",
|
|
41
|
+
"features": ", ".join(str(item) for item in features) or "-",
|
|
42
|
+
"endpoints": ", ".join(endpoint_terms(detail)) or "-",
|
|
43
|
+
"pricing": _pricing_label(pricing),
|
|
44
|
+
"context_length": detail.get("context_length") or "-",
|
|
45
|
+
"max_completion_tokens": detail.get("max_completion_tokens") or "-",
|
|
46
|
+
"api_doc_url": detail.get("api_doc_url") or "-",
|
|
47
|
+
"overview": _strip_markdown(detail.get("overview")),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@model_app.command("info")
|
|
52
|
+
@handle_errors
|
|
53
|
+
def model_info(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
model_id: Annotated[str, typer.Argument(help="Model ID to inspect.")],
|
|
56
|
+
refresh: Annotated[bool, typer.Option("--refresh", help="Refresh the public catalog cache.")] = False,
|
|
57
|
+
no_cache: Annotated[bool, typer.Option("--no-cache", help="Bypass the public catalog cache.")] = False,
|
|
58
|
+
output_format: Annotated[
|
|
59
|
+
OutputFormat | None,
|
|
60
|
+
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
61
|
+
] = None,
|
|
62
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Show public metadata for one model."""
|
|
65
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
66
|
+
detail = fetch_model_detail(model_id, refresh=refresh, no_cache=no_cache)
|
|
67
|
+
if fmt == OutputFormat.JSON:
|
|
68
|
+
output(detail, fmt)
|
|
69
|
+
return
|
|
70
|
+
output(_format_detail(detail), fmt, title="Model Info")
|
cometapi_cli/commands/models.py
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
+
from enum import Enum
|
|
6
7
|
from functools import cmp_to_key
|
|
7
|
-
from typing import Annotated
|
|
8
|
+
from typing import Annotated, Any
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
|
|
12
|
+
from ..catalog import endpoint_labels, endpoint_terms, fetch_catalog_models, parse_jsonish
|
|
11
13
|
from ..config import get_client
|
|
12
14
|
from ..errors import handle_errors
|
|
13
15
|
from ..formatters import OutputFormat, output, resolve_format
|
|
@@ -60,11 +62,132 @@ def _compare_model_rows(left: dict[str, str], right: dict[str, str]) -> int:
|
|
|
60
62
|
return _compare_model_ids(left["id"], right["id"])
|
|
61
63
|
|
|
62
64
|
|
|
65
|
+
class ModelsSource(str, Enum):
|
|
66
|
+
"""Supported model-list sources."""
|
|
67
|
+
|
|
68
|
+
CATALOG = "catalog"
|
|
69
|
+
OPENAI = "openai"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _csv_terms(raw: str | None) -> list[str]:
|
|
73
|
+
if raw is None:
|
|
74
|
+
return []
|
|
75
|
+
return [item.strip().lower() for item in raw.split(",") if item.strip()]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _contains_any(values: list[str], terms: list[str]) -> bool:
|
|
79
|
+
lowered = [value.lower() for value in values]
|
|
80
|
+
return any(term in value for term in terms for value in lowered)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _pricing_label(pricing: dict[str, Any]) -> str:
|
|
84
|
+
if pricing.get("per_request") is not None:
|
|
85
|
+
return f"${pricing['per_request']}/request"
|
|
86
|
+
if pricing.get("per_second") is not None:
|
|
87
|
+
return f"${pricing['per_second']}/second"
|
|
88
|
+
parts = []
|
|
89
|
+
if pricing.get("input") is not None:
|
|
90
|
+
parts.append(f"in ${pricing['input']}/M")
|
|
91
|
+
if pricing.get("output") is not None:
|
|
92
|
+
parts.append(f"out ${pricing['output']}/M")
|
|
93
|
+
return ", ".join(parts) if parts else "-"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _format_catalog_row(model: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
features = model.get("features") or []
|
|
98
|
+
if not isinstance(features, list):
|
|
99
|
+
features = [str(features)]
|
|
100
|
+
pricing = model.get("pricing") if isinstance(model.get("pricing"), dict) else {}
|
|
101
|
+
return {
|
|
102
|
+
"id": model.get("id", ""),
|
|
103
|
+
"provider": model.get("provider", "") or "-",
|
|
104
|
+
"model_type": model.get("model_type", "") or "-",
|
|
105
|
+
"features": ", ".join(str(item) for item in features) or "-",
|
|
106
|
+
"endpoints": ", ".join(endpoint_labels(model)) or "-",
|
|
107
|
+
"pricing": _pricing_label(pricing),
|
|
108
|
+
"context_length": model.get("context_length") or "-",
|
|
109
|
+
"max_completion_tokens": model.get("max_completion_tokens") or "-",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _filter_catalog_rows(
|
|
114
|
+
models: list[dict[str, Any]],
|
|
115
|
+
*,
|
|
116
|
+
search: str | None,
|
|
117
|
+
provider: str | None,
|
|
118
|
+
model_type: str | None,
|
|
119
|
+
feature: str | None,
|
|
120
|
+
endpoint: str | None,
|
|
121
|
+
) -> list[dict[str, Any]]:
|
|
122
|
+
provider_terms = _csv_terms(provider)
|
|
123
|
+
feature_terms = _csv_terms(feature)
|
|
124
|
+
endpoint_filter_terms = _csv_terms(endpoint)
|
|
125
|
+
search_term = search.lower().strip() if search else ""
|
|
126
|
+
type_term = model_type.lower().strip() if model_type else ""
|
|
127
|
+
|
|
128
|
+
rows: list[dict[str, Any]] = []
|
|
129
|
+
for model in models:
|
|
130
|
+
features = [str(item) for item in (model.get("features") or [])]
|
|
131
|
+
endpoints = endpoint_terms(model)
|
|
132
|
+
provider_values = [str(model.get("provider", "")), str(model.get("provider_code", ""))]
|
|
133
|
+
searchable = [
|
|
134
|
+
str(model.get("id", "")),
|
|
135
|
+
str(model.get("code", "")),
|
|
136
|
+
str(model.get("name", "")),
|
|
137
|
+
str(model.get("description", "")),
|
|
138
|
+
*provider_values,
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if search_term and not _contains_any(searchable, [search_term]):
|
|
142
|
+
continue
|
|
143
|
+
if provider_terms and not _contains_any(provider_values, provider_terms):
|
|
144
|
+
continue
|
|
145
|
+
if type_term and type_term not in str(model.get("model_type", "")).lower():
|
|
146
|
+
continue
|
|
147
|
+
if feature_terms and not _contains_any(features, feature_terms):
|
|
148
|
+
continue
|
|
149
|
+
if endpoint_filter_terms and not _contains_any(endpoints, endpoint_filter_terms):
|
|
150
|
+
continue
|
|
151
|
+
rows.append(_format_catalog_row({**model, "endpoints": parse_jsonish(model.get("endpoints"))}))
|
|
152
|
+
return rows
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _openai_rows(search: str | None, limit: int | None) -> list[dict[str, str]]:
|
|
156
|
+
client = get_client()
|
|
157
|
+
rows = [{"id": m.id} for m in client.models.list()]
|
|
158
|
+
if search:
|
|
159
|
+
term = search.lower()
|
|
160
|
+
rows = [row for row in rows if term in row["id"].lower()]
|
|
161
|
+
rows = sorted(rows, key=cmp_to_key(_compare_model_rows))
|
|
162
|
+
if limit and limit > 0:
|
|
163
|
+
rows = rows[:limit]
|
|
164
|
+
return rows
|
|
165
|
+
|
|
166
|
+
|
|
63
167
|
@handle_errors
|
|
64
168
|
def models(
|
|
65
169
|
ctx: typer.Context,
|
|
66
|
-
search: Annotated[str | None, typer.Option("--search", "-s", help="Filter models by
|
|
170
|
+
search: Annotated[str | None, typer.Option("--search", "-s", help="Filter models by catalog text.")] = None,
|
|
171
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Filter by provider code or name.")] = None,
|
|
172
|
+
model_type: Annotated[
|
|
173
|
+
str | None,
|
|
174
|
+
typer.Option("--type", "--modality", help="Filter by model type or modality."),
|
|
175
|
+
] = None,
|
|
176
|
+
feature: Annotated[
|
|
177
|
+
str | None,
|
|
178
|
+
typer.Option("--feature", "--capability", help="Filter by feature or capability."),
|
|
179
|
+
] = None,
|
|
180
|
+
endpoint: Annotated[
|
|
181
|
+
str | None,
|
|
182
|
+
typer.Option("--endpoint", help="Filter by endpoint name, method, or path."),
|
|
183
|
+
] = None,
|
|
67
184
|
limit: Annotated[int | None, typer.Option("--limit", "-l", help="Max number of models to show.")] = None,
|
|
185
|
+
source: Annotated[
|
|
186
|
+
ModelsSource,
|
|
187
|
+
typer.Option("--source", help="Model source: catalog (rich public metadata) or openai (/v1/models)."),
|
|
188
|
+
] = ModelsSource.CATALOG,
|
|
189
|
+
refresh: Annotated[bool, typer.Option("--refresh", help="Refresh the public catalog cache.")] = False,
|
|
190
|
+
no_cache: Annotated[bool, typer.Option("--no-cache", help="Bypass the public catalog cache.")] = False,
|
|
68
191
|
output_format: Annotated[
|
|
69
192
|
OutputFormat | None,
|
|
70
193
|
typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown)."),
|
|
@@ -73,23 +196,60 @@ def models(
|
|
|
73
196
|
) -> None:
|
|
74
197
|
"""List available models."""
|
|
75
198
|
fmt = resolve_format(ctx, json_output, output_format)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
199
|
+
if source == ModelsSource.OPENAI:
|
|
200
|
+
rows = _openai_rows(search, limit)
|
|
201
|
+
output(rows, fmt, title=f"Available Models ({len(rows)})", columns={"id": "cyan"})
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
catalog_models = fetch_catalog_models(refresh=refresh, no_cache=no_cache)
|
|
205
|
+
rows = _filter_catalog_rows(
|
|
206
|
+
catalog_models,
|
|
207
|
+
search=search,
|
|
208
|
+
provider=provider,
|
|
209
|
+
model_type=model_type,
|
|
210
|
+
feature=feature,
|
|
211
|
+
endpoint=endpoint,
|
|
212
|
+
)
|
|
87
213
|
if limit and limit > 0:
|
|
88
214
|
rows = rows[:limit]
|
|
89
215
|
|
|
216
|
+
if fmt == OutputFormat.TABLE:
|
|
217
|
+
table_rows = [
|
|
218
|
+
{
|
|
219
|
+
"model": row["id"],
|
|
220
|
+
"prov": row["provider"],
|
|
221
|
+
"kind": row["model_type"],
|
|
222
|
+
"price": row["pricing"],
|
|
223
|
+
"endpoint": row["endpoints"],
|
|
224
|
+
}
|
|
225
|
+
for row in rows
|
|
226
|
+
]
|
|
227
|
+
output(
|
|
228
|
+
table_rows,
|
|
229
|
+
fmt,
|
|
230
|
+
title=f"Available Models ({len(rows)})",
|
|
231
|
+
columns={
|
|
232
|
+
"model": "cyan",
|
|
233
|
+
"prov": "green",
|
|
234
|
+
"kind": "magenta",
|
|
235
|
+
"price": "red",
|
|
236
|
+
"endpoint": "blue",
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
90
241
|
output(
|
|
91
242
|
rows,
|
|
92
243
|
fmt,
|
|
93
244
|
title=f"Available Models ({len(rows)})",
|
|
94
|
-
columns={
|
|
245
|
+
columns={
|
|
246
|
+
"id": "cyan",
|
|
247
|
+
"provider": "green",
|
|
248
|
+
"model_type": "magenta",
|
|
249
|
+
"features": "yellow",
|
|
250
|
+
"endpoints": "blue",
|
|
251
|
+
"pricing": "red",
|
|
252
|
+
"context_length": "dim",
|
|
253
|
+
"max_completion_tokens": "dim",
|
|
254
|
+
},
|
|
95
255
|
)
|