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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevTorch proxy route: Google Gemini
|
|
3
|
+
|
|
4
|
+
/gemini/{path} → https://generativelanguage.googleapis.com/{path}
|
|
5
|
+
|
|
6
|
+
RACP is injected into the first user content part (body["contents"]).
|
|
7
|
+
Gemini uses "contents" instead of "messages".
|
|
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.gemini")
|
|
18
|
+
|
|
19
|
+
UPSTREAM = "https://generativelanguage.googleapis.com"
|
|
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[gemini]: 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[gemini]: 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[gemini]: 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[gemini]: 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[gemini]: could not schedule capture — %s", exc)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Gemini body injection
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _inject_racp_into_contents(body: dict, prefix: str) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Inject RACP prefix into the first user content part's text.
|
|
111
|
+
Gemini body structure:
|
|
112
|
+
{
|
|
113
|
+
"contents": [
|
|
114
|
+
{"role": "user", "parts": [{"text": "..."}]},
|
|
115
|
+
...
|
|
116
|
+
],
|
|
117
|
+
"systemInstruction": {"parts": [{"text": "..."}]} # optional
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Strategy:
|
|
121
|
+
- If body has "systemInstruction", prepend prefix there.
|
|
122
|
+
- Otherwise inject into the first user content text part.
|
|
123
|
+
"""
|
|
124
|
+
modified = dict(body)
|
|
125
|
+
|
|
126
|
+
if "systemInstruction" in modified:
|
|
127
|
+
si = dict(modified["systemInstruction"])
|
|
128
|
+
parts = list(si.get("parts", []))
|
|
129
|
+
if parts and isinstance(parts[0], dict) and "text" in parts[0]:
|
|
130
|
+
existing = parts[0]["text"]
|
|
131
|
+
parts[0] = {"text": prefix + "\n\n" + existing}
|
|
132
|
+
else:
|
|
133
|
+
parts.insert(0, {"text": prefix})
|
|
134
|
+
si["parts"] = parts
|
|
135
|
+
modified["systemInstruction"] = si
|
|
136
|
+
return modified
|
|
137
|
+
|
|
138
|
+
# Fall back to first user content part
|
|
139
|
+
contents = list(modified.get("contents", []))
|
|
140
|
+
if not contents:
|
|
141
|
+
return modified
|
|
142
|
+
|
|
143
|
+
# Find first user turn
|
|
144
|
+
for i, content in enumerate(contents):
|
|
145
|
+
if content.get("role", "user") == "user":
|
|
146
|
+
content = dict(content)
|
|
147
|
+
parts = list(content.get("parts", []))
|
|
148
|
+
if parts and isinstance(parts[0], dict) and "text" in parts[0]:
|
|
149
|
+
existing = parts[0]["text"]
|
|
150
|
+
parts[0] = {"text": prefix + "\n\n" + existing}
|
|
151
|
+
else:
|
|
152
|
+
parts.insert(0, {"text": prefix})
|
|
153
|
+
content["parts"] = parts
|
|
154
|
+
contents[i] = content
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
modified["contents"] = contents
|
|
158
|
+
return modified
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Response text extraction for capture
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _extract_text_from_response(resp_json: Any) -> str:
|
|
166
|
+
"""Extract generated text from a Gemini API response dict."""
|
|
167
|
+
try:
|
|
168
|
+
parts: list[str] = []
|
|
169
|
+
for candidate in resp_json.get("candidates", []):
|
|
170
|
+
content = candidate.get("content", {})
|
|
171
|
+
for part in content.get("parts", []):
|
|
172
|
+
text = part.get("text", "")
|
|
173
|
+
if text:
|
|
174
|
+
parts.append(text)
|
|
175
|
+
return "\n".join(parts)
|
|
176
|
+
except Exception:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Route handler
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
async def proxy_gemini(path: str, request: "Any", gcc_repo: Any) -> "Any":
|
|
185
|
+
"""
|
|
186
|
+
Proxy /gemini/{path} → https://generativelanguage.googleapis.com/{path}
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
from fastapi import Response
|
|
190
|
+
from fastapi.responses import StreamingResponse
|
|
191
|
+
import httpx
|
|
192
|
+
except ImportError:
|
|
193
|
+
raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
|
|
194
|
+
|
|
195
|
+
# -----------------------------------------------------------------------
|
|
196
|
+
# 1. Read request body
|
|
197
|
+
# -----------------------------------------------------------------------
|
|
198
|
+
try:
|
|
199
|
+
body_bytes = await request.body()
|
|
200
|
+
body: dict = json.loads(body_bytes) if body_bytes else {}
|
|
201
|
+
except Exception:
|
|
202
|
+
body = {}
|
|
203
|
+
body_bytes = b""
|
|
204
|
+
|
|
205
|
+
# -----------------------------------------------------------------------
|
|
206
|
+
# 2. Inject RACP into first user content part
|
|
207
|
+
# -----------------------------------------------------------------------
|
|
208
|
+
modified_body = body
|
|
209
|
+
try:
|
|
210
|
+
if gcc_repo is not None and gcc_repo.is_initialized():
|
|
211
|
+
prefix = _build_racp_prefix(gcc_repo)
|
|
212
|
+
if prefix:
|
|
213
|
+
modified_body = _inject_racp_into_contents(body, prefix)
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
logger.warning("devtorch proxy[gemini]: RACP injection failed — %s", exc)
|
|
216
|
+
modified_body = body
|
|
217
|
+
|
|
218
|
+
send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
|
|
219
|
+
|
|
220
|
+
# -----------------------------------------------------------------------
|
|
221
|
+
# 3. Build forwarding headers
|
|
222
|
+
# -----------------------------------------------------------------------
|
|
223
|
+
forward_headers = {
|
|
224
|
+
k: v for k, v in request.headers.items()
|
|
225
|
+
if k.lower() not in ("host", "content-length", "transfer-encoding")
|
|
226
|
+
}
|
|
227
|
+
forward_headers["host"] = "generativelanguage.googleapis.com"
|
|
228
|
+
if send_body:
|
|
229
|
+
forward_headers["content-length"] = str(len(send_body))
|
|
230
|
+
|
|
231
|
+
upstream_url = f"{UPSTREAM}/{path}"
|
|
232
|
+
if request.url.query:
|
|
233
|
+
upstream_url = f"{upstream_url}?{request.url.query}"
|
|
234
|
+
|
|
235
|
+
session_id = str(uuid.uuid4())[:8]
|
|
236
|
+
|
|
237
|
+
# Gemini uses ?alt=sse for streaming; check both stream flag and path
|
|
238
|
+
is_streaming = bool(
|
|
239
|
+
modified_body.get("stream", False)
|
|
240
|
+
or "streamGenerateContent" in path
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# -----------------------------------------------------------------------
|
|
244
|
+
# 4 & 5. Stream or buffer
|
|
245
|
+
# -----------------------------------------------------------------------
|
|
246
|
+
if is_streaming:
|
|
247
|
+
async def _stream_generator():
|
|
248
|
+
buffer_parts: list[bytes] = []
|
|
249
|
+
try:
|
|
250
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
251
|
+
async with client.stream(
|
|
252
|
+
method=request.method,
|
|
253
|
+
url=upstream_url,
|
|
254
|
+
headers=forward_headers,
|
|
255
|
+
content=send_body,
|
|
256
|
+
) as upstream_resp:
|
|
257
|
+
async for chunk in upstream_resp.aiter_bytes():
|
|
258
|
+
buffer_parts.append(chunk)
|
|
259
|
+
yield chunk
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
logger.warning("devtorch proxy[gemini]: streaming error — %s", exc)
|
|
262
|
+
|
|
263
|
+
raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
|
|
264
|
+
# Try to parse as JSON array (Gemini streams JSON objects)
|
|
265
|
+
try:
|
|
266
|
+
combined = json.loads(raw_text)
|
|
267
|
+
if isinstance(combined, list):
|
|
268
|
+
extracted = "\n".join(
|
|
269
|
+
_extract_text_from_response(item) for item in combined
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
extracted = _extract_text_from_response(combined)
|
|
273
|
+
except Exception:
|
|
274
|
+
extracted = raw_text
|
|
275
|
+
_schedule_capture(gcc_repo, extracted or raw_text, session_id)
|
|
276
|
+
|
|
277
|
+
return StreamingResponse(
|
|
278
|
+
_stream_generator(),
|
|
279
|
+
media_type="application/json",
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
try:
|
|
283
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
284
|
+
upstream_resp = await client.request(
|
|
285
|
+
method=request.method,
|
|
286
|
+
url=upstream_url,
|
|
287
|
+
headers=forward_headers,
|
|
288
|
+
content=send_body,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
resp_json = upstream_resp.json()
|
|
293
|
+
resp_text = _extract_text_from_response(resp_json) or upstream_resp.text
|
|
294
|
+
except Exception:
|
|
295
|
+
resp_text = upstream_resp.text
|
|
296
|
+
|
|
297
|
+
_schedule_capture(gcc_repo, resp_text, session_id)
|
|
298
|
+
|
|
299
|
+
excluded = {"transfer-encoding", "content-encoding", "content-length"}
|
|
300
|
+
resp_headers = {
|
|
301
|
+
k: v for k, v in upstream_resp.headers.items()
|
|
302
|
+
if k.lower() not in excluded
|
|
303
|
+
}
|
|
304
|
+
return Response(
|
|
305
|
+
content=upstream_resp.content,
|
|
306
|
+
status_code=upstream_resp.status_code,
|
|
307
|
+
headers=resp_headers,
|
|
308
|
+
media_type=upstream_resp.headers.get("content-type", "application/json"),
|
|
309
|
+
)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
logger.warning("devtorch proxy[gemini]: upstream request failed — %s", exc)
|
|
312
|
+
try:
|
|
313
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
314
|
+
fallback_resp = await client.request(
|
|
315
|
+
method=request.method,
|
|
316
|
+
url=upstream_url,
|
|
317
|
+
headers=forward_headers,
|
|
318
|
+
content=body_bytes,
|
|
319
|
+
)
|
|
320
|
+
return Response(
|
|
321
|
+
content=fallback_resp.content,
|
|
322
|
+
status_code=fallback_resp.status_code,
|
|
323
|
+
media_type=fallback_resp.headers.get("content-type", "application/json"),
|
|
324
|
+
)
|
|
325
|
+
except Exception as exc2:
|
|
326
|
+
logger.error("devtorch proxy[gemini]: fallback also failed — %s", exc2)
|
|
327
|
+
return Response(
|
|
328
|
+
content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
|
|
329
|
+
status_code=502,
|
|
330
|
+
media_type="application/json",
|
|
331
|
+
)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevTorch proxy route: Groq
|
|
3
|
+
|
|
4
|
+
/groq/{path} → https://api.groq.com/openai/v1/{path}
|
|
5
|
+
|
|
6
|
+
Groq exposes an OpenAI-compatible API. RACP is injected as a system message
|
|
7
|
+
at position 0 in body["messages"].
|
|
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.groq")
|
|
18
|
+
|
|
19
|
+
UPSTREAM = "https://api.groq.com/openai/v1"
|
|
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[groq]: 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[groq]: 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[groq]: 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[groq]: 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[groq]: 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
|
+
"""
|
|
112
|
+
parts: list[str] = []
|
|
113
|
+
for line in raw.splitlines():
|
|
114
|
+
line = line.strip()
|
|
115
|
+
if not line.startswith("data:"):
|
|
116
|
+
continue
|
|
117
|
+
data_str = line[5:].strip()
|
|
118
|
+
if data_str == "[DONE]":
|
|
119
|
+
break
|
|
120
|
+
try:
|
|
121
|
+
obj = json.loads(data_str)
|
|
122
|
+
for choice in obj.get("choices", []):
|
|
123
|
+
delta = choice.get("delta", {})
|
|
124
|
+
content = delta.get("content", "")
|
|
125
|
+
if content:
|
|
126
|
+
parts.append(content)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
return "".join(parts)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Route handler
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
async def proxy_groq(path: str, request: "Any", gcc_repo: Any) -> "Any":
|
|
137
|
+
"""
|
|
138
|
+
Proxy /groq/{path} → https://api.groq.com/openai/v1/{path}
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
from fastapi import Response
|
|
142
|
+
from fastapi.responses import StreamingResponse
|
|
143
|
+
import httpx
|
|
144
|
+
except ImportError:
|
|
145
|
+
raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
|
|
146
|
+
|
|
147
|
+
# -----------------------------------------------------------------------
|
|
148
|
+
# 1. Read request body
|
|
149
|
+
# -----------------------------------------------------------------------
|
|
150
|
+
try:
|
|
151
|
+
body_bytes = await request.body()
|
|
152
|
+
body: dict = json.loads(body_bytes) if body_bytes else {}
|
|
153
|
+
except Exception:
|
|
154
|
+
body = {}
|
|
155
|
+
body_bytes = b""
|
|
156
|
+
|
|
157
|
+
# -----------------------------------------------------------------------
|
|
158
|
+
# 2. Inject RACP as system message at position 0
|
|
159
|
+
# -----------------------------------------------------------------------
|
|
160
|
+
modified_body = body
|
|
161
|
+
try:
|
|
162
|
+
if gcc_repo is not None and gcc_repo.is_initialized():
|
|
163
|
+
prefix = _build_racp_prefix(gcc_repo)
|
|
164
|
+
if prefix:
|
|
165
|
+
messages = list(body.get("messages", []))
|
|
166
|
+
racp_msg = {"role": "system", "content": prefix}
|
|
167
|
+
if messages and messages[0].get("role") == "system":
|
|
168
|
+
existing_content = messages[0].get("content", "")
|
|
169
|
+
messages[0] = {
|
|
170
|
+
"role": "system",
|
|
171
|
+
"content": prefix + "\n\n" + existing_content,
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
messages.insert(0, racp_msg)
|
|
175
|
+
modified_body = {**body, "messages": messages}
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
logger.warning("devtorch proxy[groq]: RACP injection failed — %s", exc)
|
|
178
|
+
modified_body = body
|
|
179
|
+
|
|
180
|
+
send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
|
|
181
|
+
|
|
182
|
+
# -----------------------------------------------------------------------
|
|
183
|
+
# 3. Build forwarding headers
|
|
184
|
+
# -----------------------------------------------------------------------
|
|
185
|
+
forward_headers = {
|
|
186
|
+
k: v for k, v in request.headers.items()
|
|
187
|
+
if k.lower() not in ("host", "content-length", "transfer-encoding")
|
|
188
|
+
}
|
|
189
|
+
forward_headers["host"] = "api.groq.com"
|
|
190
|
+
if send_body:
|
|
191
|
+
forward_headers["content-length"] = str(len(send_body))
|
|
192
|
+
|
|
193
|
+
upstream_url = f"{UPSTREAM}/{path}"
|
|
194
|
+
if request.url.query:
|
|
195
|
+
upstream_url = f"{upstream_url}?{request.url.query}"
|
|
196
|
+
|
|
197
|
+
session_id = str(uuid.uuid4())[:8]
|
|
198
|
+
is_streaming = bool(modified_body.get("stream", False))
|
|
199
|
+
|
|
200
|
+
# -----------------------------------------------------------------------
|
|
201
|
+
# 4 & 5. Stream or buffer
|
|
202
|
+
# -----------------------------------------------------------------------
|
|
203
|
+
if is_streaming:
|
|
204
|
+
async def _stream_generator():
|
|
205
|
+
buffer_parts: list[bytes] = []
|
|
206
|
+
try:
|
|
207
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
208
|
+
async with client.stream(
|
|
209
|
+
method=request.method,
|
|
210
|
+
url=upstream_url,
|
|
211
|
+
headers=forward_headers,
|
|
212
|
+
content=send_body,
|
|
213
|
+
) as upstream_resp:
|
|
214
|
+
async for chunk in upstream_resp.aiter_bytes():
|
|
215
|
+
buffer_parts.append(chunk)
|
|
216
|
+
yield chunk
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
logger.warning("devtorch proxy[groq]: streaming error — %s", exc)
|
|
219
|
+
|
|
220
|
+
raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
|
|
221
|
+
extracted = _extract_text_from_sse(raw_text)
|
|
222
|
+
_schedule_capture(gcc_repo, extracted or raw_text, session_id)
|
|
223
|
+
|
|
224
|
+
return StreamingResponse(
|
|
225
|
+
_stream_generator(),
|
|
226
|
+
media_type="text/event-stream",
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
try:
|
|
230
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
231
|
+
upstream_resp = await client.request(
|
|
232
|
+
method=request.method,
|
|
233
|
+
url=upstream_url,
|
|
234
|
+
headers=forward_headers,
|
|
235
|
+
content=send_body,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
resp_json = upstream_resp.json()
|
|
240
|
+
text_parts: list[str] = []
|
|
241
|
+
for choice in resp_json.get("choices", []):
|
|
242
|
+
msg = choice.get("message", {})
|
|
243
|
+
content = msg.get("content", "")
|
|
244
|
+
if content:
|
|
245
|
+
text_parts.append(content)
|
|
246
|
+
resp_text = "\n".join(text_parts) or upstream_resp.text
|
|
247
|
+
except Exception:
|
|
248
|
+
resp_text = upstream_resp.text
|
|
249
|
+
|
|
250
|
+
_schedule_capture(gcc_repo, resp_text, session_id)
|
|
251
|
+
|
|
252
|
+
excluded = {"transfer-encoding", "content-encoding", "content-length"}
|
|
253
|
+
resp_headers = {
|
|
254
|
+
k: v for k, v in upstream_resp.headers.items()
|
|
255
|
+
if k.lower() not in excluded
|
|
256
|
+
}
|
|
257
|
+
return Response(
|
|
258
|
+
content=upstream_resp.content,
|
|
259
|
+
status_code=upstream_resp.status_code,
|
|
260
|
+
headers=resp_headers,
|
|
261
|
+
media_type=upstream_resp.headers.get("content-type", "application/json"),
|
|
262
|
+
)
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
logger.warning("devtorch proxy[groq]: upstream request failed — %s", exc)
|
|
265
|
+
try:
|
|
266
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
267
|
+
fallback_resp = await client.request(
|
|
268
|
+
method=request.method,
|
|
269
|
+
url=upstream_url,
|
|
270
|
+
headers=forward_headers,
|
|
271
|
+
content=body_bytes,
|
|
272
|
+
)
|
|
273
|
+
return Response(
|
|
274
|
+
content=fallback_resp.content,
|
|
275
|
+
status_code=fallback_resp.status_code,
|
|
276
|
+
media_type=fallback_resp.headers.get("content-type", "application/json"),
|
|
277
|
+
)
|
|
278
|
+
except Exception as exc2:
|
|
279
|
+
logger.error("devtorch proxy[groq]: fallback also failed — %s", exc2)
|
|
280
|
+
return Response(
|
|
281
|
+
content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
|
|
282
|
+
status_code=502,
|
|
283
|
+
media_type="application/json",
|
|
284
|
+
)
|