@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +126 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +151 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +225 -0
- package/out/cli/deploy/layers/identity.js +88 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +171 -17
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +260 -14
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +25 -2
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +460 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +222 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#---------------------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) AgentHippo.ai. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
|
|
3
4
|
#---------------------------------------------------------------------------------------------
|
|
4
5
|
"""
|
|
5
6
|
AgentIDE LiteLLM proxy hooks - Clean structured assistant output with separate input/output
|
|
@@ -7,11 +8,28 @@ Mimics langfuse_otel style: gen_ai.input.messages (history) + gen_ai.output.mess
|
|
|
7
8
|
|
|
8
9
|
Usage in litellm config.yaml:
|
|
9
10
|
callbacks: ["agentide_litellm_hooks_v1.clean_separated_otel_output"]
|
|
11
|
+
|
|
12
|
+
PRIVACY DEFAULT (PLAN/compliance-req.md §6 "data minimization"):
|
|
13
|
+
Prompt and completion CONTENT is NOT captured unless an operator opts in with
|
|
14
|
+
|
|
15
|
+
AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=1
|
|
16
|
+
|
|
17
|
+
Without the opt-in every span carries metadata only — model, tokens, latency, status — plus a
|
|
18
|
+
sha256 + byte length of the content so a later disclosure can still be *matched* against a
|
|
19
|
+
span without the span holding the text. This is enforced here at span-write time, in addition
|
|
20
|
+
to `turn_off_message_logging: true` in the config, so a config that re-enables message logging
|
|
21
|
+
without the env opt-in still produces redacted spans (defence in depth: the config is edited by
|
|
22
|
+
hand far more often than this file).
|
|
10
23
|
"""
|
|
11
24
|
|
|
12
25
|
from __future__ import annotations
|
|
13
26
|
from typing import Any
|
|
27
|
+
import ast
|
|
28
|
+
import hashlib
|
|
14
29
|
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
15
33
|
|
|
16
34
|
from litellm.integrations.custom_logger import CustomLogger
|
|
17
35
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
|
@@ -20,6 +38,192 @@ from opentelemetry.trace import Status, StatusCode
|
|
|
20
38
|
_OTEL_TEXT_MAX_LEN = 4000 # per text block
|
|
21
39
|
_OTEL_PREVIEW_LEN = 180 # short preview in event
|
|
22
40
|
|
|
41
|
+
# -----------------------------------------------------------------------------
|
|
42
|
+
# Content capture opt-in (fix: full capture used to be the default)
|
|
43
|
+
# -----------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
CONTENT_CAPTURE_ENV = "AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE"
|
|
46
|
+
|
|
47
|
+
_TRUTHY = frozenset({"1", "true", "yes", "on"})
|
|
48
|
+
|
|
49
|
+
# Span attributes that can carry prompt/completion text. Mirrors CONTENT_ATTRS in
|
|
50
|
+
# @agenthippo-ai/evidence-ship so the two redaction points cannot drift silently.
|
|
51
|
+
_CONTENT_ATTR_RE = re.compile(
|
|
52
|
+
r"^(gen_ai\.(input|output)\.messages"
|
|
53
|
+
r"|gen_ai\.(prompt|completion)"
|
|
54
|
+
r"|gen_ai\.system_instructions"
|
|
55
|
+
r"|llm\.(prompts|responses)"
|
|
56
|
+
r"|llm\.request\.functions\."
|
|
57
|
+
r"|error\.(message|stack_trace)"
|
|
58
|
+
r"|hidden_params"
|
|
59
|
+
r"|metadata\.requester_custom_headers)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_capture_banner_printed = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _content_capture_enabled() -> bool:
|
|
66
|
+
"""True only when an operator explicitly opted in to prompt/completion capture."""
|
|
67
|
+
return (os.environ.get(CONTENT_CAPTURE_ENV, "") or "").strip().lower() in _TRUTHY
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _announce_content_capture() -> None:
|
|
71
|
+
"""Log loudly, once, whichever mode is in force — silence is how this got shipped on."""
|
|
72
|
+
global _capture_banner_printed
|
|
73
|
+
if _capture_banner_printed:
|
|
74
|
+
return
|
|
75
|
+
_capture_banner_printed = True
|
|
76
|
+
if _content_capture_enabled():
|
|
77
|
+
print(
|
|
78
|
+
"\n"
|
|
79
|
+
"!! AGENTHIPPO EVIDENCE: FULL PROMPT/COMPLETION CAPTURE IS ON !!\n"
|
|
80
|
+
f"!! {CONTENT_CAPTURE_ENV} is set. Every span will carry prompt and completion text.\n"
|
|
81
|
+
"!! Unset it to return to metadata-only (sha256 + length) spans.\n",
|
|
82
|
+
file=sys.stderr,
|
|
83
|
+
flush=True,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
print(
|
|
87
|
+
f"[agentide-otel] content capture OFF (metadata + sha256 only). "
|
|
88
|
+
f"Set {CONTENT_CAPTURE_ENV}=1 to record prompts/completions.",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
flush=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _sha256(text: str) -> str:
|
|
95
|
+
return hashlib.sha256(text.encode("utf-8", "replace")).hexdigest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Correlation IDs Spotlight reads from the requester header blob (see FLEET_DATA_CONTRACT.md).
|
|
99
|
+
# These are minted by AgentHippo clients and carry no user content, so with capture off the
|
|
100
|
+
# blob is reduced to exactly these keys rather than dropped — unknown x-* headers from clients
|
|
101
|
+
# we do not control (e.g. x-api-key) still never reach the span in plaintext.
|
|
102
|
+
_METADATA_HEADERS_KEY = "metadata.requester_custom_headers"
|
|
103
|
+
|
|
104
|
+
_METADATA_HEADER_ALLOWLIST = frozenset({
|
|
105
|
+
"x-litellm-metadata-session-id",
|
|
106
|
+
"x-litellm-metadata-request-id",
|
|
107
|
+
"x-litellm-metadata-user-id",
|
|
108
|
+
"x-litellm-metadata-project-id",
|
|
109
|
+
"x-litellm-metadata-agent-id",
|
|
110
|
+
"x-litellm-metadata-agent-version",
|
|
111
|
+
"x-litellm-metadata-source",
|
|
112
|
+
"x-litellm-metadata-engine",
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_attr_dict(value: Any) -> Any:
|
|
117
|
+
if not isinstance(value, str):
|
|
118
|
+
return value
|
|
119
|
+
try:
|
|
120
|
+
return ast.literal_eval(value)
|
|
121
|
+
except (ValueError, SyntaxError):
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(value)
|
|
124
|
+
except ValueError:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _allowlisted_headers(headers: dict) -> dict:
|
|
129
|
+
return {
|
|
130
|
+
k.lower(): v for k, v in headers.items()
|
|
131
|
+
if isinstance(k, str) and k.lower() in _METADATA_HEADER_ALLOWLIST
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _filtered_metadata_headers(value: Any) -> str | None:
|
|
136
|
+
"""Reduce the header blob to allowlisted keys, keeping the str(dict) repr format that
|
|
137
|
+
the DuckDB extraction regexes parse. None when nothing allowlisted survives."""
|
|
138
|
+
headers = _parse_attr_dict(value)
|
|
139
|
+
if not isinstance(headers, dict):
|
|
140
|
+
return None
|
|
141
|
+
kept = _allowlisted_headers(headers)
|
|
142
|
+
return str(kept) if kept else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# LiteLLM also embeds the raw request headers as metadata.requester_metadata['headers'] on some
|
|
146
|
+
# routes (observed on /v1/chat/completions; absent on /v1/messages). The attribute's scalar keys
|
|
147
|
+
# (session_id, request_id, …) are a fallback landing spot the extraction chain parses, so the
|
|
148
|
+
# attribute itself must survive — only its embedded header dict gets the allowlist treatment.
|
|
149
|
+
_REQUESTER_METADATA_KEY = "metadata.requester_metadata"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _scrub_requester_metadata(value: Any) -> Any:
|
|
153
|
+
"""Scrubbed value, or None (caller drops the attribute) when an embedded header dict
|
|
154
|
+
cannot be safely isolated — reprs with non-literal values defeat ast.literal_eval, and
|
|
155
|
+
passing those through unparsed would leak the very headers this exists to remove."""
|
|
156
|
+
md = _parse_attr_dict(value)
|
|
157
|
+
if isinstance(md, dict) and isinstance(md.get("headers"), dict):
|
|
158
|
+
return str({**md, "headers": _allowlisted_headers(md["headers"])})
|
|
159
|
+
if isinstance(md, dict):
|
|
160
|
+
return value
|
|
161
|
+
return None if "headers" in str(value) else value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _RedactingSpan:
|
|
165
|
+
"""
|
|
166
|
+
Span proxy that drops content-bearing attributes and replaces them with a sha256 + length.
|
|
167
|
+
|
|
168
|
+
LiteLLM's own `set_attributes()` writes `gen_ai.input.messages` (the full history) and
|
|
169
|
+
friends. We cannot remove an attribute after the SDK has recorded it, so the only place to
|
|
170
|
+
stop it is between LiteLLM and the SDK — here. Everything that is not content passes through
|
|
171
|
+
untouched, so the span keeps model, tokens, latency, status, and the requester metadata the
|
|
172
|
+
evidence index reads.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, span: Any) -> None:
|
|
176
|
+
self._span = span
|
|
177
|
+
|
|
178
|
+
def __getattr__(self, name: str) -> Any: # everything we do not override
|
|
179
|
+
return getattr(self._span, name)
|
|
180
|
+
|
|
181
|
+
def set_attribute(self, key: Any, value: Any) -> Any:
|
|
182
|
+
if key == _REQUESTER_METADATA_KEY:
|
|
183
|
+
scrubbed = _scrub_requester_metadata(value)
|
|
184
|
+
if scrubbed is None:
|
|
185
|
+
return None
|
|
186
|
+
return self._span.set_attribute(key, scrubbed)
|
|
187
|
+
if isinstance(key, str) and _CONTENT_ATTR_RE.match(key):
|
|
188
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
|
|
189
|
+
self._span.set_attribute(f"{key}.sha256", _sha256(text))
|
|
190
|
+
self._span.set_attribute(f"{key}.length", len(text))
|
|
191
|
+
self._span.set_attribute(f"{key}.redacted", True)
|
|
192
|
+
if key == _METADATA_HEADERS_KEY:
|
|
193
|
+
kept = _filtered_metadata_headers(value)
|
|
194
|
+
if kept is not None:
|
|
195
|
+
self._span.set_attribute(key, kept)
|
|
196
|
+
return None
|
|
197
|
+
return self._span.set_attribute(key, value)
|
|
198
|
+
|
|
199
|
+
def set_attributes(self, attributes: Any) -> Any:
|
|
200
|
+
if isinstance(attributes, dict):
|
|
201
|
+
for key, value in attributes.items():
|
|
202
|
+
self.set_attribute(key, value)
|
|
203
|
+
return None
|
|
204
|
+
return self._span.set_attributes(attributes)
|
|
205
|
+
|
|
206
|
+
def add_event(self, name: Any, attributes: Any = None, **kwargs: Any) -> Any:
|
|
207
|
+
if isinstance(attributes, dict):
|
|
208
|
+
filtered = {}
|
|
209
|
+
for key, value in attributes.items():
|
|
210
|
+
if key == _REQUESTER_METADATA_KEY:
|
|
211
|
+
scrubbed = _scrub_requester_metadata(value)
|
|
212
|
+
if scrubbed is not None:
|
|
213
|
+
filtered[key] = scrubbed
|
|
214
|
+
elif isinstance(key, str) and _CONTENT_ATTR_RE.match(key):
|
|
215
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
|
|
216
|
+
filtered[f"{key}.sha256"] = _sha256(text)
|
|
217
|
+
filtered[f"{key}.length"] = len(text)
|
|
218
|
+
if key == _METADATA_HEADERS_KEY:
|
|
219
|
+
kept = _filtered_metadata_headers(value)
|
|
220
|
+
if kept is not None:
|
|
221
|
+
filtered[key] = kept
|
|
222
|
+
else:
|
|
223
|
+
filtered[key] = value
|
|
224
|
+
attributes = filtered
|
|
225
|
+
return self._span.add_event(name, attributes, **kwargs)
|
|
226
|
+
|
|
23
227
|
|
|
24
228
|
def _read_field(obj: Any, key: str, default: Any = None) -> Any:
|
|
25
229
|
if isinstance(obj, dict):
|
|
@@ -103,9 +307,16 @@ def _is_anthropic_route(model: Any) -> bool:
|
|
|
103
307
|
return "claude" in name or "anthropic" in name
|
|
104
308
|
|
|
105
309
|
|
|
310
|
+
# Every model known to reject the Codex built-in web-search tools. Additive by policy: a
|
|
311
|
+
# deployment that still routes to an older model keeps its sanitizer when a newer one is added,
|
|
312
|
+
# because a dropped entry fails as upstream 400s on live traffic, not as a test failure.
|
|
313
|
+
_WEB_SEARCH_UNSUPPORTED_MODELS = ("gpt-5.6-luna", "gpt-4.1-nano")
|
|
314
|
+
|
|
315
|
+
|
|
106
316
|
def _is_web_search_unsupported_model(model: Any) -> bool:
|
|
107
317
|
"""True when the upstream model rejects Codex web-search tools."""
|
|
108
|
-
|
|
318
|
+
name = _model_name(model)
|
|
319
|
+
return any(m in name for m in _WEB_SEARCH_UNSUPPORTED_MODELS)
|
|
109
320
|
|
|
110
321
|
|
|
111
322
|
# OpenAI Responses API fields that Anthropic (and some gateways) reject if forwarded verbatim.
|
|
@@ -360,7 +571,7 @@ def sanitize_messages(messages: Any) -> Any:
|
|
|
360
571
|
|
|
361
572
|
|
|
362
573
|
class ReasoningParamsSanitizer(CustomLogger):
|
|
363
|
-
"""LiteLLM proxy hook: strip reasoning controls for gpt-
|
|
574
|
+
"""LiteLLM proxy hook: strip reasoning controls for gpt-5.6-luna."""
|
|
364
575
|
|
|
365
576
|
async def async_pre_call_hook(
|
|
366
577
|
self,
|
|
@@ -410,7 +621,7 @@ class CodexProxySanitizer(CustomLogger):
|
|
|
410
621
|
LiteLLM proxy hook: strip Codex/OpenAI Responses fields/tools that break proxy routes.
|
|
411
622
|
|
|
412
623
|
- Anthropic: drop client_metadata (OpenAI-only Responses telemetry).
|
|
413
|
-
- gpt-
|
|
624
|
+
- gpt-5.6-luna: drop web_search_preview and related built-in search tools.
|
|
414
625
|
"""
|
|
415
626
|
|
|
416
627
|
def _sanitize(self, payload: dict, model: Any) -> dict:
|
|
@@ -548,8 +759,36 @@ class CleanSeparatedOtelOutput(OpenTelemetry):
|
|
|
548
759
|
- gen_ai.input.messages → full history (as set by LiteLLM)
|
|
549
760
|
- gen_ai.output.messages → only the new assistant reply (stringified structured array)
|
|
550
761
|
- Short preview event for quick visibility
|
|
762
|
+
|
|
763
|
+
Content (both directions) is recorded ONLY when AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE is set.
|
|
764
|
+
Otherwise every content attribute becomes `<key>.sha256` + `<key>.length` — enough to prove
|
|
765
|
+
which bytes a turn saw, without shipping the bytes.
|
|
551
766
|
"""
|
|
552
767
|
|
|
768
|
+
def set_attributes(self, span: Any, *args: Any, **kwargs: Any) -> Any:
|
|
769
|
+
"""
|
|
770
|
+
Redaction chokepoint. Overriding here rather than at the call site covers every path in
|
|
771
|
+
the base class that sets attributes, including ones this subclass does not override.
|
|
772
|
+
"""
|
|
773
|
+
_announce_content_capture()
|
|
774
|
+
if not _content_capture_enabled():
|
|
775
|
+
span = _RedactingSpan(span)
|
|
776
|
+
return super().set_attributes(span, *args, **kwargs)
|
|
777
|
+
|
|
778
|
+
def _get_span_name(self, kwargs: dict) -> str:
|
|
779
|
+
# Evidence pipeline indexes spans named litellm_request; main-stable semconv uses "chat …".
|
|
780
|
+
#
|
|
781
|
+
# The constant is a PRIVATE litellm symbol and this runs on the span-creation path, so an
|
|
782
|
+
# upstream rename would raise for every span — total evidence loss at layer 1, not a
|
|
783
|
+
# degraded span name. The literal is the same value; falling back to it keeps the pipeline
|
|
784
|
+
# indexing while the import is fixed.
|
|
785
|
+
try:
|
|
786
|
+
from litellm.integrations.opentelemetry import LITELLM_REQUEST_SPAN_NAME
|
|
787
|
+
|
|
788
|
+
return LITELLM_REQUEST_SPAN_NAME
|
|
789
|
+
except Exception: # pragma: no cover - depends on the pinned litellm build
|
|
790
|
+
return "litellm_request"
|
|
791
|
+
|
|
553
792
|
def _start_primary_span(
|
|
554
793
|
self,
|
|
555
794
|
kwargs: dict,
|
|
@@ -573,24 +812,30 @@ class CleanSeparatedOtelOutput(OpenTelemetry):
|
|
|
573
812
|
_extract_assistant_text(response_obj)
|
|
574
813
|
|
|
575
814
|
if text.strip():
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"gen_ai.
|
|
592
|
-
|
|
593
|
-
|
|
815
|
+
capture = _content_capture_enabled()
|
|
816
|
+
event_attrs: dict[str, Any] = {
|
|
817
|
+
# Always present: proves a reply of exactly these bytes was produced, and lets a
|
|
818
|
+
# disclosed transcript be matched back to the span without storing the text.
|
|
819
|
+
"gen_ai.output.sha256": _sha256(text),
|
|
820
|
+
"gen_ai.output.length": len(text),
|
|
821
|
+
"gen_ai.output.truncated": len(text) > _OTEL_TEXT_MAX_LEN,
|
|
822
|
+
"gen_ai.output.captured": capture,
|
|
823
|
+
"gen_ai.response.model": _read_field(kwargs, "model", "unknown"),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if capture:
|
|
827
|
+
# Structured output (separate from input history) — opt-in only.
|
|
828
|
+
structured_msgs = _make_output_messages_structured(text)
|
|
829
|
+
span.set_attribute(
|
|
830
|
+
"gen_ai.output.messages",
|
|
831
|
+
json.dumps(structured_msgs, ensure_ascii=False)
|
|
832
|
+
)
|
|
833
|
+
# Short preview event - easy to spot, low noise
|
|
834
|
+
event_attrs["gen_ai.output.preview"] = (
|
|
835
|
+
text[:_OTEL_PREVIEW_LEN] + ("..." if len(text) > _OTEL_PREVIEW_LEN else "")
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
span.add_event(name="gen_ai.output.generated", attributes=event_attrs)
|
|
594
839
|
|
|
595
840
|
span.end(end_time=self._to_ns(end_time))
|
|
596
841
|
return span
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
#---------------------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) AgentHippo.ai. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
|
|
4
|
+
#---------------------------------------------------------------------------------------------
|
|
2
5
|
|
|
3
6
|
# AgentIDE LiteLLM configuration with OpenTelemetry
|
|
4
7
|
# Use this instead of agentide-litellm.config.yaml for lightweight analytics
|
|
@@ -19,6 +22,20 @@
|
|
|
19
22
|
# OTEL_SERVICE_NAME=agentide-litellm # Service name in traces
|
|
20
23
|
# DEBUG_OTEL=true # Enable OTEL debug logging
|
|
21
24
|
|
|
25
|
+
#
|
|
26
|
+
# PRIVACY DEFAULT — metadata only. See "Content capture" below.
|
|
27
|
+
# AUTHENTICATION — general_settings.master_key is REQUIRED; the proxy fails to start without it.
|
|
28
|
+
# ROUTES — consumer/subscription-credential routes are NOT in this file. See
|
|
29
|
+
# agentide-litellm-consumer-routes.local-dev-only.config.yaml.
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
general_settings:
|
|
33
|
+
# Without a master key LiteLLM accepts unauthenticated POST /v1/chat/completions from anything
|
|
34
|
+
# that can reach the port — on a serve host that is every colocated container, and the proxy
|
|
35
|
+
# holds provider credentials. No default is supplied on purpose: an unset LITELLM_MASTER_KEY
|
|
36
|
+
# must fail closed at startup rather than quietly open the proxy.
|
|
37
|
+
master_key: os.environ/LITELLM_MASTER_KEY
|
|
38
|
+
|
|
22
39
|
litellm_settings:
|
|
23
40
|
telemetry: false # Disables sending data to LiteLLM's remote servers
|
|
24
41
|
drop_params: true
|
|
@@ -36,139 +53,88 @@ litellm_settings:
|
|
|
36
53
|
# callbacks: ["agentide_litellm_hooks_v1.structured_genai_otel"] # , "agentide_litellm_hooks.tool_order_sanitizer"]
|
|
37
54
|
# callbacks: ["agentide_litellm_hooks_v1.structured_genai_otel", "agentide_litellm_hooks_v1.reasoning_params_sanitizer"]
|
|
38
55
|
callbacks: ["agentide_litellm_hooks_v1.structured_genai_otel", "agentide_litellm_hooks_v1.codex_proxy_sanitizer", "agentide_litellm_hooks_v1.tool_order_sanitizer", "agentide_litellm_hooks_v1.empty_thinking_sanitizer", "agentide_litellm_hooks_v1.reasoning_params_sanitizer"]
|
|
39
|
-
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Content capture — OPT-IN (PLAN/compliance-req.md §6, data minimization)
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Default: prompts and completions are NOT written to spans. Every span still carries model,
|
|
61
|
+
# token counts, latency, status, requester metadata, and a sha256 + byte length of the content,
|
|
62
|
+
# so a turn stays attributable and a disclosed transcript can be matched back to it.
|
|
63
|
+
#
|
|
64
|
+
# This used to be `turn_off_message_logging: false` + `message_logging: true`, i.e. full prompt
|
|
65
|
+
# and completion capture on by default, unredacted for the serve lane.
|
|
66
|
+
#
|
|
67
|
+
# TO RECORD PROMPTS/COMPLETIONS (both steps are required — either alone keeps you redacted):
|
|
68
|
+
# 1. set turn_off_message_logging: false and callback_settings.otel.message_logging: true
|
|
69
|
+
# 2. export AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=1
|
|
70
|
+
# Step 2 is enforced in agentide_litellm_hooks_v1.py at span-write time and logs loudly when on.
|
|
71
|
+
#
|
|
72
|
+
# LANES: these hardened defaults are what Dockerfile.litellm bakes into the gateway image.
|
|
73
|
+
# setup-analytics.{sh,ps1} flips both flags ON when copying this template to a local
|
|
74
|
+
# ~/.agent-hippo/litellm-otel.yaml — the IDE/Spotlight lane records prompts by default.
|
|
75
|
+
turn_off_message_logging: true
|
|
40
76
|
|
|
41
77
|
callback_settings:
|
|
42
78
|
otel:
|
|
43
|
-
message_logging:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
model_info:
|
|
65
|
-
upstream_provider: "anthropic-setup-token"
|
|
66
|
-
|
|
67
|
-
# Add this under (existing) model_list:
|
|
68
|
-
# model_list:
|
|
69
|
-
# - model_name: ...
|
|
70
|
-
- model_name: litellm/claude-haiku-4-5-20251001
|
|
71
|
-
litellm_params:
|
|
72
|
-
model: anthropic/claude-haiku-4-5-20251001
|
|
73
|
-
top_p: null
|
|
74
|
-
drop_params: true
|
|
75
|
-
api_base: https://api.anthropic.com
|
|
76
|
-
|
|
77
|
-
- model_name: litellm/claude-sonnet-4-6
|
|
78
|
-
litellm_params:
|
|
79
|
-
model: anthropic/claude-sonnet-4-6
|
|
80
|
-
api_base: https://api.anthropic.com
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Add this under (existing) model_list:
|
|
84
|
-
# model_list:
|
|
85
|
-
# - model_name: ...
|
|
86
|
-
- model_name: litellm/gpt-5.3-codex
|
|
87
|
-
litellm_params:
|
|
88
|
-
model: openai/gpt-5.3-codex
|
|
89
|
-
store: false
|
|
90
|
-
api_key: os.environ/OPENAI_CODEX_API_KEY
|
|
91
|
-
api_base: https://chatgpt.com/backend-api/codex
|
|
92
|
-
reasoning_effort: "medium"
|
|
93
|
-
additional_drop_params: ["user", "max_output_tokens"] # Must be set at specific model -- global does not work: Force LiteLLM to remove this specific field before sending the request
|
|
94
|
-
|
|
95
|
-
- model_name: litellm/gpt-5.5
|
|
96
|
-
litellm_params:
|
|
97
|
-
model: openai/gpt-5.5
|
|
98
|
-
store: false
|
|
99
|
-
api_key: os.environ/OPENAI_API_KEY
|
|
100
|
-
api_base: https://api.openai.com/v1
|
|
101
|
-
reasoning_effort: "medium"
|
|
102
|
-
additional_drop_params: ["user", "max_output_tokens"] # Must be set at specific model -- global does not work: Force LiteLLM to remove this specific field before sending the request
|
|
103
|
-
|
|
104
|
-
# Add this under (existing) model_list:
|
|
105
|
-
# model_list:
|
|
106
|
-
# - model_name: ...
|
|
107
|
-
- model_name: litellm/gpt-5.1-codex-mini
|
|
108
|
-
litellm_params:
|
|
109
|
-
model: openai/gpt-5.1-codex-mini
|
|
110
|
-
reasoning_effort: "medium"
|
|
111
|
-
additional_drop_params: ["user", "max_output_tokens", "temperature"]
|
|
112
|
-
store: false
|
|
113
|
-
api_key: os.environ/OPENAI_CODEX_API_KEY
|
|
114
|
-
api_base: https://chatgpt.com/backend-api/codex
|
|
115
|
-
|
|
116
|
-
# Add this under (existing) model_list:
|
|
117
|
-
# model_list:
|
|
118
|
-
# - model_name: ...
|
|
119
|
-
- model_name: litellm/claude-opus-4-6-subs
|
|
120
|
-
litellm_params:
|
|
121
|
-
model: anthropic/claude-opus-4-6
|
|
122
|
-
reasoning_effort: "medium"
|
|
123
|
-
max_tokens: 64000
|
|
124
|
-
additional_drop_params: ["user", "max_output_tokens"]
|
|
125
|
-
store: false
|
|
126
|
-
api_key: os.environ/ANTHROPIC_SETUP_TOKEN
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- model_name: litellm/claude-haiku-4-5-subs
|
|
130
|
-
litellm_params:
|
|
131
|
-
model: anthropic/claude-haiku-4-5
|
|
132
|
-
reasoning_effort: "medium"
|
|
133
|
-
additional_drop_params: ["user", "max_output_tokens"]
|
|
134
|
-
store: false
|
|
135
|
-
api_key: os.environ/ANTHROPIC_SETUP_TOKEN
|
|
136
|
-
|
|
137
|
-
# Add this under (existing) model_list:
|
|
138
|
-
# model_list:
|
|
139
|
-
# - model_name: ...
|
|
140
|
-
- model_name: litellm/claude-opus-4-7-test
|
|
141
|
-
litellm_params:
|
|
142
|
-
model: anthropic/claude-opus-4-7
|
|
143
|
-
reasoning_effort: "medium"
|
|
144
|
-
additional_drop_params: ["user", "max_output_tokens"]
|
|
145
|
-
store: false
|
|
146
|
-
model_info:
|
|
147
|
-
upstream_provider: "anthropic"
|
|
79
|
+
message_logging: false # true + AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=1 records tool_calls, arguments, responses
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Model routes
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# A fresh install ships with no active routes. Uncomment `model_list:` and the
|
|
85
|
+
# provider blocks you use, then put the matching key in ~/.agent-hippo/.env
|
|
86
|
+
# (e.g. echo 'OPENAI_API_KEY=sk-...' >> ~/.agent-hippo/.env). The launcher
|
|
87
|
+
# warns at startup about any referenced env var that is not set.
|
|
88
|
+
#
|
|
89
|
+
# NOTE: `additional_drop_params` must be set per model — the global setting
|
|
90
|
+
# does not work.
|
|
91
|
+
# More providers: https://docs.litellm.ai/docs/providers
|
|
92
|
+
#
|
|
93
|
+
# Consumer/subscription-credential routes (litellm/gpt-5.3-codex,
|
|
94
|
+
# litellm/claude-opus-4-7, …) are deliberately NOT in this file — see
|
|
95
|
+
# agentide-litellm-consumer-routes.local-dev-only.config.yaml and
|
|
96
|
+
# PLAN/compliance.md §12. `litellm/gpt-5.3-codex` is the default model in
|
|
97
|
+
# several docs/manifests; it has NOT been re-pointed at a platform-API route
|
|
98
|
+
# here, because which platform model id is equivalent is a provider question,
|
|
99
|
+
# not a config one — pick one deliberately rather than inheriting a guess.
|
|
148
100
|
|
|
149
|
-
# Add this under (existing) model_list:
|
|
150
101
|
# model_list:
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
102
|
+
# # --- OpenAI (platform API) ---
|
|
103
|
+
# - model_name: litellm/gpt-5.5
|
|
104
|
+
# litellm_params:
|
|
105
|
+
# model: openai/gpt-5.5
|
|
106
|
+
# api_base: https://api.openai.com/v1
|
|
107
|
+
# api_key: os.environ/OPENAI_API_KEY
|
|
108
|
+
# drop_params: true
|
|
109
|
+
# additional_drop_params: ["user", "max_output_tokens"]
|
|
110
|
+
#
|
|
111
|
+
# # --- Anthropic (platform API) ---
|
|
112
|
+
# - model_name: litellm/claude-sonnet-4-6
|
|
113
|
+
# litellm_params:
|
|
114
|
+
# model: anthropic/claude-sonnet-4-6
|
|
115
|
+
# api_base: https://api.anthropic.com
|
|
116
|
+
# api_key: os.environ/ANTHROPIC_API_KEY
|
|
117
|
+
# drop_params: true
|
|
118
|
+
#
|
|
119
|
+
# # --- AgentHippo model gateway (fleet / Cloud Run) ---
|
|
120
|
+
# # Same endpoint as colocated agents with
|
|
121
|
+
# # AGENTHIPPO_LITELLM_BASE_URL=https://gateway.agenthippo.ai/v1.
|
|
122
|
+
# # AGENTHIPPO_GATEWAY_KEY is an ahgw_… service key (create-gateway-key on
|
|
123
|
+
# # the gateway host).
|
|
124
|
+
# - model_name: ah-auto
|
|
125
|
+
# litellm_params:
|
|
126
|
+
# # openai/ prefix required — bare `ah-auto` fails LiteLLM router
|
|
127
|
+
# # deployment (no provider).
|
|
128
|
+
# model: openai/ah-auto
|
|
129
|
+
# api_base: https://gateway.agenthippo.ai/v1
|
|
130
|
+
# api_key: os.environ/AGENTHIPPO_GATEWAY_KEY
|
|
131
|
+
# drop_params: true
|
|
132
|
+
# additional_drop_params: ["user", "max_output_tokens", "reasoning.effort", "reasoning_effort"]
|
|
133
|
+
#
|
|
134
|
+
# # --- Distributed LiteLLM OTEL: central model gateway ---
|
|
135
|
+
# - model_name: ah-auto
|
|
136
|
+
# litellm_params:
|
|
137
|
+
# model: openai/ah-auto
|
|
138
|
+
# api_base: https://gatewayv1.agenthippo.ai/v1
|
|
139
|
+
# api_key: os.environ/AGENTHIPPO_GATEWAY_KEY
|
|
140
|
+
# drop_params: true
|