langprotect-armor 0.1.0
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/.env.example +27 -0
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/langfuse/armour.d.ts +41 -0
- package/dist/langfuse/armour.d.ts.map +1 -0
- package/dist/langfuse/armour.js +78 -0
- package/dist/langfuse/armour.js.map +1 -0
- package/dist/langfuse/config.d.ts +60 -0
- package/dist/langfuse/config.d.ts.map +1 -0
- package/dist/langfuse/config.js +80 -0
- package/dist/langfuse/config.js.map +1 -0
- package/dist/langfuse/index.d.ts +60 -0
- package/dist/langfuse/index.d.ts.map +1 -0
- package/dist/langfuse/index.js +218 -0
- package/dist/langfuse/index.js.map +1 -0
- package/dist/langfuse/routing.d.ts +11 -0
- package/dist/langfuse/routing.d.ts.map +1 -0
- package/dist/langfuse/routing.js +37 -0
- package/dist/langfuse/routing.js.map +1 -0
- package/dist/langfuse/tracing.d.ts +8 -0
- package/dist/langfuse/tracing.d.ts.map +1 -0
- package/dist/langfuse/tracing.js +22 -0
- package/dist/langfuse/tracing.js.map +1 -0
- package/package.json +43 -0
package/.env.example
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ── OpenAI ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
OPENAI_API_KEY=sk-...
|
|
3
|
+
|
|
4
|
+
# ── Langfuse tracing (required) ────────────────────────────────────────────────
|
|
5
|
+
# Project keys from your Langfuse instance. LANGFUSE_BASEURL points at your
|
|
6
|
+
# self-hosted LangProtect/Langfuse URL (defaults to https://cloud.langfuse.com).
|
|
7
|
+
LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
8
|
+
LANGFUSE_SECRET_KEY=sk-lf-...
|
|
9
|
+
LANGFUSE_BASEURL=http://localhost:3000
|
|
10
|
+
|
|
11
|
+
# ── Armour security (optional) ─────────────────────────────────────────────────
|
|
12
|
+
# Set these to enable Armour input/output scanning via setSecurity(true).
|
|
13
|
+
ARMOUR_BASE_URL=https://your-armour-instance.run.app
|
|
14
|
+
ARMOUR_API_KEY=your-armour-api-key
|
|
15
|
+
# true = use sanitized_prompt when available before blocking;
|
|
16
|
+
# false = block immediately on any non-safe status.
|
|
17
|
+
ARMOUR_SECURITY_ON=true
|
|
18
|
+
# true = log only, skip security checks; false = full scanning.
|
|
19
|
+
ARMOUR_TRACE_ONLY=false
|
|
20
|
+
# Armour scan HTTP timeout, in seconds.
|
|
21
|
+
ARMOUR_SCAN_TIMEOUT=60
|
|
22
|
+
|
|
23
|
+
# ── LangProtect dashboard (used by score()) ────────────────────────────────────
|
|
24
|
+
LANGPROTECT_HOST=http://localhost:8000
|
|
25
|
+
|
|
26
|
+
# ── LiteLLM proxy (optional — required for non-OpenAI models) ──────────────────
|
|
27
|
+
LITELLM_HOST=http://localhost:4000
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quokka Labs LLP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# LangProtect TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Self-hosted LLM security and observability SDK. Every model call is scanned by Armour for threats and PII, routed to the right provider, and linked to the LangProtect dashboard — all with a single import change.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install langprotect-armor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { init, openai } from "langprotect-armor/langfuse";
|
|
19
|
+
|
|
20
|
+
init({
|
|
21
|
+
openaiApiKey: "sk-...",
|
|
22
|
+
langfusePublicKey: "pk-lf-...",
|
|
23
|
+
langfuseSecretKey: "sk-lf-...",
|
|
24
|
+
langfuseHost: "http://localhost:3000",
|
|
25
|
+
armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
26
|
+
armourApiKey: "your-armour-key",
|
|
27
|
+
securityOn: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const response = await openai.chat.completions.create({
|
|
31
|
+
model: "gpt-4o-mini",
|
|
32
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
33
|
+
sessionId: "sess_001",
|
|
34
|
+
userId: "user_42",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log(response.choices[0].message.content);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
All configuration is passed to `init()` — no `.env` file required. `init()` can be called at any point before the first `openai.chat.completions.create()` call.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { init } from "langprotect-armor/langfuse";
|
|
48
|
+
|
|
49
|
+
init({
|
|
50
|
+
// LLM provider
|
|
51
|
+
openaiApiKey: "sk-...",
|
|
52
|
+
|
|
53
|
+
// Langfuse tracing
|
|
54
|
+
langfusePublicKey: "pk-lf-...",
|
|
55
|
+
langfuseSecretKey: "sk-lf-...",
|
|
56
|
+
langfuseHost: "http://localhost:3000", // defaults to Langfuse Cloud if unset
|
|
57
|
+
|
|
58
|
+
// Armour security scanning
|
|
59
|
+
armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
60
|
+
armourApiKey: "your-armour-key",
|
|
61
|
+
securityOn: false, // true → sanitize/block unsafe input
|
|
62
|
+
// false → scanning disabled entirely
|
|
63
|
+
traceOnly: false, // true → log only, never enforce
|
|
64
|
+
scanTimeout: 60, // Armour HTTP timeout in seconds
|
|
65
|
+
|
|
66
|
+
// LangProtect dashboard (used by score())
|
|
67
|
+
langprotectHost: "http://localhost:8000",
|
|
68
|
+
|
|
69
|
+
// LiteLLM proxy (optional — enables non-OpenAI models)
|
|
70
|
+
litellmHost: "", // e.g. "http://localhost:4000"
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Parameter | Default | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `openaiApiKey` | `""` | OpenAI API key |
|
|
77
|
+
| `langfusePublicKey` | `""` | Langfuse public key (`pk-lf-…`) |
|
|
78
|
+
| `langfuseSecretKey` | `""` | Langfuse secret key (`sk-lf-…`) |
|
|
79
|
+
| `langfuseHost` | `""` | Langfuse base URL (defaults to Langfuse Cloud if unset) |
|
|
80
|
+
| `armourBaseUrl` | `""` | Armour API base URL |
|
|
81
|
+
| `armourApiKey` | `""` | Armour `X-API-Key` header |
|
|
82
|
+
| `securityOn` | `false` | Enforce sanitization / blocking |
|
|
83
|
+
| `traceOnly` | `false` | Log only — never block or sanitize |
|
|
84
|
+
| `scanTimeout` | `60` | Armour HTTP timeout (seconds) |
|
|
85
|
+
| `langprotectHost` | `http://localhost:8000` | Dashboard URL for `score()` |
|
|
86
|
+
| `litellmHost` | `""` | LiteLLM proxy URL for non-OpenAI models |
|
|
87
|
+
|
|
88
|
+
> Values passed to `init()` override environment variables. Config is read lazily on each call so order of import vs. `init()` does not matter.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
### Chat completions
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { openai } from "langprotect-armor/langfuse";
|
|
98
|
+
|
|
99
|
+
const response = await openai.chat.completions.create({
|
|
100
|
+
model: "gpt-4o",
|
|
101
|
+
messages: [{ role: "user", content: "Explain async/await" }],
|
|
102
|
+
sessionId: "sess_123",
|
|
103
|
+
userId: "u_42",
|
|
104
|
+
metadata: { turn_number: 1 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(response.choices[0].message.content);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Handling blocked responses
|
|
111
|
+
|
|
112
|
+
When `securityOn: true` and Armour hard-blocks a request, a `BlockedResponse` is returned instead of a normal completion. Use the `in` operator to narrow the type:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { openai, BlockedResponse } from "langprotect-armor/langfuse";
|
|
116
|
+
|
|
117
|
+
const response = await openai.chat.completions.create({ ... });
|
|
118
|
+
|
|
119
|
+
if ("blocked" in response) {
|
|
120
|
+
console.log(response.content); // "This response has been blocked by security policy."
|
|
121
|
+
} else {
|
|
122
|
+
console.log(response.choices[0].message.content);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Toggling security at runtime
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { setSecurity } from "langprotect-armor/langfuse";
|
|
130
|
+
|
|
131
|
+
setSecurity(true); // Armour ON
|
|
132
|
+
setSecurity(false); // Armour OFF
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Feedback scoring
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { score } from "langprotect-armor/langfuse";
|
|
139
|
+
|
|
140
|
+
await score("trace-id-abc", 1, "Correct and concise");
|
|
141
|
+
await score("trace-id-abc", -1);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`value` is `1` (positive) or `-1` (negative). Scores are posted to `{langprotectHost}/api/feedback`.
|
|
145
|
+
|
|
146
|
+
### Supported call parameters
|
|
147
|
+
|
|
148
|
+
These are stripped before the OpenAI call and used for metadata only:
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `sessionId` | `string` | Groups all turns of a conversation |
|
|
153
|
+
| `userId` | `string` | Identifies the end user |
|
|
154
|
+
| `name` | `string` | Trace label |
|
|
155
|
+
| `metadata` | `object` | Any JSON key-value context |
|
|
156
|
+
| `traceId` | `string` | Override the auto-generated UUID |
|
|
157
|
+
| `tags` | `string[]` | Labels for filtering |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Security Scanning
|
|
162
|
+
|
|
163
|
+
The SDK calls the Armour `/v1/scan` endpoint on every input and output.
|
|
164
|
+
|
|
165
|
+
### Scan modes
|
|
166
|
+
|
|
167
|
+
| `traceOnly` | `securityOn` | Behaviour |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `true` | any | Log the scan result, always proceed |
|
|
170
|
+
| `false` | `false` | Block if input is not safe, no sanitization |
|
|
171
|
+
| `false` | `true` | Sanitize if possible, block otherwise |
|
|
172
|
+
|
|
173
|
+
### Anonymization
|
|
174
|
+
|
|
175
|
+
When Armour returns a `sanitized_prompt` (PII replaced with tokens like `[PERSON_1]`, `[EMAIL_ADDRESS_1]`), the SDK:
|
|
176
|
+
|
|
177
|
+
1. Replaces the user message with the sanitized version
|
|
178
|
+
2. Injects a system prompt hint so the LLM treats tokens as real values
|
|
179
|
+
3. On the output scan, restores the original values via `sanitized_output` transparently
|
|
180
|
+
|
|
181
|
+
The caller always receives the deanonymized response.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Model Routing
|
|
186
|
+
|
|
187
|
+
The SDK detects the provider from the model name and routes accordingly:
|
|
188
|
+
|
|
189
|
+
| Model prefix | Provider | Route |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `gpt-`, `o1`, `o3`, `text-davinci`, `text-embedding` | OpenAI | Direct to OpenAI API |
|
|
192
|
+
| `claude`, `anthropic` | Anthropic | Via LiteLLM proxy |
|
|
193
|
+
| `gemini`, `palm` | Google | Via LiteLLM proxy |
|
|
194
|
+
| `llama`, `mistral`, `mixtral`, `codellama`, `phi`, `qwen` | Ollama | Via LiteLLM proxy |
|
|
195
|
+
| anything else | LiteLLM | Via LiteLLM proxy |
|
|
196
|
+
|
|
197
|
+
Non-OpenAI models require `litellmHost` to be set.
|
|
198
|
+
|
|
199
|
+
### Starting LiteLLM
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
litellm --model claude-3-5-sonnet-20241022 --port 4000
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Then in `init()`:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
init({
|
|
209
|
+
...
|
|
210
|
+
litellmHost: "http://localhost:4000",
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Adding a new model via config
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
# litellm_config.yaml
|
|
218
|
+
- model_name: phi3
|
|
219
|
+
litellm_params:
|
|
220
|
+
model: ollama/phi3
|
|
221
|
+
api_base: http://localhost:11434
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
ollama pull phi3
|
|
226
|
+
litellm --config litellm_config.yaml --port 4000
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Data Flow
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Your App
|
|
235
|
+
openai.chat.completions.create({ model: "gpt-4o", messages: [...] })
|
|
236
|
+
│
|
|
237
|
+
▼
|
|
238
|
+
langprotect SDK
|
|
239
|
+
1. Armour input scan → sanitize / block / log
|
|
240
|
+
2. LLM call → OpenAI direct or LiteLLM proxy
|
|
241
|
+
3. Armour output scan → deanonymize + eval trigger
|
|
242
|
+
│
|
|
243
|
+
▼
|
|
244
|
+
Response returned to caller
|
|
245
|
+
(eval runs as background task on LangProtect backend)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Build
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm run build # compile TypeScript to dist/
|
|
254
|
+
npm run dev # watch mode
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Troubleshooting
|
|
260
|
+
|
|
261
|
+
**`blocked` check not working**
|
|
262
|
+
|
|
263
|
+
Use the `in` operator to type-narrow correctly:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Wrong — TypeScript will complain
|
|
267
|
+
if (response.blocked) { ... }
|
|
268
|
+
|
|
269
|
+
// Correct — narrows the else branch to a normal completion
|
|
270
|
+
if ("blocked" in response) { ... }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Non-OpenAI model fails**
|
|
274
|
+
|
|
275
|
+
Set `litellmHost` in `init()` and ensure the LiteLLM proxy is running with the model configured.
|
|
276
|
+
|
|
277
|
+
**Armour scan times out**
|
|
278
|
+
|
|
279
|
+
Increase `scanTimeout` in `init()` (default 60 s). Check that `armourBaseUrl` is reachable from your environment.
|
|
280
|
+
|
|
281
|
+
**Module not found: `langprotect-armor/langfuse`**
|
|
282
|
+
|
|
283
|
+
Run `npm run build` first to compile TypeScript to `dist/`, then the package exports map resolves correctly.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/armour — Armour security scanning.
|
|
3
|
+
*
|
|
4
|
+
* All config is read lazily via getConfig() on each call, so init() values
|
|
5
|
+
* take effect immediately without requiring a specific import order.
|
|
6
|
+
*
|
|
7
|
+
* Environment variables (overridden by init()):
|
|
8
|
+
* ARMOUR_BASE_URL — Armour API base URL
|
|
9
|
+
* ARMOUR_API_KEY — Armour API key (X-API-Key header)
|
|
10
|
+
* ARMOUR_TRACE_ONLY — "true" = log only, skip security checks
|
|
11
|
+
* ARMOUR_SECURITY_ON — "true" = enable full Armour scanning
|
|
12
|
+
* ARMOUR_SCAN_TIMEOUT — scan HTTP timeout in seconds (default 60)
|
|
13
|
+
*/
|
|
14
|
+
export declare function setSecurity(enabled: boolean): void;
|
|
15
|
+
export declare function isSecurityOn(): boolean;
|
|
16
|
+
export declare function isTraceOnly(): boolean;
|
|
17
|
+
export declare class BlockedResponse {
|
|
18
|
+
readonly blocked = true;
|
|
19
|
+
readonly content: string;
|
|
20
|
+
readonly model: string;
|
|
21
|
+
readonly usage: {
|
|
22
|
+
prompt_tokens: number;
|
|
23
|
+
completion_tokens: number;
|
|
24
|
+
total_tokens: number;
|
|
25
|
+
};
|
|
26
|
+
constructor(model?: string);
|
|
27
|
+
}
|
|
28
|
+
export interface ArmourScanResult {
|
|
29
|
+
status: string;
|
|
30
|
+
risk_score?: number;
|
|
31
|
+
request_id?: string;
|
|
32
|
+
sanitized_prompt?: string;
|
|
33
|
+
sanitized_output?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function callArmourScan(userInput: string, sessionId: string, userId: string, options?: {
|
|
36
|
+
aiResponse?: string;
|
|
37
|
+
scanType?: "input" | "output";
|
|
38
|
+
traceId?: string;
|
|
39
|
+
evalTrace?: Record<string, unknown>;
|
|
40
|
+
}): Promise<ArmourScanResult>;
|
|
41
|
+
//# sourceMappingURL=armour.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"armour.d.ts","sourceRoot":"","sources":["../../src/langfuse/armour.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAElD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED,qBAAa,eAAe;IAC1B,QAAQ,CAAC,OAAO,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;gBAE/E,KAAK,SAAK;CAKvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAa,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAQ,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAQ,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAG,MAAM,EAClB,SAAS,EAAG,MAAM,EAClB,MAAM,EAAM,MAAM,EAClB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAI,OAAO,GAAG,QAAQ,CAAC;IAChC,OAAO,CAAC,EAAK,MAAM,CAAC;IACpB,SAAS,CAAC,EAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,GACL,OAAO,CAAC,gBAAgB,CAAC,CA4C3B"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/armour — Armour security scanning.
|
|
3
|
+
*
|
|
4
|
+
* All config is read lazily via getConfig() on each call, so init() values
|
|
5
|
+
* take effect immediately without requiring a specific import order.
|
|
6
|
+
*
|
|
7
|
+
* Environment variables (overridden by init()):
|
|
8
|
+
* ARMOUR_BASE_URL — Armour API base URL
|
|
9
|
+
* ARMOUR_API_KEY — Armour API key (X-API-Key header)
|
|
10
|
+
* ARMOUR_TRACE_ONLY — "true" = log only, skip security checks
|
|
11
|
+
* ARMOUR_SECURITY_ON — "true" = enable full Armour scanning
|
|
12
|
+
* ARMOUR_SCAN_TIMEOUT — scan HTTP timeout in seconds (default 60)
|
|
13
|
+
*/
|
|
14
|
+
import { getConfig } from "./config.js";
|
|
15
|
+
const _BLOCKED_MESSAGE = "This response has been blocked by security policy.";
|
|
16
|
+
// Runtime toggle — setSecurity() writes here; takes priority over getConfig().securityOn.
|
|
17
|
+
let _securityOnOverride = null;
|
|
18
|
+
export function setSecurity(enabled) {
|
|
19
|
+
_securityOnOverride = enabled;
|
|
20
|
+
}
|
|
21
|
+
export function isSecurityOn() {
|
|
22
|
+
return _securityOnOverride !== null ? _securityOnOverride : getConfig().securityOn;
|
|
23
|
+
}
|
|
24
|
+
export function isTraceOnly() {
|
|
25
|
+
return getConfig().traceOnly;
|
|
26
|
+
}
|
|
27
|
+
export class BlockedResponse {
|
|
28
|
+
blocked = true;
|
|
29
|
+
content;
|
|
30
|
+
model;
|
|
31
|
+
usage;
|
|
32
|
+
constructor(model = "") {
|
|
33
|
+
this.content = _BLOCKED_MESSAGE;
|
|
34
|
+
this.model = model;
|
|
35
|
+
this.usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function callArmourScan(userInput, sessionId, userId, options = {}) {
|
|
39
|
+
const { aiResponse = "", scanType = "input", traceId = "", evalTrace } = options;
|
|
40
|
+
const { armourBaseUrl, armourApiKey, traceOnly, scanTimeout } = getConfig();
|
|
41
|
+
if (!armourBaseUrl || !armourApiKey) {
|
|
42
|
+
return { status: "safe" };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const payload = {
|
|
46
|
+
user_input: userInput,
|
|
47
|
+
ai_response: aiResponse,
|
|
48
|
+
end_user_id: userId,
|
|
49
|
+
session_id: sessionId,
|
|
50
|
+
trace_only: traceOnly,
|
|
51
|
+
trace: traceId,
|
|
52
|
+
};
|
|
53
|
+
if (evalTrace !== undefined) {
|
|
54
|
+
payload.eval_trace = evalTrace;
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(`${armourBaseUrl}/v1/scan`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"X-API-Key": armourApiKey,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(payload),
|
|
63
|
+
signal: AbortSignal.timeout(scanTimeout * 1000),
|
|
64
|
+
});
|
|
65
|
+
const result = await response.json();
|
|
66
|
+
console.log(`[Armour] ${scanType} scan — ` +
|
|
67
|
+
`status: ${result.status} | ` +
|
|
68
|
+
`risk_score: ${result.risk_score} | ` +
|
|
69
|
+
`sanitized_prompt: ${result.sanitized_prompt} | ` +
|
|
70
|
+
`sanitized_output: ${result.sanitized_output}`);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.log(`[Armour] ${scanType} scan — exception: ${e}`);
|
|
75
|
+
return { status: "safe" }; // fail-open
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=armour.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"armour.js","sourceRoot":"","sources":["../../src/langfuse/armour.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/config — SDK configuration.
|
|
3
|
+
*
|
|
4
|
+
* Stores values set via init() and merges them on top of environment-variable
|
|
5
|
+
* defaults every time getConfig() is called, so init() values always win and
|
|
6
|
+
* take effect immediately without requiring a specific import order.
|
|
7
|
+
*/
|
|
8
|
+
export interface LangProtectConfig {
|
|
9
|
+
openaiApiKey?: string;
|
|
10
|
+
/** Langfuse project public key (pk-lf-…). Also read from LANGFUSE_PUBLIC_KEY. */
|
|
11
|
+
langfusePublicKey?: string;
|
|
12
|
+
/** Langfuse project secret key (sk-lf-…). Also read from LANGFUSE_SECRET_KEY. */
|
|
13
|
+
langfuseSecretKey?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Langfuse base URL. Defaults to https://cloud.langfuse.com.
|
|
16
|
+
* Set to your self-hosted URL when running LangProtect locally.
|
|
17
|
+
* Also read from LANGFUSE_BASEURL (JS) / LANGFUSE_HOST (Python parity).
|
|
18
|
+
*/
|
|
19
|
+
langfuseHost?: string;
|
|
20
|
+
armourBaseUrl?: string;
|
|
21
|
+
armourApiKey?: string;
|
|
22
|
+
/** Enable Armour enforcement — sanitize or block unsafe input (default false). */
|
|
23
|
+
securityOn?: boolean;
|
|
24
|
+
/** Log-only mode — scans run but never block or sanitize (default false). */
|
|
25
|
+
traceOnly?: boolean;
|
|
26
|
+
/** Armour scan HTTP timeout in seconds (default 60). */
|
|
27
|
+
scanTimeout?: number;
|
|
28
|
+
/** LangProtect dashboard URL — used by score() (default http://localhost:8000). */
|
|
29
|
+
langprotectHost?: string;
|
|
30
|
+
/** LiteLLM proxy URL — enables non-OpenAI routing when set. */
|
|
31
|
+
litellmHost?: string;
|
|
32
|
+
}
|
|
33
|
+
type ResolvedConfig = Required<LangProtectConfig>;
|
|
34
|
+
/** Returns the current effective config (env defaults merged with init() overrides). */
|
|
35
|
+
export declare function getConfig(): ResolvedConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Configure the LangProtect SDK programmatically.
|
|
38
|
+
*
|
|
39
|
+
* Call this once at application startup, before any LLM calls. Values passed
|
|
40
|
+
* here override environment variables and take effect immediately because all
|
|
41
|
+
* SDK modules read config lazily via getConfig().
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { init, openai } from "langprotect-armor/langfuse";
|
|
46
|
+
*
|
|
47
|
+
* init({
|
|
48
|
+
* openaiApiKey: "sk-...",
|
|
49
|
+
* langfusePublicKey: "pk-lf-...",
|
|
50
|
+
* langfuseSecretKey: "sk-lf-...",
|
|
51
|
+
* langfuseHost: "https://cloud.langfuse.com",
|
|
52
|
+
* armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
53
|
+
* armourApiKey: "sk-armour-...",
|
|
54
|
+
* securityOn: true,
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function init(cfg: LangProtectConfig): void;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/langfuse/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAG,MAAM,CAAC;IACvB,kFAAkF;IAClF,UAAU,CAAC,EAAK,OAAO,CAAC;IACxB,6EAA6E;IAC7E,SAAS,CAAC,EAAM,OAAO,CAAC;IACxB,wDAAwD;IACxD,WAAW,CAAC,EAAI,MAAM,CAAC;IACvB,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,KAAK,cAAc,GAAG,QAAQ,CAAC,iBAAiB,CAAC,CAAC;AAuBlD,wFAAwF;AACxF,wBAAgB,SAAS,IAAI,cAAc,CAQ1C;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAgBjD"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/config — SDK configuration.
|
|
3
|
+
*
|
|
4
|
+
* Stores values set via init() and merges them on top of environment-variable
|
|
5
|
+
* defaults every time getConfig() is called, so init() values always win and
|
|
6
|
+
* take effect immediately without requiring a specific import order.
|
|
7
|
+
*/
|
|
8
|
+
// Values explicitly set via init() — merged on top of env-var defaults.
|
|
9
|
+
let _override = {};
|
|
10
|
+
function _fromEnv() {
|
|
11
|
+
return {
|
|
12
|
+
openaiApiKey: process.env.OPENAI_API_KEY ?? "",
|
|
13
|
+
langfusePublicKey: process.env.LANGFUSE_PUBLIC_KEY ?? "",
|
|
14
|
+
langfuseSecretKey: process.env.LANGFUSE_SECRET_KEY ?? "",
|
|
15
|
+
langfuseHost: (process.env.LANGFUSE_BASEURL ?? process.env.LANGFUSE_HOST ?? "").replace(/\/$/, ""),
|
|
16
|
+
armourBaseUrl: (process.env.ARMOUR_BASE_URL ?? "").replace(/\/$/, ""),
|
|
17
|
+
armourApiKey: process.env.ARMOUR_API_KEY ?? "",
|
|
18
|
+
securityOn: (process.env.ARMOUR_SECURITY_ON ?? "false").toLowerCase() === "true",
|
|
19
|
+
traceOnly: (process.env.ARMOUR_TRACE_ONLY ?? "false").toLowerCase() === "true",
|
|
20
|
+
scanTimeout: parseInt(process.env.ARMOUR_SCAN_TIMEOUT ?? "60", 10),
|
|
21
|
+
langprotectHost: process.env.LANGPROTECT_HOST ?? "http://localhost:8000",
|
|
22
|
+
litellmHost: (process.env.LITELLM_HOST ?? "").trim(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const _stripSlash = (v) => v.replace(/\/$/, "");
|
|
26
|
+
/** Returns the current effective config (env defaults merged with init() overrides). */
|
|
27
|
+
export function getConfig() {
|
|
28
|
+
const merged = { ..._fromEnv(), ..._override };
|
|
29
|
+
// Normalize URL fields — init() overrides may carry a trailing slash that would
|
|
30
|
+
// otherwise produce double-slash request paths (e.g. host//v1/scan).
|
|
31
|
+
merged.langfuseHost = _stripSlash(merged.langfuseHost);
|
|
32
|
+
merged.armourBaseUrl = _stripSlash(merged.armourBaseUrl);
|
|
33
|
+
merged.langprotectHost = _stripSlash(merged.langprotectHost);
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Configure the LangProtect SDK programmatically.
|
|
38
|
+
*
|
|
39
|
+
* Call this once at application startup, before any LLM calls. Values passed
|
|
40
|
+
* here override environment variables and take effect immediately because all
|
|
41
|
+
* SDK modules read config lazily via getConfig().
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { init, openai } from "langprotect-armor/langfuse";
|
|
46
|
+
*
|
|
47
|
+
* init({
|
|
48
|
+
* openaiApiKey: "sk-...",
|
|
49
|
+
* langfusePublicKey: "pk-lf-...",
|
|
50
|
+
* langfuseSecretKey: "sk-lf-...",
|
|
51
|
+
* langfuseHost: "https://cloud.langfuse.com",
|
|
52
|
+
* armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
53
|
+
* armourApiKey: "sk-armour-...",
|
|
54
|
+
* securityOn: true,
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function init(cfg) {
|
|
59
|
+
_override = { ..._override, ...cfg };
|
|
60
|
+
// Mirror to env vars so that the Langfuse SDK (which reads process.env
|
|
61
|
+
// on client construction) picks up the values set here.
|
|
62
|
+
if (cfg.langfusePublicKey)
|
|
63
|
+
process.env.LANGFUSE_PUBLIC_KEY = cfg.langfusePublicKey;
|
|
64
|
+
if (cfg.langfuseSecretKey)
|
|
65
|
+
process.env.LANGFUSE_SECRET_KEY = cfg.langfuseSecretKey;
|
|
66
|
+
if (cfg.langfuseHost) {
|
|
67
|
+
const host = cfg.langfuseHost.replace(/\/$/, "");
|
|
68
|
+
process.env.LANGFUSE_BASEURL = host;
|
|
69
|
+
process.env.LANGFUSE_HOST = host;
|
|
70
|
+
}
|
|
71
|
+
if (cfg.openaiApiKey)
|
|
72
|
+
process.env.OPENAI_API_KEY = cfg.openaiApiKey;
|
|
73
|
+
if (cfg.armourBaseUrl)
|
|
74
|
+
process.env.ARMOUR_BASE_URL = cfg.armourBaseUrl.replace(/\/$/, "");
|
|
75
|
+
if (cfg.armourApiKey)
|
|
76
|
+
process.env.ARMOUR_API_KEY = cfg.armourApiKey;
|
|
77
|
+
if (cfg.litellmHost !== undefined)
|
|
78
|
+
process.env.LITELLM_HOST = cfg.litellmHost;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/langfuse/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse — public interface for LangProtect SDK.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* User code → LangProtect proxy → observeOpenAI (Langfuse tracing) → OpenAI API
|
|
6
|
+
* ↓
|
|
7
|
+
* Armour input / output scan
|
|
8
|
+
*
|
|
9
|
+
* Langfuse handles full LLM tracing (input, output, tokens, latency, cost).
|
|
10
|
+
* Armour adds security enforcement: PII detection, prompt-injection defence, policy checks.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { init, openai } from "langprotect-armor/langfuse";
|
|
15
|
+
*
|
|
16
|
+
* init({
|
|
17
|
+
* openaiApiKey: "sk-...",
|
|
18
|
+
* langfusePublicKey: "pk-lf-...",
|
|
19
|
+
* langfuseSecretKey: "sk-lf-...",
|
|
20
|
+
* langfuseHost: "https://cloud.langfuse.com",
|
|
21
|
+
* armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
22
|
+
* armourApiKey: "sk-armour-...",
|
|
23
|
+
* securityOn: true,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* const response = await openai.chat.completions.create({
|
|
27
|
+
* model: "gpt-4o",
|
|
28
|
+
* messages: [{ role: "user", content: "Hello!" }],
|
|
29
|
+
* sessionId: "sess-123",
|
|
30
|
+
* userId: "user-42",
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* if ("blocked" in response && response.blocked) {
|
|
34
|
+
* console.log(response.content);
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
import type { ChatCompletionCreateParamsNonStreaming, ChatCompletion } from "openai/resources/chat/completions";
|
|
39
|
+
import { BlockedResponse } from "./armour.js";
|
|
40
|
+
export { init } from "./config.js";
|
|
41
|
+
export { setSecurity } from "./armour.js";
|
|
42
|
+
export { score } from "./tracing.js";
|
|
43
|
+
export { BlockedResponse } from "./armour.js";
|
|
44
|
+
type TracingParams = {
|
|
45
|
+
name?: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
userId?: string;
|
|
48
|
+
tags?: string[];
|
|
49
|
+
metadata?: Record<string, unknown>;
|
|
50
|
+
traceId?: string;
|
|
51
|
+
};
|
|
52
|
+
type CreateParams = Omit<ChatCompletionCreateParamsNonStreaming, "stream" | "metadata"> & TracingParams;
|
|
53
|
+
export declare const openai: {
|
|
54
|
+
chat: {
|
|
55
|
+
completions: {
|
|
56
|
+
create(params: CreateParams): Promise<ChatCompletion | BlockedResponse>;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/langfuse/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAKH,OAAO,KAAK,EACV,sCAAsC,EACtC,cAAc,EACf,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAkB,eAAe,EAA6B,MAAM,aAAa,CAAC;AAIzF,OAAO,EAAE,IAAI,EAAE,MAAiB,aAAa,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAU,aAAa,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAgB,cAAc,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AA0C9C,KAAK,aAAa,GAAG;IACnB,IAAI,CAAC,EAAO,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAK,MAAM,CAAC;IACnB,IAAI,CAAC,EAAO,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO,CAAC,EAAI,MAAM,CAAC;CACpB,CAAC;AAKF,KAAK,YAAY,GAAG,IAAI,CAAC,sCAAsC,EAAE,QAAQ,GAAG,UAAU,CAAC,GAAG,aAAa,CAAC;AA0JxG,eAAO,MAAM,MAAM;;;2BAvJI,YAAY,GAAG,OAAO,CAAC,cAAc,GAAG,eAAe,CAAC;;;CA2J9E,CAAC"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse — public interface for LangProtect SDK.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* User code → LangProtect proxy → observeOpenAI (Langfuse tracing) → OpenAI API
|
|
6
|
+
* ↓
|
|
7
|
+
* Armour input / output scan
|
|
8
|
+
*
|
|
9
|
+
* Langfuse handles full LLM tracing (input, output, tokens, latency, cost).
|
|
10
|
+
* Armour adds security enforcement: PII detection, prompt-injection defence, policy checks.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { init, openai } from "langprotect-armor/langfuse";
|
|
15
|
+
*
|
|
16
|
+
* init({
|
|
17
|
+
* openaiApiKey: "sk-...",
|
|
18
|
+
* langfusePublicKey: "pk-lf-...",
|
|
19
|
+
* langfuseSecretKey: "sk-lf-...",
|
|
20
|
+
* langfuseHost: "https://cloud.langfuse.com",
|
|
21
|
+
* armourBaseUrl: "https://langprotect-xxx.run.app",
|
|
22
|
+
* armourApiKey: "sk-armour-...",
|
|
23
|
+
* securityOn: true,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* const response = await openai.chat.completions.create({
|
|
27
|
+
* model: "gpt-4o",
|
|
28
|
+
* messages: [{ role: "user", content: "Hello!" }],
|
|
29
|
+
* sessionId: "sess-123",
|
|
30
|
+
* userId: "user-42",
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* if ("blocked" in response && response.blocked) {
|
|
34
|
+
* console.log(response.content);
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
39
|
+
import OpenAI from "openai";
|
|
40
|
+
import { observeOpenAI } from "@langfuse/openai";
|
|
41
|
+
import { callArmourScan, BlockedResponse, isSecurityOn, isTraceOnly } from "./armour.js";
|
|
42
|
+
import { detectProvider, getLitellmClient } from "./routing.js";
|
|
43
|
+
import { getConfig } from "./config.js";
|
|
44
|
+
export { init } from "./config.js";
|
|
45
|
+
export { setSecurity } from "./armour.js";
|
|
46
|
+
export { score } from "./tracing.js";
|
|
47
|
+
export { BlockedResponse } from "./armour.js";
|
|
48
|
+
const _ANONYMIZATION_HINT = "Some values in this conversation have been anonymized with tokens " +
|
|
49
|
+
"such as [PERSON_1], [EMAIL_ADDRESS_1], [PHONE_NUMBER_1], [DATE_TIME_1], etc. " +
|
|
50
|
+
"These tokens represent real values. Respond naturally and reference them " +
|
|
51
|
+
"as-is when needed — do not ask the user to provide the actual values.";
|
|
52
|
+
/**
|
|
53
|
+
* Return the scannable text of a chat message's content.
|
|
54
|
+
*
|
|
55
|
+
* OpenAI message content is either a plain string or, for multimodal input, an
|
|
56
|
+
* array of content parts (text parts plus image parts). We concatenate the text
|
|
57
|
+
* parts so the Armour scan always receives a string. Missing content yields "".
|
|
58
|
+
*/
|
|
59
|
+
function extractText(content) {
|
|
60
|
+
if (content == null)
|
|
61
|
+
return "";
|
|
62
|
+
if (typeof content === "string")
|
|
63
|
+
return content;
|
|
64
|
+
if (Array.isArray(content)) {
|
|
65
|
+
return content
|
|
66
|
+
.filter((p) => typeof p === "object" && p !== null && p.type === "text")
|
|
67
|
+
.map(p => p.text)
|
|
68
|
+
.join(" ");
|
|
69
|
+
}
|
|
70
|
+
return String(content);
|
|
71
|
+
}
|
|
72
|
+
// Base OpenAI client — constructed lazily on first use (never at import time) so
|
|
73
|
+
// that init({ openaiApiKey }) can run first. Constructing at module load would
|
|
74
|
+
// throw if OPENAI_API_KEY is not already in the environment. Wrapped per-call
|
|
75
|
+
// with observeOpenAI for Langfuse tracing.
|
|
76
|
+
let _client = null;
|
|
77
|
+
function getClient() {
|
|
78
|
+
if (_client === null) {
|
|
79
|
+
const { openaiApiKey } = getConfig();
|
|
80
|
+
_client = new OpenAI(openaiApiKey ? { apiKey: openaiApiKey } : {});
|
|
81
|
+
}
|
|
82
|
+
return _client;
|
|
83
|
+
}
|
|
84
|
+
const _completions = {
|
|
85
|
+
async create(params) {
|
|
86
|
+
// Streaming is not supported: the SDK needs the full completion to run the
|
|
87
|
+
// Armour output scan and trigger evaluations. The type already excludes
|
|
88
|
+
// `stream`, but guard at runtime for plain-JS callers who bypass the types.
|
|
89
|
+
if (params.stream) {
|
|
90
|
+
throw new Error("langprotect: streaming responses are not supported (stream: true). " +
|
|
91
|
+
"The Armour output scan requires the complete response. Remove `stream` " +
|
|
92
|
+
"or call the OpenAI client directly for streaming.");
|
|
93
|
+
}
|
|
94
|
+
const { name, sessionId = "", userId = "", tags, traceId = "", metadata: rawMetadata, ...openaiParams } = params;
|
|
95
|
+
const model = params.model ?? "";
|
|
96
|
+
const effectiveTraceId = traceId || randomUUID();
|
|
97
|
+
const _startMs = Date.now();
|
|
98
|
+
const metadata = { ...(rawMetadata ?? {}) };
|
|
99
|
+
if (sessionId)
|
|
100
|
+
metadata.session_id ??= sessionId;
|
|
101
|
+
if (userId)
|
|
102
|
+
metadata.user_id ??= userId;
|
|
103
|
+
if (tags)
|
|
104
|
+
metadata.tags ??= tags;
|
|
105
|
+
// ── Armour input scan ──────────────────────────────────────────────────
|
|
106
|
+
const messages = [...(params.messages ?? [])];
|
|
107
|
+
const lastUser = [...messages].reverse().find(m => m.role === "user");
|
|
108
|
+
const userInput = extractText(lastUser?.content);
|
|
109
|
+
const inputScan = await callArmourScan(userInput, sessionId, userId, {
|
|
110
|
+
scanType: "input",
|
|
111
|
+
traceId: effectiveTraceId,
|
|
112
|
+
});
|
|
113
|
+
const sanitized = inputScan.sanitized_prompt;
|
|
114
|
+
Object.assign(metadata, {
|
|
115
|
+
armour_input_status: inputScan.status,
|
|
116
|
+
armour_risk_score: inputScan.risk_score ?? 0,
|
|
117
|
+
armour_request_id: inputScan.request_id,
|
|
118
|
+
armour_sanitized: sanitized !== undefined,
|
|
119
|
+
});
|
|
120
|
+
let usedSanitizedPrompt = false;
|
|
121
|
+
if (!isTraceOnly() && inputScan.status !== "safe") {
|
|
122
|
+
if (isSecurityOn() && sanitized) {
|
|
123
|
+
const idx = [...messages].reverse().findIndex(m => m.role === "user");
|
|
124
|
+
if (idx !== -1) {
|
|
125
|
+
const realIdx = messages.length - 1 - idx;
|
|
126
|
+
messages[realIdx] = { ...messages[realIdx], content: sanitized };
|
|
127
|
+
}
|
|
128
|
+
const sysIdx = messages.findIndex(m => m.role === "system");
|
|
129
|
+
if (sysIdx !== -1) {
|
|
130
|
+
const existing = messages[sysIdx].content ?? "";
|
|
131
|
+
messages[sysIdx] = {
|
|
132
|
+
...messages[sysIdx],
|
|
133
|
+
content: `${existing}\n\n${_ANONYMIZATION_HINT}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
messages.unshift({ role: "system", content: _ANONYMIZATION_HINT });
|
|
138
|
+
}
|
|
139
|
+
openaiParams.messages = messages;
|
|
140
|
+
usedSanitizedPrompt = true;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
return new BlockedResponse(model);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── LLM call via observeOpenAI (Langfuse traces automatically) ────────
|
|
147
|
+
// observeOpenAI wraps the OpenAI client for this call only, capturing
|
|
148
|
+
// input, output, tokens, latency, and cost in the Langfuse dashboard.
|
|
149
|
+
// We pass effectiveTraceId as traceName so the Langfuse trace is findable
|
|
150
|
+
// by the same ID used in the Armour scan.
|
|
151
|
+
let response;
|
|
152
|
+
const litellmClient = getLitellmClient();
|
|
153
|
+
if (litellmClient && detectProvider(model) !== "openai") {
|
|
154
|
+
// LiteLLM path: wrap the LiteLLM client with observeOpenAI too.
|
|
155
|
+
const tracedLitellm = observeOpenAI(litellmClient, {
|
|
156
|
+
traceName: effectiveTraceId,
|
|
157
|
+
sessionId: sessionId || undefined,
|
|
158
|
+
userId: userId || undefined,
|
|
159
|
+
tags: tags || undefined,
|
|
160
|
+
generationName: name || "chat",
|
|
161
|
+
generationMetadata: metadata,
|
|
162
|
+
});
|
|
163
|
+
response = await tracedLitellm.chat.completions.create(openaiParams);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const tracedClient = observeOpenAI(getClient(), {
|
|
167
|
+
traceName: effectiveTraceId,
|
|
168
|
+
sessionId: sessionId || undefined,
|
|
169
|
+
userId: userId || undefined,
|
|
170
|
+
tags: tags || undefined,
|
|
171
|
+
generationName: name || "chat",
|
|
172
|
+
generationMetadata: metadata,
|
|
173
|
+
});
|
|
174
|
+
response = await tracedClient.chat.completions.create(openaiParams);
|
|
175
|
+
}
|
|
176
|
+
// ── Collect trace metrics for Armour eval trigger ─────────────────────
|
|
177
|
+
const latency = Date.now() - _startMs;
|
|
178
|
+
const inTok = response.usage?.prompt_tokens ?? 0;
|
|
179
|
+
const outTok = response.usage?.completion_tokens ?? 0;
|
|
180
|
+
const outModel = response.model ?? model;
|
|
181
|
+
// ── Armour output scan + server-side eval trigger ─────────────────────
|
|
182
|
+
let aiReply = response.choices[0]?.message.content ?? "";
|
|
183
|
+
const evalTracePayload = {
|
|
184
|
+
trace_id: effectiveTraceId,
|
|
185
|
+
input: userInput,
|
|
186
|
+
output: aiReply,
|
|
187
|
+
model: outModel,
|
|
188
|
+
latency_ms: latency,
|
|
189
|
+
prompt_tokens: inTok,
|
|
190
|
+
completion_tokens: outTok,
|
|
191
|
+
cost_usd: null,
|
|
192
|
+
session_id: sessionId,
|
|
193
|
+
user_id: userId,
|
|
194
|
+
metadata,
|
|
195
|
+
};
|
|
196
|
+
const outputScan = await callArmourScan("", sessionId, userId, {
|
|
197
|
+
aiResponse: aiReply,
|
|
198
|
+
scanType: "output",
|
|
199
|
+
traceId: effectiveTraceId,
|
|
200
|
+
evalTrace: evalTracePayload,
|
|
201
|
+
});
|
|
202
|
+
// ── De-anonymization — restore real values in the AI reply ────────────
|
|
203
|
+
if (usedSanitizedPrompt && outputScan.sanitized_output) {
|
|
204
|
+
aiReply = outputScan.sanitized_output;
|
|
205
|
+
if (response.choices[0]?.message) {
|
|
206
|
+
response.choices[0].message.content = aiReply;
|
|
207
|
+
}
|
|
208
|
+
console.log("[Armour] deanonymization — restored=yes");
|
|
209
|
+
}
|
|
210
|
+
return response;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
export const openai = {
|
|
214
|
+
chat: {
|
|
215
|
+
completions: _completions,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/langfuse/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/routing — Model provider detection and LiteLLM client setup.
|
|
3
|
+
*
|
|
4
|
+
* Environment variables (overridden by init()):
|
|
5
|
+
* LITELLM_HOST — LiteLLM proxy base URL (optional; required for non-OpenAI models)
|
|
6
|
+
*/
|
|
7
|
+
import OpenAI from "openai";
|
|
8
|
+
/** Returns the LiteLLM client if a host is configured, otherwise null. */
|
|
9
|
+
export declare function getLitellmClient(): OpenAI | null;
|
|
10
|
+
export declare function detectProvider(model: string): string;
|
|
11
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/langfuse/routing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,MAAM,MAAY,QAAQ,CAAC;AAOlC,0EAA0E;AAC1E,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAQhD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAapD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/routing — Model provider detection and LiteLLM client setup.
|
|
3
|
+
*
|
|
4
|
+
* Environment variables (overridden by init()):
|
|
5
|
+
* LITELLM_HOST — LiteLLM proxy base URL (optional; required for non-OpenAI models)
|
|
6
|
+
*/
|
|
7
|
+
import OpenAI from "openai";
|
|
8
|
+
import { getConfig } from "./config.js";
|
|
9
|
+
// Cached per litellmHost value so we don't recreate on every call.
|
|
10
|
+
let _cachedHost = "";
|
|
11
|
+
let _cachedClient = null;
|
|
12
|
+
/** Returns the LiteLLM client if a host is configured, otherwise null. */
|
|
13
|
+
export function getLitellmClient() {
|
|
14
|
+
const host = getConfig().litellmHost;
|
|
15
|
+
if (!host)
|
|
16
|
+
return null;
|
|
17
|
+
if (host !== _cachedHost) {
|
|
18
|
+
_cachedHost = host;
|
|
19
|
+
_cachedClient = new OpenAI({ baseURL: `${host}/v1`, apiKey: "anything" });
|
|
20
|
+
}
|
|
21
|
+
return _cachedClient;
|
|
22
|
+
}
|
|
23
|
+
export function detectProvider(model) {
|
|
24
|
+
const m = (model ?? "").toLowerCase();
|
|
25
|
+
if (m.startsWith("gpt-") || m.startsWith("o1") || m.startsWith("o3") ||
|
|
26
|
+
m.startsWith("text-davinci") || m.startsWith("text-embedding"))
|
|
27
|
+
return "openai";
|
|
28
|
+
if (m.startsWith("claude") || m.startsWith("anthropic"))
|
|
29
|
+
return "anthropic";
|
|
30
|
+
if (m.startsWith("gemini") || m.startsWith("palm"))
|
|
31
|
+
return "google";
|
|
32
|
+
if (m.startsWith("llama") || m.startsWith("mistral") || m.startsWith("mixtral") ||
|
|
33
|
+
m.startsWith("codellama") || m.startsWith("phi") || m.startsWith("qwen"))
|
|
34
|
+
return "ollama";
|
|
35
|
+
return "litellm";
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/langfuse/routing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,MAAM,MAAY,QAAQ,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,mEAAmE;AACnE,IAAI,WAAW,GAAK,EAAE,CAAC;AACvB,IAAI,aAAa,GAAkB,IAAI,CAAC;AAExC,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB;IAC9B,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC,WAAW,CAAC;IACrC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACzB,WAAW,GAAK,IAAI,CAAC;QACrB,aAAa,GAAG,IAAI,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,IACE,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAChE,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC;QAC9D,OAAO,QAAQ,CAAC;IAClB,IAAI,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IAC5E,IAAI,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;QAAQ,OAAO,QAAQ,CAAC;IAC1E,IACE,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC;QAC3E,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;QACxE,OAAO,QAAQ,CAAC;IAClB,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/tracing — Feedback scoring.
|
|
3
|
+
*
|
|
4
|
+
* Environment variables (overridden by init()):
|
|
5
|
+
* LANGPROTECT_HOST (default: http://localhost:8000)
|
|
6
|
+
*/
|
|
7
|
+
export declare function score(traceId: string, value: number, comment?: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=tracing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/langfuse/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,wBAAsB,KAAK,CACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAI,MAAM,EACf,OAAO,SAAK,GACX,OAAO,CAAC,IAAI,CAAC,CAYf"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* langprotect-armor/langfuse/tracing — Feedback scoring.
|
|
3
|
+
*
|
|
4
|
+
* Environment variables (overridden by init()):
|
|
5
|
+
* LANGPROTECT_HOST (default: http://localhost:8000)
|
|
6
|
+
*/
|
|
7
|
+
import { getConfig } from "./config.js";
|
|
8
|
+
export async function score(traceId, value, comment = "") {
|
|
9
|
+
const host = getConfig().langprotectHost;
|
|
10
|
+
try {
|
|
11
|
+
await fetch(`${host}/api/feedback`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ trace_id: traceId, value, comment }),
|
|
15
|
+
signal: AbortSignal.timeout(5_000),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// fail-open — scoring errors must never surface to the caller
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=tracing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.js","sourceRoot":"","sources":["../../src/langfuse/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAAe,EACf,KAAe,EACf,OAAO,GAAG,EAAE;IAEZ,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC,eAAe,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,IAAI,eAAe,EAAE;YAClC,MAAM,EAAG,MAAM;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAK,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;YAC9D,MAAM,EAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACpC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "langprotect-armor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Self-hosted LLM observability SDK — trace any model call to your own dashboard",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Quokka-Labs-LLP/langprotect-armor-sdk.git",
|
|
9
|
+
"directory": "typescript"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Quokka-Labs-LLP/langprotect-armor-sdk#readme",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/langfuse/index.js",
|
|
14
|
+
"types": "./dist/langfuse/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./langfuse": {
|
|
17
|
+
"types": "./dist/langfuse/index.d.ts",
|
|
18
|
+
"import": "./dist/langfuse/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
".env.example"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@langfuse/openai": "^5.4.1",
|
|
37
|
+
"openai": ">=4 <6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.0.0",
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|