@evalguard/openai 1.1.1 → 1.2.1
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/README.md +235 -123
- package/dist/cost.d.ts +2 -0
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +5 -0
- package/dist/cost.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +420 -95
- package/dist/index.js.map +1 -1
- package/dist/surface.d.ts +83 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +328 -0
- package/dist/surface.js.map +1 -0
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -1,123 +1,235 @@
|
|
|
1
|
-
# @evalguard/openai
|
|
2
|
-
|
|
3
|
-
Drop-in OpenAI SDK wrapper that adds real-time guardrails, trace logging, and cost tracking via [EvalGuard](https://evalguard.ai).
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @evalguard/openai openai
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1
|
+
# @evalguard/openai
|
|
2
|
+
|
|
3
|
+
Drop-in OpenAI SDK wrapper that adds real-time guardrails, trace logging, and cost tracking via [EvalGuard](https://evalguard.ai).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @evalguard/openai openai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> ### ⚠️ ESM-only — requires `"type": "module"`
|
|
12
|
+
>
|
|
13
|
+
> `@evalguard/openai` ships **ES modules only** (`"type": "module"`, no CJS build). The
|
|
14
|
+
> quickstart below **will not type-check or run** in a default CommonJS
|
|
15
|
+
> TypeScript project — you get `TS1479` ("the referenced file is an ECMAScript
|
|
16
|
+
> module and cannot be imported with `require`") and, because the peer SDK's
|
|
17
|
+
> types then resolve under a different module mode, a confusing
|
|
18
|
+
> `TS2345 … Property '#private' … refers to a different member` on the very
|
|
19
|
+
> first call.
|
|
20
|
+
>
|
|
21
|
+
> **To use this package, your consuming project must be ESM:**
|
|
22
|
+
>
|
|
23
|
+
> ```jsonc
|
|
24
|
+
> // package.json
|
|
25
|
+
> { "type": "module" }
|
|
26
|
+
> ```
|
|
27
|
+
> ```jsonc
|
|
28
|
+
> // tsconfig.json
|
|
29
|
+
> { "compilerOptions": { "module": "node16", "moduleResolution": "node16" } }
|
|
30
|
+
> ```
|
|
31
|
+
>
|
|
32
|
+
> **Staying on CommonJS?** A dynamic `import()` works from CJS — but it is
|
|
33
|
+
> **not enough on its own**. `openai` must be loaded dynamically **too**:
|
|
34
|
+
> a static `import OpenAI from "openai"` in a CJS file resolves the peer's
|
|
35
|
+
> types under CJS rules while the wrapper's resolve under ESM rules, so the
|
|
36
|
+
> two `OpenAI` classes are different types and the very first call still fails
|
|
37
|
+
> with `TS2345 … separate declarations of a private property`. Both imports
|
|
38
|
+
> must be dynamic:
|
|
39
|
+
>
|
|
40
|
+
> …and both `await`s must sit **inside an async function**. Top-level `await`
|
|
41
|
+
> is an ESM-only feature, so a bare `await import(...)` at file scope in a CJS
|
|
42
|
+
> module is `TS1309: The current file is a CommonJS module and cannot use
|
|
43
|
+
> 'await' at the top level` — which is why this block is an `async function`
|
|
44
|
+
> and not four loose statements:
|
|
45
|
+
>
|
|
46
|
+
> ```typescript
|
|
47
|
+
> // ✅ compiles under module/moduleResolution "node16", no "type": "module"
|
|
48
|
+
> async function main() {
|
|
49
|
+
> const { wrapOpenAI } = await import("@evalguard/openai");
|
|
50
|
+
> const { default: OpenAI } = await import("openai");
|
|
51
|
+
>
|
|
52
|
+
> const openai = wrapOpenAI(new OpenAI(), {
|
|
53
|
+
> apiKey: "eg_...",
|
|
54
|
+
> projectId: "proj_...",
|
|
55
|
+
> });
|
|
56
|
+
>
|
|
57
|
+
> // …then use `openai` exactly as the quickstart below does.
|
|
58
|
+
> return openai;
|
|
59
|
+
> }
|
|
60
|
+
>
|
|
61
|
+
> void main();
|
|
62
|
+
> ```
|
|
63
|
+
>
|
|
64
|
+
> ```typescript
|
|
65
|
+
> // ❌ still fails — the peer import is static
|
|
66
|
+
> import OpenAI from "openai";
|
|
67
|
+
>
|
|
68
|
+
> async function main() {
|
|
69
|
+
> const { wrapOpenAI } = await import("@evalguard/openai");
|
|
70
|
+
> wrapOpenAI(new OpenAI(), { apiKey: "eg_..." });
|
|
71
|
+
> }
|
|
72
|
+
> // error TS2345: Argument of type 'OpenAI' is not assignable to parameter of
|
|
73
|
+
> // type 'OpenAI'. Types have separate declarations of a private property.
|
|
74
|
+
> ```
|
|
75
|
+
>
|
|
76
|
+
> Every other snippet in this README uses the static `import OpenAI from
|
|
77
|
+
> "openai"` form for readability. Under CommonJS you must convert **both**
|
|
78
|
+
> lines, not just the `@evalguard/openai` one.
|
|
79
|
+
>
|
|
80
|
+
> Node.js ≥ 22.12 can also `require()` an ESM module directly
|
|
81
|
+
> (`require(esm)`), but TypeScript still type-checks the import under CJS
|
|
82
|
+
> rules, so the dynamic-import form above is the supported path.
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import OpenAI from "openai";
|
|
88
|
+
import { wrapOpenAI } from "@evalguard/openai";
|
|
89
|
+
|
|
90
|
+
const openai = wrapOpenAI(new OpenAI(), {
|
|
91
|
+
apiKey: "eg_...",
|
|
92
|
+
projectId: "proj_...",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Use exactly like the normal OpenAI SDK — guardrails are automatic
|
|
96
|
+
const response = await openai.chat.completions.create({
|
|
97
|
+
model: "gpt-4o",
|
|
98
|
+
messages: [{ role: "user", content: "Hello, how are you?" }],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log(response.choices[0].message.content);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Streaming
|
|
105
|
+
|
|
106
|
+
Streaming works transparently. The wrapper intercepts chunks to log the assembled response without affecting stream behavior.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const stream = await openai.chat.completions.create({
|
|
110
|
+
model: "gpt-4o",
|
|
111
|
+
messages: [{ role: "user", content: "Write a poem about AI safety" }],
|
|
112
|
+
stream: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for await (const chunk of stream) {
|
|
116
|
+
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const openai = wrapOpenAI(new OpenAI(), {
|
|
124
|
+
// Required: your EvalGuard API key
|
|
125
|
+
apiKey: "eg_...",
|
|
126
|
+
|
|
127
|
+
// Optional: EvalGuard API base URL (default: https://evalguard.ai/api/v1)
|
|
128
|
+
baseUrl: "https://your-evalguard-instance.com/api/v1",
|
|
129
|
+
|
|
130
|
+
// Optional: block requests that fail guardrails (default: true)
|
|
131
|
+
blockOnViolation: true,
|
|
132
|
+
|
|
133
|
+
// Optional: log all requests to EvalGuard (default: true)
|
|
134
|
+
enableLogging: true,
|
|
135
|
+
|
|
136
|
+
// Optional: project ID for organizing traces
|
|
137
|
+
projectId: "proj_...",
|
|
138
|
+
|
|
139
|
+
// Optional: custom metadata attached to every trace
|
|
140
|
+
metadata: { environment: "production", service: "chatbot" },
|
|
141
|
+
|
|
142
|
+
// Optional: callback when a guardrail violation is detected
|
|
143
|
+
onViolation: (result) => {
|
|
144
|
+
console.warn("Guardrail violation:", result.violations);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## What It Does
|
|
150
|
+
|
|
151
|
+
| Phase | Action |
|
|
152
|
+
|-------|--------|
|
|
153
|
+
| **Pre-request** | Sends the prompt to EvalGuard's firewall for prompt injection detection, PII scanning, and toxicity checks |
|
|
154
|
+
| **LLM call** | Passes through to the real OpenAI API unchanged |
|
|
155
|
+
| **Post-response** | Logs model, tokens, latency, cost, and guardrail results as a trace to EvalGuard |
|
|
156
|
+
|
|
157
|
+
## Fail-Closed by Default
|
|
158
|
+
|
|
159
|
+
If EvalGuard is unreachable (network error, timeout, 5xx) **and** `blockOnViolation` is `true` (the default), the wrapper **throws `EvalGuardViolationError`** (`type: "guardrail_unavailable"`) and does **not** call OpenAI — it fails *closed*, so an EvalGuard outage cannot silently bypass your guardrails.
|
|
160
|
+
|
|
161
|
+
To fail *open* instead (pass the request through to OpenAI on an EvalGuard outage), set `blockOnViolation: false`, or set the env var `EVALGUARD_GUARDRAIL_FAIL_OPEN_LEGACY=1` to restore the legacy global fail-open behavior.
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
When `blockOnViolation` is `true` (default) and a guardrail check fails:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { EvalGuardViolationError } from "@evalguard/openai";
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const response = await openai.chat.completions.create({
|
|
172
|
+
model: "gpt-4o",
|
|
173
|
+
messages: [{ role: "user", content: "malicious prompt..." }],
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof EvalGuardViolationError) {
|
|
177
|
+
console.log("Blocked:", error.violations);
|
|
178
|
+
// [{ type: "prompt_injection", severity: "critical", message: "..." }]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Set `blockOnViolation: false` to log violations without blocking:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const openai = wrapOpenAI(new OpenAI(), {
|
|
187
|
+
apiKey: "eg_...",
|
|
188
|
+
blockOnViolation: false,
|
|
189
|
+
onViolation: (result) => {
|
|
190
|
+
// Log but don't block
|
|
191
|
+
analytics.track("guardrail_violation", result);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Cost estimates: unpriced models are reported as unpriced
|
|
197
|
+
|
|
198
|
+
`estimateCost()` used to invent a price for any model outside a ~60-row table:
|
|
199
|
+
`estimateCost("gpt-5", 1000, 500)` and `estimateCost("totally-unknown-model",
|
|
200
|
+
1000, 500)` both returned `0.0105` from a blended $0.003/$0.015-per-1k
|
|
201
|
+
fallback — with no flag and no warning. A FinOps figure you cannot tell apart
|
|
202
|
+
from a real vendor price is worse than no figure at all.
|
|
203
|
+
|
|
204
|
+
Two things changed:
|
|
205
|
+
|
|
206
|
+
- **Coverage** — 2,200+ model ids now resolve to a real, sourced rate
|
|
207
|
+
(generated from EvalGuard's own pricing database, which is synced from the
|
|
208
|
+
LiteLLM catalogue). `gpt-5` is priced correctly.
|
|
209
|
+
- **Honesty** — a genuinely unknown model is now *visibly* unknown.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { estimateCostDetailed, isModelPriced } from "@evalguard/openai";
|
|
213
|
+
|
|
214
|
+
estimateCostDetailed("gpt-5", 1000, 500);
|
|
215
|
+
// { model: "gpt-5", costUsd: 0.00625, priced: true, pricingSource: "catalog" }
|
|
216
|
+
|
|
217
|
+
estimateCostDetailed("totally-unknown-model", 1000, 500);
|
|
218
|
+
// { model: "totally-unknown-model",
|
|
219
|
+
// costUsd: null, // <- never a fabricated number
|
|
220
|
+
// priced: false,
|
|
221
|
+
// pricingSource: "unpriced",
|
|
222
|
+
// blendedFallbackUsd: 0.0105 } // <- opt-in rough figure, clearly labelled
|
|
223
|
+
|
|
224
|
+
isModelPriced("totally-unknown-model"); // false
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`estimateCost()` still returns a `number` for backwards compatibility, but it
|
|
228
|
+
now emits a one-time `console.warn` naming the unpriced model. Traces carry
|
|
229
|
+
`costPricingSource` alongside `cost`, and `cost` is `null` for an unpriced
|
|
230
|
+
model rather than a guess, so your EvalGuard dashboard shows "unpriced" instead
|
|
231
|
+
of a fake dollar amount.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
Apache-2.0
|
package/dist/cost.d.ts
CHANGED
package/dist/cost.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAMvD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/cost.js
CHANGED
|
@@ -3,4 +3,9 @@
|
|
|
3
3
|
// source of truth (and Gemini / future-provider pricing rolls in
|
|
4
4
|
// without touching this package).
|
|
5
5
|
export { estimateCost } from "@evalguard/wrapper-core";
|
|
6
|
+
// 2026-07-30 (audit finding 5): `estimateCostDetailed` is re-exported alongside
|
|
7
|
+
// it. The blended fallback above is no longer SILENT — an unpriced model now
|
|
8
|
+
// reports `{ costUsd: null, priced: false }` instead of a number a customer
|
|
9
|
+
// cannot tell apart from a real vendor price.
|
|
10
|
+
export { estimateCostDetailed, isModelPriced } from "@evalguard/wrapper-core";
|
|
6
11
|
//# sourceMappingURL=cost.js.map
|
package/dist/cost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uEAAuE;AACvE,iEAAiE;AACjE,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uEAAuE;AACvE,iEAAiE;AACjE,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD,gFAAgF;AAChF,6EAA6E;AAC7E,4EAA4E;AAC5E,8CAA8C;AAC9C,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
2
|
import { type EvalGuardConfig } from "@evalguard/wrapper-core";
|
|
3
3
|
export { GuardrailClient } from "./guardrail-client.js";
|
|
4
|
-
export { EvalGuardViolationError, EvalGuardOutputViolationError, estimateCost, } from "@evalguard/wrapper-core";
|
|
5
|
-
export type { EvalGuardConfig, GuardrailCheckResult, GuardrailViolation, TraceLogData, OutputScoreResult, ByokKeyResolver, RetryConfig, CircuitConfig, EvalOnResponseConfig, } from "@evalguard/wrapper-core";
|
|
4
|
+
export { EvalGuardViolationError, EvalGuardOutputViolationError, estimateCost, estimateCostDetailed, isModelPriced, } from "@evalguard/wrapper-core";
|
|
5
|
+
export type { CostEstimate, PricingSource, EvalGuardConfig, GuardrailCheckResult, GuardrailViolation, TraceLogData, OutputScoreResult, ByokKeyResolver, RetryConfig, CircuitConfig, EvalOnResponseConfig, } from "@evalguard/wrapper-core";
|
|
6
6
|
/**
|
|
7
7
|
* Wrap an existing OpenAI client to add EvalGuard guardrails, logging, and
|
|
8
8
|
* cost tracking. Returns a proxied client that is fully type-compatible with
|
|
@@ -10,7 +10,7 @@ export type { EvalGuardConfig, GuardrailCheckResult, GuardrailViolation, TraceLo
|
|
|
10
10
|
*
|
|
11
11
|
* ```ts
|
|
12
12
|
* import OpenAI from "openai";
|
|
13
|
-
* import { wrapOpenAI } from "
|
|
13
|
+
* import { wrapOpenAI } from "@evalguard/openai";
|
|
14
14
|
*
|
|
15
15
|
* const openai = wrapOpenAI(new OpenAI(), {
|
|
16
16
|
* apiKey: "eg_...",
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,EAQL,KAAK,eAAe,EAGrB,MAAM,yBAAyB,CAAC;AAWjC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,EACZ,oBAAoB,EACpB,aAAa,GACd,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,YAAY,EACZ,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AA8DjC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAyE1E"}
|