create-workframe 0.1.18 → 0.1.20
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.
- package/README.md +1 -1
- package/bin/create-workframe.js +73 -9
- package/package.json +1 -1
- package/workframe-api/action_proxy.py +8 -0
- package/workframe-api/chat_sessions.py +37 -6
- package/workframe-api/chat_stream.py +16 -0
- package/workframe-api/db_schema.py +29 -0
- package/workframe-api/handler_modules/handler_admin.py +32 -0
- package/workframe-api/handler_modules/handler_auth.py +6 -4
- package/workframe-api/handler_modules/handler_chat.py +17 -26
- package/workframe-api/handler_modules/handler_provider.py +13 -14
- package/workframe-api/handler_modules/handler_workspace.py +60 -2
- package/workframe-api/hermes_profiles.py +31 -9
- package/workframe-api/llm_proxy.py +18 -0
- package/workframe-api/mention_invoke.py +29 -24
- package/workframe-api/message_reactions.py +139 -0
- package/workframe-api/model_surface.py +329 -113
- package/workframe-api/package.json +4 -4
- package/workframe-api/profile_gateway.py +14 -6
- package/workframe-api/provider_bindings.py +118 -17
- package/workframe-api/provider_bootstrap.py +74 -38
- package/workframe-api/provider_model_catalog.py +261 -0
- package/workframe-api/rooms.py +4 -3
- package/workframe-api/route_registry.py +4 -0
- package/workframe-api/runtime_cohort.py +118 -23
- package/workframe-api/scripts/run-python.mjs +28 -0
- package/workframe-api/server.py +44 -12
- package/workframe-api/test_agent_identity_authority.py +96 -0
- package/workframe-api/test_chat_attribution_history.py +59 -0
- package/workframe-api/test_enrich_room_messages.py +43 -2
- package/workframe-api/test_llm_proxy_encoding.py +33 -0
- package/workframe-api/test_message_reactions.py +151 -0
- package/workframe-api/test_model_routing_reconcile.py +122 -0
- package/workframe-api/test_model_surface_consistency.py +45 -5
- package/workframe-api/test_oauth_llm.py +1 -1
- package/workframe-api/test_profile_gateway_health.py +37 -0
- package/workframe-api/test_provider_bindings.py +115 -1
- package/workframe-api/test_provider_model_catalog.py +27 -0
- package/workframe-api/test_provider_model_surface.py +2 -2
- package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
- package/workframe-api/test_server_reexports.py +2 -0
- package/workframe-api/test_user_llm_prefs.py +27 -0
- package/workframe-api/test_workspace_file_actions.py +119 -0
- package/workframe-api/turn_credentials.py +41 -0
- package/workframe-api/turn_overlay.py +95 -34
- package/workframe-api/user_prefs.py +15 -0
- package/workframe-api/workspace_bootstrap.py +1 -0
- package/workframe-api/workspace_files.py +124 -0
- package/workframe-ui/public/assets/{arc-CSddOtMa.js → arc-BkATcoqt.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-yusDFKmK.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BSiUei3D.js → architectureDiagram-3BPJPVTR-31_yAiUS.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BhKTULPo.js → blockDiagram-GPEHLZMM-ClM6Si9T.js} +1 -1
- package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Bv_xZi-a.js → c4Diagram-AAUBKEIU-DJzEktF3.js} +1 -1
- package/workframe-ui/public/assets/channel-Nb_UjZ3O.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bh0WiqFE.js → chunk-2J33WTMH-DPFiyb5O.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-qN09pRuK.js → chunk-3OPIFGDE-DXNJzuCn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-Dz6FVuMa.js → chunk-4BX2VUAB-DDNibGdr.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-DWtPHVfR.js → chunk-55IACEB6-Co3_ulwA.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-CyOUeDAo.js → chunk-5ZQYHXKU-Cmxz8o4d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BrICvWwx.js → chunk-727SXJPM-CDAQGesU.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CQu2SmLM.js → chunk-AQP2D5EJ-x-swy3kc.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-uJbYXNNG.js → chunk-BSJP7CBP-28nU0JOE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CCS_FtVZ.js → chunk-CSCIHK7Q-FUURFika.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CoLUtPiK.js → chunk-FMBD7UC4-B4RgRO2R.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DLW4gvEW.js → chunk-KSCS5N6A-CkuppW3T.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CMt0LJAd.js → chunk-L5ZTLDWV-DyrPSaz3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Bn-maIlt.js → chunk-LZXEDZCA-DFaZSDNV.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CG7yoApR.js → chunk-ND2GUHAM-DXI0OJxg.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-B4dIvbii.js → chunk-NZK2D7GU-CyOp1ZG2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B5spDyXy.js → chunk-O5CBEL6O-tZZMCfbx.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_TtOXi5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-BjSJQEIY.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-D5c0TMX6.js → chunk-XPW4576I-B7_2gS3x.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DFRM3DZv.js → cose-bilkent-S5V4N54A-BWJaHw_L.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CnUdKeJa.js → dagre-BM42HDAG-Bu5MadGV.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ds3-e-hW.js → diagram-2AECGRRQ-ckLZkDFD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-B41zx8FF.js → diagram-5GNKFQAL-Cf6Abx1Y.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D83tI9I8.js → diagram-KO2AKTUF-TLk7_5mJ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-h_EuFyLI.js → diagram-LMA3HP47-ouNxsAb0.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-CpU-d3RZ.js → diagram-OG6HWLK6-BMCBQ35D.js} +1 -1
- package/workframe-ui/public/assets/{dist-_igeSJ9r.js → dist-C9bxMexz.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-jLFoncRU.js → erDiagram-TEJ5UH35-BN3e6jb1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bqd_sTz0.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C9IaNgCy.js → flowDiagram-I6XJVG4X-BiinzZrd.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-A9FhfFc5.js → ganttDiagram-6RSMTGT7-AYE8oBBG.js} +1 -1
- package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BqK_IjZb.js → gitGraph-WXDBUCRP-mIq_E2vs.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CDVqxhyW.js → gitGraphDiagram-PVQCEYII-Cf8IFEcO.js} +1 -1
- package/workframe-ui/public/assets/index-BnfFRGR3.js +133 -0
- package/workframe-ui/public/assets/index-COshPjF3.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-C2EKRglL.js → info-J43DQDTF--_3kCaIQ.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-azQtFzpU.js → infoDiagram-5YYISTIA-BuIOyYMu.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BuDOLv1t.js → ishikawaDiagram-YF4QCWOH-D3bbsXQ8.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-ypTHFoT0.js → journeyDiagram-JHISSGLW-BrZ26C8L.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Cgr78J_x.js → kanban-definition-UN3LZRKU-CJiUyUOr.js} +1 -1
- package/workframe-ui/public/assets/{line-CcwgaMdv.js → line-Bee1snmU.js} +1 -1
- package/workframe-ui/public/assets/{linear-CswIlWaW.js → linear-dfD-zlI7.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CQYhP37X.js → mermaid-parser.core-D6IqqDSH.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Cz1XNBaK.js → mermaid.core-CuuxYt2r.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BJLu0829.js → mindmap-definition-RKZ34NQL-T7W2fkwA.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-AB1qh96V.js → packet-YPE3B663-CiXMDasj.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-CokIXLdT.js → pie-LRSECV5Y-CUx1_96f.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyrwQn5F.js → pieDiagram-4H26LBE5-DfR96FUO.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-C_Fdg3F6.js → quadrantDiagram-W4KKPZXB-Dj6mqgQE.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C21Xfrrg.js → radar-GUYGQ44K-DKT2q98y.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ChhaWgaF.js → requirementDiagram-4Y6WPE33-DYDnBVw9.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-C5T4sDTt.js → sankeyDiagram-5OEKKPKP-BSkxpjjO.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DRks8i3A.js → sequenceDiagram-3UESZ5HK-Dz6SNH9S.js} +1 -1
- package/workframe-ui/public/assets/{src-BQu5waHd.js → src-DC521nE_.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BI-AKYta.js → stateDiagram-AJRCARHV-D4ZoF5NP.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DknveEiR.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DnAIWzEc.js → timeline-definition-PNZ67QCA-d0ezg1Hu.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-BEF_sjx7.js → treeView-BLDUP644-kXyaqwIy.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BcbL7g5c.js → treemap-LRROVOQU-g168p7-O.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DAoIbCkz.js → vennDiagram-CIIHVFJN-C0_QsmRl.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-3SOmFBFw.js → wardley-L42UT6IY-BNFCdzxQ.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DlQV_-LH.js → wardleyDiagram-YWT4CUSO-BOwUS9sY.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-CVoTTRHF.js → xychartDiagram-2RQKCTM6-Cxr1ZiKh.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CDDNM1ni.js +0 -1
- package/workframe-ui/public/assets/channel-R52LrLO3.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_9MbT10.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-VJadzJmf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-kzOxPvtj.js +0 -1
- package/workframe-ui/public/assets/index-Bco9IDMA.css +0 -1
- package/workframe-ui/public/assets/index-Do34PZS2.js +0 -132
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-vJyhFZbx.js +0 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Account-scoped live model discovery for Workframe's LLM providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
_TTL_SEC = int(os.environ.get("WORKFRAME_MODEL_CATALOG_TTL", "900"))
|
|
16
|
+
_ERROR_TTL_SEC = int(os.environ.get("WORKFRAME_MODEL_CATALOG_ERROR_TTL", "30"))
|
|
17
|
+
_CACHE: dict[tuple[str, str], dict[str, Any]] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _request_json(url: str, *, headers: dict[str, str] | None = None, timeout: float = 8) -> dict[str, Any]:
|
|
21
|
+
request = urllib.request.Request(url, headers=headers or {}, method="GET")
|
|
22
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
23
|
+
payload = json.loads(response.read())
|
|
24
|
+
return payload if isinstance(payload, dict) else {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _row(provider: str, model: str, label: str = "", description: str = "") -> dict[str, str]:
|
|
28
|
+
model_id = str(model or "").strip()
|
|
29
|
+
return {
|
|
30
|
+
"provider": provider,
|
|
31
|
+
"billing_provider": provider,
|
|
32
|
+
"model": model_id,
|
|
33
|
+
"label": str(label or model_id).strip() or model_id,
|
|
34
|
+
"description": str(description or model_id).strip() or model_id,
|
|
35
|
+
"catalog_source": "live",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _openrouter_models(api_key: str, timeout: float) -> list[dict[str, str]]:
|
|
40
|
+
payload = _request_json(
|
|
41
|
+
"https://openrouter.ai/api/v1/models?output_modalities=text",
|
|
42
|
+
headers={
|
|
43
|
+
"Authorization": f"Bearer {api_key}",
|
|
44
|
+
"HTTP-Referer": "https://workfra.me",
|
|
45
|
+
"X-Title": "Workframe",
|
|
46
|
+
},
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
)
|
|
49
|
+
rows: list[dict[str, str]] = []
|
|
50
|
+
for item in payload.get("data") or []:
|
|
51
|
+
if not isinstance(item, dict):
|
|
52
|
+
continue
|
|
53
|
+
model_id = str(item.get("id") or "").strip()
|
|
54
|
+
if not model_id:
|
|
55
|
+
continue
|
|
56
|
+
name = str(item.get("name") or model_id.rsplit("/", 1)[-1]).strip()
|
|
57
|
+
rows.append(_row("openrouter", model_id, name, model_id))
|
|
58
|
+
return sorted(rows, key=lambda item: item["label"].lower())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _openai_models(api_key: str, timeout: float) -> list[dict[str, str]]:
|
|
62
|
+
payload = _request_json(
|
|
63
|
+
"https://api.openai.com/v1/models",
|
|
64
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
)
|
|
67
|
+
excluded = ("audio", "embedding", "image", "moderation", "realtime", "search", "transcribe", "tts", "whisper")
|
|
68
|
+
rows: list[dict[str, str]] = []
|
|
69
|
+
for item in payload.get("data") or []:
|
|
70
|
+
if not isinstance(item, dict):
|
|
71
|
+
continue
|
|
72
|
+
model_id = str(item.get("id") or "").strip()
|
|
73
|
+
lower = model_id.lower()
|
|
74
|
+
if not model_id or any(token in lower for token in excluded):
|
|
75
|
+
continue
|
|
76
|
+
if not lower.startswith(("gpt-", "chatgpt-", "o1", "o3", "o4")):
|
|
77
|
+
continue
|
|
78
|
+
rows.append(_row("openai", model_id))
|
|
79
|
+
return sorted(rows, key=lambda item: item["model"].lower())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _anthropic_models(api_key: str, timeout: float) -> list[dict[str, str]]:
|
|
83
|
+
payload = _request_json(
|
|
84
|
+
"https://api.anthropic.com/v1/models?limit=1000",
|
|
85
|
+
headers={"x-api-key": api_key, "anthropic-version": "2023-06-01"},
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
rows: list[dict[str, str]] = []
|
|
89
|
+
for item in payload.get("data") or []:
|
|
90
|
+
if not isinstance(item, dict):
|
|
91
|
+
continue
|
|
92
|
+
model_id = str(item.get("id") or "").strip()
|
|
93
|
+
if model_id:
|
|
94
|
+
rows.append(_row("anthropic", model_id, str(item.get("display_name") or model_id)))
|
|
95
|
+
return rows
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _google_models(api_key: str, timeout: float) -> list[dict[str, str]]:
|
|
99
|
+
query = urllib.parse.urlencode({"key": api_key, "pageSize": "1000"})
|
|
100
|
+
payload = _request_json(f"https://generativelanguage.googleapis.com/v1beta/models?{query}", timeout=timeout)
|
|
101
|
+
rows: list[dict[str, str]] = []
|
|
102
|
+
for item in payload.get("models") or []:
|
|
103
|
+
if not isinstance(item, dict) or "generateContent" not in (item.get("supportedGenerationMethods") or []):
|
|
104
|
+
continue
|
|
105
|
+
model_id = str(item.get("name") or "").removeprefix("models/").strip()
|
|
106
|
+
if model_id:
|
|
107
|
+
rows.append(_row(
|
|
108
|
+
"google", model_id, str(item.get("displayName") or model_id),
|
|
109
|
+
str(item.get("description") or model_id),
|
|
110
|
+
))
|
|
111
|
+
return rows
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _deepseek_models(api_key: str, timeout: float) -> list[dict[str, str]]:
|
|
115
|
+
payload = _request_json(
|
|
116
|
+
"https://api.deepseek.com/models",
|
|
117
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
)
|
|
120
|
+
return [
|
|
121
|
+
_row("deepseek", str(item.get("id") or ""))
|
|
122
|
+
for item in payload.get("data") or []
|
|
123
|
+
if isinstance(item, dict) and str(item.get("id") or "").strip()
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _codex_models(access_token: str, timeout: float) -> list[dict[str, str]]:
|
|
128
|
+
payload = _request_json(
|
|
129
|
+
"https://chatgpt.com/backend-api/codex/models?client_version=1.0.0",
|
|
130
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
131
|
+
timeout=timeout,
|
|
132
|
+
)
|
|
133
|
+
sortable: list[tuple[int, dict[str, str]]] = []
|
|
134
|
+
for item in payload.get("models") or []:
|
|
135
|
+
if not isinstance(item, dict):
|
|
136
|
+
continue
|
|
137
|
+
model_id = str(item.get("slug") or "").strip()
|
|
138
|
+
visibility = str(item.get("visibility") or "").strip().lower()
|
|
139
|
+
if not model_id or visibility in {"hide", "hidden"}:
|
|
140
|
+
continue
|
|
141
|
+
priority = item.get("priority")
|
|
142
|
+
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
|
143
|
+
display = str(item.get("display_name") or item.get("displayName") or model_id).strip()
|
|
144
|
+
sortable.append((rank, _row("codex", model_id, display)))
|
|
145
|
+
sortable.sort(key=lambda item: (item[0], item[1]["model"]))
|
|
146
|
+
return [item[1] for item in sortable]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_FETCHERS = {
|
|
150
|
+
"openrouter": _openrouter_models,
|
|
151
|
+
"openai": _openai_models,
|
|
152
|
+
"anthropic": _anthropic_models,
|
|
153
|
+
"google": _google_models,
|
|
154
|
+
"deepseek": _deepseek_models,
|
|
155
|
+
"codex": _codex_models,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cache_key(provider: str, credential: str) -> tuple[str, str]:
|
|
160
|
+
digest = hashlib.sha256(credential.encode("utf-8")).hexdigest()[:20] if credential else ""
|
|
161
|
+
return provider, digest
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def discover(provider: str, credential: str, *, timeout: float = 8) -> tuple[list[dict[str, str]], dict[str, str]]:
|
|
165
|
+
provider_id = str(provider or "").strip().lower()
|
|
166
|
+
secret = str(credential or "").strip()
|
|
167
|
+
if provider_id not in _FETCHERS:
|
|
168
|
+
return [], {"status": "unsupported", "message": "Use a custom model id for this provider."}
|
|
169
|
+
if not secret:
|
|
170
|
+
return [], {"status": "unauthenticated", "message": "Reconnect this provider to refresh its models."}
|
|
171
|
+
key = _cache_key(provider_id, secret)
|
|
172
|
+
now = time.monotonic()
|
|
173
|
+
cached = _CACHE.get(key)
|
|
174
|
+
if cached:
|
|
175
|
+
ttl = _TTL_SEC if cached.get("status") == "live" else _ERROR_TTL_SEC
|
|
176
|
+
if now - float(cached.get("at") or 0) < ttl:
|
|
177
|
+
return list(cached.get("rows") or []), dict(cached.get("meta") or {})
|
|
178
|
+
try:
|
|
179
|
+
rows = _FETCHERS[provider_id](secret, timeout)
|
|
180
|
+
meta = {
|
|
181
|
+
"status": "live" if rows else "empty",
|
|
182
|
+
"message": f"{len(rows)} models available now." if rows else "The provider returned no chat models.",
|
|
183
|
+
}
|
|
184
|
+
except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError, OSError, ValueError):
|
|
185
|
+
rows = []
|
|
186
|
+
meta = {"status": "error", "message": "Live catalog unavailable. Retry or enter a custom model id."}
|
|
187
|
+
_CACHE[key] = {"at": now, "status": meta["status"], "rows": rows, "meta": meta}
|
|
188
|
+
return list(rows), dict(meta)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def discover_many(credentials: dict[str, str], *, timeout: float = 8) -> tuple[list[dict[str, str]], dict[str, dict[str, str]]]:
|
|
192
|
+
rows: list[dict[str, str]] = []
|
|
193
|
+
statuses: dict[str, dict[str, str]] = {}
|
|
194
|
+
if not credentials:
|
|
195
|
+
return rows, statuses
|
|
196
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, len(credentials))) as pool:
|
|
197
|
+
pending = {
|
|
198
|
+
pool.submit(discover, provider, credential, timeout=timeout): provider
|
|
199
|
+
for provider, credential in credentials.items()
|
|
200
|
+
}
|
|
201
|
+
for future in concurrent.futures.as_completed(pending):
|
|
202
|
+
provider = pending[future]
|
|
203
|
+
try:
|
|
204
|
+
provider_rows, meta = future.result()
|
|
205
|
+
except Exception:
|
|
206
|
+
provider_rows, meta = [], {
|
|
207
|
+
"status": "error",
|
|
208
|
+
"message": "Live catalog unavailable. Retry or enter a custom model id.",
|
|
209
|
+
}
|
|
210
|
+
rows.extend(provider_rows)
|
|
211
|
+
statuses[provider] = meta
|
|
212
|
+
return rows, statuses
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def oauth_access_token(auth: dict[str, Any] | None, hermes_auth_id: str) -> str:
|
|
216
|
+
"""Read an OAuth access token from Hermes provider or credential-pool layouts."""
|
|
217
|
+
if not isinstance(auth, dict):
|
|
218
|
+
return ""
|
|
219
|
+
raw = str(hermes_auth_id or "").strip().lower()
|
|
220
|
+
keys = {raw, raw.replace("-", ""), raw.replace("-", "_")}
|
|
221
|
+
if "-" in raw:
|
|
222
|
+
keys.add(raw.split("-")[-1])
|
|
223
|
+
|
|
224
|
+
def token_from(entry: Any) -> str:
|
|
225
|
+
if not isinstance(entry, dict):
|
|
226
|
+
return ""
|
|
227
|
+
tokens = entry.get("tokens") if isinstance(entry.get("tokens"), dict) else {}
|
|
228
|
+
for source in (tokens, entry):
|
|
229
|
+
for field in ("access_token", "accessToken", "token"):
|
|
230
|
+
value = str(source.get(field) or "").strip()
|
|
231
|
+
if value:
|
|
232
|
+
return value
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
providers = auth.get("providers")
|
|
236
|
+
if isinstance(providers, dict):
|
|
237
|
+
for key, entry in providers.items():
|
|
238
|
+
if str(key).lower() in keys:
|
|
239
|
+
token = token_from(entry)
|
|
240
|
+
if token:
|
|
241
|
+
return token
|
|
242
|
+
credentials = auth.get("credentials")
|
|
243
|
+
if isinstance(credentials, list):
|
|
244
|
+
for entry in credentials:
|
|
245
|
+
if not isinstance(entry, dict):
|
|
246
|
+
continue
|
|
247
|
+
provider = str(entry.get("provider") or entry.get("id") or "").lower()
|
|
248
|
+
if provider in keys:
|
|
249
|
+
token = token_from(entry)
|
|
250
|
+
if token:
|
|
251
|
+
return token
|
|
252
|
+
pool = auth.get("credential_pool")
|
|
253
|
+
if isinstance(pool, dict):
|
|
254
|
+
for key, entries in pool.items():
|
|
255
|
+
if str(key).lower() not in keys or not isinstance(entries, list):
|
|
256
|
+
continue
|
|
257
|
+
for entry in entries:
|
|
258
|
+
token = token_from(entry)
|
|
259
|
+
if token:
|
|
260
|
+
return token
|
|
261
|
+
return ""
|
package/workframe-api/rooms.py
CHANGED
|
@@ -650,6 +650,7 @@ def _ensure_workspace_agent_profile_row(
|
|
|
650
650
|
role: str = "",
|
|
651
651
|
tagline: str = "",
|
|
652
652
|
is_native: bool = False,
|
|
653
|
+
preserve_existing_identity: bool = False,
|
|
653
654
|
) -> str | None:
|
|
654
655
|
"""Ensure agent_profiles row exists for a workspace (default workspace when id omitted)."""
|
|
655
656
|
slug = str(slug or "").strip()
|
|
@@ -686,11 +687,11 @@ def _ensure_workspace_agent_profile_row(
|
|
|
686
687
|
if existing:
|
|
687
688
|
agent_id = str(existing["id"])
|
|
688
689
|
patches: dict[str, Any] = {}
|
|
689
|
-
if display_name.strip():
|
|
690
|
+
if display_name.strip() and not preserve_existing_identity:
|
|
690
691
|
patches["display_name"] = display_name.strip()
|
|
691
|
-
if tagline.strip():
|
|
692
|
+
if tagline.strip() and not preserve_existing_identity:
|
|
692
693
|
patches["tagline"] = tagline.strip()
|
|
693
|
-
if role.strip():
|
|
694
|
+
if role.strip() and not preserve_existing_identity:
|
|
694
695
|
patches["role"] = role.strip()
|
|
695
696
|
if patches:
|
|
696
697
|
sets = [f"{col} = ?" for col in patches]
|
|
@@ -81,6 +81,7 @@ ROUTES: tuple[Route, ...] = (
|
|
|
81
81
|
Route("GET", "/api/me/credentials", AuthLevel.SESSION, "_route_get_me_credentials"),
|
|
82
82
|
Route("GET", "/api/me/providers", AuthLevel.SESSION, "_route_get_me_providers"),
|
|
83
83
|
Route("GET", "/api/me/cohort", AuthLevel.SESSION, "_route_get_me_cohort"),
|
|
84
|
+
Route("GET", "/api/message-reactions", AuthLevel.SESSION, "_route_get_message_reactions"),
|
|
84
85
|
Route("GET", "/api/user/credentials", AuthLevel.SESSION, "_route_get_user_credentials"),
|
|
85
86
|
Route("GET", "/api/hermes/profiles/status", AuthLevel.SESSION, "_route_get_hermes_profiles_status"),
|
|
86
87
|
Route("GET", "/api/oauth/github/callback", AuthLevel.SESSION, "_route_get_oauth_github_callback"),
|
|
@@ -128,7 +129,10 @@ ROUTES: tuple[Route, ...] = (
|
|
|
128
129
|
Route("POST", "/api/content/delete", AuthLevel.SESSION, "_route_post_content_delete"),
|
|
129
130
|
Route("POST", "/api/files/write", AuthLevel.SESSION, "_route_post_files_write"),
|
|
130
131
|
Route("POST", "/api/files/upload", AuthLevel.SESSION, "_route_post_files_upload"),
|
|
132
|
+
Route("POST", "/api/files/archive", AuthLevel.SESSION, "_route_post_files_archive"),
|
|
133
|
+
Route("POST", "/api/files/delete", AuthLevel.SESSION, "_route_post_files_delete"),
|
|
131
134
|
Route("POST", "/api/chat/dispatch", AuthLevel.SESSION, "_route_post_chat_dispatch"),
|
|
135
|
+
Route("POST", "/api/message-reactions/toggle", AuthLevel.SESSION, "_route_post_message_reactions_toggle"),
|
|
132
136
|
Route("POST", "/api/hermes/profiles/create", AuthLevel.SESSION, "_route_post_hermes_profiles_create"),
|
|
133
137
|
Route("POST", "/api/hermes/profiles/start", AuthLevel.SESSION, "_route_post_hermes_profiles_start"),
|
|
134
138
|
Route("POST", "/api/hermes/profiles/stop", AuthLevel.SESSION, "_route_post_hermes_profiles_stop"),
|
|
@@ -38,6 +38,8 @@ def _prepare_runtime_profile_credentials(
|
|
|
38
38
|
runtime: str,
|
|
39
39
|
user_id: str,
|
|
40
40
|
workspace_id: str = "",
|
|
41
|
+
*,
|
|
42
|
+
wait_healthy: bool = True,
|
|
41
43
|
) -> bool:
|
|
42
44
|
"""Strip stale profile secrets, then overlay the runtime profile owner's keys.
|
|
43
45
|
|
|
@@ -57,7 +59,86 @@ def _prepare_runtime_profile_credentials(
|
|
|
57
59
|
_srv()._reconcile_profile_llm_for_user(runtime, user, workspace_id)
|
|
58
60
|
prov = _srv()._llm_billing_provider(runtime, user_id=user, workspace_id=workspace_id)
|
|
59
61
|
_srv()._ensure_profile_llm_proxy(runtime, prov)
|
|
60
|
-
|
|
62
|
+
ready = _srv()._user_can_use_llm(user, workspace_id, prov)
|
|
63
|
+
if ready:
|
|
64
|
+
# Pre-bind the opaque runtime credential while provisioning/opening the
|
|
65
|
+
# agent. The first chat turn must never be responsible for restarting a
|
|
66
|
+
# credential-less Hermes gateway.
|
|
67
|
+
_srv()._try_overlay_turn_provider_env(
|
|
68
|
+
runtime,
|
|
69
|
+
user,
|
|
70
|
+
workspace_id,
|
|
71
|
+
prov,
|
|
72
|
+
f"runtime-bootstrap:{runtime}",
|
|
73
|
+
wait_healthy=wait_healthy,
|
|
74
|
+
)
|
|
75
|
+
return ready
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _refresh_user_runtime_credentials(
|
|
79
|
+
user_id: str,
|
|
80
|
+
workspace_id: str = "",
|
|
81
|
+
*,
|
|
82
|
+
wait_healthy: bool = False,
|
|
83
|
+
) -> int:
|
|
84
|
+
"""Refresh all of a user's agent runtimes after a credential change."""
|
|
85
|
+
user = str(user_id or "").strip()
|
|
86
|
+
if not user:
|
|
87
|
+
return 0
|
|
88
|
+
user_part = re.sub(
|
|
89
|
+
r"[^a-z0-9]+",
|
|
90
|
+
"-",
|
|
91
|
+
_srv()._user_hermes_dir_slug(user).lower(),
|
|
92
|
+
).strip("-")[:20] or "user"
|
|
93
|
+
prefix = f"u-{user_part}-"
|
|
94
|
+
profiles_dir = _srv().HERMES_DATA / "profiles"
|
|
95
|
+
if not profiles_dir.is_dir():
|
|
96
|
+
return 0
|
|
97
|
+
refreshed = 0
|
|
98
|
+
for path in profiles_dir.iterdir():
|
|
99
|
+
if not path.is_dir() or not path.name.startswith(prefix):
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
if _prepare_runtime_profile_credentials(
|
|
103
|
+
path.name,
|
|
104
|
+
user,
|
|
105
|
+
workspace_id,
|
|
106
|
+
wait_healthy=wait_healthy,
|
|
107
|
+
):
|
|
108
|
+
refreshed += 1
|
|
109
|
+
except (OSError, RuntimeError, ValueError):
|
|
110
|
+
continue
|
|
111
|
+
return refreshed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _refresh_workspace_runtime_credentials(
|
|
115
|
+
workspace_id: str,
|
|
116
|
+
*,
|
|
117
|
+
wait_healthy: bool = False,
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Refresh active member runtimes after a company credential changes."""
|
|
120
|
+
workspace = str(workspace_id or "").strip()
|
|
121
|
+
if not workspace:
|
|
122
|
+
return 0
|
|
123
|
+
conn = _srv()._workframe_db()
|
|
124
|
+
try:
|
|
125
|
+
rows = conn.execute(
|
|
126
|
+
"""
|
|
127
|
+
SELECT DISTINCT user_id FROM workspace_memberships
|
|
128
|
+
WHERE workspace_id = ? AND status = 'active' AND deleted_at IS NULL
|
|
129
|
+
""",
|
|
130
|
+
(workspace,),
|
|
131
|
+
).fetchall()
|
|
132
|
+
finally:
|
|
133
|
+
conn.close()
|
|
134
|
+
return sum(
|
|
135
|
+
_refresh_user_runtime_credentials(
|
|
136
|
+
str(row["user_id"]),
|
|
137
|
+
workspace,
|
|
138
|
+
wait_healthy=wait_healthy,
|
|
139
|
+
)
|
|
140
|
+
for row in rows
|
|
141
|
+
)
|
|
61
142
|
|
|
62
143
|
|
|
63
144
|
def _resolve_runtime_owner(runtime: str) -> tuple[str, str] | None:
|
|
@@ -111,12 +192,9 @@ def _user_owner_name(user_id: str) -> str:
|
|
|
111
192
|
|
|
112
193
|
def _runtime_display_label(user_id: str, template_slug: str, workspace_id: str = "") -> str:
|
|
113
194
|
template = _srv().safe_profile_slug(str(template_slug or "").strip())
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
reg_runtime = _srv()._agent_registry_row(runtime)
|
|
118
|
-
if str(reg_runtime.get("display_name") or "").strip():
|
|
119
|
-
return str(reg_runtime["display_name"]).strip()
|
|
195
|
+
# Runtime profiles are credential/session proxies. Their registry rows may
|
|
196
|
+
# contain stale metadata from older builds, but can never rename the shared
|
|
197
|
+
# workspace agent.
|
|
120
198
|
custom = _srv()._agent_db_display_name(template, workspace_id)
|
|
121
199
|
if custom:
|
|
122
200
|
return custom
|
|
@@ -124,11 +202,11 @@ def _runtime_display_label(user_id: str, template_slug: str, workspace_id: str =
|
|
|
124
202
|
if reg.get("display_name"):
|
|
125
203
|
return str(reg["display_name"])
|
|
126
204
|
if _srv()._is_native_profile(template) or template.endswith("-agent"):
|
|
127
|
-
return
|
|
205
|
+
return _srv()._native_display_name()
|
|
128
206
|
role = _srv()._agent_db_display_name(template, workspace_id) or _srv()._agent_registry_row(template).get("display_name")
|
|
129
207
|
if not role:
|
|
130
208
|
role = template.replace("-", " ").title()
|
|
131
|
-
return
|
|
209
|
+
return str(role)
|
|
132
210
|
|
|
133
211
|
|
|
134
212
|
def _cohort_alias(user_id: str, template_slug: str) -> str:
|
|
@@ -465,6 +543,8 @@ def _write_workframe_cohort_manifest(user_id: str, workspace_id: str, cohort: li
|
|
|
465
543
|
path = _srv()._profile_dir(runtime) / "WORKFRAME_COHORT.md"
|
|
466
544
|
try:
|
|
467
545
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
546
|
+
if path.is_file() and path.read_text(encoding="utf-8") == body:
|
|
547
|
+
continue
|
|
468
548
|
path.write_text(body, encoding="utf-8")
|
|
469
549
|
except OSError:
|
|
470
550
|
pass
|
|
@@ -616,6 +696,11 @@ def _copy_text_without_bom(src: Path, dst: Path) -> None:
|
|
|
616
696
|
if raw.startswith(b"\xef\xbb\xbf"):
|
|
617
697
|
raw = raw[3:]
|
|
618
698
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
699
|
+
try:
|
|
700
|
+
if dst.is_file() and dst.read_bytes() == raw:
|
|
701
|
+
return
|
|
702
|
+
except OSError:
|
|
703
|
+
pass
|
|
619
704
|
dst.write_bytes(raw)
|
|
620
705
|
|
|
621
706
|
|
|
@@ -648,10 +733,7 @@ def _backfill_runtime_identity(runtime: str, template: str) -> None:
|
|
|
648
733
|
for name in ("SOUL.md", "AGENTS.md", "SETUP.md"):
|
|
649
734
|
src = tdir / name
|
|
650
735
|
dst = rdir / name
|
|
651
|
-
if
|
|
652
|
-
if _srv()._write_profile_soul_if_stub(runtime, template):
|
|
653
|
-
continue
|
|
654
|
-
if src.is_file() and not dst.is_file():
|
|
736
|
+
if src.is_file():
|
|
655
737
|
try:
|
|
656
738
|
_copy_text_without_bom(src, dst)
|
|
657
739
|
except OSError:
|
|
@@ -680,13 +762,15 @@ def _profile_toolsets_ready(profile: str, want: tuple[str, ...] | list[str]) ->
|
|
|
680
762
|
if not names:
|
|
681
763
|
return True
|
|
682
764
|
ts = cfg.get("toolsets") if isinstance(cfg.get("toolsets"), list) else []
|
|
683
|
-
|
|
684
|
-
return False
|
|
765
|
+
global_ready = all(name in ts for name in names)
|
|
685
766
|
pts = cfg.get("platform_toolsets") if isinstance(cfg.get("platform_toolsets"), dict) else {}
|
|
767
|
+
platform_ready = True
|
|
686
768
|
for platform in ("api_server", "cli"):
|
|
687
769
|
plat = pts.get(platform) if isinstance(pts.get(platform), list) else []
|
|
688
770
|
if not all(name in plat for name in names):
|
|
689
|
-
|
|
771
|
+
platform_ready = False
|
|
772
|
+
if not global_ready and not platform_ready:
|
|
773
|
+
return False
|
|
690
774
|
agent = cfg.get("agent") if isinstance(cfg.get("agent"), dict) else {}
|
|
691
775
|
disabled = agent.get("disabled_toolsets") if isinstance(agent.get("disabled_toolsets"), list) else []
|
|
692
776
|
return not any(name in disabled for name in names)
|
|
@@ -724,16 +808,20 @@ def ensure_runtime_profile(
|
|
|
724
808
|
ok, out, _port = _srv()._configure_profile_api(runtime)
|
|
725
809
|
if not ok:
|
|
726
810
|
raise ValueError(f"runtime profile api config failed: {out}")
|
|
727
|
-
|
|
728
|
-
if not skills_dir.is_dir() or not any(skills_dir.iterdir()):
|
|
729
|
-
_backfill_runtime_identity(runtime, template)
|
|
811
|
+
_backfill_runtime_identity(runtime, template)
|
|
730
812
|
if not _profile_toolsets_ready(runtime, _srv()._chat_toolsets_for_profile(runtime)):
|
|
731
813
|
_srv()._ensure_profile_toolsets(runtime)
|
|
732
814
|
_ensure_profile_terminal_cwd(runtime)
|
|
733
|
-
#
|
|
734
|
-
#
|
|
735
|
-
#
|
|
736
|
-
|
|
815
|
+
# A runtime isolates credentials/gateway state, not agent preferences.
|
|
816
|
+
# Repair model drift before credential preparation so DM, room mention,
|
|
817
|
+
# kanban, and cron all execute the model owned by the agent template.
|
|
818
|
+
ok_model, model_error = _srv()._sync_runtime_model_from_template(
|
|
819
|
+
runtime,
|
|
820
|
+
template,
|
|
821
|
+
user_id=user_id,
|
|
822
|
+
)
|
|
823
|
+
if not ok_model:
|
|
824
|
+
raise ValueError(f"runtime model sync failed: {model_error}")
|
|
737
825
|
_prepare_runtime_profile_credentials(runtime, user_id, workspace_id)
|
|
738
826
|
_srv()._ensure_profile_proxy_headers(runtime)
|
|
739
827
|
return
|
|
@@ -759,4 +847,11 @@ def ensure_runtime_profile(
|
|
|
759
847
|
raise ValueError(f"runtime profile api config failed: {out}")
|
|
760
848
|
_srv()._ensure_profile_toolsets(runtime)
|
|
761
849
|
_ensure_profile_terminal_cwd(runtime)
|
|
850
|
+
ok_model, model_error = _srv()._sync_runtime_model_from_template(
|
|
851
|
+
runtime,
|
|
852
|
+
template,
|
|
853
|
+
user_id=user_id,
|
|
854
|
+
)
|
|
855
|
+
if not ok_model:
|
|
856
|
+
raise ValueError(f"runtime model sync failed: {model_error}")
|
|
762
857
|
_prepare_runtime_profile_credentials(runtime, user_id, workspace_id)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
const requestedArgs = process.argv.slice(2)
|
|
4
|
+
const candidates = process.platform === 'win32'
|
|
5
|
+
? [
|
|
6
|
+
{ command: 'python', prefix: [] },
|
|
7
|
+
{ command: 'py', prefix: ['-3'] },
|
|
8
|
+
{ command: 'python3', prefix: [] },
|
|
9
|
+
]
|
|
10
|
+
: [
|
|
11
|
+
{ command: 'python3', prefix: [] },
|
|
12
|
+
{ command: 'python', prefix: [] },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
const result = spawnSync(candidate.command, [...candidate.prefix, ...requestedArgs], {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
})
|
|
19
|
+
if (result.error?.code === 'ENOENT') continue
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error(`Could not run ${candidate.command}: ${result.error.message}`)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
process.exit(result.status ?? 1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.error('Python 3 was not found. Install Python 3 and ensure py, python, or python3 is available.')
|
|
28
|
+
process.exit(1)
|
package/workframe-api/server.py
CHANGED
|
@@ -109,6 +109,8 @@ file_read = workspace_files.file_read
|
|
|
109
109
|
file_write = workspace_files.file_write
|
|
110
110
|
file_upload_binary = workspace_files.file_upload_binary
|
|
111
111
|
file_raw = workspace_files.file_raw
|
|
112
|
+
files_archive = workspace_files.files_archive
|
|
113
|
+
files_delete = workspace_files.files_delete
|
|
112
114
|
|
|
113
115
|
import activity_feed
|
|
114
116
|
|
|
@@ -162,6 +164,8 @@ _assign_agent_avatar = avatar_registry._assign_agent_avatar
|
|
|
162
164
|
# WF-032: runtime_cohort re-exports
|
|
163
165
|
_runtime_profile_slug = runtime_cohort._runtime_profile_slug
|
|
164
166
|
_prepare_runtime_profile_credentials = runtime_cohort._prepare_runtime_profile_credentials
|
|
167
|
+
_refresh_user_runtime_credentials = runtime_cohort._refresh_user_runtime_credentials
|
|
168
|
+
_refresh_workspace_runtime_credentials = runtime_cohort._refresh_workspace_runtime_credentials
|
|
165
169
|
_resolve_runtime_owner = runtime_cohort._resolve_runtime_owner
|
|
166
170
|
_user_handle = runtime_cohort._user_handle
|
|
167
171
|
_user_owner_name = runtime_cohort._user_owner_name
|
|
@@ -269,6 +273,7 @@ _resolve_models_profile = model_surface._resolve_models_profile
|
|
|
269
273
|
_model_suggestion_row = model_surface._model_suggestion_row
|
|
270
274
|
_augment_model_suggestions = model_surface._augment_model_suggestions
|
|
271
275
|
_billing_provider_from_block = model_surface._billing_provider_from_block
|
|
276
|
+
_normalized_profile_fallback_chain = model_surface._normalized_profile_fallback_chain
|
|
272
277
|
_llm_billing_provider = model_surface._llm_billing_provider
|
|
273
278
|
_profile_llm_proxy_matches_billing = model_surface._profile_llm_proxy_matches_billing
|
|
274
279
|
_profile_routing_matches_billing = model_surface._profile_routing_matches_billing
|
|
@@ -278,6 +283,7 @@ _catalog_row_for_billing_provider = model_surface._catalog_row_for_billing_provi
|
|
|
278
283
|
_provider_display_label = model_surface._provider_display_label
|
|
279
284
|
_model_catalog_rows_for_provider = model_surface._model_catalog_rows_for_provider
|
|
280
285
|
_suggestions_for_connected_llm_providers = model_surface._suggestions_for_connected_llm_providers
|
|
286
|
+
_live_suggestions_for_connected_llm_providers = model_surface._live_suggestions_for_connected_llm_providers
|
|
281
287
|
_model_id_vendor_and_bare = model_surface._model_id_vendor_and_bare
|
|
282
288
|
_persisted_model_id = model_surface._persisted_model_id
|
|
283
289
|
_apply_model_persisted = model_surface._apply_model_persisted
|
|
@@ -1382,6 +1388,7 @@ _user_action_env_specs = turn_overlay._user_action_env_specs
|
|
|
1382
1388
|
_overlay_turn_user_env = turn_overlay._overlay_turn_user_env
|
|
1383
1389
|
_apply_action_credential_lease = turn_overlay._apply_action_credential_lease
|
|
1384
1390
|
_revoke_turn_credential_lease = turn_overlay._revoke_turn_credential_lease
|
|
1391
|
+
_revoke_runtime_llm_leases = turn_overlay._revoke_runtime_llm_leases
|
|
1385
1392
|
_require_user_provider = turn_overlay._require_user_provider
|
|
1386
1393
|
_require_runtime_owner_provider = turn_overlay._require_runtime_owner_provider
|
|
1387
1394
|
_strip_profile_llm_env = turn_overlay._strip_profile_llm_env
|
|
@@ -1467,11 +1474,32 @@ def _enrich_room_messages(conn: sqlite3.Connection, messages: list[Any]) -> list
|
|
|
1467
1474
|
enriched: list[dict[str, Any]] = []
|
|
1468
1475
|
for row in messages:
|
|
1469
1476
|
item = dict(row)
|
|
1477
|
+
# Provider/model attribution is turn evidence, not mutable agent state.
|
|
1478
|
+
# New agent messages persist it in messages.metadata. Legacy messages
|
|
1479
|
+
# without that evidence intentionally render without a model badge rather
|
|
1480
|
+
# than being relabelled whenever the agent's current model changes.
|
|
1481
|
+
metadata: dict[str, Any] = {}
|
|
1482
|
+
raw_metadata = item.get("metadata")
|
|
1483
|
+
if isinstance(raw_metadata, dict):
|
|
1484
|
+
metadata = raw_metadata
|
|
1485
|
+
elif isinstance(raw_metadata, str) and raw_metadata.strip():
|
|
1486
|
+
try:
|
|
1487
|
+
parsed = json.loads(raw_metadata)
|
|
1488
|
+
if isinstance(parsed, dict):
|
|
1489
|
+
metadata = parsed
|
|
1490
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
1491
|
+
metadata = {}
|
|
1492
|
+
turn_model = str(metadata.get("model") or "").strip()
|
|
1493
|
+
turn_provider = str(metadata.get("llm_provider") or "").strip()
|
|
1494
|
+
if turn_model:
|
|
1495
|
+
item["model"] = turn_model
|
|
1496
|
+
if turn_provider:
|
|
1497
|
+
item["llm_provider"] = turn_provider
|
|
1470
1498
|
agent_id = str(item.get("sender_agent_id") or "").strip()
|
|
1471
1499
|
if agent_id:
|
|
1472
1500
|
agent = conn.execute(
|
|
1473
1501
|
"""
|
|
1474
|
-
SELECT slug, display_name
|
|
1502
|
+
SELECT slug, display_name
|
|
1475
1503
|
FROM agent_profiles WHERE id = ? AND deleted_at IS NULL
|
|
1476
1504
|
""",
|
|
1477
1505
|
(agent_id,),
|
|
@@ -1479,17 +1507,6 @@ def _enrich_room_messages(conn: sqlite3.Connection, messages: list[Any]) -> list
|
|
|
1479
1507
|
if agent:
|
|
1480
1508
|
item["sender_agent_slug"] = str(agent["slug"])
|
|
1481
1509
|
item["sender_agent_name"] = str(agent["display_name"] or agent["slug"])
|
|
1482
|
-
model_name = str(agent["model_name"] or "").strip()
|
|
1483
|
-
model_provider = str(agent["model_provider"] or "").strip()
|
|
1484
|
-
slug = str(agent["slug"] or "").strip()
|
|
1485
|
-
if not model_name and slug:
|
|
1486
|
-
model_name = str(_read_model_block(slug).get("default") or "").strip()
|
|
1487
|
-
if not model_provider and slug:
|
|
1488
|
-
model_provider = _llm_billing_provider(slug)
|
|
1489
|
-
if model_name:
|
|
1490
|
-
item["model"] = model_name
|
|
1491
|
-
if model_provider:
|
|
1492
|
-
item["llm_provider"] = model_provider
|
|
1493
1510
|
enriched.append(item)
|
|
1494
1511
|
return enriched
|
|
1495
1512
|
|
|
@@ -1542,10 +1559,13 @@ def _sync_agent_profile_db(profile: str, fields: dict[str, Any]) -> None:
|
|
|
1542
1559
|
"tagline": "tagline",
|
|
1543
1560
|
"role": "role",
|
|
1544
1561
|
"avatar_url": "avatar_url",
|
|
1562
|
+
"model_provider": "model_provider",
|
|
1563
|
+
"model_name": "model_name",
|
|
1545
1564
|
}
|
|
1546
1565
|
updates = {column: fields[key] for key, column in column_map.items() if key in fields}
|
|
1547
1566
|
if not updates:
|
|
1548
1567
|
return
|
|
1568
|
+
_ensure_workframe_db_schema()
|
|
1549
1569
|
conn = _workframe_db()
|
|
1550
1570
|
try:
|
|
1551
1571
|
now_ts = str(int(time.time()))
|
|
@@ -1555,6 +1575,18 @@ def _sync_agent_profile_db(profile: str, fields: dict[str, Any]) -> None:
|
|
|
1555
1575
|
f"UPDATE agent_profiles SET {', '.join(sets)}, updated_at = ? WHERE slug = ? AND deleted_at IS NULL",
|
|
1556
1576
|
vals,
|
|
1557
1577
|
)
|
|
1578
|
+
if "display_name" in updates and str(updates["display_name"] or "").strip():
|
|
1579
|
+
conn.execute(
|
|
1580
|
+
"""
|
|
1581
|
+
UPDATE rooms
|
|
1582
|
+
SET name = ?, updated_at = ?
|
|
1583
|
+
WHERE room_type = 'direct' AND deleted_at IS NULL
|
|
1584
|
+
AND agent_profile_id IN (
|
|
1585
|
+
SELECT id FROM agent_profiles WHERE slug = ? AND deleted_at IS NULL
|
|
1586
|
+
)
|
|
1587
|
+
""",
|
|
1588
|
+
(str(updates["display_name"]).strip(), now_ts, slug),
|
|
1589
|
+
)
|
|
1558
1590
|
conn.commit()
|
|
1559
1591
|
finally:
|
|
1560
1592
|
conn.close()
|