devtorch-core 3.0.1__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.
Files changed (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,279 @@
1
+ """
2
+ DevTorch proxy route: Ollama
3
+
4
+ /ollama/{path} → http://localhost:11434/{path}
5
+
6
+ RACP is injected as a system message at position 0 in body["messages"].
7
+ Ollama's /api/chat endpoint uses the same message shape as OpenAI.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import uuid
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger("devtorch.proxy.ollama")
18
+
19
+ UPSTREAM = "http://localhost:11434"
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Lightweight RACP prefix builder
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _build_racp_prefix(gcc_repo: Any) -> str:
27
+ if gcc_repo is None:
28
+ return ""
29
+
30
+ parts: list[str] = []
31
+
32
+ try:
33
+ racp_prompt = gcc_repo.prompt_get()
34
+ if racp_prompt:
35
+ parts.append(racp_prompt.strip())
36
+ except Exception as exc:
37
+ logger.debug("devtorch proxy[ollama]: prompt_get failed — %s", exc)
38
+
39
+ try:
40
+ theta = gcc_repo.get_theta()
41
+ cv = theta.get("coordination_vector", {})
42
+ if cv:
43
+ def _mean_conf(entry: Any) -> float:
44
+ if isinstance(entry, dict):
45
+ return float(entry.get("mean_confidence", 0.0))
46
+ return 0.0
47
+
48
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
49
+ if top:
50
+ lines = ["[DevTorch Θ — top concepts]"]
51
+ for concept, data in top:
52
+ conf = _mean_conf(data)
53
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
54
+ parts.append("\n".join(lines))
55
+ except Exception as exc:
56
+ logger.debug("devtorch proxy[ollama]: theta_summary failed — %s", exc)
57
+
58
+ try:
59
+ bundle = gcc_repo.context_bundle(
60
+ k_tokens=8000,
61
+ policy={"sensitivity_policy": "default"},
62
+ )
63
+ artifacts = bundle.get("artifacts", [])
64
+ if artifacts:
65
+ lines = ["[DevTorch context bundle]"]
66
+ for art in artifacts:
67
+ path = art.get("path", "?")
68
+ reason = art.get("reason", "")
69
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
70
+ parts.append("\n".join(lines))
71
+ except Exception as exc:
72
+ logger.debug("devtorch proxy[ollama]: context_bundle failed — %s", exc)
73
+
74
+ return "\n\n".join(parts)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Capture helper
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
82
+ async def _do_capture() -> None:
83
+ try:
84
+ from devtorch_core.wrapper.base import CaptureOrchestrator
85
+ orchestrator = CaptureOrchestrator(gcc_repo)
86
+ orchestrator.capture(
87
+ response_text=response_text,
88
+ thinking_blocks=[],
89
+ session_id=session_id,
90
+ )
91
+ except Exception as exc:
92
+ logger.warning("devtorch proxy[ollama]: capture failed — %s", exc)
93
+
94
+ try:
95
+ loop = asyncio.get_event_loop()
96
+ if loop.is_running():
97
+ asyncio.ensure_future(_do_capture())
98
+ else:
99
+ loop.run_until_complete(_do_capture())
100
+ except Exception as exc:
101
+ logger.warning("devtorch proxy[ollama]: could not schedule capture — %s", exc)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # SSE accumulator: Ollama streams newline-delimited JSON objects
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def _extract_text_from_ndjson(raw: str) -> str:
109
+ """
110
+ Parse Ollama's NDJSON stream and concatenate message content.
111
+ """
112
+ parts: list[str] = []
113
+ for line in raw.splitlines():
114
+ line = line.strip()
115
+ if not line:
116
+ continue
117
+ try:
118
+ obj = json.loads(line)
119
+ msg = obj.get("message", {})
120
+ content = msg.get("content", "")
121
+ if content:
122
+ parts.append(content)
123
+ except Exception:
124
+ pass
125
+ return "".join(parts)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Route handler
130
+ # ---------------------------------------------------------------------------
131
+
132
+ async def proxy_ollama(path: str, request: "Any", gcc_repo: Any) -> "Any":
133
+ """
134
+ Proxy /ollama/{path} → http://localhost:11434/{path}
135
+ """
136
+ try:
137
+ from fastapi import Response
138
+ from fastapi.responses import StreamingResponse
139
+ import httpx
140
+ except ImportError:
141
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
142
+
143
+ # -----------------------------------------------------------------------
144
+ # 1. Read request body
145
+ # -----------------------------------------------------------------------
146
+ try:
147
+ body_bytes = await request.body()
148
+ body: dict = json.loads(body_bytes) if body_bytes else {}
149
+ except Exception:
150
+ body = {}
151
+ body_bytes = b""
152
+
153
+ # -----------------------------------------------------------------------
154
+ # 2. Inject RACP as system message at position 0
155
+ # -----------------------------------------------------------------------
156
+ modified_body = body
157
+ try:
158
+ if gcc_repo is not None and gcc_repo.is_initialized():
159
+ prefix = _build_racp_prefix(gcc_repo)
160
+ if prefix:
161
+ messages = list(body.get("messages", []))
162
+ racp_msg = {"role": "system", "content": prefix}
163
+ if messages and messages[0].get("role") == "system":
164
+ existing_content = messages[0].get("content", "")
165
+ messages[0] = {
166
+ "role": "system",
167
+ "content": prefix + "\n\n" + existing_content,
168
+ }
169
+ else:
170
+ messages.insert(0, racp_msg)
171
+ modified_body = {**body, "messages": messages}
172
+ except Exception as exc:
173
+ logger.warning("devtorch proxy[ollama]: RACP injection failed — %s", exc)
174
+ modified_body = body
175
+
176
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
177
+
178
+ # -----------------------------------------------------------------------
179
+ # 3. Build forwarding headers
180
+ # -----------------------------------------------------------------------
181
+ forward_headers = {
182
+ k: v for k, v in request.headers.items()
183
+ if k.lower() not in ("host", "content-length", "transfer-encoding")
184
+ }
185
+ forward_headers["host"] = "localhost:11434"
186
+ if send_body:
187
+ forward_headers["content-length"] = str(len(send_body))
188
+
189
+ upstream_url = f"{UPSTREAM}/{path}"
190
+ if request.url.query:
191
+ upstream_url = f"{upstream_url}?{request.url.query}"
192
+
193
+ session_id = str(uuid.uuid4())[:8]
194
+ is_streaming = bool(modified_body.get("stream", False))
195
+
196
+ # -----------------------------------------------------------------------
197
+ # 4 & 5. Stream or buffer
198
+ # -----------------------------------------------------------------------
199
+ if is_streaming:
200
+ async def _stream_generator():
201
+ buffer_parts: list[bytes] = []
202
+ try:
203
+ async with httpx.AsyncClient(timeout=300.0) as client:
204
+ async with client.stream(
205
+ method=request.method,
206
+ url=upstream_url,
207
+ headers=forward_headers,
208
+ content=send_body,
209
+ ) as upstream_resp:
210
+ async for chunk in upstream_resp.aiter_bytes():
211
+ buffer_parts.append(chunk)
212
+ yield chunk
213
+ except Exception as exc:
214
+ logger.warning("devtorch proxy[ollama]: streaming error — %s", exc)
215
+
216
+ raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
217
+ extracted = _extract_text_from_ndjson(raw_text)
218
+ _schedule_capture(gcc_repo, extracted or raw_text, session_id)
219
+
220
+ return StreamingResponse(
221
+ _stream_generator(),
222
+ media_type="application/x-ndjson",
223
+ )
224
+ else:
225
+ try:
226
+ async with httpx.AsyncClient(timeout=300.0) as client:
227
+ upstream_resp = await client.request(
228
+ method=request.method,
229
+ url=upstream_url,
230
+ headers=forward_headers,
231
+ content=send_body,
232
+ )
233
+
234
+ try:
235
+ resp_json = upstream_resp.json()
236
+ text_parts: list[str] = []
237
+ msg = resp_json.get("message", {})
238
+ content = msg.get("content", "")
239
+ if content:
240
+ text_parts.append(content)
241
+ resp_text = "\n".join(text_parts) or upstream_resp.text
242
+ except Exception:
243
+ resp_text = upstream_resp.text
244
+
245
+ _schedule_capture(gcc_repo, resp_text, session_id)
246
+
247
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
248
+ resp_headers = {
249
+ k: v for k, v in upstream_resp.headers.items()
250
+ if k.lower() not in excluded
251
+ }
252
+ return Response(
253
+ content=upstream_resp.content,
254
+ status_code=upstream_resp.status_code,
255
+ headers=resp_headers,
256
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
257
+ )
258
+ except Exception as exc:
259
+ logger.warning("devtorch proxy[ollama]: upstream request failed — %s", exc)
260
+ try:
261
+ async with httpx.AsyncClient(timeout=300.0) as client:
262
+ fallback_resp = await client.request(
263
+ method=request.method,
264
+ url=upstream_url,
265
+ headers=forward_headers,
266
+ content=body_bytes,
267
+ )
268
+ return Response(
269
+ content=fallback_resp.content,
270
+ status_code=fallback_resp.status_code,
271
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
272
+ )
273
+ except Exception as exc2:
274
+ logger.error("devtorch proxy[ollama]: fallback also failed — %s", exc2)
275
+ return Response(
276
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
277
+ status_code=502,
278
+ media_type="application/json",
279
+ )
@@ -0,0 +1,287 @@
1
+ """
2
+ DevTorch proxy route: OpenAI
3
+
4
+ /openai/{path} → https://api.openai.com/{path}
5
+
6
+ RACP is injected as a system message at position 0 in body["messages"].
7
+ Streaming SSE format (data: {...}\\n\\n) is handled transparently.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import uuid
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger("devtorch.proxy.openai")
18
+
19
+ UPSTREAM = "https://api.openai.com"
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Lightweight RACP prefix builder (mirrors anthropic.py to avoid circulars)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _build_racp_prefix(gcc_repo: Any) -> str:
27
+ if gcc_repo is None:
28
+ return ""
29
+
30
+ parts: list[str] = []
31
+
32
+ try:
33
+ racp_prompt = gcc_repo.prompt_get()
34
+ if racp_prompt:
35
+ parts.append(racp_prompt.strip())
36
+ except Exception as exc:
37
+ logger.debug("devtorch proxy[openai]: prompt_get failed — %s", exc)
38
+
39
+ try:
40
+ theta = gcc_repo.get_theta()
41
+ cv = theta.get("coordination_vector", {})
42
+ if cv:
43
+ def _mean_conf(entry: Any) -> float:
44
+ if isinstance(entry, dict):
45
+ return float(entry.get("mean_confidence", 0.0))
46
+ return 0.0
47
+
48
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
49
+ if top:
50
+ lines = ["[DevTorch Θ — top concepts]"]
51
+ for concept, data in top:
52
+ conf = _mean_conf(data)
53
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
54
+ parts.append("\n".join(lines))
55
+ except Exception as exc:
56
+ logger.debug("devtorch proxy[openai]: theta_summary failed — %s", exc)
57
+
58
+ try:
59
+ bundle = gcc_repo.context_bundle(
60
+ k_tokens=8000,
61
+ policy={"sensitivity_policy": "default"},
62
+ )
63
+ artifacts = bundle.get("artifacts", [])
64
+ if artifacts:
65
+ lines = ["[DevTorch context bundle]"]
66
+ for art in artifacts:
67
+ path = art.get("path", "?")
68
+ reason = art.get("reason", "")
69
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
70
+ parts.append("\n".join(lines))
71
+ except Exception as exc:
72
+ logger.debug("devtorch proxy[openai]: context_bundle failed — %s", exc)
73
+
74
+ return "\n\n".join(parts)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Capture helper
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
82
+ async def _do_capture() -> None:
83
+ try:
84
+ from devtorch_core.wrapper.base import CaptureOrchestrator
85
+ orchestrator = CaptureOrchestrator(gcc_repo)
86
+ orchestrator.capture(
87
+ response_text=response_text,
88
+ thinking_blocks=[],
89
+ session_id=session_id,
90
+ )
91
+ except Exception as exc:
92
+ logger.warning("devtorch proxy[openai]: capture failed — %s", exc)
93
+
94
+ try:
95
+ loop = asyncio.get_event_loop()
96
+ if loop.is_running():
97
+ asyncio.ensure_future(_do_capture())
98
+ else:
99
+ loop.run_until_complete(_do_capture())
100
+ except Exception as exc:
101
+ logger.warning("devtorch proxy[openai]: could not schedule capture — %s", exc)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # SSE accumulator: extract assistant text from streamed chunks
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def _extract_text_from_sse(raw: str) -> str:
109
+ """
110
+ Parse SSE stream and concatenate content delta text for capture.
111
+ Handles OpenAI's `data: {...}` format.
112
+ """
113
+ parts: list[str] = []
114
+ for line in raw.splitlines():
115
+ line = line.strip()
116
+ if not line.startswith("data:"):
117
+ continue
118
+ data_str = line[5:].strip()
119
+ if data_str == "[DONE]":
120
+ break
121
+ try:
122
+ obj = json.loads(data_str)
123
+ for choice in obj.get("choices", []):
124
+ delta = choice.get("delta", {})
125
+ content = delta.get("content", "")
126
+ if content:
127
+ parts.append(content)
128
+ except Exception:
129
+ pass
130
+ return "".join(parts)
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Route handler
135
+ # ---------------------------------------------------------------------------
136
+
137
+ async def proxy_openai(path: str, request: "Any", gcc_repo: Any) -> "Any":
138
+ """
139
+ Proxy /openai/{path} → https://api.openai.com/{path}
140
+ """
141
+ try:
142
+ from fastapi import Response
143
+ from fastapi.responses import StreamingResponse
144
+ import httpx
145
+ except ImportError:
146
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
147
+
148
+ # -----------------------------------------------------------------------
149
+ # 1. Read request body
150
+ # -----------------------------------------------------------------------
151
+ try:
152
+ body_bytes = await request.body()
153
+ body: dict = json.loads(body_bytes) if body_bytes else {}
154
+ except Exception:
155
+ body = {}
156
+ body_bytes = b""
157
+
158
+ # -----------------------------------------------------------------------
159
+ # 2. Inject RACP as system message at position 0
160
+ # -----------------------------------------------------------------------
161
+ modified_body = body
162
+ try:
163
+ if gcc_repo is not None and gcc_repo.is_initialized():
164
+ prefix = _build_racp_prefix(gcc_repo)
165
+ if prefix:
166
+ messages = list(body.get("messages", []))
167
+ racp_msg = {"role": "system", "content": prefix}
168
+ # Merge with existing system message if present, else prepend
169
+ if messages and messages[0].get("role") == "system":
170
+ existing_content = messages[0].get("content", "")
171
+ messages[0] = {
172
+ "role": "system",
173
+ "content": prefix + "\n\n" + existing_content,
174
+ }
175
+ else:
176
+ messages.insert(0, racp_msg)
177
+ modified_body = {**body, "messages": messages}
178
+ except Exception as exc:
179
+ logger.warning("devtorch proxy[openai]: RACP injection failed — %s", exc)
180
+ modified_body = body
181
+
182
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
183
+
184
+ # -----------------------------------------------------------------------
185
+ # 3. Build forwarding headers
186
+ # -----------------------------------------------------------------------
187
+ forward_headers = {
188
+ k: v for k, v in request.headers.items()
189
+ if k.lower() not in ("host", "content-length", "transfer-encoding")
190
+ }
191
+ forward_headers["host"] = "api.openai.com"
192
+ if send_body:
193
+ forward_headers["content-length"] = str(len(send_body))
194
+
195
+ upstream_url = f"{UPSTREAM}/{path}"
196
+ if request.url.query:
197
+ upstream_url = f"{upstream_url}?{request.url.query}"
198
+
199
+ session_id = str(uuid.uuid4())[:8]
200
+ is_streaming = bool(modified_body.get("stream", False))
201
+
202
+ # -----------------------------------------------------------------------
203
+ # 4 & 5. Stream or buffer
204
+ # -----------------------------------------------------------------------
205
+ if is_streaming:
206
+ async def _stream_generator():
207
+ buffer_parts: list[bytes] = []
208
+ try:
209
+ async with httpx.AsyncClient(timeout=300.0) as client:
210
+ async with client.stream(
211
+ method=request.method,
212
+ url=upstream_url,
213
+ headers=forward_headers,
214
+ content=send_body,
215
+ ) as upstream_resp:
216
+ async for chunk in upstream_resp.aiter_bytes():
217
+ buffer_parts.append(chunk)
218
+ yield chunk
219
+ except Exception as exc:
220
+ logger.warning("devtorch proxy[openai]: streaming error — %s", exc)
221
+
222
+ raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
223
+ extracted = _extract_text_from_sse(raw_text)
224
+ _schedule_capture(gcc_repo, extracted or raw_text, session_id)
225
+
226
+ return StreamingResponse(
227
+ _stream_generator(),
228
+ media_type="text/event-stream",
229
+ )
230
+ else:
231
+ try:
232
+ async with httpx.AsyncClient(timeout=300.0) as client:
233
+ upstream_resp = await client.request(
234
+ method=request.method,
235
+ url=upstream_url,
236
+ headers=forward_headers,
237
+ content=send_body,
238
+ )
239
+
240
+ # Extract assistant text for capture
241
+ try:
242
+ resp_json = upstream_resp.json()
243
+ text_parts: list[str] = []
244
+ for choice in resp_json.get("choices", []):
245
+ msg = choice.get("message", {})
246
+ content = msg.get("content", "")
247
+ if content:
248
+ text_parts.append(content)
249
+ resp_text = "\n".join(text_parts) or upstream_resp.text
250
+ except Exception:
251
+ resp_text = upstream_resp.text
252
+
253
+ _schedule_capture(gcc_repo, resp_text, session_id)
254
+
255
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
256
+ resp_headers = {
257
+ k: v for k, v in upstream_resp.headers.items()
258
+ if k.lower() not in excluded
259
+ }
260
+ return Response(
261
+ content=upstream_resp.content,
262
+ status_code=upstream_resp.status_code,
263
+ headers=resp_headers,
264
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
265
+ )
266
+ except Exception as exc:
267
+ logger.warning("devtorch proxy[openai]: upstream request failed — %s", exc)
268
+ try:
269
+ async with httpx.AsyncClient(timeout=300.0) as client:
270
+ fallback_resp = await client.request(
271
+ method=request.method,
272
+ url=upstream_url,
273
+ headers=forward_headers,
274
+ content=body_bytes,
275
+ )
276
+ return Response(
277
+ content=fallback_resp.content,
278
+ status_code=fallback_resp.status_code,
279
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
280
+ )
281
+ except Exception as exc2:
282
+ logger.error("devtorch proxy[openai]: fallback also failed — %s", exc2)
283
+ return Response(
284
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
285
+ status_code=502,
286
+ media_type="application/json",
287
+ )