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,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevTorch proxy route: Anthropic
|
|
3
|
+
|
|
4
|
+
/anthropic/{path} → https://api.anthropic.com/{path}
|
|
5
|
+
|
|
6
|
+
Request pipeline:
|
|
7
|
+
1. Parse body as JSON.
|
|
8
|
+
2. Inject RACP system prefix into body["system"].
|
|
9
|
+
3. Forward to api.anthropic.com with original headers (x-api-key passed through).
|
|
10
|
+
4. Stream chunks back while buffering.
|
|
11
|
+
5. After stream closes, schedule .GCC/ capture as non-blocking asyncio task.
|
|
12
|
+
|
|
13
|
+
All errors degrade gracefully — the original LLM response is always returned.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("devtorch.proxy.anthropic")
|
|
25
|
+
|
|
26
|
+
UPSTREAM = "https://api.anthropic.com"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Lightweight RACP prefix builder (no import from wrapper.base to avoid circulars)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def _build_racp_prefix(gcc_repo: Any) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Build the RACP system prefix from a GCCRepository instance.
|
|
36
|
+
Returns empty string on any error so callers can safely concatenate.
|
|
37
|
+
"""
|
|
38
|
+
if gcc_repo is None:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
parts: list[str] = []
|
|
42
|
+
|
|
43
|
+
# 1. RACP canonical system prompt
|
|
44
|
+
try:
|
|
45
|
+
racp_prompt = gcc_repo.prompt_get()
|
|
46
|
+
if racp_prompt:
|
|
47
|
+
parts.append(racp_prompt.strip())
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
logger.debug("devtorch proxy: prompt_get failed — %s", exc)
|
|
50
|
+
|
|
51
|
+
# 2. Θ top-10 concepts by mean_confidence
|
|
52
|
+
try:
|
|
53
|
+
theta = gcc_repo.get_theta()
|
|
54
|
+
cv = theta.get("coordination_vector", {})
|
|
55
|
+
if cv:
|
|
56
|
+
def _mean_conf(entry: Any) -> float:
|
|
57
|
+
if isinstance(entry, dict):
|
|
58
|
+
return float(entry.get("mean_confidence", 0.0))
|
|
59
|
+
return 0.0
|
|
60
|
+
|
|
61
|
+
top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
|
|
62
|
+
if top:
|
|
63
|
+
lines = ["[DevTorch Θ — top concepts]"]
|
|
64
|
+
for concept, data in top:
|
|
65
|
+
conf = _mean_conf(data)
|
|
66
|
+
lines.append(f" {concept}: mean_confidence={conf:.3f}")
|
|
67
|
+
parts.append("\n".join(lines))
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.debug("devtorch proxy: theta_summary failed — %s", exc)
|
|
70
|
+
|
|
71
|
+
# 3. Context bundle summary
|
|
72
|
+
try:
|
|
73
|
+
bundle = gcc_repo.context_bundle(
|
|
74
|
+
k_tokens=8000,
|
|
75
|
+
policy={"sensitivity_policy": "default"},
|
|
76
|
+
)
|
|
77
|
+
artifacts = bundle.get("artifacts", [])
|
|
78
|
+
if artifacts:
|
|
79
|
+
lines = ["[DevTorch context bundle]"]
|
|
80
|
+
for art in artifacts:
|
|
81
|
+
path = art.get("path", "?")
|
|
82
|
+
reason = art.get("reason", "")
|
|
83
|
+
lines.append(f" - {path}: {reason}" if reason else f" - {path}")
|
|
84
|
+
parts.append("\n".join(lines))
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
logger.debug("devtorch proxy: context_bundle failed — %s", exc)
|
|
87
|
+
|
|
88
|
+
return "\n\n".join(parts)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Capture helper
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
|
|
96
|
+
"""Schedule .GCC/ capture as a fire-and-forget asyncio task."""
|
|
97
|
+
async def _do_capture() -> None:
|
|
98
|
+
try:
|
|
99
|
+
from devtorch_core.wrapper.base import CaptureOrchestrator
|
|
100
|
+
orchestrator = CaptureOrchestrator(gcc_repo)
|
|
101
|
+
orchestrator.capture(
|
|
102
|
+
response_text=response_text,
|
|
103
|
+
thinking_blocks=[],
|
|
104
|
+
session_id=session_id,
|
|
105
|
+
)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.warning("devtorch proxy: capture failed — %s", exc)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
loop = asyncio.get_event_loop()
|
|
111
|
+
if loop.is_running():
|
|
112
|
+
asyncio.ensure_future(_do_capture())
|
|
113
|
+
else:
|
|
114
|
+
loop.run_until_complete(_do_capture())
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
logger.warning("devtorch proxy: could not schedule capture — %s", exc)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Route handler
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
async def proxy_anthropic(path: str, request: "Any", gcc_repo: Any) -> "Any":
|
|
124
|
+
"""
|
|
125
|
+
Proxy /anthropic/{path} → https://api.anthropic.com/{path}
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
from fastapi import Response
|
|
129
|
+
from fastapi.responses import StreamingResponse
|
|
130
|
+
import httpx
|
|
131
|
+
except ImportError:
|
|
132
|
+
raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
|
|
133
|
+
|
|
134
|
+
# -----------------------------------------------------------------------
|
|
135
|
+
# 1. Read request body
|
|
136
|
+
# -----------------------------------------------------------------------
|
|
137
|
+
try:
|
|
138
|
+
body_bytes = await request.body()
|
|
139
|
+
body: dict = json.loads(body_bytes) if body_bytes else {}
|
|
140
|
+
except Exception:
|
|
141
|
+
body = {}
|
|
142
|
+
body_bytes = b""
|
|
143
|
+
|
|
144
|
+
# -----------------------------------------------------------------------
|
|
145
|
+
# 2. Inject RACP prefix into body["system"]
|
|
146
|
+
# -----------------------------------------------------------------------
|
|
147
|
+
modified_body = body
|
|
148
|
+
injection_failed = False
|
|
149
|
+
try:
|
|
150
|
+
if gcc_repo is not None and gcc_repo.is_initialized():
|
|
151
|
+
prefix = _build_racp_prefix(gcc_repo)
|
|
152
|
+
if prefix:
|
|
153
|
+
existing = body.get("system", "")
|
|
154
|
+
separator = "\n\n" if existing else ""
|
|
155
|
+
modified_body = {**body, "system": prefix + separator + existing}
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
logger.warning("devtorch proxy: RACP injection failed — %s", exc)
|
|
158
|
+
injection_failed = True
|
|
159
|
+
modified_body = body
|
|
160
|
+
|
|
161
|
+
send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
|
|
162
|
+
|
|
163
|
+
# -----------------------------------------------------------------------
|
|
164
|
+
# 3. Build forwarding headers (replace host, keep everything else)
|
|
165
|
+
# -----------------------------------------------------------------------
|
|
166
|
+
forward_headers = {
|
|
167
|
+
k: v for k, v in request.headers.items()
|
|
168
|
+
if k.lower() not in ("host", "content-length", "transfer-encoding")
|
|
169
|
+
}
|
|
170
|
+
forward_headers["host"] = "api.anthropic.com"
|
|
171
|
+
if send_body:
|
|
172
|
+
forward_headers["content-length"] = str(len(send_body))
|
|
173
|
+
|
|
174
|
+
upstream_url = f"{UPSTREAM}/{path}"
|
|
175
|
+
if request.url.query:
|
|
176
|
+
upstream_url = f"{upstream_url}?{request.url.query}"
|
|
177
|
+
|
|
178
|
+
session_id = str(uuid.uuid4())[:8]
|
|
179
|
+
is_streaming = bool(modified_body.get("stream", False))
|
|
180
|
+
|
|
181
|
+
# -----------------------------------------------------------------------
|
|
182
|
+
# 4 & 5. Stream or buffer response; schedule capture after completion
|
|
183
|
+
# -----------------------------------------------------------------------
|
|
184
|
+
if is_streaming:
|
|
185
|
+
async def _stream_generator():
|
|
186
|
+
buffer_parts: list[bytes] = []
|
|
187
|
+
try:
|
|
188
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
189
|
+
async with client.stream(
|
|
190
|
+
method=request.method,
|
|
191
|
+
url=upstream_url,
|
|
192
|
+
headers=forward_headers,
|
|
193
|
+
content=send_body,
|
|
194
|
+
) as upstream_resp:
|
|
195
|
+
async for chunk in upstream_resp.aiter_bytes():
|
|
196
|
+
buffer_parts.append(chunk)
|
|
197
|
+
yield chunk
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
logger.warning("devtorch proxy: streaming error — %s", exc)
|
|
200
|
+
|
|
201
|
+
# After stream done, fire capture
|
|
202
|
+
full_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
|
|
203
|
+
_schedule_capture(gcc_repo, full_text, session_id)
|
|
204
|
+
|
|
205
|
+
response_headers = {}
|
|
206
|
+
# We can't get upstream status before streaming; return 200 with streamed content
|
|
207
|
+
return StreamingResponse(
|
|
208
|
+
_stream_generator(),
|
|
209
|
+
media_type="text/event-stream",
|
|
210
|
+
headers=response_headers,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Non-streaming: buffer the full response
|
|
214
|
+
try:
|
|
215
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
216
|
+
upstream_resp = await client.request(
|
|
217
|
+
method=request.method,
|
|
218
|
+
url=upstream_url,
|
|
219
|
+
headers=forward_headers,
|
|
220
|
+
content=send_body,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Schedule capture after receiving full response
|
|
224
|
+
try:
|
|
225
|
+
resp_text = upstream_resp.text
|
|
226
|
+
_schedule_capture(gcc_repo, resp_text, session_id)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Filter hop-by-hop headers
|
|
231
|
+
excluded = {"transfer-encoding", "content-encoding", "content-length"}
|
|
232
|
+
resp_headers = {
|
|
233
|
+
k: v for k, v in upstream_resp.headers.items()
|
|
234
|
+
if k.lower() not in excluded
|
|
235
|
+
}
|
|
236
|
+
return Response(
|
|
237
|
+
content=upstream_resp.content,
|
|
238
|
+
status_code=upstream_resp.status_code,
|
|
239
|
+
headers=resp_headers,
|
|
240
|
+
media_type=upstream_resp.headers.get("content-type", "application/json"),
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.warning("devtorch proxy: upstream request failed — %s", exc)
|
|
244
|
+
# Forward original unmodified request as fallback
|
|
245
|
+
try:
|
|
246
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
247
|
+
fallback_resp = await client.request(
|
|
248
|
+
method=request.method,
|
|
249
|
+
url=upstream_url,
|
|
250
|
+
headers=forward_headers,
|
|
251
|
+
content=body_bytes,
|
|
252
|
+
)
|
|
253
|
+
return Response(
|
|
254
|
+
content=fallback_resp.content,
|
|
255
|
+
status_code=fallback_resp.status_code,
|
|
256
|
+
media_type=fallback_resp.headers.get("content-type", "application/json"),
|
|
257
|
+
)
|
|
258
|
+
except Exception as exc2:
|
|
259
|
+
logger.error("devtorch proxy: fallback also failed — %s", exc2)
|
|
260
|
+
return Response(
|
|
261
|
+
content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
|
|
262
|
+
status_code=502,
|
|
263
|
+
media_type="application/json",
|
|
264
|
+
)
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevTorch proxy route: Azure OpenAI
|
|
3
|
+
|
|
4
|
+
/azure-openai/{path} → {AZURE_OPENAI_ENDPOINT}/{path}?api-version={...}
|
|
5
|
+
|
|
6
|
+
The Azure OpenAI endpoint and API version are resolved from request headers
|
|
7
|
+
first, then environment variables:
|
|
8
|
+
- AZURE_OPENAI_ENDPOINT (e.g. https://myresource.openai.azure.com)
|
|
9
|
+
- AZURE_OPENAI_API_VERSION (e.g. 2024-06-01)
|
|
10
|
+
|
|
11
|
+
If the caller passes X-Azure-OpenAI-Endpoint and X-Azure-OpenAI-Api-Version
|
|
12
|
+
headers, those override the environment. The api-key header is passed through
|
|
13
|
+
unchanged.
|
|
14
|
+
|
|
15
|
+
RACP is injected as a system message at position 0 in body["messages"], just
|
|
16
|
+
like the OpenAI route.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import uuid
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("devtorch.proxy.azure_openai")
|
|
28
|
+
|
|
29
|
+
DEFAULT_API_VERSION = "2024-06-01"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Endpoint resolution
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _resolve_endpoint(request: "Any") -> tuple[str, str]:
|
|
37
|
+
"""
|
|
38
|
+
Resolve (endpoint, api_version) from headers or environment.
|
|
39
|
+
"""
|
|
40
|
+
headers = {k.lower(): v for k, v in request.headers.items()}
|
|
41
|
+
endpoint = headers.get("x-azure-openai-endpoint", "")
|
|
42
|
+
api_version = headers.get("x-azure-openai-api-version", "")
|
|
43
|
+
|
|
44
|
+
if not endpoint:
|
|
45
|
+
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "")
|
|
46
|
+
if not api_version:
|
|
47
|
+
api_version = os.environ.get("AZURE_OPENAI_API_VERSION", DEFAULT_API_VERSION)
|
|
48
|
+
|
|
49
|
+
return endpoint.rstrip("/"), api_version
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Lightweight RACP prefix builder
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def _build_racp_prefix(gcc_repo: Any) -> str:
|
|
57
|
+
if gcc_repo is None:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
parts: list[str] = []
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
racp_prompt = gcc_repo.prompt_get()
|
|
64
|
+
if racp_prompt:
|
|
65
|
+
parts.append(racp_prompt.strip())
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
logger.debug("devtorch proxy[azure_openai]: prompt_get failed — %s", exc)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
theta = gcc_repo.get_theta()
|
|
71
|
+
cv = theta.get("coordination_vector", {})
|
|
72
|
+
if cv:
|
|
73
|
+
def _mean_conf(entry: Any) -> float:
|
|
74
|
+
if isinstance(entry, dict):
|
|
75
|
+
return float(entry.get("mean_confidence", 0.0))
|
|
76
|
+
return 0.0
|
|
77
|
+
|
|
78
|
+
top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
|
|
79
|
+
if top:
|
|
80
|
+
lines = ["[DevTorch Θ — top concepts]"]
|
|
81
|
+
for concept, data in top:
|
|
82
|
+
conf = _mean_conf(data)
|
|
83
|
+
lines.append(f" {concept}: mean_confidence={conf:.3f}")
|
|
84
|
+
parts.append("\n".join(lines))
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
logger.debug("devtorch proxy[azure_openai]: theta_summary failed — %s", exc)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
bundle = gcc_repo.context_bundle(
|
|
90
|
+
k_tokens=8000,
|
|
91
|
+
policy={"sensitivity_policy": "default"},
|
|
92
|
+
)
|
|
93
|
+
artifacts = bundle.get("artifacts", [])
|
|
94
|
+
if artifacts:
|
|
95
|
+
lines = ["[DevTorch context bundle]"]
|
|
96
|
+
for art in artifacts:
|
|
97
|
+
path = art.get("path", "?")
|
|
98
|
+
reason = art.get("reason", "")
|
|
99
|
+
lines.append(f" - {path}: {reason}" if reason else f" - {path}")
|
|
100
|
+
parts.append("\n".join(lines))
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
logger.debug("devtorch proxy[azure_openai]: context_bundle failed — %s", exc)
|
|
103
|
+
|
|
104
|
+
return "\n\n".join(parts)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Capture helper
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
|
|
112
|
+
async def _do_capture() -> None:
|
|
113
|
+
try:
|
|
114
|
+
from devtorch_core.wrapper.base import CaptureOrchestrator
|
|
115
|
+
orchestrator = CaptureOrchestrator(gcc_repo)
|
|
116
|
+
orchestrator.capture(
|
|
117
|
+
response_text=response_text,
|
|
118
|
+
thinking_blocks=[],
|
|
119
|
+
session_id=session_id,
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.warning("devtorch proxy[azure_openai]: capture failed — %s", exc)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
loop = asyncio.get_event_loop()
|
|
126
|
+
if loop.is_running():
|
|
127
|
+
asyncio.ensure_future(_do_capture())
|
|
128
|
+
else:
|
|
129
|
+
loop.run_until_complete(_do_capture())
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
logger.warning("devtorch proxy[azure_openai]: could not schedule capture — %s", exc)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# SSE accumulator: extract assistant text from streamed chunks
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def _extract_text_from_sse(raw: str) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Parse SSE stream and concatenate content delta text for capture.
|
|
141
|
+
"""
|
|
142
|
+
parts: list[str] = []
|
|
143
|
+
for line in raw.splitlines():
|
|
144
|
+
line = line.strip()
|
|
145
|
+
if not line.startswith("data:"):
|
|
146
|
+
continue
|
|
147
|
+
data_str = line[5:].strip()
|
|
148
|
+
if data_str == "[DONE]":
|
|
149
|
+
break
|
|
150
|
+
try:
|
|
151
|
+
obj = json.loads(data_str)
|
|
152
|
+
for choice in obj.get("choices", []):
|
|
153
|
+
delta = choice.get("delta", {})
|
|
154
|
+
content = delta.get("content", "")
|
|
155
|
+
if content:
|
|
156
|
+
parts.append(content)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
return "".join(parts)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Route handler
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
async def proxy_azure_openai(path: str, request: "Any", gcc_repo: Any) -> "Any":
|
|
167
|
+
"""
|
|
168
|
+
Proxy /azure-openai/{path} → Azure OpenAI endpoint.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
from fastapi import Response
|
|
172
|
+
from fastapi.responses import StreamingResponse
|
|
173
|
+
import httpx
|
|
174
|
+
except ImportError:
|
|
175
|
+
raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
|
|
176
|
+
|
|
177
|
+
# -----------------------------------------------------------------------
|
|
178
|
+
# 1. Read request body
|
|
179
|
+
# -----------------------------------------------------------------------
|
|
180
|
+
try:
|
|
181
|
+
body_bytes = await request.body()
|
|
182
|
+
body: dict = json.loads(body_bytes) if body_bytes else {}
|
|
183
|
+
except Exception:
|
|
184
|
+
body = {}
|
|
185
|
+
body_bytes = b""
|
|
186
|
+
|
|
187
|
+
# -----------------------------------------------------------------------
|
|
188
|
+
# 2. Resolve endpoint and inject RACP
|
|
189
|
+
# -----------------------------------------------------------------------
|
|
190
|
+
endpoint, api_version = _resolve_endpoint(request)
|
|
191
|
+
if not endpoint:
|
|
192
|
+
return Response(
|
|
193
|
+
content=json.dumps(
|
|
194
|
+
{"error": "missing Azure OpenAI endpoint", "detail": "Set AZURE_OPENAI_ENDPOINT or X-Azure-OpenAI-Endpoint header"}
|
|
195
|
+
).encode(),
|
|
196
|
+
status_code=400,
|
|
197
|
+
media_type="application/json",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
modified_body = body
|
|
201
|
+
try:
|
|
202
|
+
if gcc_repo is not None and gcc_repo.is_initialized():
|
|
203
|
+
prefix = _build_racp_prefix(gcc_repo)
|
|
204
|
+
if prefix:
|
|
205
|
+
messages = list(body.get("messages", []))
|
|
206
|
+
racp_msg = {"role": "system", "content": prefix}
|
|
207
|
+
if messages and messages[0].get("role") == "system":
|
|
208
|
+
existing_content = messages[0].get("content", "")
|
|
209
|
+
messages[0] = {
|
|
210
|
+
"role": "system",
|
|
211
|
+
"content": prefix + "\n\n" + existing_content,
|
|
212
|
+
}
|
|
213
|
+
else:
|
|
214
|
+
messages.insert(0, racp_msg)
|
|
215
|
+
modified_body = {**body, "messages": messages}
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.warning("devtorch proxy[azure_openai]: RACP injection failed — %s", exc)
|
|
218
|
+
modified_body = body
|
|
219
|
+
|
|
220
|
+
send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
|
|
221
|
+
|
|
222
|
+
# -----------------------------------------------------------------------
|
|
223
|
+
# 3. Build forwarding headers
|
|
224
|
+
# -----------------------------------------------------------------------
|
|
225
|
+
forward_headers = {
|
|
226
|
+
k: v for k, v in request.headers.items()
|
|
227
|
+
if k.lower() not in (
|
|
228
|
+
"host",
|
|
229
|
+
"content-length",
|
|
230
|
+
"transfer-encoding",
|
|
231
|
+
"x-azure-openai-endpoint",
|
|
232
|
+
"x-azure-openai-api-version",
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
try:
|
|
236
|
+
host = endpoint.split("//", 1)[1].split("/", 1)[0]
|
|
237
|
+
except Exception:
|
|
238
|
+
host = endpoint
|
|
239
|
+
forward_headers["host"] = host
|
|
240
|
+
if send_body:
|
|
241
|
+
forward_headers["content-length"] = str(len(send_body))
|
|
242
|
+
|
|
243
|
+
upstream_url = f"{endpoint}/{path}" if path else endpoint
|
|
244
|
+
if request.url.query:
|
|
245
|
+
upstream_url = f"{upstream_url}?{request.url.query}"
|
|
246
|
+
else:
|
|
247
|
+
upstream_url = f"{upstream_url}?api-version={api_version}"
|
|
248
|
+
|
|
249
|
+
session_id = str(uuid.uuid4())[:8]
|
|
250
|
+
is_streaming = bool(modified_body.get("stream", False))
|
|
251
|
+
|
|
252
|
+
# -----------------------------------------------------------------------
|
|
253
|
+
# 4 & 5. Stream or buffer
|
|
254
|
+
# -----------------------------------------------------------------------
|
|
255
|
+
if is_streaming:
|
|
256
|
+
async def _stream_generator():
|
|
257
|
+
buffer_parts: list[bytes] = []
|
|
258
|
+
try:
|
|
259
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
260
|
+
async with client.stream(
|
|
261
|
+
method=request.method,
|
|
262
|
+
url=upstream_url,
|
|
263
|
+
headers=forward_headers,
|
|
264
|
+
content=send_body,
|
|
265
|
+
) as upstream_resp:
|
|
266
|
+
async for chunk in upstream_resp.aiter_bytes():
|
|
267
|
+
buffer_parts.append(chunk)
|
|
268
|
+
yield chunk
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
logger.warning("devtorch proxy[azure_openai]: streaming error — %s", exc)
|
|
271
|
+
|
|
272
|
+
raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
|
|
273
|
+
extracted = _extract_text_from_sse(raw_text)
|
|
274
|
+
_schedule_capture(gcc_repo, extracted or raw_text, session_id)
|
|
275
|
+
|
|
276
|
+
return StreamingResponse(
|
|
277
|
+
_stream_generator(),
|
|
278
|
+
media_type="text/event-stream",
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
try:
|
|
282
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
283
|
+
upstream_resp = await client.request(
|
|
284
|
+
method=request.method,
|
|
285
|
+
url=upstream_url,
|
|
286
|
+
headers=forward_headers,
|
|
287
|
+
content=send_body,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
resp_json = upstream_resp.json()
|
|
292
|
+
text_parts: list[str] = []
|
|
293
|
+
for choice in resp_json.get("choices", []):
|
|
294
|
+
msg = choice.get("message", {})
|
|
295
|
+
content = msg.get("content", "")
|
|
296
|
+
if content:
|
|
297
|
+
text_parts.append(content)
|
|
298
|
+
resp_text = "\n".join(text_parts) or upstream_resp.text
|
|
299
|
+
except Exception:
|
|
300
|
+
resp_text = upstream_resp.text
|
|
301
|
+
|
|
302
|
+
_schedule_capture(gcc_repo, resp_text, session_id)
|
|
303
|
+
|
|
304
|
+
excluded = {"transfer-encoding", "content-encoding", "content-length"}
|
|
305
|
+
resp_headers = {
|
|
306
|
+
k: v for k, v in upstream_resp.headers.items()
|
|
307
|
+
if k.lower() not in excluded
|
|
308
|
+
}
|
|
309
|
+
return Response(
|
|
310
|
+
content=upstream_resp.content,
|
|
311
|
+
status_code=upstream_resp.status_code,
|
|
312
|
+
headers=resp_headers,
|
|
313
|
+
media_type=upstream_resp.headers.get("content-type", "application/json"),
|
|
314
|
+
)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
logger.warning("devtorch proxy[azure_openai]: upstream request failed — %s", exc)
|
|
317
|
+
try:
|
|
318
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
319
|
+
fallback_resp = await client.request(
|
|
320
|
+
method=request.method,
|
|
321
|
+
url=upstream_url,
|
|
322
|
+
headers=forward_headers,
|
|
323
|
+
content=body_bytes,
|
|
324
|
+
)
|
|
325
|
+
return Response(
|
|
326
|
+
content=fallback_resp.content,
|
|
327
|
+
status_code=fallback_resp.status_code,
|
|
328
|
+
media_type=fallback_resp.headers.get("content-type", "application/json"),
|
|
329
|
+
)
|
|
330
|
+
except Exception as exc2:
|
|
331
|
+
logger.error("devtorch proxy[azure_openai]: fallback also failed — %s", exc2)
|
|
332
|
+
return Response(
|
|
333
|
+
content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
|
|
334
|
+
status_code=502,
|
|
335
|
+
media_type="application/json",
|
|
336
|
+
)
|