@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 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
- ## Quick Start
12
-
13
- ```typescript
14
- import OpenAI from "openai";
15
- import { wrapOpenAI } from "@evalguard/openai";
16
-
17
- const openai = wrapOpenAI(new OpenAI(), {
18
- apiKey: "eg_...",
19
- projectId: "proj_...",
20
- });
21
-
22
- // Use exactly like the normal OpenAI SDK — guardrails are automatic
23
- const response = await openai.chat.completions.create({
24
- model: "gpt-4o",
25
- messages: [{ role: "user", content: "Hello, how are you?" }],
26
- });
27
-
28
- console.log(response.choices[0].message.content);
29
- ```
30
-
31
- ## Streaming
32
-
33
- Streaming works transparently. The wrapper intercepts chunks to log the assembled response without affecting stream behavior.
34
-
35
- ```typescript
36
- const stream = await openai.chat.completions.create({
37
- model: "gpt-4o",
38
- messages: [{ role: "user", content: "Write a poem about AI safety" }],
39
- stream: true,
40
- });
41
-
42
- for await (const chunk of stream) {
43
- process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
44
- }
45
- ```
46
-
47
- ## Configuration
48
-
49
- ```typescript
50
- const openai = wrapOpenAI(new OpenAI(), {
51
- // Required: your EvalGuard API key
52
- apiKey: "eg_...",
53
-
54
- // Optional: EvalGuard API base URL (default: https://evalguard.ai/api/v1)
55
- baseUrl: "https://your-evalguard-instance.com/api/v1",
56
-
57
- // Optional: block requests that fail guardrails (default: true)
58
- blockOnViolation: true,
59
-
60
- // Optional: log all requests to EvalGuard (default: true)
61
- enableLogging: true,
62
-
63
- // Optional: project ID for organizing traces
64
- projectId: "proj_...",
65
-
66
- // Optional: custom metadata attached to every trace
67
- metadata: { environment: "production", service: "chatbot" },
68
-
69
- // Optional: callback when a guardrail violation is detected
70
- onViolation: (result) => {
71
- console.warn("Guardrail violation:", result.violations);
72
- },
73
- });
74
- ```
75
-
76
- ## What It Does
77
-
78
- | Phase | Action |
79
- |-------|--------|
80
- | **Pre-request** | Sends the prompt to EvalGuard's firewall for prompt injection detection, PII scanning, and toxicity checks |
81
- | **LLM call** | Passes through to the real OpenAI API unchanged |
82
- | **Post-response** | Logs model, tokens, latency, cost, and guardrail results as a trace to EvalGuard |
83
-
84
- ## Fail-Open Design
85
-
86
- If EvalGuard is unreachable (network error, timeout, 5xx), the wrapper passes requests through to OpenAI directly. Your application never breaks because of EvalGuard downtime.
87
-
88
- ## Error Handling
89
-
90
- When `blockOnViolation` is `true` (default) and a guardrail check fails:
91
-
92
- ```typescript
93
- import { EvalGuardViolationError } from "@evalguard/openai";
94
-
95
- try {
96
- const response = await openai.chat.completions.create({
97
- model: "gpt-4o",
98
- messages: [{ role: "user", content: "malicious prompt..." }],
99
- });
100
- } catch (error) {
101
- if (error instanceof EvalGuardViolationError) {
102
- console.log("Blocked:", error.violations);
103
- // [{ type: "prompt_injection", severity: "critical", message: "..." }]
104
- }
105
- }
106
- ```
107
-
108
- Set `blockOnViolation: false` to log violations without blocking:
109
-
110
- ```typescript
111
- const openai = wrapOpenAI(new OpenAI(), {
112
- apiKey: "eg_...",
113
- blockOnViolation: false,
114
- onViolation: (result) => {
115
- // Log but don't block
116
- analytics.track("guardrail_violation", result);
117
- },
118
- });
119
- ```
120
-
121
- ## License
122
-
123
- MIT
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
@@ -1,2 +1,4 @@
1
1
  export { estimateCost } from "@evalguard/wrapper-core";
2
+ export { estimateCostDetailed, isModelPriced } from "@evalguard/wrapper-core";
3
+ export type { CostEstimate, PricingSource } from "@evalguard/wrapper-core";
2
4
  //# sourceMappingURL=cost.d.ts.map
@@ -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 "evalguardai-openai";
13
+ * import { wrapOpenAI } from "@evalguard/openai";
14
14
  *
15
15
  * const openai = wrapOpenAI(new OpenAI(), {
16
16
  * apiKey: "eg_...",
@@ -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,EAML,KAAK,eAAe,EAGrB,MAAM,yBAAyB,CAAC;AAWjC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,GACb,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AA4BjC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CA2B1E"}
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"}