general-augment-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.
- general_augment_cli-0.1.0.dist-info/METADATA +180 -0
- general_augment_cli-0.1.0.dist-info/RECORD +42 -0
- general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
- general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
- platform_cli/__init__.py +5 -0
- platform_cli/branding.py +27 -0
- platform_cli/client.py +179 -0
- platform_cli/commands/__init__.py +1 -0
- platform_cli/commands/approvals.py +150 -0
- platform_cli/commands/auth.py +96 -0
- platform_cli/commands/billing.py +143 -0
- platform_cli/commands/channels.py +212 -0
- platform_cli/commands/deploy.py +72 -0
- platform_cli/commands/dev.py +38 -0
- platform_cli/commands/doctor.py +170 -0
- platform_cli/commands/identity.py +433 -0
- platform_cli/commands/init.py +55 -0
- platform_cli/commands/integrate.py +94 -0
- platform_cli/commands/keys.py +116 -0
- platform_cli/commands/logs.py +43 -0
- platform_cli/commands/mcp.py +258 -0
- platform_cli/commands/memory.py +316 -0
- platform_cli/commands/mock.py +30 -0
- platform_cli/commands/model_providers.py +226 -0
- platform_cli/commands/observability.py +174 -0
- platform_cli/commands/onboarding.py +72 -0
- platform_cli/commands/projects.py +302 -0
- platform_cli/commands/skills.py +116 -0
- platform_cli/commands/smoke.py +280 -0
- platform_cli/commands/status.py +49 -0
- platform_cli/commands/tools.py +179 -0
- platform_cli/commands/users.py +150 -0
- platform_cli/commands/validate.py +96 -0
- platform_cli/commands/verify.py +648 -0
- platform_cli/config.py +114 -0
- platform_cli/errors.py +103 -0
- platform_cli/local_mock.py +1392 -0
- platform_cli/main.py +130 -0
- platform_cli/openapi.py +1048 -0
- platform_cli/output.py +47 -0
- platform_cli/readiness.py +176 -0
- platform_cli/runtime.py +22 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Memory management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
10
|
+
from platform_cli.output import print_json, print_success, table
|
|
11
|
+
from platform_cli.runtime import Runtime
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Manage tenant user memory.")
|
|
14
|
+
|
|
15
|
+
MEMORY_FACT_TYPES = {"preference", "fact", "entity", "summary"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("store")
|
|
19
|
+
def store_memory(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
fact: Annotated[str, typer.Argument(help="Durable memory fact to store.")],
|
|
22
|
+
user: Annotated[str, typer.Option("--user", help="Tenant app user id.")],
|
|
23
|
+
project: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Option(help="Project id, slug, or name when using a management key."),
|
|
26
|
+
] = None,
|
|
27
|
+
fact_type: Annotated[
|
|
28
|
+
str,
|
|
29
|
+
typer.Option("--fact-type", help="Memory fact type: preference, fact, entity, summary."),
|
|
30
|
+
] = "fact",
|
|
31
|
+
importance: Annotated[
|
|
32
|
+
float,
|
|
33
|
+
typer.Option("--importance", min=0.0, max=1.0, help="Memory importance score."),
|
|
34
|
+
] = 0.8,
|
|
35
|
+
source: Annotated[
|
|
36
|
+
str,
|
|
37
|
+
typer.Option(help="Non-secret source label for this memory write."),
|
|
38
|
+
] = "genaug-cli-memory",
|
|
39
|
+
metadata: Annotated[
|
|
40
|
+
list[str] | None,
|
|
41
|
+
typer.Option("--metadata", help="Metadata as key=value. Repeatable."),
|
|
42
|
+
] = None,
|
|
43
|
+
idempotency_key: Annotated[
|
|
44
|
+
str | None,
|
|
45
|
+
typer.Option(help="Replay-safe key for retryable memory writes."),
|
|
46
|
+
] = None,
|
|
47
|
+
json_output: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
50
|
+
] = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Store one explicit memory fact for a tenant app user."""
|
|
53
|
+
normalized_fact_type = _fact_type(fact_type)
|
|
54
|
+
runtime: Runtime = ctx.obj
|
|
55
|
+
payload = {
|
|
56
|
+
"user_id": user,
|
|
57
|
+
"fact": fact,
|
|
58
|
+
"fact_type": normalized_fact_type,
|
|
59
|
+
"importance_score": importance,
|
|
60
|
+
"source": source,
|
|
61
|
+
"metadata": _metadata_pairs(metadata or []),
|
|
62
|
+
}
|
|
63
|
+
if idempotency_key:
|
|
64
|
+
payload["idempotency_key"] = idempotency_key
|
|
65
|
+
with runtime.client() as client:
|
|
66
|
+
project_payload, headers = _project_context(client, project)
|
|
67
|
+
response = client.app(
|
|
68
|
+
"POST",
|
|
69
|
+
"/api/v1/agent/memory/store",
|
|
70
|
+
json=payload,
|
|
71
|
+
headers=headers,
|
|
72
|
+
)
|
|
73
|
+
if json_output:
|
|
74
|
+
print_json(response)
|
|
75
|
+
return
|
|
76
|
+
table(
|
|
77
|
+
"Stored memory",
|
|
78
|
+
["Field", "Value"],
|
|
79
|
+
[
|
|
80
|
+
["Project", _project_label(project_payload)],
|
|
81
|
+
["User", user],
|
|
82
|
+
["Memory ID", _value(response, "memory_id")],
|
|
83
|
+
["Type", normalized_fact_type],
|
|
84
|
+
["Source", _value(response, "source") or source],
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("search")
|
|
90
|
+
def search_memory(
|
|
91
|
+
ctx: typer.Context,
|
|
92
|
+
user: Annotated[str, typer.Option("--user", help="Tenant app user id.")],
|
|
93
|
+
project: Annotated[
|
|
94
|
+
str | None,
|
|
95
|
+
typer.Option(help="Project id, slug, or name when using a management key."),
|
|
96
|
+
] = None,
|
|
97
|
+
query: Annotated[
|
|
98
|
+
str,
|
|
99
|
+
typer.Option("--query", "-q", help="Semantic memory query. Empty returns recent facts."),
|
|
100
|
+
] = "",
|
|
101
|
+
limit: Annotated[int, typer.Option(min=1, max=50, help="Maximum facts to return.")] = 10,
|
|
102
|
+
min_similarity: Annotated[
|
|
103
|
+
float,
|
|
104
|
+
typer.Option("--min-similarity", min=0.0, max=1.0, help="Minimum semantic similarity."),
|
|
105
|
+
] = 0.7,
|
|
106
|
+
fact_type: Annotated[
|
|
107
|
+
str | None,
|
|
108
|
+
typer.Option("--fact-type", help="Optional fact type filter."),
|
|
109
|
+
] = None,
|
|
110
|
+
min_importance: Annotated[
|
|
111
|
+
float | None,
|
|
112
|
+
typer.Option("--min-importance", min=0.0, max=1.0, help="Optional importance filter."),
|
|
113
|
+
] = None,
|
|
114
|
+
source: Annotated[
|
|
115
|
+
str | None,
|
|
116
|
+
typer.Option(help="Optional source filter."),
|
|
117
|
+
] = None,
|
|
118
|
+
json_output: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
121
|
+
] = False,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Search one tenant user's memory facts."""
|
|
124
|
+
payload: dict[str, Any] = {
|
|
125
|
+
"user_id": user,
|
|
126
|
+
"query": query,
|
|
127
|
+
"limit": limit,
|
|
128
|
+
"min_similarity": min_similarity,
|
|
129
|
+
}
|
|
130
|
+
if fact_type is not None:
|
|
131
|
+
payload["fact_type"] = _fact_type(fact_type)
|
|
132
|
+
if min_importance is not None:
|
|
133
|
+
payload["min_importance"] = min_importance
|
|
134
|
+
if source:
|
|
135
|
+
payload["source"] = source
|
|
136
|
+
runtime: Runtime = ctx.obj
|
|
137
|
+
with runtime.client() as client:
|
|
138
|
+
_, headers = _project_context(client, project)
|
|
139
|
+
response = client.app(
|
|
140
|
+
"POST",
|
|
141
|
+
"/api/v1/agent/memory/search",
|
|
142
|
+
json=payload,
|
|
143
|
+
headers=headers,
|
|
144
|
+
)
|
|
145
|
+
if json_output:
|
|
146
|
+
print_json(response)
|
|
147
|
+
return
|
|
148
|
+
facts = response.get("facts", []) if isinstance(response, dict) else []
|
|
149
|
+
rows = [
|
|
150
|
+
[
|
|
151
|
+
fact.get("id") or fact.get("memory_id") or "",
|
|
152
|
+
fact.get("fact_type", ""),
|
|
153
|
+
fact.get("content", ""),
|
|
154
|
+
fact.get("importance_score", ""),
|
|
155
|
+
fact.get("similarity", ""),
|
|
156
|
+
fact.get("source", ""),
|
|
157
|
+
]
|
|
158
|
+
for fact in facts
|
|
159
|
+
if isinstance(fact, dict)
|
|
160
|
+
]
|
|
161
|
+
table(
|
|
162
|
+
f"Memory for {user}",
|
|
163
|
+
["ID", "Type", "Content", "Importance", "Similarity", "Source"],
|
|
164
|
+
rows,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command("profile")
|
|
169
|
+
def memory_profile(
|
|
170
|
+
ctx: typer.Context,
|
|
171
|
+
user: Annotated[str, typer.Option("--user", help="Tenant app user id.")],
|
|
172
|
+
project: Annotated[
|
|
173
|
+
str | None,
|
|
174
|
+
typer.Option(help="Project id, slug, or name when using a management key."),
|
|
175
|
+
] = None,
|
|
176
|
+
json_output: Annotated[
|
|
177
|
+
bool,
|
|
178
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
179
|
+
] = False,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Show one tenant user's memory profile and recent facts."""
|
|
182
|
+
runtime: Runtime = ctx.obj
|
|
183
|
+
with runtime.client() as client:
|
|
184
|
+
_, headers = _project_context(client, project)
|
|
185
|
+
response = client.app(
|
|
186
|
+
"GET",
|
|
187
|
+
f"/api/v1/agent/memory/profile/{encode_path_segment(user)}",
|
|
188
|
+
headers=headers,
|
|
189
|
+
)
|
|
190
|
+
if json_output:
|
|
191
|
+
print_json(response)
|
|
192
|
+
return
|
|
193
|
+
profile = response.get("profile", {}) if isinstance(response, dict) else {}
|
|
194
|
+
recent = response.get("recent_facts", []) if isinstance(response, dict) else []
|
|
195
|
+
table(
|
|
196
|
+
f"Memory profile for {user}",
|
|
197
|
+
["Field", "Value"],
|
|
198
|
+
[
|
|
199
|
+
["General Augment user", _value(response, "general_augment_user_id")],
|
|
200
|
+
["Total facts", _value(response, "total_facts")],
|
|
201
|
+
["Profile keys", ", ".join(sorted(profile)) if isinstance(profile, dict) else ""],
|
|
202
|
+
["Recent facts", len(recent) if isinstance(recent, list) else 0],
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.command("delete")
|
|
208
|
+
def delete_memory(
|
|
209
|
+
ctx: typer.Context,
|
|
210
|
+
memory_id: Annotated[str, typer.Argument(help="Memory fact id to delete.")],
|
|
211
|
+
user: Annotated[str, typer.Option("--user", help="Tenant app user id.")],
|
|
212
|
+
project: Annotated[
|
|
213
|
+
str | None,
|
|
214
|
+
typer.Option(help="Project id, slug, or name when using a management key."),
|
|
215
|
+
] = None,
|
|
216
|
+
json_output: Annotated[
|
|
217
|
+
bool,
|
|
218
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
219
|
+
] = False,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Delete one memory fact for one tenant app user."""
|
|
222
|
+
runtime: Runtime = ctx.obj
|
|
223
|
+
with runtime.client() as client:
|
|
224
|
+
_, headers = _project_context(client, project)
|
|
225
|
+
response = client.app(
|
|
226
|
+
"DELETE",
|
|
227
|
+
f"/api/v1/agent/memory/{encode_path_segment(memory_id)}",
|
|
228
|
+
params={"user_id": user},
|
|
229
|
+
headers=headers,
|
|
230
|
+
)
|
|
231
|
+
if json_output:
|
|
232
|
+
print_json(response)
|
|
233
|
+
return
|
|
234
|
+
status = response.get("status", "deleted") if isinstance(response, dict) else "deleted"
|
|
235
|
+
print_success(f"Memory {memory_id} {status}.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command("purge-user")
|
|
239
|
+
def purge_user_memory(
|
|
240
|
+
ctx: typer.Context,
|
|
241
|
+
user: Annotated[str, typer.Option("--user", help="Tenant app user id.")],
|
|
242
|
+
project: Annotated[
|
|
243
|
+
str | None,
|
|
244
|
+
typer.Option(help="Project id, slug, or name when using a management key."),
|
|
245
|
+
] = None,
|
|
246
|
+
yes: Annotated[
|
|
247
|
+
bool,
|
|
248
|
+
typer.Option("--yes", help="Confirm purging all scoped memory for this app user."),
|
|
249
|
+
] = False,
|
|
250
|
+
json_output: Annotated[
|
|
251
|
+
bool,
|
|
252
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
253
|
+
] = False,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Purge all memory facts for one tenant app user."""
|
|
256
|
+
if not yes and not typer.confirm(f"Purge all memory for app user {user}?"):
|
|
257
|
+
raise typer.Exit(1)
|
|
258
|
+
runtime: Runtime = ctx.obj
|
|
259
|
+
with runtime.client() as client:
|
|
260
|
+
_, headers = _project_context(client, project)
|
|
261
|
+
response = client.app(
|
|
262
|
+
"DELETE",
|
|
263
|
+
f"/api/v1/agent/memory/user/{encode_path_segment(user)}",
|
|
264
|
+
headers=headers,
|
|
265
|
+
)
|
|
266
|
+
if json_output:
|
|
267
|
+
print_json(response)
|
|
268
|
+
return
|
|
269
|
+
deleted_count = response.get("deleted_count", 0) if isinstance(response, dict) else 0
|
|
270
|
+
print_success(f"Purged {deleted_count} memory fact(s) for {user}.")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _project_context(
|
|
274
|
+
client: Any,
|
|
275
|
+
project: str | None,
|
|
276
|
+
) -> tuple[dict[str, Any] | None, dict[str, str]]:
|
|
277
|
+
"""Return project metadata and app-facing project context headers."""
|
|
278
|
+
if not project:
|
|
279
|
+
return None, {}
|
|
280
|
+
project_payload = resolve_project(client, project)
|
|
281
|
+
return project_payload, {"X-Project-ID": str(project_payload["id"])}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _project_label(project: dict[str, Any] | None) -> str:
|
|
285
|
+
"""Return a compact display label for project context."""
|
|
286
|
+
if not project:
|
|
287
|
+
return "configured project"
|
|
288
|
+
return str(project.get("slug") or project.get("name") or project.get("id") or "")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _metadata_pairs(values: list[str]) -> dict[str, str]:
|
|
292
|
+
"""Parse repeated key=value metadata flags."""
|
|
293
|
+
parsed: dict[str, str] = {}
|
|
294
|
+
for item in values:
|
|
295
|
+
key, separator, value = item.partition("=")
|
|
296
|
+
if not separator or not key.strip():
|
|
297
|
+
raise typer.BadParameter("--metadata values must use key=value.")
|
|
298
|
+
parsed[key.strip()] = value
|
|
299
|
+
return parsed
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _fact_type(value: str) -> str:
|
|
303
|
+
"""Validate memory fact type for the public memory API."""
|
|
304
|
+
normalized = value.strip().lower()
|
|
305
|
+
if normalized not in MEMORY_FACT_TYPES:
|
|
306
|
+
raise typer.BadParameter(
|
|
307
|
+
"--fact-type must be one of: " + ", ".join(sorted(MEMORY_FACT_TYPES))
|
|
308
|
+
)
|
|
309
|
+
return normalized
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _value(payload: object, key: str) -> object:
|
|
313
|
+
"""Safely read a value from a response mapping."""
|
|
314
|
+
if isinstance(payload, dict):
|
|
315
|
+
return payload.get(key, "")
|
|
316
|
+
return ""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Local mock server command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.local_mock import DEFAULT_HOST, DEFAULT_PORT, run_server
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mock(
|
|
13
|
+
host: Annotated[
|
|
14
|
+
str,
|
|
15
|
+
typer.Option("--host", help="Interface to bind the local mock server to."),
|
|
16
|
+
] = DEFAULT_HOST,
|
|
17
|
+
port: Annotated[
|
|
18
|
+
int,
|
|
19
|
+
typer.Option("--port", min=1, max=65535, help="TCP port for the local mock server."),
|
|
20
|
+
] = DEFAULT_PORT,
|
|
21
|
+
quiet: Annotated[
|
|
22
|
+
bool,
|
|
23
|
+
typer.Option("--quiet", help="Suppress per-request HTTP access logs."),
|
|
24
|
+
] = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Run the local General Augment HTTP mock for app contract tests."""
|
|
27
|
+
try:
|
|
28
|
+
run_server(host, port, quiet=quiet)
|
|
29
|
+
except KeyboardInterrupt:
|
|
30
|
+
typer.echo("\nStopped General Augment local mock.", err=True)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Tenant-owned model provider credential commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
11
|
+
from platform_cli.output import print_json, print_success, table
|
|
12
|
+
from platform_cli.runtime import Runtime
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage tenant-owned model provider keys.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def list_model_providers(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
21
|
+
json_output: Annotated[
|
|
22
|
+
bool,
|
|
23
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
24
|
+
] = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""List model provider credentials without exposing secrets."""
|
|
27
|
+
runtime: Runtime = ctx.obj
|
|
28
|
+
with runtime.client() as client:
|
|
29
|
+
project_payload = resolve_project(client, project)
|
|
30
|
+
response = client.admin(
|
|
31
|
+
"GET",
|
|
32
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/model-providers",
|
|
33
|
+
)
|
|
34
|
+
if json_output:
|
|
35
|
+
print_json(response)
|
|
36
|
+
return
|
|
37
|
+
items = response.get("items", []) if isinstance(response, dict) else []
|
|
38
|
+
rows = [_provider_row(item) for item in items if isinstance(item, dict)]
|
|
39
|
+
table(
|
|
40
|
+
"Model providers",
|
|
41
|
+
["Provider", "Status", "API Mode", "Base URL", "Prefixes", "Validated At"],
|
|
42
|
+
rows,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("set")
|
|
47
|
+
def set_model_provider(
|
|
48
|
+
ctx: typer.Context,
|
|
49
|
+
provider: Annotated[str, typer.Argument(help="Provider id, such as openai or anthropic.")],
|
|
50
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
51
|
+
api_key: Annotated[
|
|
52
|
+
str | None,
|
|
53
|
+
typer.Option("--api-key", help="Provider API key. Omit to enter a hidden prompt."),
|
|
54
|
+
] = None,
|
|
55
|
+
api_key_env: Annotated[
|
|
56
|
+
str | None,
|
|
57
|
+
typer.Option("--api-key-env", help="Read the provider API key from this env var."),
|
|
58
|
+
] = None,
|
|
59
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="Optional base URL.")] = None,
|
|
60
|
+
api_mode: Annotated[str | None, typer.Option("--api-mode", help="Optional API mode.")] = None,
|
|
61
|
+
model_prefix: Annotated[
|
|
62
|
+
list[str] | None,
|
|
63
|
+
typer.Option("--model-prefix", help="Model prefix this key can serve. Repeatable."),
|
|
64
|
+
] = None,
|
|
65
|
+
json_output: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
68
|
+
] = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Store or rotate one tenant-owned model provider API key."""
|
|
71
|
+
secret = _provider_api_key(api_key=api_key, api_key_env=api_key_env)
|
|
72
|
+
payload: dict[str, object] = {"api_key": secret}
|
|
73
|
+
if base_url is not None:
|
|
74
|
+
payload["base_url"] = base_url
|
|
75
|
+
if api_mode is not None:
|
|
76
|
+
payload["api_mode"] = api_mode
|
|
77
|
+
if model_prefix:
|
|
78
|
+
payload["model_prefixes"] = list(model_prefix)
|
|
79
|
+
runtime: Runtime = ctx.obj
|
|
80
|
+
with runtime.client() as client:
|
|
81
|
+
project_payload = resolve_project(client, project)
|
|
82
|
+
response = client.admin(
|
|
83
|
+
"PUT",
|
|
84
|
+
(
|
|
85
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/model-providers/"
|
|
86
|
+
f"{encode_path_segment(provider)}"
|
|
87
|
+
),
|
|
88
|
+
json=payload,
|
|
89
|
+
)
|
|
90
|
+
if json_output:
|
|
91
|
+
print_json(response)
|
|
92
|
+
return
|
|
93
|
+
stored_provider = _value(response, "provider") or provider
|
|
94
|
+
print_success(f"Stored model provider credential for {stored_provider}.")
|
|
95
|
+
table(
|
|
96
|
+
"Model provider",
|
|
97
|
+
["Field", "Value"],
|
|
98
|
+
[
|
|
99
|
+
["Provider", _value(response, "provider") or provider],
|
|
100
|
+
["Status", _value(response, "status")],
|
|
101
|
+
["API Mode", _value(response, "api_mode")],
|
|
102
|
+
["Base URL Configured", _value(response, "base_url_configured")],
|
|
103
|
+
["Model Prefixes", _prefixes(response)],
|
|
104
|
+
["Validated At", _value(response, "last_validated_at")],
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("health")
|
|
110
|
+
def check_model_provider_health(
|
|
111
|
+
ctx: typer.Context,
|
|
112
|
+
provider: Annotated[str, typer.Argument(help="Provider id, such as openai or anthropic.")],
|
|
113
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
114
|
+
json_output: Annotated[
|
|
115
|
+
bool,
|
|
116
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
117
|
+
] = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Validate one stored model provider credential."""
|
|
120
|
+
runtime: Runtime = ctx.obj
|
|
121
|
+
with runtime.client() as client:
|
|
122
|
+
project_payload = resolve_project(client, project)
|
|
123
|
+
response = client.admin(
|
|
124
|
+
"POST",
|
|
125
|
+
(
|
|
126
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/model-providers/"
|
|
127
|
+
f"{encode_path_segment(provider)}/health-check"
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
if json_output:
|
|
131
|
+
print_json(response)
|
|
132
|
+
return
|
|
133
|
+
table(
|
|
134
|
+
"Model provider health",
|
|
135
|
+
["Field", "Value"],
|
|
136
|
+
[
|
|
137
|
+
["Provider", _value(response, "provider") or provider],
|
|
138
|
+
["Status", _value(response, "status")],
|
|
139
|
+
["Message", _value(response, "message")],
|
|
140
|
+
["HTTP Status", _value(response, "status_code")],
|
|
141
|
+
["Retryable", _value(response, "retryable")],
|
|
142
|
+
["Latency MS", _value(response, "latency_ms")],
|
|
143
|
+
["Validated At", _value(response, "last_validated_at")],
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command("revoke")
|
|
149
|
+
def revoke_model_provider(
|
|
150
|
+
ctx: typer.Context,
|
|
151
|
+
provider: Annotated[str, typer.Argument(help="Provider id, such as openai or anthropic.")],
|
|
152
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
153
|
+
yes: Annotated[
|
|
154
|
+
bool,
|
|
155
|
+
typer.Option("--yes", help="Confirm revoking this tenant-owned provider key."),
|
|
156
|
+
] = False,
|
|
157
|
+
json_output: Annotated[
|
|
158
|
+
bool,
|
|
159
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
160
|
+
] = False,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Revoke one tenant-owned model provider credential."""
|
|
163
|
+
if not yes and not typer.confirm(f"Revoke model provider credential for {provider}?"):
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
runtime: Runtime = ctx.obj
|
|
166
|
+
with runtime.client() as client:
|
|
167
|
+
project_payload = resolve_project(client, project)
|
|
168
|
+
response = client.admin(
|
|
169
|
+
"DELETE",
|
|
170
|
+
(
|
|
171
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/model-providers/"
|
|
172
|
+
f"{encode_path_segment(provider)}"
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
if json_output:
|
|
176
|
+
print_json(response)
|
|
177
|
+
return
|
|
178
|
+
revoked_provider = _value(response, "provider") or provider
|
|
179
|
+
print_success(f"Revoked model provider credential for {revoked_provider}.")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _provider_api_key(*, api_key: str | None, api_key_env: str | None) -> str:
|
|
183
|
+
"""Resolve a provider API key from an option, env var, or hidden prompt."""
|
|
184
|
+
|
|
185
|
+
if api_key and api_key_env:
|
|
186
|
+
raise typer.BadParameter("Use only one of --api-key or --api-key-env.")
|
|
187
|
+
if api_key_env:
|
|
188
|
+
value = os.getenv(api_key_env)
|
|
189
|
+
if not value:
|
|
190
|
+
raise typer.BadParameter(f"Environment variable {api_key_env} is not set.")
|
|
191
|
+
return value
|
|
192
|
+
if api_key:
|
|
193
|
+
return api_key
|
|
194
|
+
return str(typer.prompt("Provider API key", hide_input=True))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _provider_row(item: dict[str, object]) -> list[object]:
|
|
198
|
+
"""Return one model provider list row."""
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
item.get("provider", ""),
|
|
202
|
+
item.get("status", ""),
|
|
203
|
+
item.get("api_mode", "") or "",
|
|
204
|
+
item.get("base_url_configured", ""),
|
|
205
|
+
_prefixes(item),
|
|
206
|
+
item.get("last_validated_at", "") or "",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _prefixes(payload: object) -> str:
|
|
211
|
+
"""Return model prefixes as a printable string."""
|
|
212
|
+
|
|
213
|
+
if not isinstance(payload, dict):
|
|
214
|
+
return ""
|
|
215
|
+
prefixes = payload.get("model_prefixes")
|
|
216
|
+
if not isinstance(prefixes, list):
|
|
217
|
+
return ""
|
|
218
|
+
return ", ".join(str(prefix) for prefix in prefixes)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _value(payload: object, key: str) -> object:
|
|
222
|
+
"""Safely read one response value."""
|
|
223
|
+
|
|
224
|
+
if isinstance(payload, dict):
|
|
225
|
+
return payload.get(key, "")
|
|
226
|
+
return ""
|