Skip to content

Commit be849c0

Browse files
committed
Return Guardrail token usage
1 parent d77409c commit be849c0

19 files changed

+887
-115
lines changed

docs/agents_sdk_integration.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,28 @@ const agent = await GuardrailAgent.create(
111111
- Explore available guardrails for your use case
112112
- Learn about pipeline configuration in our [quickstart](./quickstart.md)
113113
- For more details on the OpenAI Agents SDK, refer to the [Agent SDK documentation](https://openai.github.io/openai-agents-js/).
114+
115+
## Token Usage Tracking
116+
117+
!!! warning "JavaScript Agents SDK Limitation"
118+
The JavaScript Agents SDK (`@openai/agents`) does not currently return guardrail results in the `RunResult` object. This means `totalGuardrailTokenUsage()` cannot retrieve token counts from Agents SDK runs.
119+
120+
**For token usage tracking, use `GuardrailsOpenAI` instead of `GuardrailAgent`.** The Python Agents SDK does support this feature.
121+
122+
When a guardrail **triggers** (throws `InputGuardrailTripwireTriggered` or `OutputGuardrailTripwireTriggered`), token usage IS available in the error's result object:
123+
124+
```typescript
125+
try {
126+
const result = await Runner.run(agent, userInput);
127+
} catch (error) {
128+
if (error.constructor.name === 'InputGuardrailTripwireTriggered') {
129+
// Token usage available when guardrail triggers
130+
const usage = error.result?.output?.outputInfo?.token_usage;
131+
if (usage) {
132+
console.log(`Guardrail tokens: ${usage.total_tokens}`);
133+
}
134+
}
135+
}
136+
```
137+
138+
For full token usage tracking across all guardrail runs (passing and failing), use the `GuardrailsOpenAI` client instead - see the [quickstart](./quickstart.md#token-usage-tracking) for details.

docs/quickstart.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,75 @@ const client = await GuardrailsOpenAI.create(
205205
);
206206
```
207207

208+
## Token Usage Tracking
209+
210+
LLM-based guardrails (Jailbreak, custom prompt checks, etc.) consume tokens. Keep track of those costs with the `totalGuardrailTokenUsage` helper:
211+
212+
```typescript
213+
import { GuardrailsOpenAI, totalGuardrailTokenUsage } from '@openai/guardrails';
214+
215+
const client = await GuardrailsOpenAI.create(CONFIG);
216+
const response = await client.guardrails.responses.create({
217+
model: 'gpt-4.1-mini',
218+
input: 'Hello!',
219+
});
220+
221+
const tokens = totalGuardrailTokenUsage(response);
222+
console.log(`Guardrail tokens used: ${tokens.total_tokens}`);
223+
// => Guardrail tokens used: 425
224+
```
225+
226+
The helper returns:
227+
228+
```typescript
229+
{
230+
prompt_tokens: 300, // Sum of prompt tokens across all LLM guardrails
231+
completion_tokens: 125, // Sum of completion tokens
232+
total_tokens: 425, // Total guardrail tokens
233+
}
234+
```
235+
236+
### Works With GuardrailsOpenAI Clients
237+
238+
`totalGuardrailTokenUsage` works across all client types and endpoints:
239+
240+
- **OpenAI** - sync and async clients
241+
- **Azure OpenAI** - sync and async clients
242+
- **Third-party providers** - any OpenAI-compatible API wrapper
243+
- **Endpoints** - both `responses` and `chat.completions`
244+
- **Streaming** - capture from the final chunk
245+
246+
```typescript
247+
// OpenAI client responses
248+
const response = await client.guardrails.responses.create(...);
249+
const tokens = totalGuardrailTokenUsage(response);
250+
251+
// Streaming – use the final chunk
252+
let lastChunk: unknown;
253+
for await (const chunk of stream) {
254+
lastChunk = chunk;
255+
}
256+
const streamingTokens = lastChunk ? totalGuardrailTokenUsage(lastChunk) : null;
257+
```
258+
259+
**Note:** The JavaScript Agents SDK (`@openai/agents`) does not currently populate guardrail results in the `RunResult` object, so `totalGuardrailTokenUsage()` will return empty results for Agents SDK runs.
260+
261+
### Per-Guardrail Usage
262+
263+
Each guardrail result includes its own `token_usage` entry:
264+
265+
```typescript
266+
const response = await client.guardrails.responses.create(...);
267+
for (const gr of response.guardrail_results.allResults) {
268+
const usage = gr.info.token_usage;
269+
if (usage) {
270+
console.log(`${gr.info.guardrail_name}: ${usage.total_tokens} tokens`);
271+
}
272+
}
273+
```
274+
275+
Non-LLM guardrails (PII, Moderation, URL Filter, etc.) do not consume tokens, so `token_usage` will be omitted.
276+
208277
## Next Steps
209278

210279
- Explore TypeScript [examples](https://github.com/openai/openai-guardrails-js/tree/main/examples) for advanced patterns

examples/basic/hello_world.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
*/
99

1010
import * as readline from 'readline';
11-
import { GuardrailsOpenAI, GuardrailTripwireTriggered } from '../../src';
11+
import { GuardrailsOpenAI, GuardrailTripwireTriggered, totalGuardrailTokenUsage } from '../../src';
1212

13-
// Pipeline configuration with preflight PII masking and input guardrails
13+
// Pipeline configuration with preflight and input guardrails
1414
const PIPELINE_CONFIG = {
1515
version: 1,
1616
pre_flight: {
1717
version: 1,
1818
guardrails: [
1919
{
20-
name: 'Contains PII',
20+
name: 'Moderation',
21+
config: { categories: ['hate', 'violence'] },
22+
},
23+
{
24+
name: 'Jailbreak',
2125
config: {
22-
entities: ['US_SSN', 'PHONE_NUMBER', 'EMAIL_ADDRESS'],
23-
block: true, // Use masking mode (default) - masks PII without blocking
26+
model: 'gpt-4.1-mini',
27+
confidence_threshold: 0.7,
2428
},
2529
},
2630
],
@@ -76,6 +80,10 @@ async function processInput(
7680
// Show guardrail results if any were run
7781
if (response.guardrail_results.allResults.length > 0) {
7882
console.log(`[dim]Guardrails checked: ${response.guardrail_results.allResults.length}[/dim]`);
83+
const usage = totalGuardrailTokenUsage(response);
84+
if (usage.total_tokens !== null) {
85+
console.log(`[dim]Token usage: ${JSON.stringify(usage)}[/dim]`);
86+
}
7987
}
8088

8189
return response.id;

examples/basic/local_model.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Example: Guardrail bundle using Ollama's Gemma3 model with GuardrailsClient.
33
*/
44

5-
import { GuardrailsOpenAI, GuardrailTripwireTriggered } from '../../src';
5+
import { GuardrailsOpenAI, GuardrailTripwireTriggered, totalGuardrailTokenUsage } from '../../src';
66
import * as readline from 'readline';
77
import { OpenAI } from 'openai';
88

@@ -46,6 +46,10 @@ async function processInput(
4646
// Access response content using standard OpenAI API
4747
const responseContent = response.choices[0].message.content ?? '';
4848
console.log(`\nAssistant output: ${responseContent}\n`);
49+
const usage = totalGuardrailTokenUsage(response);
50+
if (usage.total_tokens !== null) {
51+
console.log(`Token usage: ${usage.total_tokens}`);
52+
}
4953

5054
// Guardrails passed - now safe to add to conversation history
5155
conversation.push({ role: 'user', content: userInput });

examples/basic/multiturn_with_prompt_injection_detection.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
*/
2727

2828
import * as readline from 'readline';
29-
import { GuardrailsOpenAI, GuardrailTripwireTriggered, GuardrailsResponse } from '../../src';
29+
import {
30+
GuardrailsOpenAI,
31+
GuardrailTripwireTriggered,
32+
GuardrailsResponse,
33+
totalGuardrailTokenUsage,
34+
} from '../../src';
3035

3136
// Tool implementations (mocked)
3237
function get_horoscope(sign: string): { horoscope: string } {
@@ -299,6 +304,15 @@ async function main(malicious: boolean = false): Promise<void> {
299304

300305
printGuardrailResults('initial', response);
301306

307+
const initialUsage = totalGuardrailTokenUsage(response);
308+
if (initialUsage.total_tokens !== null) {
309+
console.log(
310+
`[dim]Guardrail tokens (initial): ${initialUsage.total_tokens} · prompt=${
311+
initialUsage.prompt_tokens ?? 0
312+
}, completion=${initialUsage.completion_tokens ?? 0}[/dim]`
313+
);
314+
}
315+
302316
assistantOutputs = response.output ?? [];
303317

304318
// Guardrails passed - now safe to add user message to conversation history
@@ -394,6 +408,14 @@ async function main(malicious: boolean = false): Promise<void> {
394408
});
395409

396410
printGuardrailResults('final', response);
411+
const finalUsage = totalGuardrailTokenUsage(response);
412+
if (finalUsage.total_tokens !== null) {
413+
console.log(
414+
`[dim]Guardrail tokens (final): ${finalUsage.total_tokens} · prompt=${
415+
finalUsage.prompt_tokens ?? 0
416+
}, completion=${finalUsage.completion_tokens ?? 0}[/dim]`
417+
);
418+
}
397419
console.log(`\n🤖 Assistant: ${response.output_text}`);
398420

399421
// Guardrails passed - now safe to add tool results and assistant responses to history

examples/basic/streaming.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Streams output using console logging.
44
*/
55

6-
import { GuardrailsOpenAI, GuardrailTripwireTriggered } from '../../src';
6+
import { GuardrailsOpenAI, GuardrailTripwireTriggered, totalGuardrailTokenUsage } from '../../src';
77
import * as readline from 'readline';
88

99
// Define your pipeline configuration
@@ -14,10 +14,14 @@ const PIPELINE_CONFIG = {
1414
version: 1,
1515
guardrails: [
1616
{
17-
name: 'Contains PII',
17+
name: 'Moderation',
18+
config: { categories: ['hate', 'violence'] },
19+
},
20+
{
21+
name: 'Jailbreak',
1822
config: {
19-
entities: ['US_SSN', 'PHONE_NUMBER', 'EMAIL_ADDRESS'],
20-
block: false, // Use masking mode (default) - masks PII without blocking
23+
model: 'gpt-4.1-mini',
24+
confidence_threshold: 0.7,
2125
},
2226
},
2327
],
@@ -49,6 +53,7 @@ const PIPELINE_CONFIG = {
4953
config: {
5054
entities: ['US_SSN', 'PHONE_NUMBER', 'EMAIL_ADDRESS'],
5155
block: true, // Use blocking mode on output
56+
detect_encoded_pii: false,
5257
},
5358
},
5459
],
@@ -78,8 +83,10 @@ async function processInput(
7883
console.log(outputText);
7984

8085
let responseIdToReturn: string | null = null;
86+
let lastChunk: unknown = null;
8187

8288
for await (const chunk of stream) {
89+
lastChunk = chunk;
8390
// Access streaming response exactly like native OpenAI API
8491
if ('delta' in chunk && chunk.delta && typeof chunk.delta === 'string') {
8592
outputText += chunk.delta;
@@ -99,6 +106,14 @@ async function processInput(
99106
}
100107

101108
console.log(); // New line after streaming
109+
110+
if (lastChunk) {
111+
const usage = totalGuardrailTokenUsage(lastChunk);
112+
if (usage.total_tokens !== null) {
113+
console.log(`[dim]📊 Guardrail tokens: ${usage.total_tokens}[/dim]`);
114+
}
115+
}
116+
102117
return responseIdToReturn;
103118
} catch (error) {
104119
if (error instanceof GuardrailTripwireTriggered) {

src/__tests__/unit/agents.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ describe('GuardrailAgent', () => {
451451
expect(result.outputInfo.input).toBe('Latest user message with additional context.');
452452
});
453453

454-
it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => {
454+
it('should handle guardrail execution errors based on raiseGuardrailErrors setting', async () => {
455455
process.env.OPENAI_API_KEY = 'test';
456456
const config = {
457457
version: 1,
@@ -547,4 +547,68 @@ describe('GuardrailAgent', () => {
547547
);
548548
});
549549
});
550+
551+
it('propagates guardrail metadata to outputInfo on success', async () => {
552+
process.env.OPENAI_API_KEY = 'test';
553+
const config = {
554+
version: 1,
555+
input: {
556+
version: 1,
557+
guardrails: [{ name: 'Jailbreak', config: {} }],
558+
},
559+
};
560+
561+
const { instantiateGuardrails } = await import('../../runtime');
562+
vi.mocked(instantiateGuardrails).mockImplementationOnce(() =>
563+
Promise.resolve([
564+
{
565+
definition: {
566+
name: 'Jailbreak',
567+
description: 'Test guardrail',
568+
mediaType: 'text/plain',
569+
configSchema: z.object({}),
570+
checkFn: vi.fn(),
571+
metadata: {},
572+
ctxRequirements: z.object({}),
573+
schema: () => ({}),
574+
instantiate: vi.fn(),
575+
},
576+
config: {},
577+
run: vi.fn().mockResolvedValue({
578+
tripwireTriggered: false,
579+
info: {
580+
guardrail_name: 'Jailbreak',
581+
flagged: false,
582+
token_usage: {
583+
prompt_tokens: 42,
584+
completion_tokens: 10,
585+
total_tokens: 52,
586+
},
587+
},
588+
}),
589+
} as unknown as Parameters<typeof instantiateGuardrails>[0] extends Promise<infer T>
590+
? T extends readonly (infer U)[]
591+
? U
592+
: never
593+
: never,
594+
])
595+
);
596+
597+
const agent = (await GuardrailAgent.create(
598+
config,
599+
'Metadata Agent',
600+
'Test instructions'
601+
)) as MockAgent;
602+
603+
const guardrailFunction = agent.inputGuardrails[0];
604+
const result = await guardrailFunction.execute('payload');
605+
606+
expect(result.tripwireTriggered).toBe(false);
607+
expect(result.outputInfo.guardrail_name).toBe('Jailbreak');
608+
expect(result.outputInfo.token_usage).toEqual({
609+
prompt_tokens: 42,
610+
completion_tokens: 10,
611+
total_tokens: 52,
612+
});
613+
});
550614
});

0 commit comments

Comments
 (0)