@vaxelia/ai-openai 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/README.md +149 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +206 -0
- package/dist/internal.d.ts +40 -0
- package/dist/internal.js +3 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @vaxelia/ai-openai
|
|
2
|
+
|
|
3
|
+
A **drop-in, compliance-instrumented** wrapper around the official OpenAI SDK
|
|
4
|
+
([`openai`](https://www.npmjs.com/package/openai)). Every non-streaming
|
|
5
|
+
`chat.completions.create` call is logged to your tenant's decision-log endpoint
|
|
6
|
+
via [`@vaxelia/ai-core`](../sdk-ai-core/README.md) — with its resilient,
|
|
7
|
+
encrypted-on-disk buffer — so an AI decision is never silently lost. Every other
|
|
8
|
+
method of the OpenAI SDK passes straight through to the real client untouched.
|
|
9
|
+
|
|
10
|
+
`openai` is a regular dependency of this package, so the wrapper is a true
|
|
11
|
+
single-install drop-in: you do not install the OpenAI SDK separately.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @vaxelia/ai-openai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node.js >= 20.
|
|
20
|
+
|
|
21
|
+
## The one-line swap
|
|
22
|
+
|
|
23
|
+
Change your import — keep everything else:
|
|
24
|
+
|
|
25
|
+
```diff
|
|
26
|
+
- import OpenAI from 'openai'
|
|
27
|
+
+ import OpenAI from '@vaxelia/ai-openai'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import OpenAI from '@vaxelia/ai-openai'
|
|
34
|
+
|
|
35
|
+
const client = new OpenAI({
|
|
36
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
37
|
+
// Optional compliance block. Every field falls back to an env var (below),
|
|
38
|
+
// so you can also pass nothing here and configure entirely from the env.
|
|
39
|
+
compliance: {
|
|
40
|
+
aiSystemId: process.env.VAXELIA_AI_SYSTEM_ID,
|
|
41
|
+
tenantApiUrl: process.env.VAXELIA_TENANT_API_URL,
|
|
42
|
+
apiKey: process.env.VAXELIA_API_KEY,
|
|
43
|
+
bufferKey: process.env.VAXELIA_BUFFER_KEY,
|
|
44
|
+
logger: (level, msg, fields) =>
|
|
45
|
+
console[level === 'debug' ? 'log' : level](JSON.stringify({ level, msg, ...fields })),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Identical to the official SDK. The decision is logged for you.
|
|
50
|
+
const completion = await client.chat.completions.create({
|
|
51
|
+
model: 'gpt-4o',
|
|
52
|
+
messages: [{ role: 'user', content: 'Summarize the attached contract.' }],
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With env vars set, the `compliance` block can be omitted entirely:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Reaching the raw client
|
|
63
|
+
|
|
64
|
+
For methods this wrapper does not instrument, the underlying OpenAI client is
|
|
65
|
+
available at `client.raw`, and `client.chat.completions` proxies every other
|
|
66
|
+
completions method (`retrieve`, `update`, `list`, `delete`, `parse`, `runTools`,
|
|
67
|
+
`stream`, …) directly to the real resource.
|
|
68
|
+
|
|
69
|
+
### Response helpers (`.withResponse()` / `.asResponse()`)
|
|
70
|
+
|
|
71
|
+
The instrumented non-streaming `chat.completions.create` returns the SDK's
|
|
72
|
+
`APIPromise`, not a plain `Promise`, so the official response helpers keep
|
|
73
|
+
working:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const { data, response, request_id } = await client.chat.completions.create(params).withResponse()
|
|
77
|
+
const raw = await client.chat.completions.create(params).asResponse()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The compliance decision is logged exactly once after the response is obtained,
|
|
81
|
+
regardless of whether you `await` the call, use `.withResponse()`, or use
|
|
82
|
+
`.asResponse()`.
|
|
83
|
+
|
|
84
|
+
## Configuration & environment variables
|
|
85
|
+
|
|
86
|
+
The `compliance` block is a `ReporterConfig` from `@vaxelia/ai-core`. Every field
|
|
87
|
+
is optional and falls back to a matching environment variable (12-factor):
|
|
88
|
+
|
|
89
|
+
| `compliance` field | Environment variable | Description |
|
|
90
|
+
| ------------------ | ------------------------ | ------------------------------------------------------------ |
|
|
91
|
+
| `aiSystemId` | `VAXELIA_AI_SYSTEM_ID` | Registered AI system identifier. |
|
|
92
|
+
| `tenantApiUrl` | `VAXELIA_TENANT_API_URL` | Full decision-log POST endpoint URL. |
|
|
93
|
+
| `apiKey` | `VAXELIA_API_KEY` | Bearer token for the tenant API. |
|
|
94
|
+
| `bufferKey` | `VAXELIA_BUFFER_KEY` | 32-byte AES-256-GCM key, base64-encoded. Required to buffer. |
|
|
95
|
+
|
|
96
|
+
For the encrypted disk buffer, the fail-closed buffer-key rule, retention, and
|
|
97
|
+
backoff behaviour, see the [`@vaxelia/ai-core` README](../sdk-ai-core/README.md).
|
|
98
|
+
Note: when buffering is enabled (the default) and no valid 32-byte buffer key is
|
|
99
|
+
resolvable, **construction throws** (`MissingBufferKeyError` /
|
|
100
|
+
`InvalidBufferKeyError`) — this wrapper surfaces that error rather than swallow
|
|
101
|
+
it. To run without a disk buffer, set `compliance.bufferingEnabled: false`.
|
|
102
|
+
|
|
103
|
+
## What gets logged
|
|
104
|
+
|
|
105
|
+
On each instrumented `chat.completions.create` call, one decision envelope is
|
|
106
|
+
reported:
|
|
107
|
+
|
|
108
|
+
| Field | Source |
|
|
109
|
+
| ------------ | ---------------------------------------------------------------------------- |
|
|
110
|
+
| `aiSystemId` | `compliance.aiSystemId` / `VAXELIA_AI_SYSTEM_ID` |
|
|
111
|
+
| `modelUsed` | the response's `model`, falling back to the request `model` |
|
|
112
|
+
| `input` | the `chat.completions.create` request params |
|
|
113
|
+
| `output` | the provider response (or, on failure, `{ error }` — no raw error/secrets) |
|
|
114
|
+
| `status` | `completed` on success, `failed` when the provider call throws |
|
|
115
|
+
| `decidedAt` | ISO-8601 timestamp at the moment of the call |
|
|
116
|
+
|
|
117
|
+
## Reporter-error ordering (no silent drops)
|
|
118
|
+
|
|
119
|
+
The provider response is always obtained **first**; only then is the decision
|
|
120
|
+
logged. If `@vaxelia/ai-core` cannot deliver the decision *and* cannot durably
|
|
121
|
+
buffer it, it throws (`DecisionNotRecordedError`) — and this wrapper lets that
|
|
122
|
+
error surface to your caller. This is deliberate: the whole point of the
|
|
123
|
+
compliance layer is that a recorded decision is never silently dropped. If you
|
|
124
|
+
prefer delivery failures to be buffered instead of thrown, keep buffering enabled
|
|
125
|
+
(the default) with a valid buffer key — transient failures (network, 5xx, 429)
|
|
126
|
+
are then retried with backoff and buffered to encrypted disk for later flush.
|
|
127
|
+
|
|
128
|
+
On a **failed** provider call, the `failed` decision is reported first and then
|
|
129
|
+
the original provider error is rethrown unchanged.
|
|
130
|
+
|
|
131
|
+
## Scope & limitations
|
|
132
|
+
|
|
133
|
+
- **Streaming is not instrumented in v1.** A call with `stream: true` passes
|
|
134
|
+
straight through to the real client uninstrumented, so streaming usage never
|
|
135
|
+
breaks — but no decision is logged for it yet. Streaming compliance capture is
|
|
136
|
+
a planned enhancement.
|
|
137
|
+
- Only `chat.completions.create` is instrumented. All other SDK methods and
|
|
138
|
+
resources (`responses`, `embeddings`, `files`, …) delegate to the real client;
|
|
139
|
+
reach `client.raw` for the underlying object when needed.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pnpm --filter @vaxelia/ai-openai test # vitest
|
|
145
|
+
pnpm --filter @vaxelia/ai-openai build # tsc -> dist
|
|
146
|
+
pnpm --filter @vaxelia/ai-openai lint # eslint
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
See [CLAUDE.md](./CLAUDE.md) for AI-specific conventions.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type OpenAISDK from 'openai';
|
|
2
|
+
import { type OpenAIComplianceOptions } from './internal';
|
|
3
|
+
export type { OpenAIComplianceOptions } from './internal';
|
|
4
|
+
/**
|
|
5
|
+
* Drop-in, compliance-instrumented replacement for the official OpenAI client.
|
|
6
|
+
* Swap `import OpenAI from 'openai'` for `import OpenAI from '@vaxelia/ai-openai'`
|
|
7
|
+
* and keep the rest of your code: every non-streaming `chat.completions.create`
|
|
8
|
+
* call is logged to your tenant's decision-log endpoint via `@vaxelia/ai-core`
|
|
9
|
+
* (with the resilient encrypted disk buffer), and all other SDK methods pass
|
|
10
|
+
* through to the real client untouched.
|
|
11
|
+
*
|
|
12
|
+
* Streaming calls (`stream: true`) currently pass through uninstrumented —
|
|
13
|
+
* streaming compliance capture is a planned enhancement (see README).
|
|
14
|
+
*
|
|
15
|
+
* Reporter-error ordering: the provider response is obtained first; only then is
|
|
16
|
+
* the decision logged. If logging cannot deliver *and* cannot buffer the
|
|
17
|
+
* decision, `@vaxelia/ai-core` throws (hard-fail, not silent drop) and that
|
|
18
|
+
* error surfaces to the caller — deliberately, so a compliance decision is never
|
|
19
|
+
* silently lost.
|
|
20
|
+
*/
|
|
21
|
+
export declare class OpenAI {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(options?: OpenAIComplianceOptions);
|
|
24
|
+
/**
|
|
25
|
+
* The instrumented `chat` resource. Mirrors the real client's `chat` surface;
|
|
26
|
+
* `chat.completions.create` is wrapped for compliance logging while every
|
|
27
|
+
* other method delegates to the underlying client.
|
|
28
|
+
*/
|
|
29
|
+
readonly chat: OpenAISDK['chat'];
|
|
30
|
+
/**
|
|
31
|
+
* The underlying, un-instrumented OpenAI client. Reach for it when you need a
|
|
32
|
+
* method this wrapper does not yet instrument and want the raw provider object
|
|
33
|
+
* (e.g. advanced streaming helpers, the responses or embeddings resources).
|
|
34
|
+
*/
|
|
35
|
+
get raw(): OpenAISDK;
|
|
36
|
+
}
|
|
37
|
+
export default OpenAI;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { ComplianceReporter } from '@vaxelia/ai-core';
|
|
2
|
+
import { RealOpenAI, } from './internal';
|
|
3
|
+
/**
|
|
4
|
+
* Render a thrown provider error as a compliance-safe envelope. We never embed
|
|
5
|
+
* the raw `Error` (it may carry headers, request bodies, or credentials in
|
|
6
|
+
* provider SDK error subclasses) — only the human-readable message.
|
|
7
|
+
*/
|
|
8
|
+
function errorOutput(err) {
|
|
9
|
+
if (err instanceof Error)
|
|
10
|
+
return { error: err.message };
|
|
11
|
+
return { error: String(err) };
|
|
12
|
+
}
|
|
13
|
+
/** A request is streaming when `stream: true` is set on the params. */
|
|
14
|
+
function isStreaming(params) {
|
|
15
|
+
return (typeof params === 'object' &&
|
|
16
|
+
params !== null &&
|
|
17
|
+
params.stream === true);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Drop-in, compliance-instrumented replacement for the official OpenAI client.
|
|
21
|
+
* Swap `import OpenAI from 'openai'` for `import OpenAI from '@vaxelia/ai-openai'`
|
|
22
|
+
* and keep the rest of your code: every non-streaming `chat.completions.create`
|
|
23
|
+
* call is logged to your tenant's decision-log endpoint via `@vaxelia/ai-core`
|
|
24
|
+
* (with the resilient encrypted disk buffer), and all other SDK methods pass
|
|
25
|
+
* through to the real client untouched.
|
|
26
|
+
*
|
|
27
|
+
* Streaming calls (`stream: true`) currently pass through uninstrumented —
|
|
28
|
+
* streaming compliance capture is a planned enhancement (see README).
|
|
29
|
+
*
|
|
30
|
+
* Reporter-error ordering: the provider response is obtained first; only then is
|
|
31
|
+
* the decision logged. If logging cannot deliver *and* cannot buffer the
|
|
32
|
+
* decision, `@vaxelia/ai-core` throws (hard-fail, not silent drop) and that
|
|
33
|
+
* error surfaces to the caller — deliberately, so a compliance decision is never
|
|
34
|
+
* silently lost.
|
|
35
|
+
*/
|
|
36
|
+
export class OpenAI {
|
|
37
|
+
#client;
|
|
38
|
+
#reporter;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
const { compliance, __clientFactory, ...clientOptions } = options;
|
|
41
|
+
// Construct the reporter first so fail-closed buffer-key validation runs at
|
|
42
|
+
// construction time (matching the bare @vaxelia/ai-core contract).
|
|
43
|
+
this.#reporter = new ComplianceReporter(compliance ?? {});
|
|
44
|
+
const factory = __clientFactory ?? ((opts) => new RealOpenAI(opts));
|
|
45
|
+
this.#client = factory(clientOptions);
|
|
46
|
+
// Build an instrumented `chat` facade. The completion path is nested
|
|
47
|
+
// (`chat` -> `completions` -> `create`), so we proxy `chat` to override only
|
|
48
|
+
// `completions`, and proxy `completions` to override only `create`. Every
|
|
49
|
+
// other property of `chat`/`completions` delegates to the real resource so
|
|
50
|
+
// the rest of the SDK surface (`completions.list`, `chat.completions.parse`,
|
|
51
|
+
// `runTools`, `stream`, ...) stays intact.
|
|
52
|
+
const realChat = this.#client.chat;
|
|
53
|
+
const instrumentedCompletions = this.#buildInstrumentedCompletions(realChat.completions);
|
|
54
|
+
this.chat = new Proxy(realChat, {
|
|
55
|
+
get: (target, prop, receiver) => {
|
|
56
|
+
if (prop === 'completions')
|
|
57
|
+
return instrumentedCompletions;
|
|
58
|
+
const value = Reflect.get(target, prop, receiver);
|
|
59
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The instrumented `chat` resource. Mirrors the real client's `chat` surface;
|
|
65
|
+
* `chat.completions.create` is wrapped for compliance logging while every
|
|
66
|
+
* other method delegates to the underlying client.
|
|
67
|
+
*/
|
|
68
|
+
chat;
|
|
69
|
+
/**
|
|
70
|
+
* The underlying, un-instrumented OpenAI client. Reach for it when you need a
|
|
71
|
+
* method this wrapper does not yet instrument and want the raw provider object
|
|
72
|
+
* (e.g. advanced streaming helpers, the responses or embeddings resources).
|
|
73
|
+
*/
|
|
74
|
+
get raw() {
|
|
75
|
+
return this.#client;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the instrumented `completions` resource: a Proxy over the real
|
|
79
|
+
* resource that overrides only `create` and delegates everything else
|
|
80
|
+
* (`retrieve`, `update`, `list`, `delete`, `parse`, `runTools`, `stream`) with
|
|
81
|
+
* `this` bound to the real resource.
|
|
82
|
+
*/
|
|
83
|
+
#buildInstrumentedCompletions(realCompletions) {
|
|
84
|
+
const instrumentedCreate = this.#instrumentedCreate.bind(this);
|
|
85
|
+
return new Proxy(realCompletions, {
|
|
86
|
+
get(target, prop, receiver) {
|
|
87
|
+
if (prop === 'create')
|
|
88
|
+
return instrumentedCreate;
|
|
89
|
+
const value = Reflect.get(target, prop, receiver);
|
|
90
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Instrumented `chat.completions.create`. Streaming calls pass through
|
|
96
|
+
* unchanged (preserving the SDK return). Non-streaming calls return an
|
|
97
|
+
* `APIPromise` wrapper that fires the compliance `logDecision` (after the
|
|
98
|
+
* response is obtained) on whichever consumption path the caller uses —
|
|
99
|
+
* `await`, `.withResponse()`, or `.asResponse()` — so those helpers keep
|
|
100
|
+
* working and the declared `chat` type is honoured (no plain-Promise type lie).
|
|
101
|
+
* Reporter ordering and hard-fail surfacing match the failure path.
|
|
102
|
+
*/
|
|
103
|
+
#instrumentedCreate(params, requestOptions) {
|
|
104
|
+
const realCreate = this.#client.chat.completions.create.bind(this.#client.chat.completions);
|
|
105
|
+
// Streaming is out of scope for v1 compliance capture; pass it through so the
|
|
106
|
+
// wrapper never breaks streaming usage. (Documented limitation.)
|
|
107
|
+
if (isStreaming(params)) {
|
|
108
|
+
return realCreate(params, requestOptions);
|
|
109
|
+
}
|
|
110
|
+
const apiPromise = realCreate(params, requestOptions);
|
|
111
|
+
return this.#instrumentNonStreaming(apiPromise, params);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap a non-streaming `APIPromise` so compliance logging fires once after the
|
|
115
|
+
* response is obtained, regardless of how the caller consumes it, while the
|
|
116
|
+
* `APIPromise`'s `.withResponse()`/`.asResponse()` helpers (and its `Promise`
|
|
117
|
+
* surface) are preserved. We override only the public consumption methods via a
|
|
118
|
+
* `Proxy`; the wrapper still passes `instanceof APIPromise`/`Promise`.
|
|
119
|
+
*/
|
|
120
|
+
#instrumentNonStreaming(apiPromise, params) {
|
|
121
|
+
// The provider response has been obtained when this resolves; log the
|
|
122
|
+
// decision exactly once and let any reporter hard-fail surface here. Reuse
|
|
123
|
+
// `withResponse()` so a single underlying parse feeds every consumption path
|
|
124
|
+
// (the SDK memoises the parse), and so `.asResponse()` can log too.
|
|
125
|
+
let logged;
|
|
126
|
+
const reportOnce = () => {
|
|
127
|
+
if (logged === undefined) {
|
|
128
|
+
logged = apiPromise.withResponse().then(async (withResponse) => {
|
|
129
|
+
await this.#logCompleted(withResponse.data, params);
|
|
130
|
+
return withResponse;
|
|
131
|
+
}, async (err) => {
|
|
132
|
+
await this.#logFailed(err, params);
|
|
133
|
+
throw err;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return logged;
|
|
137
|
+
};
|
|
138
|
+
return new Proxy(apiPromise, {
|
|
139
|
+
get: (target, prop, receiver) => {
|
|
140
|
+
switch (prop) {
|
|
141
|
+
case 'then':
|
|
142
|
+
return (onfulfilled, onrejected) => reportOnce()
|
|
143
|
+
.then((withResponse) => withResponse.data)
|
|
144
|
+
.then(onfulfilled, onrejected);
|
|
145
|
+
case 'catch':
|
|
146
|
+
return (onrejected) => reportOnce()
|
|
147
|
+
.then((withResponse) => withResponse.data)
|
|
148
|
+
.catch(onrejected);
|
|
149
|
+
case 'finally':
|
|
150
|
+
return (onfinally) => reportOnce()
|
|
151
|
+
.then((withResponse) => withResponse.data)
|
|
152
|
+
.finally(onfinally);
|
|
153
|
+
case 'withResponse':
|
|
154
|
+
return () => reportOnce();
|
|
155
|
+
case 'asResponse':
|
|
156
|
+
return () => reportOnce().then((withResponse) => withResponse.response);
|
|
157
|
+
default: {
|
|
158
|
+
const value = Reflect.get(target, prop, receiver);
|
|
159
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Log a `completed` decision. If the reporter cannot deliver and cannot buffer,
|
|
167
|
+
* it throws — and we let that surface (after the response was obtained), never
|
|
168
|
+
* silently dropping the decision.
|
|
169
|
+
*/
|
|
170
|
+
async #logCompleted(response, params) {
|
|
171
|
+
await this.#reporter.logDecision({
|
|
172
|
+
aiSystemId: '',
|
|
173
|
+
modelUsed: this.#responseModel(response, params),
|
|
174
|
+
input: params,
|
|
175
|
+
output: response,
|
|
176
|
+
status: 'completed',
|
|
177
|
+
decidedAt: new Date().toISOString(),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Log a `failed` decision for a provider error. The caller still receives the
|
|
182
|
+
* ORIGINAL provider error (rethrown by the consumption path), and the error is
|
|
183
|
+
* captured as `{ error }` only — never the raw `Error` (it may carry secrets).
|
|
184
|
+
*/
|
|
185
|
+
async #logFailed(err, params) {
|
|
186
|
+
await this.#reporter.logDecision({
|
|
187
|
+
aiSystemId: '',
|
|
188
|
+
modelUsed: this.#requestModel(params),
|
|
189
|
+
input: params,
|
|
190
|
+
output: errorOutput(err),
|
|
191
|
+
status: 'failed',
|
|
192
|
+
decidedAt: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
#responseModel(response, params) {
|
|
196
|
+
const fromResponse = response?.model;
|
|
197
|
+
if (typeof fromResponse === 'string' && fromResponse !== '')
|
|
198
|
+
return fromResponse;
|
|
199
|
+
return this.#requestModel(params);
|
|
200
|
+
}
|
|
201
|
+
#requestModel(params) {
|
|
202
|
+
const fromRequest = params?.model;
|
|
203
|
+
return typeof fromRequest === 'string' ? fromRequest : '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export default OpenAI;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import OpenAISDK from 'openai';
|
|
2
|
+
import type { ClientOptions } from 'openai';
|
|
3
|
+
import type { ReporterConfig } from '@vaxelia/ai-core';
|
|
4
|
+
/**
|
|
5
|
+
* The subset of the underlying OpenAI client surface this wrapper needs to
|
|
6
|
+
* reach directly: the nested `chat.completions.create` completion path. The full
|
|
7
|
+
* client exposes far more; the wrapper preserves the rest by proxying the real
|
|
8
|
+
* resources (see {@link OpenAI}). This type exists so tests can inject a fake
|
|
9
|
+
* without depending on the entire SDK shape.
|
|
10
|
+
*/
|
|
11
|
+
export interface FakeOpenAIClient {
|
|
12
|
+
chat: {
|
|
13
|
+
completions: {
|
|
14
|
+
create: (params: unknown, options?: unknown) => Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options accepted by the compliance-instrumented {@link OpenAI} client: every
|
|
20
|
+
* option the official SDK accepts, plus an optional `compliance` block that
|
|
21
|
+
* configures the shared {@link ComplianceReporter}. The `compliance` block is
|
|
22
|
+
* stripped before the remaining options are handed to the real SDK, so this is a
|
|
23
|
+
* true drop-in replacement.
|
|
24
|
+
*/
|
|
25
|
+
export interface OpenAIComplianceOptions extends ClientOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Compliance reporter configuration. Every field falls back to the matching
|
|
28
|
+
* environment variable (see `@vaxelia/ai-core`), so zero-config-from-env
|
|
29
|
+
* works. Omit entirely to configure the reporter purely from the environment.
|
|
30
|
+
*/
|
|
31
|
+
compliance?: ReporterConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Test-only seam: supply the underlying OpenAI client instead of letting the
|
|
34
|
+
* wrapper construct one. Never used in production; not part of the public
|
|
35
|
+
* contract. Underscore-prefixed to signal "internal".
|
|
36
|
+
*/
|
|
37
|
+
__clientFactory?: (options: ClientOptions) => unknown;
|
|
38
|
+
}
|
|
39
|
+
/** The concrete underlying-client constructor, narrowed for reuse. */
|
|
40
|
+
export declare const RealOpenAI: typeof OpenAISDK;
|
package/dist/internal.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vaxelia/ai-openai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in, compliance-instrumented wrapper around the official OpenAI SDK (openai). Logs every AI decision via @vaxelia/ai-core.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"openai": "^6.39.1",
|
|
26
|
+
"@vaxelia/ai-core": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.1.0",
|
|
30
|
+
"eslint": "^9.32.0",
|
|
31
|
+
"typescript": "^5.3.0",
|
|
32
|
+
"typescript-eslint": "^8.59.4",
|
|
33
|
+
"vitest": "^1.6.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"lint": "eslint src test --ext .ts"
|
|
43
|
+
}
|
|
44
|
+
}
|