owui-cli 0.4.0__tar.gz → 0.5.1__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.
- {owui_cli-0.4.0 → owui_cli-0.5.1}/AGENTS.md +4 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/PKG-INFO +1 -1
- {owui_cli-0.4.0 → owui_cli-0.5.1}/pyproject.toml +1 -1
- owui_cli-0.5.1/src/owui_cli/__init__.py +1 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/src/owui_cli/cli.py +149 -79
- owui_cli-0.5.1/uv.lock +91 -0
- owui_cli-0.4.0/src/owui_cli/__init__.py +0 -1
- owui_cli-0.4.0/token-viewer.html +0 -103
- {owui_cli-0.4.0 → owui_cli-0.5.1}/.github/workflows/publish.yml +0 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/.gitignore +0 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/LICENSE +0 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/README.md +0 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/src/owui_cli/data/api-schema.json +0 -0
- {owui_cli-0.4.0 → owui_cli-0.5.1}/update-schema.py +0 -0
|
@@ -13,6 +13,10 @@ Bump both when cutting a release.
|
|
|
13
13
|
|
|
14
14
|
Every push to `main` auto-publishes to PyPI via GitHub Actions trusted publishing. Bump the version before pushing, and don't push broken code to main.
|
|
15
15
|
|
|
16
|
+
## Local development install
|
|
17
|
+
|
|
18
|
+
After making changes, reinstall locally with `uv tool install --force .` so your modified `owui-cli` is the one on your PATH.
|
|
19
|
+
|
|
16
20
|
## Testing before committing
|
|
17
21
|
|
|
18
22
|
Always verify changes work against a **production Open WebUI server** before committing. Set `OWUI_URL` and `OWUI_TOKEN` and run the relevant commands to confirm they succeed with real API responses — don't rely on local-only reasoning.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -43,8 +43,8 @@ def _api(url: str, path: str) -> str:
|
|
|
43
43
|
return f"{url}{path}"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _get(c: httpx.Client, url: str, path: str, token: str) -> httpx.Response:
|
|
47
|
-
r = c.get(_api(url, path), headers=_headers(token))
|
|
46
|
+
def _get(c: httpx.Client, url: str, path: str, token: str, params=None) -> httpx.Response:
|
|
47
|
+
r = c.get(_api(url, path), headers=_headers(token), params=params)
|
|
48
48
|
r.raise_for_status()
|
|
49
49
|
return r
|
|
50
50
|
|
|
@@ -369,81 +369,96 @@ functions_res = Resource("functions", "/api/v1/functions",
|
|
|
369
369
|
workspace_path="/workspace/functions")
|
|
370
370
|
|
|
371
371
|
|
|
372
|
-
# ── valves (user
|
|
372
|
+
# ── valves (admin/global and per-user, for tools and functions) ──────
|
|
373
|
+
#
|
|
374
|
+
# OWUI plugins can declare two valve schemas:
|
|
375
|
+
# class Valves(BaseModel) -> /api/v1/{kind}/id/{id}/valves[/spec|/update]
|
|
376
|
+
# class UserValves(BaseModel) -> /api/v1/{kind}/id/{id}/valves/user[/spec|/update]
|
|
377
|
+
#
|
|
378
|
+
# The unprefixed `valves*` commands target the admin/global path (most common
|
|
379
|
+
# case: an admin-set API key). The `valves-user*` commands target the per-user
|
|
380
|
+
# path (the calling user's personal valves on the plugin).
|
|
373
381
|
|
|
374
|
-
def _valves_get(url, token, kind, item_id):
|
|
375
|
-
"""GET valves for a tool or function. kind is 'tools' or 'functions'."""
|
|
382
|
+
def _valves_get(url, token, kind, item_id, scope=""):
|
|
376
383
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
377
|
-
r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves
|
|
384
|
+
r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token)
|
|
378
385
|
out(r.json())
|
|
379
386
|
|
|
380
|
-
def _valves_spec(url, token, kind, item_id):
|
|
381
|
-
"""GET the UserValves spec (schema) for a tool or function."""
|
|
387
|
+
def _valves_spec(url, token, kind, item_id, scope=""):
|
|
382
388
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
383
|
-
r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/
|
|
389
|
+
r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/spec", token)
|
|
384
390
|
out(r.json())
|
|
385
391
|
|
|
386
|
-
def _valves_set(url, token, kind, item_id, json_path):
|
|
387
|
-
"""POST updated user valves from a JSON file."""
|
|
392
|
+
def _valves_set(url, token, kind, item_id, json_path, scope=""):
|
|
388
393
|
with open(json_path) as f:
|
|
389
394
|
payload = json.load(f)
|
|
390
395
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
391
|
-
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/
|
|
396
|
+
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, payload)
|
|
392
397
|
out(r.json())
|
|
393
398
|
|
|
394
|
-
def _valves_set_field(url, token, kind, item_id, key, value):
|
|
395
|
-
"""Set a single field
|
|
399
|
+
def _valves_set_field(url, token, kind, item_id, key, value, scope=""):
|
|
400
|
+
"""Set a single field. Value is parsed as JSON; falls back to string."""
|
|
396
401
|
try:
|
|
397
402
|
parsed = json.loads(value)
|
|
398
403
|
except (json.JSONDecodeError, ValueError):
|
|
399
404
|
parsed = value
|
|
400
405
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
401
|
-
current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves
|
|
406
|
+
current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token).json()
|
|
402
407
|
current[key] = parsed
|
|
403
|
-
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/
|
|
408
|
+
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, current)
|
|
404
409
|
out(r.json())
|
|
405
410
|
|
|
406
|
-
def _valves_unset_field(url, token, kind, item_id, key):
|
|
407
|
-
"""Remove a single field from the user valves."""
|
|
411
|
+
def _valves_unset_field(url, token, kind, item_id, key, scope=""):
|
|
408
412
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
409
|
-
current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves
|
|
413
|
+
current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token).json()
|
|
410
414
|
if key not in current:
|
|
411
415
|
die(f"key '{key}' not found in valves")
|
|
412
416
|
del current[key]
|
|
413
|
-
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/
|
|
417
|
+
r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, current)
|
|
414
418
|
out(r.json())
|
|
415
419
|
|
|
416
|
-
# Wrappers
|
|
417
|
-
def tools_valves_get(url, token, item_id):
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
420
|
+
# Wrappers — admin/global valves (unprefixed: the common case)
|
|
421
|
+
def tools_valves_get(url, token, item_id): _valves_get(url, token, "tools", item_id)
|
|
422
|
+
def tools_valves_spec(url, token, item_id): _valves_spec(url, token, "tools", item_id)
|
|
423
|
+
def tools_valves_set(url, token, item_id, json_path): _valves_set(url, token, "tools", item_id, json_path)
|
|
424
|
+
def tools_valves_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "tools", item_id, key, value)
|
|
425
|
+
def tools_valves_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "tools", item_id, key)
|
|
426
|
+
|
|
427
|
+
def functions_valves_get(url, token, item_id): _valves_get(url, token, "functions", item_id)
|
|
428
|
+
def functions_valves_spec(url, token, item_id): _valves_spec(url, token, "functions", item_id)
|
|
429
|
+
def functions_valves_set(url, token, item_id, json_path): _valves_set(url, token, "functions", item_id, json_path)
|
|
430
|
+
def functions_valves_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "functions", item_id, key, value)
|
|
431
|
+
def functions_valves_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "functions", item_id, key)
|
|
432
|
+
|
|
433
|
+
# Wrappers — per-user valves (`valves-user*`)
|
|
434
|
+
def tools_valves_user_get(url, token, item_id): _valves_get(url, token, "tools", item_id, "/user")
|
|
435
|
+
def tools_valves_user_spec(url, token, item_id): _valves_spec(url, token, "tools", item_id, "/user")
|
|
436
|
+
def tools_valves_user_set(url, token, item_id, json_path): _valves_set(url, token, "tools", item_id, json_path, "/user")
|
|
437
|
+
def tools_valves_user_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "tools", item_id, key, value, "/user")
|
|
438
|
+
def tools_valves_user_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "tools", item_id, key, "/user")
|
|
439
|
+
|
|
440
|
+
def functions_valves_user_get(url, token, item_id): _valves_get(url, token, "functions", item_id, "/user")
|
|
441
|
+
def functions_valves_user_spec(url, token, item_id): _valves_spec(url, token, "functions", item_id, "/user")
|
|
442
|
+
def functions_valves_user_set(url, token, item_id, json_path): _valves_set(url, token, "functions", item_id, json_path, "/user")
|
|
443
|
+
def functions_valves_user_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "functions", item_id, key, value, "/user")
|
|
444
|
+
def functions_valves_user_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "functions", item_id, key, "/user")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ── functions toggle (activate / deactivate, and global) ─────────────
|
|
448
|
+
|
|
449
|
+
def functions_toggle(url, token, item_id):
|
|
450
|
+
"""Toggle a function's is_active flag."""
|
|
451
|
+
with httpx.Client(timeout=TIMEOUT) as c:
|
|
452
|
+
r = _post(c, url, f"/api/v1/functions/id/{item_id}/toggle", token)
|
|
453
|
+
f = r.json()
|
|
454
|
+
out(f"{item_id} {'active' if f.get('is_active') else 'inactive'}")
|
|
444
455
|
|
|
445
|
-
def
|
|
446
|
-
|
|
456
|
+
def functions_toggle_global(url, token, item_id):
|
|
457
|
+
"""Toggle a function's is_global flag (applies to all models)."""
|
|
458
|
+
with httpx.Client(timeout=TIMEOUT) as c:
|
|
459
|
+
r = _post(c, url, f"/api/v1/functions/id/{item_id}/toggle/global", token)
|
|
460
|
+
f = r.json()
|
|
461
|
+
out(f"{item_id} {'global' if f.get('is_global') else 'not global'}")
|
|
447
462
|
|
|
448
463
|
|
|
449
464
|
class SkillsResource(Resource):
|
|
@@ -575,17 +590,16 @@ def models_show(url, token, model_id):
|
|
|
575
590
|
if JSON_OUTPUT:
|
|
576
591
|
out(m)
|
|
577
592
|
return
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
params = info.get("params") or {}
|
|
593
|
+
meta = m.get("meta") or {}
|
|
594
|
+
params = m.get("params") or {}
|
|
581
595
|
pairs = [("id", m.get("id","")), ("name", m.get("name","")),
|
|
582
|
-
("base",
|
|
583
|
-
("active", str(
|
|
596
|
+
("base", m.get("base_model_id","(none)") or "(none)"),
|
|
597
|
+
("active", str(m.get("is_active","?"))),
|
|
584
598
|
("tools", ", ".join(meta.get("toolIds") or []) or "(none)"),
|
|
585
|
-
("filters", ", ".join(
|
|
599
|
+
("filters", ", ".join(meta.get("filterIds") or []) or "(none)"),
|
|
586
600
|
("knowledge", ", ".join(k.get("name","?") for k in (meta.get("knowledge") or [])) or "(none)"),
|
|
587
|
-
("system", f"{len(params.get('system'
|
|
588
|
-
("grants", str(len(
|
|
601
|
+
("system", f"{len(params.get('system') or '')} chars"),
|
|
602
|
+
("grants", str(len(m.get("access_grants") or [])))]
|
|
589
603
|
out_kv(pairs)
|
|
590
604
|
|
|
591
605
|
def models_create(url, token, json_path):
|
|
@@ -609,23 +623,38 @@ def models_delete(url, token, model_id):
|
|
|
609
623
|
out(f"deleted {model_id}")
|
|
610
624
|
|
|
611
625
|
def _models_fetch(c, url, token, model_id):
|
|
612
|
-
"""Fetch a model by ID, returning the parsed JSON."""
|
|
626
|
+
"""Fetch a model by ID, returning the parsed JSON (flat ModelModel shape)."""
|
|
613
627
|
r = _get(c, url, f"/api/v1/models/model?id={model_id}", token)
|
|
614
628
|
return r.json()
|
|
615
629
|
|
|
630
|
+
def _models_form(model):
|
|
631
|
+
"""Build a ModelForm update payload from a fetched flat model.
|
|
632
|
+
|
|
633
|
+
OWUI's Model schema is flat: top-level `meta`, `params`, `base_model_id`,
|
|
634
|
+
`name`, `is_active`, `access_grants`. The /model/update endpoint accepts a
|
|
635
|
+
ModelForm of the same shape (extra keys ignored). Rebuilding explicitly
|
|
636
|
+
avoids leaking response-only fields (user, write_access, timestamps) and
|
|
637
|
+
ensures `meta`/`params` round-trip intact.
|
|
638
|
+
"""
|
|
639
|
+
return {
|
|
640
|
+
"id": model.get("id"),
|
|
641
|
+
"base_model_id": model.get("base_model_id"),
|
|
642
|
+
"name": model.get("name", model.get("id", "")),
|
|
643
|
+
"meta": model.get("meta") or {},
|
|
644
|
+
"params": model.get("params") or {},
|
|
645
|
+
"access_grants": model.get("access_grants") or [],
|
|
646
|
+
"is_active": model.get("is_active", True),
|
|
647
|
+
}
|
|
648
|
+
|
|
616
649
|
def models_set_tools(url, token, model_id, *tool_ids):
|
|
617
650
|
"""Set the tool bindings for a workspace model (pass no IDs to clear)."""
|
|
618
651
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
619
652
|
model = _models_fetch(c, url, token, model_id)
|
|
620
|
-
|
|
621
|
-
meta = info.setdefault("meta", {})
|
|
622
|
-
params = info.setdefault("params", {})
|
|
653
|
+
form = _models_form(model)
|
|
623
654
|
ids = list(tool_ids)
|
|
624
|
-
meta
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
model["info"] = info
|
|
628
|
-
r = _post(c, url, "/api/v1/models/model/update", token, model)
|
|
655
|
+
# OWUI binds per-model tools via meta.toolIds.
|
|
656
|
+
form["meta"]["toolIds"] = ids
|
|
657
|
+
r = _post(c, url, "/api/v1/models/model/update", token, form)
|
|
629
658
|
label = ", ".join(ids) if ids else "(none)"
|
|
630
659
|
out(f"tools for {model_id}: {label}")
|
|
631
660
|
|
|
@@ -633,12 +662,12 @@ def models_set_filters(url, token, model_id, *filter_ids):
|
|
|
633
662
|
"""Set the filter bindings for a workspace model (pass no IDs to clear)."""
|
|
634
663
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
635
664
|
model = _models_fetch(c, url, token, model_id)
|
|
636
|
-
|
|
637
|
-
params = info.setdefault("params", {})
|
|
665
|
+
form = _models_form(model)
|
|
638
666
|
ids = list(filter_ids)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
667
|
+
# OWUI reads per-model filters from meta.filterIds
|
|
668
|
+
# (see backend/open_webui/utils/filter.py).
|
|
669
|
+
form["meta"]["filterIds"] = ids
|
|
670
|
+
r = _post(c, url, "/api/v1/models/model/update", token, form)
|
|
642
671
|
label = ", ".join(ids) if ids else "(none)"
|
|
643
672
|
out(f"filters for {model_id}: {label}")
|
|
644
673
|
|
|
@@ -691,9 +720,20 @@ def models_pull_all(url, token, out_dir="."):
|
|
|
691
720
|
# ── knowledge (special: files subresource, file/remove is destructive) ─
|
|
692
721
|
|
|
693
722
|
def knowledge_list(url, token):
|
|
723
|
+
kbs = []
|
|
724
|
+
page = 1
|
|
694
725
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
695
|
-
|
|
696
|
-
|
|
726
|
+
while True:
|
|
727
|
+
data = _get(c, url, "/api/v1/knowledge/", token, params={"page": page}).json()
|
|
728
|
+
batch = data["items"] if isinstance(data, dict) else data
|
|
729
|
+
if not batch:
|
|
730
|
+
break
|
|
731
|
+
kbs.extend(batch)
|
|
732
|
+
if isinstance(data, dict):
|
|
733
|
+
if len(kbs) >= data.get("total", 0): break
|
|
734
|
+
else:
|
|
735
|
+
break
|
|
736
|
+
page += 1
|
|
697
737
|
rows = [{"id": k.get("id",""), "name": k.get("name",""),
|
|
698
738
|
"desc": (k.get("description") or "")[:50]}
|
|
699
739
|
for k in sorted(kbs, key=lambda k: k.get("name",""))]
|
|
@@ -744,12 +784,25 @@ def knowledge_remove_file(url, token, kb_id, file_id):
|
|
|
744
784
|
# ── files ─────────────────────────────────────────────────────────────
|
|
745
785
|
|
|
746
786
|
def files_list(url, token):
|
|
787
|
+
rows = []
|
|
788
|
+
page = 1
|
|
747
789
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
790
|
+
while True:
|
|
791
|
+
data = _get(c, url, "/api/v1/files/", token,
|
|
792
|
+
params={"page": page, "content": "false"}).json()
|
|
793
|
+
# OWUI >= 0.9 returns {items, total}; older versions return a bare list.
|
|
794
|
+
batch = data["items"] if isinstance(data, dict) else data
|
|
795
|
+
if not batch:
|
|
796
|
+
break
|
|
797
|
+
rows.extend({"id": f.get("id",""),
|
|
798
|
+
"name": (f.get("meta") or {}).get("name") or f.get("filename",""),
|
|
799
|
+
"size": str((f.get("meta") or {}).get("size","?"))}
|
|
800
|
+
for f in batch)
|
|
801
|
+
if isinstance(data, dict):
|
|
802
|
+
if len(rows) >= data.get("total", 0): break
|
|
803
|
+
else:
|
|
804
|
+
break # bare-list response is unpaginated
|
|
805
|
+
page += 1
|
|
753
806
|
out_table(rows, [("FILE_ID","id",36), ("NAME","name",30), ("SIZE","size",8)])
|
|
754
807
|
|
|
755
808
|
def files_show(url, token, file_id):
|
|
@@ -1119,11 +1172,23 @@ COMMANDS.update({
|
|
|
1119
1172
|
("tools", "valves-set"): (tools_valves_set, "<id> <valves.json>", (2, 2)),
|
|
1120
1173
|
("tools", "valves-set-field"): (tools_valves_set_field, "<id> <key> <value>", (3, 3)),
|
|
1121
1174
|
("tools", "valves-unset-field"):(tools_valves_unset_field, "<id> <key>", (2, 2)),
|
|
1175
|
+
("tools", "valves-user"): (tools_valves_user_get, "<id>", (1, 1)),
|
|
1176
|
+
("tools", "valves-user-spec"): (tools_valves_user_spec, "<id>", (1, 1)),
|
|
1177
|
+
("tools", "valves-user-set"): (tools_valves_user_set, "<id> <valves.json>", (2, 2)),
|
|
1178
|
+
("tools", "valves-user-set-field"): (tools_valves_user_set_field, "<id> <key> <value>", (3, 3)),
|
|
1179
|
+
("tools", "valves-user-unset-field"):(tools_valves_user_unset_field, "<id> <key>", (2, 2)),
|
|
1122
1180
|
("functions", "valves"): (functions_valves_get, "<id>", (1, 1)),
|
|
1123
1181
|
("functions", "valves-spec"): (functions_valves_spec, "<id>", (1, 1)),
|
|
1124
1182
|
("functions", "valves-set"): (functions_valves_set, "<id> <valves.json>", (2, 2)),
|
|
1125
1183
|
("functions", "valves-set-field"): (functions_valves_set_field, "<id> <key> <value>", (3, 3)),
|
|
1126
1184
|
("functions", "valves-unset-field"):(functions_valves_unset_field, "<id> <key>", (2, 2)),
|
|
1185
|
+
("functions", "valves-user"): (functions_valves_user_get, "<id>", (1, 1)),
|
|
1186
|
+
("functions", "valves-user-spec"): (functions_valves_user_spec, "<id>", (1, 1)),
|
|
1187
|
+
("functions", "valves-user-set"): (functions_valves_user_set, "<id> <valves.json>", (2, 2)),
|
|
1188
|
+
("functions", "valves-user-set-field"): (functions_valves_user_set_field, "<id> <key> <value>", (3, 3)),
|
|
1189
|
+
("functions", "valves-user-unset-field"):(functions_valves_user_unset_field, "<id> <key>", (2, 2)),
|
|
1190
|
+
("functions", "toggle"): (functions_toggle, "<id>", (1, 1)),
|
|
1191
|
+
("functions", "toggle-global"): (functions_toggle_global, "<id>", (1, 1)),
|
|
1127
1192
|
("models", "list"): (models_list, "", (0, 0)),
|
|
1128
1193
|
("models", "show"): (models_show, "<id>", (1, 1)),
|
|
1129
1194
|
("models", "create"): (models_create, "<model.json>", (1, 1)),
|
|
@@ -1217,7 +1282,12 @@ def main():
|
|
|
1217
1282
|
resource, command = args[0], args[1]
|
|
1218
1283
|
key = (resource, command)
|
|
1219
1284
|
|
|
1285
|
+
# Subcommands are plural (chats, models, etc.) to mirror the OWUI API
|
|
1286
|
+
# paths (/api/v1/chats/, /api/v1/models, ...), not typical CLI convention.
|
|
1220
1287
|
if key not in COMMANDS:
|
|
1288
|
+
plural = resource + "s"
|
|
1289
|
+
if (plural, command) in COMMANDS:
|
|
1290
|
+
die(f"resources are plural: use '{plural} {command}' not '{resource} {command}'")
|
|
1221
1291
|
die(f"unknown: {resource} {command}")
|
|
1222
1292
|
|
|
1223
1293
|
fn, arg_spec, (min_args, max_args) = COMMANDS[key]
|
owui_cli-0.5.1/uv.lock
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 2
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "anyio"
|
|
7
|
+
version = "4.13.0"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "idna" },
|
|
11
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
12
|
+
]
|
|
13
|
+
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
|
14
|
+
wheels = [
|
|
15
|
+
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[[package]]
|
|
19
|
+
name = "certifi"
|
|
20
|
+
version = "2026.2.25"
|
|
21
|
+
source = { registry = "https://pypi.org/simple" }
|
|
22
|
+
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
|
23
|
+
wheels = [
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "h11"
|
|
29
|
+
version = "0.16.0"
|
|
30
|
+
source = { registry = "https://pypi.org/simple" }
|
|
31
|
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
32
|
+
wheels = [
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[package]]
|
|
37
|
+
name = "httpcore"
|
|
38
|
+
version = "1.0.9"
|
|
39
|
+
source = { registry = "https://pypi.org/simple" }
|
|
40
|
+
dependencies = [
|
|
41
|
+
{ name = "certifi" },
|
|
42
|
+
{ name = "h11" },
|
|
43
|
+
]
|
|
44
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
45
|
+
wheels = [
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "httpx"
|
|
51
|
+
version = "0.28.1"
|
|
52
|
+
source = { registry = "https://pypi.org/simple" }
|
|
53
|
+
dependencies = [
|
|
54
|
+
{ name = "anyio" },
|
|
55
|
+
{ name = "certifi" },
|
|
56
|
+
{ name = "httpcore" },
|
|
57
|
+
{ name = "idna" },
|
|
58
|
+
]
|
|
59
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
60
|
+
wheels = [
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "idna"
|
|
66
|
+
version = "3.11"
|
|
67
|
+
source = { registry = "https://pypi.org/simple" }
|
|
68
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
69
|
+
wheels = [
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[[package]]
|
|
74
|
+
name = "owui-cli"
|
|
75
|
+
version = "0.2.0"
|
|
76
|
+
source = { editable = "." }
|
|
77
|
+
dependencies = [
|
|
78
|
+
{ name = "httpx" },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[package.metadata]
|
|
82
|
+
requires-dist = [{ name = "httpx", specifier = ">=0.27" }]
|
|
83
|
+
|
|
84
|
+
[[package]]
|
|
85
|
+
name = "typing-extensions"
|
|
86
|
+
version = "4.15.0"
|
|
87
|
+
source = { registry = "https://pypi.org/simple" }
|
|
88
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
89
|
+
wheels = [
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
91
|
+
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|
owui_cli-0.4.0/token-viewer.html
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>OWUI Token Viewer</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
-
body {
|
|
10
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
11
|
-
background: #1a1a2e;
|
|
12
|
-
color: #eee;
|
|
13
|
-
min-height: 100vh;
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
justify-content: center;
|
|
17
|
-
padding: 1rem;
|
|
18
|
-
}
|
|
19
|
-
.card {
|
|
20
|
-
background: #16213e;
|
|
21
|
-
border-radius: 12px;
|
|
22
|
-
padding: 1.5rem;
|
|
23
|
-
max-width: 480px;
|
|
24
|
-
width: 100%;
|
|
25
|
-
box-shadow: 0 8px 32px rgba(0,0,0,.4);
|
|
26
|
-
}
|
|
27
|
-
h1 { font-size: 1.25rem; margin-bottom: .25rem; }
|
|
28
|
-
.sub { color: #888; font-size: .85rem; margin-bottom: 1rem; }
|
|
29
|
-
.token-box {
|
|
30
|
-
background: #0f0f23;
|
|
31
|
-
border: 1px solid #333;
|
|
32
|
-
border-radius: 8px;
|
|
33
|
-
padding: .75rem;
|
|
34
|
-
word-break: break-all;
|
|
35
|
-
font-family: "SF Mono", "Fira Code", monospace;
|
|
36
|
-
font-size: .8rem;
|
|
37
|
-
line-height: 1.5;
|
|
38
|
-
max-height: 40vh;
|
|
39
|
-
overflow-y: auto;
|
|
40
|
-
user-select: all;
|
|
41
|
-
-webkit-user-select: all;
|
|
42
|
-
}
|
|
43
|
-
.token-box.empty { color: #f55; font-family: sans-serif; font-size: .9rem; }
|
|
44
|
-
.btn {
|
|
45
|
-
display: inline-block;
|
|
46
|
-
margin-top: .75rem;
|
|
47
|
-
padding: .6rem 1.2rem;
|
|
48
|
-
background: #0f3460;
|
|
49
|
-
color: #eee;
|
|
50
|
-
border: none;
|
|
51
|
-
border-radius: 8px;
|
|
52
|
-
font-size: .9rem;
|
|
53
|
-
cursor: pointer;
|
|
54
|
-
-webkit-tap-highlight-color: transparent;
|
|
55
|
-
}
|
|
56
|
-
.btn:active { background: #1a5276; }
|
|
57
|
-
.copied { color: #5f8; font-size: .85rem; margin-left: .5rem; opacity: 0; transition: opacity .2s; }
|
|
58
|
-
.copied.show { opacity: 1; }
|
|
59
|
-
</style>
|
|
60
|
-
</head>
|
|
61
|
-
<body>
|
|
62
|
-
<div class="card">
|
|
63
|
-
<h1>🔑 OWUI Token</h1>
|
|
64
|
-
<p class="sub">Reads <code>token</code> from localStorage</p>
|
|
65
|
-
<div id="token" class="token-box empty">Checking…</div>
|
|
66
|
-
<button class="btn" id="copy" style="display:none">Copy to clipboard</button>
|
|
67
|
-
<span class="copied" id="copied">✓ Copied!</span>
|
|
68
|
-
</div>
|
|
69
|
-
<script>
|
|
70
|
-
const el = document.getElementById('token');
|
|
71
|
-
const copyBtn = document.getElementById('copy');
|
|
72
|
-
const copiedMsg = document.getElementById('copied');
|
|
73
|
-
const raw = localStorage.getItem('token');
|
|
74
|
-
|
|
75
|
-
if (raw) {
|
|
76
|
-
el.textContent = raw;
|
|
77
|
-
el.classList.remove('empty');
|
|
78
|
-
copyBtn.style.display = 'inline-block';
|
|
79
|
-
} else {
|
|
80
|
-
el.textContent = 'No "token" found in localStorage.\nMake sure you\'re on the same origin as Open WebUI.';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
copyBtn.addEventListener('click', () => {
|
|
84
|
-
navigator.clipboard.writeText(raw).then(() => {
|
|
85
|
-
copiedMsg.classList.add('show');
|
|
86
|
-
setTimeout(() => copiedMsg.classList.remove('show'), 1500);
|
|
87
|
-
}).catch(() => {
|
|
88
|
-
// fallback for older mobile browsers
|
|
89
|
-
const ta = document.createElement('textarea');
|
|
90
|
-
ta.value = raw;
|
|
91
|
-
ta.style.position = 'fixed';
|
|
92
|
-
ta.style.opacity = 0;
|
|
93
|
-
document.body.appendChild(ta);
|
|
94
|
-
ta.select();
|
|
95
|
-
document.execCommand('copy');
|
|
96
|
-
document.body.removeChild(ta);
|
|
97
|
-
copiedMsg.classList.add('show');
|
|
98
|
-
setTimeout(() => copiedMsg.classList.remove('show'), 1500);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
</script>
|
|
102
|
-
</body>
|
|
103
|
-
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|