qbraid-cli 0.13.2__tar.gz → 0.13.3__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.
- {qbraid_cli-0.13.2/qbraid_cli.egg-info → qbraid_cli-0.13.3}/PKG-INFO +2 -2
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/pyproject.toml +5 -2
- qbraid_cli-0.13.3/qbraid_cli/ai/__init__.py +11 -0
- qbraid_cli-0.13.3/qbraid_cli/ai/app.py +390 -0
- qbraid_cli-0.13.3/qbraid_cli/ai/token.py +66 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/main.py +3 -1
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3/qbraid_cli.egg-info}/PKG-INFO +2 -2
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/SOURCES.txt +3 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/entry_points.txt +1 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/requires.txt +1 -1
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/LICENSE +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/README.md +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/agents/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/setup.cfg +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,7 +30,7 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.3.3
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.3.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.3.11
|
|
34
34
|
Provides-Extra: compute
|
|
35
35
|
Requires-Dist: qbraid-core[compute]>=0.3.3; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qbraid-cli"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.3"
|
|
8
8
|
description = "Command Line Interface for interacting with all parts of the qBraid platform."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
"rich>=10.11.0",
|
|
33
33
|
"click<=8.3.3",
|
|
34
34
|
"questionary>=2.0.0",
|
|
35
|
-
"qbraid-core>=0.3.
|
|
35
|
+
"qbraid-core>=0.3.11",
|
|
36
36
|
]
|
|
37
37
|
requires-python = ">= 3.10"
|
|
38
38
|
|
|
@@ -49,6 +49,9 @@ dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
51
51
|
qbraid = "qbraid_cli.main:app"
|
|
52
|
+
# Separate entry point on purpose: agents re-run this on a timer, and routing it
|
|
53
|
+
# through the full CLI would import every command group to print one string.
|
|
54
|
+
qbraid-ai-token = "qbraid_cli.ai.token:main"
|
|
52
55
|
|
|
53
56
|
[tool.black]
|
|
54
57
|
line-length = 110
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright (c) 2026, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module for the 'qbraid ai' namespace.
|
|
6
|
+
|
|
7
|
+
Deliberately empty. ``qbraid_cli.ai.token`` is the console script the agents
|
|
8
|
+
re-run on a timer, and re-exporting the command module here would drag typer
|
|
9
|
+
and rich into the one process whose stdout is a credential — the cost the
|
|
10
|
+
separate entry point exists to avoid. Import ``qbraid_cli.ai.app`` directly.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# Copyright (c) 2026, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module defining commands in the 'qbraid ai' namespace.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from qbraid_cli.handlers import handle_error
|
|
16
|
+
|
|
17
|
+
ai_app = typer.Typer(
|
|
18
|
+
help=(
|
|
19
|
+
"Use qBraid as your AI provider.\n\n"
|
|
20
|
+
"Point Claude Code or Codex at the qBraid AI proxy so requests run on "
|
|
21
|
+
"your qBraid plan instead of a separate vendor subscription, and switch "
|
|
22
|
+
"back whenever you want.\n\n"
|
|
23
|
+
"Quick start: qbraid ai connect claude"
|
|
24
|
+
),
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
_AGENT_LABELS = {"claude": "Claude Code", "codex": "Codex"}
|
|
31
|
+
|
|
32
|
+
_AUTH_LABELS = {
|
|
33
|
+
"api_key_helper": "token command (auto-refreshing)",
|
|
34
|
+
"auth_command": "token command (auto-refreshing)",
|
|
35
|
+
"api_key": "API key written into the config",
|
|
36
|
+
"env_key": "API key read from an environment variable",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _warn_if_token_command_missing() -> None:
|
|
41
|
+
"""Warn when the agents won't be able to run the token command.
|
|
42
|
+
|
|
43
|
+
They invoke it by bare name, so a `pip install --user` whose bin directory
|
|
44
|
+
isn't on PATH — or a venv the agent doesn't inherit — means both agents
|
|
45
|
+
401 forever with nothing in our own output suggesting why.
|
|
46
|
+
"""
|
|
47
|
+
import shutil
|
|
48
|
+
|
|
49
|
+
executable = _ai().token_command()[0]
|
|
50
|
+
if shutil.which(executable):
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
console.print()
|
|
54
|
+
console.print(
|
|
55
|
+
f"[yellow]⚠ '{executable}' is not on PATH.[/yellow] The agents run it to fetch "
|
|
56
|
+
"a credential, so they will fail to authenticate until it resolves — "
|
|
57
|
+
"check that the directory pip installed it into is on your PATH."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ai():
|
|
62
|
+
"""Import the core AI gateway, or explain why it isn't there.
|
|
63
|
+
|
|
64
|
+
The module landed in qbraid-core 0.3.11. An older core is a realistic
|
|
65
|
+
install state (the pin only takes effect on a fresh resolve), and the bare
|
|
66
|
+
ImportError surfaces as a full traceback — the convention elsewhere in this
|
|
67
|
+
CLI is a single actionable line.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
from qbraid_core.services import ai
|
|
71
|
+
except ImportError as err: # pragma: no cover - depends on installed core
|
|
72
|
+
handle_error(
|
|
73
|
+
message=(
|
|
74
|
+
"This command needs qbraid-core>=0.3.11 (the AI gateway module "
|
|
75
|
+
"is missing from the installed version). Upgrade with: "
|
|
76
|
+
"pip install -U qbraid-core"
|
|
77
|
+
),
|
|
78
|
+
include_traceback=False,
|
|
79
|
+
)
|
|
80
|
+
raise typer.Exit(code=1) from err
|
|
81
|
+
return ai
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _agents_from(agent: str) -> list[str]:
|
|
85
|
+
"""Expand the 'all' shorthand into concrete agent names."""
|
|
86
|
+
supported = _ai().SUPPORTED_AGENTS
|
|
87
|
+
|
|
88
|
+
if agent == "all":
|
|
89
|
+
return list(supported)
|
|
90
|
+
if agent not in supported:
|
|
91
|
+
console.print(
|
|
92
|
+
f"[red]Unknown agent '{agent}'.[/red] Expected one of: " f"{', '.join(supported)}, or 'all'."
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(code=2)
|
|
95
|
+
return [agent]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _print_claude_caveats() -> None:
|
|
99
|
+
"""Consequences of pointing Claude Code at a non-Anthropic host.
|
|
100
|
+
|
|
101
|
+
All three are documented Claude Code behaviours, and all three surprise
|
|
102
|
+
people who weren't told up front.
|
|
103
|
+
"""
|
|
104
|
+
console.print()
|
|
105
|
+
console.print("[bold]Worth knowing:[/bold]")
|
|
106
|
+
console.print(" • This overrides a Claude Pro/Max/Team subscription — requests now bill to qBraid.")
|
|
107
|
+
console.print(
|
|
108
|
+
" • MCP tool search is disabled for non-Anthropic hosts. Set "
|
|
109
|
+
"ENABLE_TOOL_SEARCH=true if the proxy forwards tool_reference blocks."
|
|
110
|
+
)
|
|
111
|
+
console.print(" • Remote Control is disabled while pointed off api.anthropic.com.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@ai_app.command(name="connect")
|
|
115
|
+
def ai_connect( # pylint: disable=too-many-branches
|
|
116
|
+
agent: str = typer.Argument("all", help="Agent to configure: claude, codex, or all"),
|
|
117
|
+
model: Optional[str] = typer.Option(
|
|
118
|
+
None, "--model", "-m", help="Model id to pin (see `qbraid ai models`)"
|
|
119
|
+
),
|
|
120
|
+
refresh_interval: Optional[int] = typer.Option(
|
|
121
|
+
None,
|
|
122
|
+
"--refresh-interval",
|
|
123
|
+
help="Seconds between credential refreshes (default: 300)",
|
|
124
|
+
),
|
|
125
|
+
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the qBraid API base URL"),
|
|
126
|
+
plaintext_key: bool = typer.Option(
|
|
127
|
+
False,
|
|
128
|
+
"--plaintext-key",
|
|
129
|
+
help="Write the API key into the config instead of using a token command",
|
|
130
|
+
),
|
|
131
|
+
):
|
|
132
|
+
"""Route an agent through the qBraid AI proxy.
|
|
133
|
+
|
|
134
|
+
Writes to the agent's own config — Claude Code's settings.json, Codex's
|
|
135
|
+
config.toml — and records whatever it replaces so `qbraid ai disconnect`
|
|
136
|
+
can put it back.
|
|
137
|
+
|
|
138
|
+
The credential is not written into either config by default. Both agents
|
|
139
|
+
are pointed at a command that prints a token, so the key stays in your
|
|
140
|
+
qBraid config and a rotated token is picked up automatically.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
qbraid ai connect claude
|
|
144
|
+
qbraid ai connect all --model claude-sonnet-4-6
|
|
145
|
+
"""
|
|
146
|
+
ai = _ai()
|
|
147
|
+
|
|
148
|
+
agents = _agents_from(agent)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
_, kind = ai.resolve_credential()
|
|
152
|
+
except ai.AiGatewayError as err:
|
|
153
|
+
handle_error(message=str(err), include_traceback=False)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
api_key = None
|
|
157
|
+
if plaintext_key:
|
|
158
|
+
if kind == "access_token":
|
|
159
|
+
console.print(
|
|
160
|
+
"[yellow]--plaintext-key is only meaningful for API keys; "
|
|
161
|
+
"access tokens rotate and must use the token command.[/yellow]"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
api_key, _ = ai.resolve_credential()
|
|
165
|
+
if "codex" in agents:
|
|
166
|
+
console.print(
|
|
167
|
+
"[yellow]--plaintext-key applies to Claude Code only; "
|
|
168
|
+
"Codex keeps using the token command.[/yellow]"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for name in agents:
|
|
172
|
+
try:
|
|
173
|
+
result = ai.connect_agent(
|
|
174
|
+
name,
|
|
175
|
+
api_url=api_url,
|
|
176
|
+
api_key=api_key,
|
|
177
|
+
model=model,
|
|
178
|
+
refresh_seconds=refresh_interval,
|
|
179
|
+
)
|
|
180
|
+
except ai.AiGatewayError as err:
|
|
181
|
+
handle_error(message=str(err), include_traceback=False)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
label = _AGENT_LABELS.get(name, name)
|
|
185
|
+
console.print(f"[green]✓[/green] {label} → [cyan]{result['gateway_url']}[/cyan]")
|
|
186
|
+
console.print(f" config: [dim]{result['path']}[/dim]")
|
|
187
|
+
if result.get("model"):
|
|
188
|
+
console.print(f" model: [dim]{result['model']}[/dim]")
|
|
189
|
+
cred = "access token" if kind == "access_token" else "API key"
|
|
190
|
+
how = _AUTH_LABELS.get(result.get("auth_mode"), result.get("auth_mode", "?"))
|
|
191
|
+
console.print(f" auth: [dim]qBraid {cred} via {how}[/dim]")
|
|
192
|
+
|
|
193
|
+
if not plaintext_key:
|
|
194
|
+
_warn_if_token_command_missing()
|
|
195
|
+
|
|
196
|
+
if "claude" in agents:
|
|
197
|
+
_print_claude_caveats()
|
|
198
|
+
|
|
199
|
+
console.print()
|
|
200
|
+
console.print("[dim]Restart the agent to pick up the new configuration.[/dim]")
|
|
201
|
+
console.print("[dim]Revert any time with: qbraid ai disconnect[/dim]")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@ai_app.command(name="disconnect")
|
|
205
|
+
def ai_disconnect(
|
|
206
|
+
agent: str = typer.Argument("all", help="Agent to restore: claude, codex, or all"),
|
|
207
|
+
):
|
|
208
|
+
"""Restore an agent's previous configuration.
|
|
209
|
+
|
|
210
|
+
Puts back exactly what was there before connecting — including removing
|
|
211
|
+
settings that weren't present to begin with.
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
qbraid ai disconnect claude
|
|
215
|
+
"""
|
|
216
|
+
ai = _ai()
|
|
217
|
+
|
|
218
|
+
for name in _agents_from(agent):
|
|
219
|
+
try:
|
|
220
|
+
result = ai.disconnect_agent(name)
|
|
221
|
+
except ai.AiGatewayError as err:
|
|
222
|
+
handle_error(message=str(err), include_traceback=False)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
label = _AGENT_LABELS.get(name, name)
|
|
226
|
+
if result["status"] == "not_configured":
|
|
227
|
+
console.print(f"[dim]○[/dim] {label} was not configured.")
|
|
228
|
+
else:
|
|
229
|
+
restored = "restored previous settings" if result.get("restored") else "cleared"
|
|
230
|
+
console.print(f"[green]✓[/green] {label} — {restored}")
|
|
231
|
+
|
|
232
|
+
console.print()
|
|
233
|
+
# A running agent keeps its settings in memory and rewrites the file on
|
|
234
|
+
# exit, which silently puts our config back. Observed with both Claude Code
|
|
235
|
+
# and Codex, so this is guidance the user genuinely needs, not boilerplate.
|
|
236
|
+
console.print(
|
|
237
|
+
"[yellow]If the agent is running, stop it and re-run this command.[/yellow] "
|
|
238
|
+
"A running agent can rewrite its config from memory and undo the change."
|
|
239
|
+
)
|
|
240
|
+
console.print("[dim]Confirm with: qbraid ai status[/dim]")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@ai_app.command(name="status")
|
|
244
|
+
def ai_status(
|
|
245
|
+
check: bool = typer.Option(False, "--check", help="Also verify the credential against the proxy"),
|
|
246
|
+
):
|
|
247
|
+
"""Show which agents are routed through qBraid.
|
|
248
|
+
|
|
249
|
+
Pass --check to confirm the credential is actually accepted — useful when
|
|
250
|
+
a session has started failing and you want to know whether it expired.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
qbraid ai status --check
|
|
254
|
+
"""
|
|
255
|
+
ai = _ai()
|
|
256
|
+
|
|
257
|
+
status = ai.gateway_status()
|
|
258
|
+
|
|
259
|
+
table = Table(title="qBraid AI gateway")
|
|
260
|
+
table.add_column("Agent")
|
|
261
|
+
table.add_column("Routed")
|
|
262
|
+
table.add_column("Endpoint")
|
|
263
|
+
table.add_column("Model")
|
|
264
|
+
|
|
265
|
+
for name, info in status.items():
|
|
266
|
+
table.add_row(
|
|
267
|
+
_AGENT_LABELS.get(name, name),
|
|
268
|
+
"[green]yes[/green]" if info.get("connected") else "[dim]no[/dim]",
|
|
269
|
+
info.get("base_url") or "-",
|
|
270
|
+
info.get("model") or "-",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
console.print(table)
|
|
274
|
+
|
|
275
|
+
if not check:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
_, kind = ai.resolve_credential()
|
|
280
|
+
except ai.AiGatewayError as err:
|
|
281
|
+
console.print(f"[red]✗[/red] {err}")
|
|
282
|
+
raise typer.Exit(code=1) from err
|
|
283
|
+
|
|
284
|
+
console.print()
|
|
285
|
+
try:
|
|
286
|
+
ai.list_models()
|
|
287
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
288
|
+
status_code = getattr(err, "status_code", None)
|
|
289
|
+
if status_code in (401, 403):
|
|
290
|
+
# The one case where the credential is actually the problem.
|
|
291
|
+
console.print(f"[red]✗[/red] credential rejected ({status_code})")
|
|
292
|
+
console.print()
|
|
293
|
+
console.print(ai.credential_expired_hint(kind))
|
|
294
|
+
elif status_code is not None:
|
|
295
|
+
console.print(f"[red]✗[/red] proxy returned {status_code}: {err}")
|
|
296
|
+
console.print("[dim]Your credential may be fine — this looks server-side.[/dim]")
|
|
297
|
+
else:
|
|
298
|
+
console.print(f"[red]✗[/red] could not reach the proxy: {err}")
|
|
299
|
+
console.print("[dim]Check your connection and `qbraid configure` URL.[/dim]")
|
|
300
|
+
raise typer.Exit(code=1) from err
|
|
301
|
+
|
|
302
|
+
console.print(
|
|
303
|
+
"[green]✓[/green] credential accepted "
|
|
304
|
+
f"[dim]({'access token' if kind == 'access_token' else 'API key'})[/dim]"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@ai_app.command(name="models")
|
|
309
|
+
def ai_models():
|
|
310
|
+
"""List models available on the qBraid proxy.
|
|
311
|
+
|
|
312
|
+
The proxy matches model ids exactly, so pick one of these when setting
|
|
313
|
+
`--model` — an id the proxy doesn't know is rejected on the first request.
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
qbraid ai models
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
models = _ai().list_models()
|
|
320
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
321
|
+
handle_error(message=f"Could not list models: {err}", include_traceback=False)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if not models:
|
|
325
|
+
console.print("[yellow]No models returned.[/yellow]")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
table = Table(title="Available models")
|
|
329
|
+
table.add_column("ID")
|
|
330
|
+
table.add_column("Name")
|
|
331
|
+
table.add_column("Backend")
|
|
332
|
+
table.add_column("Max tokens", justify="right")
|
|
333
|
+
|
|
334
|
+
for item in models:
|
|
335
|
+
if not isinstance(item, dict):
|
|
336
|
+
table.add_row(str(item), "-", "-", "-")
|
|
337
|
+
continue
|
|
338
|
+
# The listing is OpenAI-compatible, so the qBraid metadata (name,
|
|
339
|
+
# provider, limits) is nested under `_qbraid` rather than top level.
|
|
340
|
+
meta = item.get("_qbraid") or {}
|
|
341
|
+
max_tokens = meta.get("maxTokens")
|
|
342
|
+
table.add_row(
|
|
343
|
+
item.get("id", "-"),
|
|
344
|
+
meta.get("name", "-"),
|
|
345
|
+
meta.get("provider", "-"),
|
|
346
|
+
f"{max_tokens:,}" if isinstance(max_tokens, int) else "-",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
console.print(table)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@ai_app.command(name="quota")
|
|
353
|
+
def ai_quota():
|
|
354
|
+
"""Show remaining qBraid AI quota.
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
qbraid ai quota
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
quota = _ai().get_quota()
|
|
361
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
362
|
+
handle_error(message=f"Could not fetch quota: {err}", include_traceback=False)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
if not quota:
|
|
366
|
+
console.print("[yellow]No quota information returned.[/yellow]")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
for key, value in quota.items():
|
|
370
|
+
console.print(f" {key}: [cyan]{value}[/cyan]")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@ai_app.command(name="token", hidden=True)
|
|
374
|
+
def ai_token():
|
|
375
|
+
"""Print the qBraid credential for agents.
|
|
376
|
+
|
|
377
|
+
Hidden because it exists for Claude Code's `apiKeyHelper` and Codex's
|
|
378
|
+
`[model_providers.qbraid.auth]` command rather than for people. Prints the
|
|
379
|
+
credential and nothing else, so the agents can read it from stdout.
|
|
380
|
+
"""
|
|
381
|
+
ai = _ai()
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
credential, _ = ai.resolve_credential()
|
|
385
|
+
except ai.AiGatewayError as err:
|
|
386
|
+
# stderr, so a failure never looks like a token on stdout.
|
|
387
|
+
typer.echo(str(err), err=True)
|
|
388
|
+
raise typer.Exit(code=1) from err
|
|
389
|
+
|
|
390
|
+
typer.echo(credential)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright (c) 2026, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Standalone entry point that prints the qBraid credential to stdout.
|
|
6
|
+
|
|
7
|
+
This exists as its own console script rather than as `qbraid ai token` because
|
|
8
|
+
it is the one command that runs unattended, forever, on every configured agent
|
|
9
|
+
— Claude Code re-invokes it via ``apiKeyHelper`` and Codex via
|
|
10
|
+
``[model_providers.qbraid.auth]``, both on a timer.
|
|
11
|
+
|
|
12
|
+
Going through ``qbraid_cli.main`` would import every command group in the CLI
|
|
13
|
+
(~0.9s here) to print one string. Importing only what's needed costs ~0.2s, and
|
|
14
|
+
— more to the point — keeps this path from getting slower every time a new
|
|
15
|
+
command group is added.
|
|
16
|
+
|
|
17
|
+
Deliberately has no argument parsing: it prints the credential and nothing else,
|
|
18
|
+
so an agent reading stdout can never pick up a stray banner or prompt.
|
|
19
|
+
|
|
20
|
+
The credential is written to the binary stdout buffer rather than via ``print``
|
|
21
|
+
so the bytes are identical on every platform. Text-mode stdout on Windows
|
|
22
|
+
translates ``\n`` into ``\r\n``, which would append a stray carriage return
|
|
23
|
+
to the credential an agent reads.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _emit(credential: str) -> None:
|
|
30
|
+
"""Write the credential and exactly one newline, byte-for-byte.
|
|
31
|
+
|
|
32
|
+
Goes through ``sys.stdout.buffer`` to bypass the text layer: on Windows
|
|
33
|
+
that layer turns the newline into ``\r\n``, and an agent that does not
|
|
34
|
+
strip the carriage return sends it as part of the credential and 401s.
|
|
35
|
+
Falls back to ``print`` when stdout has been replaced by an object with no
|
|
36
|
+
binary buffer (pytest's capture, for one).
|
|
37
|
+
"""
|
|
38
|
+
buffer = getattr(sys.stdout, "buffer", None)
|
|
39
|
+
if buffer is None:
|
|
40
|
+
print(credential)
|
|
41
|
+
return
|
|
42
|
+
buffer.write(credential.encode("utf-8") + b"\n")
|
|
43
|
+
buffer.flush()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> int:
|
|
47
|
+
"""Print the credential, or explain on stderr why there isn't one."""
|
|
48
|
+
try:
|
|
49
|
+
from qbraid_core.services.ai import AiGatewayError, resolve_credential
|
|
50
|
+
except ImportError as err: # pragma: no cover - broken install
|
|
51
|
+
print(f"qbraid-core is not importable: {err}", file=sys.stderr)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
credential, _ = resolve_credential()
|
|
56
|
+
except AiGatewayError as err:
|
|
57
|
+
# stderr only — a failure must never look like a token on stdout.
|
|
58
|
+
print(str(err), file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
_emit(credential)
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__": # pragma: no cover
|
|
66
|
+
sys.exit(main())
|
|
@@ -12,6 +12,7 @@ import typer
|
|
|
12
12
|
from qbraid_cli.account import account_app
|
|
13
13
|
from qbraid_cli.admin import admin_app
|
|
14
14
|
from qbraid_cli.agents import agents_app
|
|
15
|
+
from qbraid_cli.ai.app import ai_app
|
|
15
16
|
from qbraid_cli.compute import compute_app
|
|
16
17
|
from qbraid_cli.configure import configure_app
|
|
17
18
|
from qbraid_cli.devices import devices_app
|
|
@@ -32,7 +33,7 @@ except ImportError:
|
|
|
32
33
|
|
|
33
34
|
app = typer.Typer(
|
|
34
35
|
help=(
|
|
35
|
-
"The qBraid CLI —
|
|
36
|
+
"The qBraid CLI — A unified interface for AI agents, quantum computing,\n"
|
|
36
37
|
"Python environments, and cloud compute.\n\n"
|
|
37
38
|
"Whether you're running AI coding agents locally or on cloud servers,\n"
|
|
38
39
|
"managing quantum experiments across providers, or setting up reproducible\n"
|
|
@@ -43,6 +44,7 @@ app = typer.Typer(
|
|
|
43
44
|
|
|
44
45
|
app.add_typer(admin_app, name="admin")
|
|
45
46
|
app.add_typer(agents_app, name="agents")
|
|
47
|
+
app.add_typer(ai_app, name="ai")
|
|
46
48
|
app.add_typer(compute_app, name="compute")
|
|
47
49
|
app.add_typer(lab_app, name="lab")
|
|
48
50
|
app.add_typer(configure_app, name="configure")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,7 +30,7 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.3.3
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.3.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.3.11
|
|
34
34
|
Provides-Extra: compute
|
|
35
35
|
Requires-Dist: qbraid-core[compute]>=0.3.3; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
@@ -22,6 +22,9 @@ qbraid_cli/admin/headers.py
|
|
|
22
22
|
qbraid_cli/admin/validation.py
|
|
23
23
|
qbraid_cli/agents/__init__.py
|
|
24
24
|
qbraid_cli/agents/app.py
|
|
25
|
+
qbraid_cli/ai/__init__.py
|
|
26
|
+
qbraid_cli/ai/app.py
|
|
27
|
+
qbraid_cli/ai/token.py
|
|
25
28
|
qbraid_cli/compute/__init__.py
|
|
26
29
|
qbraid_cli/compute/app.py
|
|
27
30
|
qbraid_cli/compute/profiles.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|