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,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
|
+
)
|