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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """CometAPI CLI — professional terminal interface for CometAPI."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
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
 
@@ -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
@@ -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[OutputFormat | None, typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown).")] = None,
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)."""
@@ -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[str | None, typer.Option("--model", "-m", help="Model to use (default: from config or gpt-5.4).")] = None,
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[float | None, typer.Option("--temperature", "-t", help="Sampling temperature (0.0–2.0).")] = None,
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[OutputFormat | None, typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown).")] = None,
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[OutputFormat | None, typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown).")] = None,
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[str, typer.Argument(help="Configuration key (api_key, access_token, base_url, default_model, output_format).")],
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[str, typer.Argument(help="Configuration key to remove (api_key, access_token, base_url, default_model, output_format).")],
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()
@@ -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[OutputFormat | None, typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown).")] = None,
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."""
@@ -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[OutputFormat | None, typer.Option("--format", "-f", help="Output format (table, json, yaml, csv, markdown).")] = None,
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")
@@ -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 name.")] = None,
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
- client = get_client()
77
- result = client.models.list()
78
-
79
- rows = [{"id": m.id} for m in result]
80
-
81
- if search:
82
- term = search.lower()
83
- rows = [r for r in rows if term in r["id"].lower()]
84
-
85
- rows = sorted(rows, key=cmp_to_key(_compare_model_rows))
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={"id": "cyan"},
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
  )