anthale 0.2.0 → 0.3.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.
@@ -0,0 +1,428 @@
1
+ import type { PolicyEnforceParams } from '../resources/organizations/policies';
2
+ import { buildPolicyEnforcer, type PolicyEnforcerOptions, messagesFromValue, normalizeRole } from './core';
3
+
4
+ type MaybePromise<T> = T | PromiseLike<T>;
5
+
6
+ interface CreateFn {
7
+ (...args: any[]): MaybePromise<any>;
8
+ }
9
+
10
+ interface OpenAIClientLike {
11
+ responses?: { create?: unknown };
12
+ chat?: { completions?: { create?: unknown } };
13
+ }
14
+
15
+ /**
16
+ * Options for {@link guardOpenAIClient}.
17
+ *
18
+ * Inherits all policy enforcer options (`policyId`, `apiKey`, `client`,
19
+ * `metadata`).
20
+ */
21
+ export interface GuardOpenAIClientOptions extends PolicyEnforcerOptions {}
22
+
23
+ const OPENAI_GUARDED = Symbol.for('anthale.openai.guarded');
24
+
25
+ let streamWarningIssued = false;
26
+
27
+ function warnStreamBuffering(): void {
28
+ if (streamWarningIssued) {
29
+ return;
30
+ }
31
+
32
+ streamWarningIssued = true;
33
+ console.warn(
34
+ 'Anthale does not support real-time stream analysis. OpenAI stream outputs are buffered and analyzed once the stream completes.',
35
+ );
36
+ }
37
+
38
+ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
39
+ return typeof value === 'object' && value !== null && Symbol.asyncIterator in value;
40
+ }
41
+
42
+ function extractMessagesFromRequest(
43
+ endpoint: 'responses' | 'chat_completions',
44
+ payload: Record<string, unknown>,
45
+ ): PolicyEnforceParams.Message[] {
46
+ if (endpoint === 'responses') {
47
+ const messages: PolicyEnforceParams.Message[] = [];
48
+ if ('instructions' in payload) {
49
+ messages.push(...messagesFromValue(payload['instructions'], 'system'));
50
+ }
51
+
52
+ if ('input' in payload) {
53
+ messages.push(...messagesFromValue(payload['input'], 'user'));
54
+ }
55
+
56
+ return messages;
57
+ }
58
+
59
+ return messagesFromValue(payload['messages'], 'user');
60
+ }
61
+
62
+ function extractMessagesFromResponsesPayload(
63
+ payload: Record<string, unknown>,
64
+ ): PolicyEnforceParams.Message[] {
65
+ const messages: PolicyEnforceParams.Message[] = [];
66
+ let hasAssistantTextInOutput = false;
67
+
68
+ const output = payload['output'];
69
+ if (Array.isArray(output)) {
70
+ for (const item of output) {
71
+ if (!item || typeof item !== 'object') {
72
+ continue;
73
+ }
74
+
75
+ const record = item as Record<string, unknown>;
76
+ const role = normalizeRole(record['role']);
77
+ const contentMessages = messagesFromValue(record['content'], role);
78
+ if (role === 'assistant' && contentMessages.length > 0) {
79
+ hasAssistantTextInOutput = true;
80
+ }
81
+
82
+ messages.push(...contentMessages);
83
+ if ('arguments' in record) {
84
+ messages.push(...messagesFromValue(record['arguments'], 'assistant'));
85
+ }
86
+ }
87
+ }
88
+
89
+ if (!hasAssistantTextInOutput && 'output_text' in payload) {
90
+ messages.push(...messagesFromValue(payload['output_text'], 'assistant'));
91
+ }
92
+
93
+ return messages;
94
+ }
95
+
96
+ function extractMessagesFromChatCompletionsPayload(
97
+ payload: Record<string, unknown>,
98
+ ): PolicyEnforceParams.Message[] {
99
+ const messages: PolicyEnforceParams.Message[] = [];
100
+ const choices = payload['choices'];
101
+
102
+ if (!Array.isArray(choices)) {
103
+ return messages;
104
+ }
105
+
106
+ for (const choice of choices) {
107
+ if (!choice || typeof choice !== 'object') {
108
+ continue;
109
+ }
110
+
111
+ const choiceRecord = choice as Record<string, unknown>;
112
+ const message = choiceRecord['message'];
113
+ if (!message || typeof message !== 'object') {
114
+ continue;
115
+ }
116
+
117
+ const messageRecord = message as Record<string, unknown>;
118
+ messages.push(...messagesFromValue(messageRecord, 'assistant'));
119
+
120
+ const toolCalls = messageRecord['tool_calls'];
121
+ if (Array.isArray(toolCalls)) {
122
+ for (const toolCall of toolCalls) {
123
+ if (!toolCall || typeof toolCall !== 'object') {
124
+ continue;
125
+ }
126
+
127
+ const toolCallRecord = toolCall as Record<string, unknown>;
128
+ const fn = toolCallRecord['function'];
129
+ if (!fn || typeof fn !== 'object') {
130
+ continue;
131
+ }
132
+
133
+ const fnRecord = fn as Record<string, unknown>;
134
+ messages.push(...messagesFromValue(fnRecord['arguments'], 'assistant'));
135
+ }
136
+ }
137
+ }
138
+
139
+ return messages;
140
+ }
141
+
142
+ function extractMessagesFromResponse(
143
+ endpoint: 'responses' | 'chat_completions',
144
+ payload: unknown,
145
+ ): PolicyEnforceParams.Message[] {
146
+ if (!payload || typeof payload !== 'object') {
147
+ return [];
148
+ }
149
+
150
+ const record = payload as Record<string, unknown>;
151
+ if (endpoint === 'responses') {
152
+ return extractMessagesFromResponsesPayload(record);
153
+ }
154
+
155
+ return extractMessagesFromChatCompletionsPayload(record);
156
+ }
157
+
158
+ function extractMessagesFromStreamChunks(
159
+ endpoint: 'responses' | 'chat_completions',
160
+ chunks: unknown[],
161
+ ): PolicyEnforceParams.Message[] {
162
+ if (endpoint === 'chat_completions') {
163
+ const contentParts: string[] = [];
164
+ const argumentParts: string[] = [];
165
+
166
+ for (const chunk of chunks) {
167
+ if (!chunk || typeof chunk !== 'object') {
168
+ continue;
169
+ }
170
+
171
+ const record = chunk as Record<string, unknown>;
172
+ const choices = record['choices'];
173
+ if (!Array.isArray(choices)) {
174
+ continue;
175
+ }
176
+
177
+ for (const choice of choices) {
178
+ if (!choice || typeof choice !== 'object') {
179
+ continue;
180
+ }
181
+
182
+ const delta = (choice as Record<string, unknown>)['delta'];
183
+ if (!delta || typeof delta !== 'object') {
184
+ continue;
185
+ }
186
+
187
+ const deltaRecord = delta as Record<string, unknown>;
188
+ if (typeof deltaRecord['content'] === 'string') {
189
+ contentParts.push(deltaRecord['content']);
190
+ }
191
+
192
+ const toolCalls = deltaRecord['tool_calls'];
193
+ if (Array.isArray(toolCalls)) {
194
+ for (const toolCall of toolCalls) {
195
+ if (!toolCall || typeof toolCall !== 'object') {
196
+ continue;
197
+ }
198
+
199
+ const fn = (toolCall as Record<string, unknown>)['function'];
200
+ if (!fn || typeof fn !== 'object') {
201
+ continue;
202
+ }
203
+
204
+ const args = (fn as Record<string, unknown>)['arguments'];
205
+ if (typeof args === 'string') argumentParts.push(args);
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ const messages: PolicyEnforceParams.Message[] = [];
212
+ if (contentParts.length > 0) {
213
+ messages.push({ role: 'assistant', content: contentParts.join('') });
214
+ }
215
+
216
+ if (argumentParts.length > 0) {
217
+ messages.push({ role: 'assistant', content: argumentParts.join('') });
218
+ }
219
+
220
+ return messages;
221
+ }
222
+
223
+ const outputTextParts: string[] = [];
224
+ let completedPayload: Record<string, unknown> | null = null;
225
+
226
+ for (const chunk of chunks) {
227
+ if (!chunk || typeof chunk !== 'object') {
228
+ continue;
229
+ }
230
+
231
+ const record = chunk as Record<string, unknown>;
232
+ const type = typeof record['type'] === 'string' ? record['type'] : '';
233
+
234
+ if (type === 'response.output_text.delta') {
235
+ if (typeof record['delta'] === 'string') {
236
+ outputTextParts.push(record['delta']);
237
+ }
238
+
239
+ continue;
240
+ }
241
+
242
+ if (type === 'response.completed') {
243
+ const response = record['response'];
244
+ if (response && typeof response === 'object') {
245
+ completedPayload = response as Record<string, unknown>;
246
+ }
247
+ }
248
+ }
249
+
250
+ if (completedPayload) {
251
+ return extractMessagesFromResponsesPayload(completedPayload);
252
+ }
253
+
254
+ if (outputTextParts.length > 0) {
255
+ return [{ role: 'assistant', content: outputTextParts.join('') }];
256
+ }
257
+
258
+ return [];
259
+ }
260
+
261
+ async function wrapStreamWithPolicy<T>(
262
+ stream: AsyncIterable<T>,
263
+ onComplete: (chunks: T[]) => Promise<void>,
264
+ ): Promise<AsyncIterable<T>> {
265
+ async function* generator(): AsyncGenerator<T> {
266
+ const chunks: T[] = [];
267
+ for await (const chunk of stream) {
268
+ chunks.push(chunk);
269
+ yield chunk;
270
+ }
271
+
272
+ await onComplete(chunks);
273
+ }
274
+
275
+ return generator();
276
+ }
277
+
278
+ async function wrapOpenAICallResult(
279
+ result: unknown,
280
+ endpoint: 'responses' | 'chat_completions',
281
+ requestMessages: PolicyEnforceParams.Message[],
282
+ onEnforceOutput: (messages: PolicyEnforceParams.Message[]) => Promise<void>,
283
+ isStream: boolean,
284
+ ): Promise<unknown> {
285
+ if (isStream && isAsyncIterable(result)) {
286
+ warnStreamBuffering();
287
+ return wrapStreamWithPolicy(result, async (chunks) => {
288
+ const responseMessages = extractMessagesFromStreamChunks(endpoint, chunks as unknown[]);
289
+ if (responseMessages.length > 0) {
290
+ await onEnforceOutput([...requestMessages, ...responseMessages]);
291
+ }
292
+ });
293
+ }
294
+
295
+ const responseMessages = extractMessagesFromResponse(endpoint, result);
296
+ if (responseMessages.length > 0) {
297
+ await onEnforceOutput([...requestMessages, ...responseMessages]);
298
+ }
299
+
300
+ return result;
301
+ }
302
+
303
+ function patchCreateMethod(
304
+ target: Record<string, unknown>,
305
+ key: string,
306
+ endpoint: 'responses' | 'chat_completions',
307
+ options: GuardOpenAIClientOptions,
308
+ ): boolean {
309
+ const maybeFn = target[key];
310
+ if (typeof maybeFn !== 'function') {
311
+ return false;
312
+ }
313
+
314
+ const enforcer = buildPolicyEnforcer(options);
315
+ const original = maybeFn as CreateFn;
316
+
317
+ target[key] = function anthaleGuardedCreate(this: unknown, ...args: unknown[]): Promise<unknown> {
318
+ const payload = (args[0] as Record<string, unknown>) ?? {};
319
+ const requestMessages = extractMessagesFromRequest(endpoint, payload);
320
+
321
+ const execute = async (): Promise<unknown> => {
322
+ if (requestMessages.length > 0) {
323
+ await enforcer.enforce({ direction: 'input', messages: requestMessages });
324
+ }
325
+
326
+ const result = await original.apply(this, args);
327
+ const isStream = Boolean(payload['stream']);
328
+
329
+ return wrapOpenAICallResult(
330
+ result,
331
+ endpoint,
332
+ requestMessages,
333
+ async (messages) => {
334
+ await enforcer.enforce({ direction: 'output', messages });
335
+ },
336
+ isStream,
337
+ );
338
+ };
339
+
340
+ return execute();
341
+ };
342
+
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * Guard an existing OpenAI client instance in place.
348
+ *
349
+ * Supported call sites:
350
+ * - `client.responses.create(...)`
351
+ * - `client.chat.completions.create(...)`
352
+ *
353
+ * Behavior:
354
+ * - Enforces `input` policy before request execution.
355
+ * - Enforces `output` policy after response generation.
356
+ * - For streaming calls, buffers chunks and enforces once on stream completion.
357
+ *
358
+ * The same client object is returned and marked as guarded for idempotency.
359
+ *
360
+ * @param openAIClient - OpenAI SDK client instance to instrument.
361
+ * @param options - Anthale policy enforcer options.
362
+ * @returns The same `openAIClient` instance, now guarded.
363
+ * @throws {TypeError} If no supported OpenAI create method is exposed.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * import OpenAI from 'openai';
368
+ * import { guardOpenAIClient } from 'anthale/integrations/openai';
369
+ *
370
+ * const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
371
+ * guardOpenAIClient(openai, {
372
+ * policyId: 'pol_123',
373
+ * apiKey: process.env.ANTHALE_API_KEY,
374
+ * metadata: { tenantId: 'acme' },
375
+ * });
376
+ *
377
+ * const response = await openai.responses.create({
378
+ * model: 'gpt-4.1-mini',
379
+ * input: 'Hello!',
380
+ * });
381
+ * ```
382
+ */
383
+ export function guardOpenAIClient<TClient extends OpenAIClientLike>(
384
+ openAIClient: TClient,
385
+ options: GuardOpenAIClientOptions,
386
+ ): TClient {
387
+ if (!openAIClient || typeof openAIClient !== 'object') {
388
+ throw new TypeError('OpenAI client must be an object.');
389
+ }
390
+
391
+ if ((openAIClient as Record<symbol, unknown>)[OPENAI_GUARDED]) {
392
+ return openAIClient;
393
+ }
394
+
395
+ const responsesPatched =
396
+ openAIClient.responses && typeof openAIClient.responses === 'object' ?
397
+ patchCreateMethod(openAIClient.responses as Record<string, unknown>, 'create', 'responses', options)
398
+ : false;
399
+
400
+ const chatPatched =
401
+ (
402
+ openAIClient.chat &&
403
+ typeof openAIClient.chat === 'object' &&
404
+ (openAIClient.chat as Record<string, unknown>)['completions'] &&
405
+ typeof (openAIClient.chat as Record<string, unknown>)['completions'] === 'object'
406
+ ) ?
407
+ patchCreateMethod(
408
+ ((openAIClient.chat as Record<string, unknown>)['completions'] as Record<string, unknown>) ?? {},
409
+ 'create',
410
+ 'chat_completions',
411
+ options,
412
+ )
413
+ : false;
414
+
415
+ if (!responsesPatched && !chatPatched) {
416
+ throw new TypeError(
417
+ "OpenAI client must expose either 'responses.create' or 'chat.completions.create' methods to be guarded.",
418
+ );
419
+ }
420
+
421
+ (openAIClient as unknown as Record<symbol, boolean>)[OPENAI_GUARDED] = true;
422
+ return openAIClient;
423
+ }
424
+
425
+ /**
426
+ * Alias of {@link guardOpenAIClient}.
427
+ */
428
+ export const guardClient = guardOpenAIClient;
@@ -6,6 +6,9 @@ import { APIPromise } from '../../core/api-promise';
6
6
  import { RequestOptions } from '../../internal/request-options';
7
7
  import { path } from '../../internal/utils/path';
8
8
 
9
+ /**
10
+ * Policy lifecycle management.
11
+ */
9
12
  export class Policies extends APIResource {
10
13
  /**
11
14
  * Evaluates a set of messages against the specified policy and returns guardrail
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.0'; // x-release-please-version
1
+ export const VERSION = '0.3.0'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.3.0";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.3.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.2.0'; // x-release-please-version
4
+ exports.VERSION = '0.3.0'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.2.0'; // x-release-please-version
1
+ export const VERSION = '0.3.0'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map