cometapi-cli 0.1.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 +3 -0
- cometapi_cli/app.py +85 -0
- cometapi_cli/client.py +270 -0
- cometapi_cli/commands/__init__.py +1 -0
- cometapi_cli/commands/account.py +39 -0
- cometapi_cli/commands/balance.py +56 -0
- cometapi_cli/commands/chat.py +104 -0
- cometapi_cli/commands/chat_repl.py +229 -0
- cometapi_cli/commands/config_cmd.py +174 -0
- cometapi_cli/commands/doctor.py +144 -0
- cometapi_cli/commands/logs.py +326 -0
- cometapi_cli/commands/models.py +44 -0
- cometapi_cli/commands/repl.py +134 -0
- cometapi_cli/commands/stats.py +39 -0
- cometapi_cli/commands/tasks.py +130 -0
- cometapi_cli/commands/tokens.py +87 -0
- cometapi_cli/config.py +102 -0
- cometapi_cli/console.py +8 -0
- cometapi_cli/constants.py +55 -0
- cometapi_cli/errors.py +113 -0
- cometapi_cli/formatters.py +156 -0
- cometapi_cli/main.py +8 -0
- cometapi_cli-0.1.0.dist-info/METADATA +228 -0
- cometapi_cli-0.1.0.dist-info/RECORD +27 -0
- cometapi_cli-0.1.0.dist-info/WHEEL +4 -0
- cometapi_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cometapi_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
cometapi_cli/__init__.py
ADDED
cometapi_cli/app.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CometAPI CLI — main Typer application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import click.core
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from cometapi_cli import __version__
|
|
11
|
+
from cometapi_cli.formatters import OutputFormat
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="cometapi",
|
|
15
|
+
help="CometAPI CLI — interact with CometAPI from the terminal.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
rich_markup_mode="rich",
|
|
18
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _version_callback(value: bool) -> None:
|
|
23
|
+
if value:
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
27
|
+
typer.echo(f"cometapi-cli {__version__} (python {py_ver})")
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback()
|
|
32
|
+
def main(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
version: Annotated[
|
|
35
|
+
bool | None,
|
|
36
|
+
typer.Option("--version", "-V", help="Show version and exit.", callback=_version_callback, is_eager=True),
|
|
37
|
+
] = None,
|
|
38
|
+
output_format: Annotated[
|
|
39
|
+
OutputFormat,
|
|
40
|
+
typer.Option("--format", "-f", help="Output format."),
|
|
41
|
+
] = OutputFormat.TABLE,
|
|
42
|
+
json_output: Annotated[
|
|
43
|
+
bool,
|
|
44
|
+
typer.Option("--json", help="Output as JSON (shortcut for --format json)."),
|
|
45
|
+
] = False,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""CometAPI CLI — interact with CometAPI from the terminal."""
|
|
48
|
+
ctx.ensure_object(dict)
|
|
49
|
+
# Only set format in context when explicitly passed by user
|
|
50
|
+
if json_output:
|
|
51
|
+
ctx.obj["format"] = OutputFormat.JSON
|
|
52
|
+
elif ctx.get_parameter_source("output_format") == click.core.ParameterSource.COMMANDLINE:
|
|
53
|
+
ctx.obj["format"] = output_format
|
|
54
|
+
# Otherwise leave ctx.obj["format"] unset — resolve_format() will fall through to config file
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Register commands
|
|
58
|
+
from cometapi_cli.commands.account import account # noqa: E402
|
|
59
|
+
from cometapi_cli.commands.balance import balance # noqa: E402
|
|
60
|
+
from cometapi_cli.commands.chat import chat # noqa: E402
|
|
61
|
+
from cometapi_cli.commands.config_cmd import config_app, init # noqa: E402
|
|
62
|
+
from cometapi_cli.commands.doctor import doctor # noqa: E402
|
|
63
|
+
from cometapi_cli.commands.logs import logs # noqa: E402
|
|
64
|
+
from cometapi_cli.commands.models import models # noqa: E402
|
|
65
|
+
from cometapi_cli.commands.repl import run_repl # noqa: E402
|
|
66
|
+
from cometapi_cli.commands.stats import stats # noqa: E402
|
|
67
|
+
from cometapi_cli.commands.tasks import tasks # noqa: E402
|
|
68
|
+
from cometapi_cli.commands.tokens import tokens # noqa: E402
|
|
69
|
+
|
|
70
|
+
app.command()(chat)
|
|
71
|
+
app.command()(balance)
|
|
72
|
+
app.command()(models)
|
|
73
|
+
app.command()(account)
|
|
74
|
+
app.command()(stats)
|
|
75
|
+
app.command()(tokens)
|
|
76
|
+
app.command()(logs)
|
|
77
|
+
app.command()(tasks)
|
|
78
|
+
app.command()(init)
|
|
79
|
+
app.command()(doctor)
|
|
80
|
+
app.command(name="repl")(run_repl)
|
|
81
|
+
app.add_typer(config_app)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
app()
|
cometapi_cli/client.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""CometAPI client — inherits from OpenAI's official client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import openai
|
|
9
|
+
|
|
10
|
+
COMETAPI_BASE_URL = "https://api.cometapi.com/v1"
|
|
11
|
+
COMETAPI_DASHBOARD_BASE = "https://api.cometapi.com"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CometClient(openai.OpenAI):
|
|
15
|
+
"""Synchronous CometAPI client.
|
|
16
|
+
|
|
17
|
+
Inherits all OpenAI functionality (chat.completions, embeddings, etc.)
|
|
18
|
+
and adds CometAPI-specific endpoints.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_access_token: str | None
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
api_key: str | None = None,
|
|
27
|
+
access_token: str | None = None,
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
api_key = api_key or os.environ.get("COMETAPI_KEY")
|
|
32
|
+
if not api_key:
|
|
33
|
+
raise openai.OpenAIError(
|
|
34
|
+
"CometAPI key is required. Pass api_key= or set COMETAPI_KEY env var."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self._access_token = access_token or os.environ.get("COMETAPI_ACCESS_TOKEN")
|
|
38
|
+
|
|
39
|
+
super().__init__(
|
|
40
|
+
api_key=api_key,
|
|
41
|
+
base_url=base_url or os.environ.get("COMETAPI_BASE_URL") or COMETAPI_BASE_URL,
|
|
42
|
+
**kwargs,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# -- Account management (access-token auth) --------------------------------
|
|
46
|
+
|
|
47
|
+
def _account_request(self, method: str, path: str, *, params: dict | None = None) -> dict:
|
|
48
|
+
"""Make an authenticated request to a CometAPI account endpoint."""
|
|
49
|
+
if not self._access_token:
|
|
50
|
+
raise openai.OpenAIError(
|
|
51
|
+
"CometAPI access token is required for account endpoints. "
|
|
52
|
+
"Pass access_token= or set COMETAPI_ACCESS_TOKEN env var."
|
|
53
|
+
)
|
|
54
|
+
response = self._client.request(
|
|
55
|
+
method,
|
|
56
|
+
f"{COMETAPI_DASHBOARD_BASE}{path}",
|
|
57
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
58
|
+
params=params,
|
|
59
|
+
)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
return response.json()
|
|
62
|
+
|
|
63
|
+
def get_balance(self, *, source: str | None = None) -> dict:
|
|
64
|
+
"""Get the current user's account balance."""
|
|
65
|
+
QUOTA_PER_UNIT = 500_000.0
|
|
66
|
+
|
|
67
|
+
use_user = (source == "account") or (source is None and self._access_token)
|
|
68
|
+
|
|
69
|
+
if use_user and self._access_token:
|
|
70
|
+
try:
|
|
71
|
+
resp = self._account_request("GET", "/api/user/self")
|
|
72
|
+
data = resp.get("data", resp)
|
|
73
|
+
quota = data.get("quota", 0)
|
|
74
|
+
used_quota = data.get("used_quota", 0)
|
|
75
|
+
return {
|
|
76
|
+
"balance": quota / QUOTA_PER_UNIT,
|
|
77
|
+
"used": used_quota / QUOTA_PER_UNIT,
|
|
78
|
+
"total_topped_up": (quota + used_quota) / QUOTA_PER_UNIT,
|
|
79
|
+
"currency": "USD",
|
|
80
|
+
"source": "user",
|
|
81
|
+
}
|
|
82
|
+
except Exception:
|
|
83
|
+
if source == "account":
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
87
|
+
|
|
88
|
+
sub_resp = self._client.get(
|
|
89
|
+
f"{self.base_url}dashboard/billing/subscription",
|
|
90
|
+
headers=headers,
|
|
91
|
+
)
|
|
92
|
+
sub_resp.raise_for_status()
|
|
93
|
+
sub = sub_resp.json()
|
|
94
|
+
|
|
95
|
+
usage_resp = self._client.get(
|
|
96
|
+
f"{self.base_url}dashboard/billing/usage",
|
|
97
|
+
headers=headers,
|
|
98
|
+
)
|
|
99
|
+
usage_resp.raise_for_status()
|
|
100
|
+
usage = usage_resp.json()
|
|
101
|
+
|
|
102
|
+
hard_limit = sub.get("hard_limit_usd", 0)
|
|
103
|
+
total_usage = usage.get("total_usage", 0) / 100
|
|
104
|
+
|
|
105
|
+
if hard_limit >= 100_000_000:
|
|
106
|
+
balance = -1
|
|
107
|
+
else:
|
|
108
|
+
balance = hard_limit - total_usage
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"balance": balance,
|
|
112
|
+
"used": total_usage,
|
|
113
|
+
"total_topped_up": hard_limit if hard_limit < 100_000_000 else -1,
|
|
114
|
+
"currency": "USD",
|
|
115
|
+
"source": "billing",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def get_self(self) -> dict:
|
|
119
|
+
"""Get the current user's profile (requires access token)."""
|
|
120
|
+
return self._account_request("GET", "/api/user/self")
|
|
121
|
+
|
|
122
|
+
def get_user_stats(self) -> dict:
|
|
123
|
+
"""Get the current user's usage statistics (requires access token)."""
|
|
124
|
+
return self._account_request("GET", "/api/user/self/stats")
|
|
125
|
+
|
|
126
|
+
# -- Token management -------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def list_tokens(self, *, page: int = 1, page_size: int = 20) -> dict:
|
|
129
|
+
"""List the user's API keys/tokens (requires access token)."""
|
|
130
|
+
return self._account_request("GET", "/api/token/", params={"p": page, "page_size": page_size})
|
|
131
|
+
|
|
132
|
+
def search_tokens(self, keyword: str = "", *, token: str = "") -> dict:
|
|
133
|
+
"""Search API keys/tokens by keyword or token value (requires access token)."""
|
|
134
|
+
params: dict[str, Any] = {}
|
|
135
|
+
if keyword:
|
|
136
|
+
params["keyword"] = keyword
|
|
137
|
+
if token:
|
|
138
|
+
params["token"] = token
|
|
139
|
+
return self._account_request("GET", "/api/token/search", params=params)
|
|
140
|
+
|
|
141
|
+
# -- Logs -------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def list_logs(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
page: int = 1,
|
|
147
|
+
page_size: int = 20,
|
|
148
|
+
log_type: int | None = None,
|
|
149
|
+
model_name: str | None = None,
|
|
150
|
+
token_name: str | None = None,
|
|
151
|
+
start_timestamp: int | None = None,
|
|
152
|
+
end_timestamp: int | None = None,
|
|
153
|
+
group: str | None = None,
|
|
154
|
+
) -> dict:
|
|
155
|
+
"""List the user's usage logs (requires access token)."""
|
|
156
|
+
params: dict[str, Any] = {"p": page, "page_size": page_size}
|
|
157
|
+
if log_type is not None:
|
|
158
|
+
params["type"] = log_type
|
|
159
|
+
if model_name:
|
|
160
|
+
params["model_name"] = model_name
|
|
161
|
+
if token_name:
|
|
162
|
+
params["token_name"] = token_name
|
|
163
|
+
if start_timestamp is not None:
|
|
164
|
+
params["start_timestamp"] = start_timestamp
|
|
165
|
+
if end_timestamp is not None:
|
|
166
|
+
params["end_timestamp"] = end_timestamp
|
|
167
|
+
if group:
|
|
168
|
+
params["group"] = group
|
|
169
|
+
return self._account_request("GET", "/api/log/self", params=params)
|
|
170
|
+
|
|
171
|
+
def search_logs(self, keyword: str) -> dict:
|
|
172
|
+
"""Search usage logs by keyword (requires access token)."""
|
|
173
|
+
return self._account_request("GET", "/api/log/self/search", params={"keyword": keyword})
|
|
174
|
+
|
|
175
|
+
def get_log_stat(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
log_type: int | None = None,
|
|
179
|
+
model_name: str | None = None,
|
|
180
|
+
token_name: str | None = None,
|
|
181
|
+
start_timestamp: int | None = None,
|
|
182
|
+
end_timestamp: int | None = None,
|
|
183
|
+
group: str | None = None,
|
|
184
|
+
request_id: str | None = None,
|
|
185
|
+
) -> dict:
|
|
186
|
+
"""Get aggregated log statistics."""
|
|
187
|
+
params: dict[str, Any] = {}
|
|
188
|
+
if log_type is not None:
|
|
189
|
+
params["type"] = log_type
|
|
190
|
+
if model_name:
|
|
191
|
+
params["model_name"] = model_name
|
|
192
|
+
if token_name:
|
|
193
|
+
params["token_name"] = token_name
|
|
194
|
+
if start_timestamp is not None:
|
|
195
|
+
params["start_timestamp"] = start_timestamp
|
|
196
|
+
if end_timestamp is not None:
|
|
197
|
+
params["end_timestamp"] = end_timestamp
|
|
198
|
+
if group:
|
|
199
|
+
params["group"] = group
|
|
200
|
+
if request_id:
|
|
201
|
+
params["request_id"] = request_id
|
|
202
|
+
return self._account_request("GET", "/api/log/self/stat", params=params)
|
|
203
|
+
|
|
204
|
+
def export_logs(
|
|
205
|
+
self,
|
|
206
|
+
*,
|
|
207
|
+
log_type: int | None = None,
|
|
208
|
+
model_name: str | None = None,
|
|
209
|
+
token_name: str | None = None,
|
|
210
|
+
start_timestamp: int | None = None,
|
|
211
|
+
end_timestamp: int | None = None,
|
|
212
|
+
group: str | None = None,
|
|
213
|
+
) -> bytes:
|
|
214
|
+
"""Export usage logs as CSV (requires access token)."""
|
|
215
|
+
if not self._access_token:
|
|
216
|
+
raise openai.OpenAIError(
|
|
217
|
+
"CometAPI access token is required for account endpoints. "
|
|
218
|
+
"Pass access_token= or set COMETAPI_ACCESS_TOKEN env var."
|
|
219
|
+
)
|
|
220
|
+
params: dict[str, Any] = {}
|
|
221
|
+
if log_type is not None:
|
|
222
|
+
params["type"] = log_type
|
|
223
|
+
if model_name:
|
|
224
|
+
params["model_name"] = model_name
|
|
225
|
+
if token_name:
|
|
226
|
+
params["token_name"] = token_name
|
|
227
|
+
if start_timestamp is not None:
|
|
228
|
+
params["start_timestamp"] = start_timestamp
|
|
229
|
+
if end_timestamp is not None:
|
|
230
|
+
params["end_timestamp"] = end_timestamp
|
|
231
|
+
if group:
|
|
232
|
+
params["group"] = group
|
|
233
|
+
response = self._client.request(
|
|
234
|
+
"GET",
|
|
235
|
+
f"{COMETAPI_DASHBOARD_BASE}/api/log/self/export",
|
|
236
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
237
|
+
params=params,
|
|
238
|
+
)
|
|
239
|
+
response.raise_for_status()
|
|
240
|
+
return response.content
|
|
241
|
+
|
|
242
|
+
# -- Tasks ------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def list_tasks(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
page: int = 1,
|
|
248
|
+
page_size: int = 20,
|
|
249
|
+
platform: str | None = None,
|
|
250
|
+
task_id: str | None = None,
|
|
251
|
+
status: str | None = None,
|
|
252
|
+
action: str | None = None,
|
|
253
|
+
start_timestamp: int | None = None,
|
|
254
|
+
end_timestamp: int | None = None,
|
|
255
|
+
) -> dict:
|
|
256
|
+
"""List the user's async task logs (requires access token)."""
|
|
257
|
+
params: dict[str, Any] = {"p": page, "page_size": page_size}
|
|
258
|
+
if platform:
|
|
259
|
+
params["platform"] = platform
|
|
260
|
+
if task_id:
|
|
261
|
+
params["task_id"] = task_id
|
|
262
|
+
if status:
|
|
263
|
+
params["status"] = status
|
|
264
|
+
if action:
|
|
265
|
+
params["action"] = action
|
|
266
|
+
if start_timestamp is not None:
|
|
267
|
+
params["start_timestamp"] = start_timestamp
|
|
268
|
+
if end_timestamp is not None:
|
|
269
|
+
params["end_timestamp"] = end_timestamp
|
|
270
|
+
return self._account_request("GET", "/api/task/self", params=params)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CometAPI CLI commands."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Account command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import get_client
|
|
10
|
+
from ..errors import handle_errors
|
|
11
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@handle_errors
|
|
15
|
+
def account(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
18
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Show your CometAPI account profile (requires access token)."""
|
|
21
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
22
|
+
client = get_client(require_access_token=True)
|
|
23
|
+
resp = client.get_self()
|
|
24
|
+
data = resp.get("data", {})
|
|
25
|
+
|
|
26
|
+
display = {
|
|
27
|
+
"id": data.get("id", "N/A"),
|
|
28
|
+
"username": data.get("username", "N/A"),
|
|
29
|
+
"display_name": data.get("display_name", "N/A"),
|
|
30
|
+
"email": data.get("email", "N/A"),
|
|
31
|
+
"role": data.get("role", "N/A"),
|
|
32
|
+
"status": data.get("status", "N/A"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# For JSON output, pass raw data for maximum information
|
|
36
|
+
if fmt == OutputFormat.JSON:
|
|
37
|
+
output(data, fmt)
|
|
38
|
+
else:
|
|
39
|
+
output(display, fmt, title="CometAPI Account")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Balance command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import get_client
|
|
10
|
+
from ..errors import handle_errors
|
|
11
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@handle_errors
|
|
15
|
+
def balance(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
source: Annotated[
|
|
18
|
+
str | None,
|
|
19
|
+
typer.Option("--source", "-s", help="Data source: 'account' (full account) or 'token' (current API key)."),
|
|
20
|
+
] = None,
|
|
21
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
22
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Show your CometAPI account balance.
|
|
25
|
+
|
|
26
|
+
By default shows account-level balance (requires access token), falling back
|
|
27
|
+
to per-token billing stats. Use --source to force a specific view.
|
|
28
|
+
"""
|
|
29
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
30
|
+
client = get_client()
|
|
31
|
+
raw = client.get_balance(source=source)
|
|
32
|
+
|
|
33
|
+
# For JSON output, pass raw data for maximum information
|
|
34
|
+
if fmt == OutputFormat.JSON:
|
|
35
|
+
output(raw, fmt)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Build human-friendly table display
|
|
39
|
+
bal = raw.get("balance", 0)
|
|
40
|
+
used = raw.get("used", 0)
|
|
41
|
+
total = raw.get("total_topped_up", 0)
|
|
42
|
+
data_source = raw.get("source", "unknown")
|
|
43
|
+
|
|
44
|
+
if data_source == "billing":
|
|
45
|
+
display: dict[str, str] = {
|
|
46
|
+
"limit": f"${bal:,.2f}" if bal >= 0 else "Unlimited",
|
|
47
|
+
"used": f"${used:,.2f}",
|
|
48
|
+
}
|
|
49
|
+
else:
|
|
50
|
+
display = {
|
|
51
|
+
"available_balance": f"${bal:,.2f}" if bal >= 0 else "Unlimited",
|
|
52
|
+
"used": f"${used:,.2f}",
|
|
53
|
+
"total_topped_up": f"${total:,.2f}" if total >= 0 else "N/A",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
output(display, fmt, title="CometAPI Balance")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Chat command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ..config import get_client, get_default_model
|
|
12
|
+
from ..errors import handle_errors
|
|
13
|
+
from ..formatters import OutputFormat, resolve_format
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@handle_errors
|
|
17
|
+
def chat(
|
|
18
|
+
ctx: typer.Context,
|
|
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.")] = None,
|
|
21
|
+
system: Annotated[str | None, typer.Option("--system", "-s", help="System prompt.")] = None,
|
|
22
|
+
temperature: Annotated[float | None, typer.Option("--temperature", "-t", help="Sampling temperature.")] = None,
|
|
23
|
+
max_tokens: Annotated[int | None, typer.Option("--max-tokens", help="Max tokens in response.")] = None,
|
|
24
|
+
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.")] = None,
|
|
26
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Send a chat message, or start interactive REPL (no args)."""
|
|
29
|
+
# No message → enter REPL mode
|
|
30
|
+
if message is None:
|
|
31
|
+
from .chat_repl import run_chat_repl
|
|
32
|
+
|
|
33
|
+
run_chat_repl(
|
|
34
|
+
model=model,
|
|
35
|
+
system=system,
|
|
36
|
+
temperature=temperature,
|
|
37
|
+
max_tokens=max_tokens,
|
|
38
|
+
stream=stream,
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
from rich.markdown import Markdown
|
|
43
|
+
|
|
44
|
+
from ..console import console
|
|
45
|
+
|
|
46
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
47
|
+
client = get_client()
|
|
48
|
+
resolved_model = model or get_default_model()
|
|
49
|
+
|
|
50
|
+
messages: list[dict[str, str]] = []
|
|
51
|
+
if system:
|
|
52
|
+
messages.append({"role": "system", "content": system})
|
|
53
|
+
messages.append({"role": "user", "content": message})
|
|
54
|
+
|
|
55
|
+
kwargs: dict = {"model": resolved_model, "messages": messages}
|
|
56
|
+
if temperature is not None:
|
|
57
|
+
kwargs["temperature"] = temperature
|
|
58
|
+
if max_tokens is not None:
|
|
59
|
+
kwargs["max_tokens"] = max_tokens
|
|
60
|
+
|
|
61
|
+
# JSON output: disable streaming, return structured data
|
|
62
|
+
if fmt == OutputFormat.JSON:
|
|
63
|
+
kwargs["stream"] = False
|
|
64
|
+
response = client.chat.completions.create(**kwargs)
|
|
65
|
+
text = response.choices[0].message.content or ""
|
|
66
|
+
print(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"model": getattr(response, "model", resolved_model),
|
|
70
|
+
"role": "assistant",
|
|
71
|
+
"content": text,
|
|
72
|
+
"usage": {
|
|
73
|
+
"prompt_tokens": getattr(response.usage, "prompt_tokens", None),
|
|
74
|
+
"completion_tokens": getattr(response.usage, "completion_tokens", None),
|
|
75
|
+
"total_tokens": getattr(response.usage, "total_tokens", None),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
ensure_ascii=False,
|
|
79
|
+
indent=2,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Interactive output
|
|
85
|
+
if stream:
|
|
86
|
+
kwargs["stream"] = True
|
|
87
|
+
stream_response = client.chat.completions.create(**kwargs)
|
|
88
|
+
full_text = ""
|
|
89
|
+
for chunk in stream_response:
|
|
90
|
+
if not chunk.choices:
|
|
91
|
+
continue
|
|
92
|
+
delta = chunk.choices[0].delta.content or ""
|
|
93
|
+
sys.stdout.write(delta)
|
|
94
|
+
sys.stdout.flush()
|
|
95
|
+
full_text += delta
|
|
96
|
+
sys.stdout.write("\n")
|
|
97
|
+
sys.stdout.flush()
|
|
98
|
+
console.print(f"[dim]Model: {resolved_model}[/dim]")
|
|
99
|
+
else:
|
|
100
|
+
kwargs["stream"] = False
|
|
101
|
+
response = client.chat.completions.create(**kwargs)
|
|
102
|
+
text = response.choices[0].message.content or ""
|
|
103
|
+
console.print(Markdown(text))
|
|
104
|
+
console.print(f"[dim]Model: {getattr(response, 'model', resolved_model)}[/dim]")
|