autoval 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 +83 -0
- package/dist/anthropic.d.ts +4 -0
- package/dist/anthropic.js +59 -0
- package/dist/clickhouse.d.ts +25 -0
- package/dist/clickhouse.js +67 -0
- package/dist/google.d.ts +5 -0
- package/dist/google.js +82 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +69 -0
- package/dist/openai.d.ts +5 -0
- package/dist/openai.js +55 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# autoval
|
|
2
|
+
|
|
3
|
+
> Two lines. Any codebase. Your LLM calls become observable, evaluable, and PR-fixable.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install autoval
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import OpenAI from 'openai';
|
|
11
|
+
import { autoval } from 'autoval';
|
|
12
|
+
|
|
13
|
+
const client = new OpenAI();
|
|
14
|
+
autoval.instrument(client); // ← that's it
|
|
15
|
+
|
|
16
|
+
const res = await client.chat.completions.create({
|
|
17
|
+
model: 'gpt-4o',
|
|
18
|
+
messages: [{ role: 'user', content: prompt }],
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every call now writes a row to `autoval.llm_call_logs` in your ClickHouse. The Autoval agent reads from that table, scans for unsafe outputs, web-grounds judgments via Nimble, generates eval test cases, and opens PRs against your prompt file.
|
|
23
|
+
|
|
24
|
+
## Supported SDKs
|
|
25
|
+
|
|
26
|
+
| Provider | Detection | Method wrapped |
|
|
27
|
+
|----------|-----------|----------------|
|
|
28
|
+
| OpenAI (`openai`) | `client.chat.completions.create` | `chat.completions.create` |
|
|
29
|
+
| Anthropic (`@anthropic-ai/sdk`) | `client.messages.create` | `messages.create` |
|
|
30
|
+
| Google (`@google/generative-ai`) | `client.getGenerativeModel(...)` | `model.generateContent` |
|
|
31
|
+
|
|
32
|
+
Unknown clients pass through with a console warning — no exceptions.
|
|
33
|
+
|
|
34
|
+
## Config
|
|
35
|
+
|
|
36
|
+
By default the package reads ClickHouse credentials from env:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
CLICKHOUSE_URL=https://xxxx.clickhouse.cloud:8443
|
|
40
|
+
CLICKHOUSE_USER=default
|
|
41
|
+
CLICKHOUSE_PASSWORD=...
|
|
42
|
+
CLICKHOUSE_DATABASE=autoval
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or call `autoval.configure({ ... })` once at startup:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
autoval.configure({
|
|
49
|
+
clickhouseUrl: process.env.CH_URL,
|
|
50
|
+
clickhouseUser: 'default',
|
|
51
|
+
clickhousePassword: process.env.CH_PASSWORD,
|
|
52
|
+
clickhouseDatabase: 'autoval',
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Schema
|
|
57
|
+
|
|
58
|
+
Each LLM call writes a row with:
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
CREATE TABLE autoval.llm_call_logs (
|
|
62
|
+
id String,
|
|
63
|
+
input String,
|
|
64
|
+
output String,
|
|
65
|
+
model String,
|
|
66
|
+
latency_ms UInt32,
|
|
67
|
+
scored UInt8 DEFAULT 0,
|
|
68
|
+
timestamp DateTime64(3) DEFAULT now()
|
|
69
|
+
) ENGINE = MergeTree()
|
|
70
|
+
ORDER BY timestamp;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`scored = 0` rows are what the Autoval scanner picks up. After the agent reviews a row it sets `scored = 1`.
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
|
|
77
|
+
- Insert is `await`-ed inside the wrapper. On serverless (Vercel, AWS Lambda) unawaited promises get cancelled, so we await to guarantee the row lands. Adds ~150ms per call.
|
|
78
|
+
- Insert errors are caught and logged — never thrown — so a ClickHouse hiccup can't break your user-facing chat.
|
|
79
|
+
- The wrapper mutates the client in-place. If you need an un-instrumented client, instantiate a fresh one.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentAnthropic = instrumentAnthropic;
|
|
4
|
+
const clickhouse_1 = require("./clickhouse");
|
|
5
|
+
function isAnthropicClient(client) {
|
|
6
|
+
if (typeof client !== 'object' || client === null)
|
|
7
|
+
return false;
|
|
8
|
+
const messages = client.messages;
|
|
9
|
+
if (typeof messages !== 'object' || messages === null)
|
|
10
|
+
return false;
|
|
11
|
+
return typeof messages.create === 'function';
|
|
12
|
+
}
|
|
13
|
+
function summarizeInput(params) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (params.system)
|
|
16
|
+
parts.push(`system: ${params.system}`);
|
|
17
|
+
if (Array.isArray(params.messages)) {
|
|
18
|
+
for (const m of params.messages) {
|
|
19
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
20
|
+
parts.push(`${m.role}: ${c}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return parts.join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
function extractOutput(res) {
|
|
26
|
+
return (res.content ?? [])
|
|
27
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
28
|
+
.map((b) => b.text ?? '')
|
|
29
|
+
.join('');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wraps `client.messages.create` so every call logs input+output to ClickHouse.
|
|
33
|
+
*/
|
|
34
|
+
function instrumentAnthropic(client) {
|
|
35
|
+
if (!isAnthropicClient(client))
|
|
36
|
+
return client;
|
|
37
|
+
const messages = client.messages;
|
|
38
|
+
const original = messages.create.bind(messages);
|
|
39
|
+
messages.create = async (params, ...rest) => {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const p = (params ?? {});
|
|
42
|
+
const result = await original(params, ...rest);
|
|
43
|
+
const latencyMs = Date.now() - start;
|
|
44
|
+
try {
|
|
45
|
+
const res = result;
|
|
46
|
+
await (0, clickhouse_1.logLlmCall)({
|
|
47
|
+
input: summarizeInput(p),
|
|
48
|
+
output: extractOutput(res),
|
|
49
|
+
model: p.model ?? res.model ?? 'unknown',
|
|
50
|
+
latency_ms: latencyMs,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('[autoval] Anthropic log wrapper failed:', err);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AutovalConfig {
|
|
2
|
+
clickhouseUrl?: string;
|
|
3
|
+
clickhouseUser?: string;
|
|
4
|
+
clickhousePassword?: string;
|
|
5
|
+
clickhouseDatabase?: string;
|
|
6
|
+
/** Override the table name. Defaults to `autoval.llm_call_logs`. */
|
|
7
|
+
table?: string;
|
|
8
|
+
/** Disable logging entirely (useful in tests). */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function configure(config: AutovalConfig): void;
|
|
12
|
+
export declare function newCallId(prefix?: string): string;
|
|
13
|
+
export interface LlmCallLog {
|
|
14
|
+
id?: string;
|
|
15
|
+
input: string;
|
|
16
|
+
output: string;
|
|
17
|
+
model: string;
|
|
18
|
+
latency_ms: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Insert a single LLM-call row into ClickHouse. Awaitable — callers
|
|
22
|
+
* decide whether to await (recommended on serverless) or fire-and-forget.
|
|
23
|
+
* Errors are caught and logged; never throws.
|
|
24
|
+
*/
|
|
25
|
+
export declare function logLlmCall(entry: LlmCallLog): Promise<string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configure = configure;
|
|
4
|
+
exports.newCallId = newCallId;
|
|
5
|
+
exports.logLlmCall = logLlmCall;
|
|
6
|
+
const client_1 = require("@clickhouse/client");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
let _client = null;
|
|
9
|
+
let _disabled = false;
|
|
10
|
+
let _config = {
|
|
11
|
+
table: 'autoval.llm_call_logs',
|
|
12
|
+
};
|
|
13
|
+
function configure(config) {
|
|
14
|
+
_config = { ..._config, ...config };
|
|
15
|
+
_disabled = config.disabled ?? false;
|
|
16
|
+
_client = null; // force re-init on next call
|
|
17
|
+
}
|
|
18
|
+
function getClient() {
|
|
19
|
+
if (_disabled)
|
|
20
|
+
return null;
|
|
21
|
+
if (_client)
|
|
22
|
+
return _client;
|
|
23
|
+
const url = _config.clickhouseUrl ?? process.env.CLICKHOUSE_URL;
|
|
24
|
+
const username = _config.clickhouseUser ?? process.env.CLICKHOUSE_USER;
|
|
25
|
+
const password = _config.clickhousePassword ?? process.env.CLICKHOUSE_PASSWORD;
|
|
26
|
+
const database = _config.clickhouseDatabase ?? process.env.CLICKHOUSE_DATABASE;
|
|
27
|
+
if (!url || !username || !password || !database) {
|
|
28
|
+
console.warn('[autoval] ClickHouse env not configured — logging disabled. Set CLICKHOUSE_URL/USER/PASSWORD/DATABASE or call autoval.configure({...}).');
|
|
29
|
+
_disabled = true;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
_client = (0, client_1.createClient)({ url, username, password, database });
|
|
33
|
+
return _client;
|
|
34
|
+
}
|
|
35
|
+
function newCallId(prefix = 'ev') {
|
|
36
|
+
return `${prefix}_${(0, crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Insert a single LLM-call row into ClickHouse. Awaitable — callers
|
|
40
|
+
* decide whether to await (recommended on serverless) or fire-and-forget.
|
|
41
|
+
* Errors are caught and logged; never throws.
|
|
42
|
+
*/
|
|
43
|
+
async function logLlmCall(entry) {
|
|
44
|
+
const id = entry.id ?? newCallId('ev');
|
|
45
|
+
const client = getClient();
|
|
46
|
+
if (!client)
|
|
47
|
+
return id;
|
|
48
|
+
try {
|
|
49
|
+
await client.insert({
|
|
50
|
+
table: _config.table,
|
|
51
|
+
values: [
|
|
52
|
+
{
|
|
53
|
+
id,
|
|
54
|
+
input: entry.input,
|
|
55
|
+
output: entry.output,
|
|
56
|
+
model: entry.model,
|
|
57
|
+
latency_ms: entry.latency_ms,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
format: 'JSONEachRow',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error('[autoval] log insert failed:', err);
|
|
65
|
+
}
|
|
66
|
+
return id;
|
|
67
|
+
}
|
package/dist/google.d.ts
ADDED
package/dist/google.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentGoogle = instrumentGoogle;
|
|
4
|
+
const clickhouse_1 = require("./clickhouse");
|
|
5
|
+
function isGoogleClient(client) {
|
|
6
|
+
if (typeof client !== 'object' || client === null)
|
|
7
|
+
return false;
|
|
8
|
+
return typeof client.getGenerativeModel === 'function';
|
|
9
|
+
}
|
|
10
|
+
function summarizeInput(req) {
|
|
11
|
+
if (typeof req === 'string')
|
|
12
|
+
return req;
|
|
13
|
+
const r = (req ?? {});
|
|
14
|
+
if (typeof r.contents === 'string')
|
|
15
|
+
return r.contents;
|
|
16
|
+
if (Array.isArray(r.contents)) {
|
|
17
|
+
return r.contents
|
|
18
|
+
.map((c) => {
|
|
19
|
+
if (typeof c === 'string')
|
|
20
|
+
return c;
|
|
21
|
+
if (c && typeof c === 'object' && 'parts' in c) {
|
|
22
|
+
const parts = c.parts;
|
|
23
|
+
return parts.map((p) => (p && typeof p === 'object' && 'text' in p ? p.text : '')).join('');
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
})
|
|
27
|
+
.join('\n');
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(req).slice(0, 4000);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function extractOutput(res) {
|
|
37
|
+
const out = res.response;
|
|
38
|
+
if (!out)
|
|
39
|
+
return '';
|
|
40
|
+
if (typeof out.text === 'function') {
|
|
41
|
+
try {
|
|
42
|
+
return out.text();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// fall through
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const parts = out.candidates?.[0]?.content?.parts ?? [];
|
|
49
|
+
return parts.map((p) => p.text ?? '').join('');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wraps `client.getGenerativeModel(...)` so every model returned has its
|
|
53
|
+
* `generateContent` method wrapped to log to ClickHouse.
|
|
54
|
+
*/
|
|
55
|
+
function instrumentGoogle(client) {
|
|
56
|
+
if (!isGoogleClient(client))
|
|
57
|
+
return client;
|
|
58
|
+
const originalGetModel = client.getGenerativeModel.bind(client);
|
|
59
|
+
client.getGenerativeModel = (params) => {
|
|
60
|
+
const model = originalGetModel(params);
|
|
61
|
+
const originalGen = model.generateContent.bind(model);
|
|
62
|
+
model.generateContent = async (req, ...rest) => {
|
|
63
|
+
const start = Date.now();
|
|
64
|
+
const result = await originalGen(req, ...rest);
|
|
65
|
+
const latencyMs = Date.now() - start;
|
|
66
|
+
try {
|
|
67
|
+
await (0, clickhouse_1.logLlmCall)({
|
|
68
|
+
input: summarizeInput(req),
|
|
69
|
+
output: extractOutput(result),
|
|
70
|
+
model: params.model,
|
|
71
|
+
latency_ms: latencyMs,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error('[autoval] Google log wrapper failed:', err);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
return model;
|
|
80
|
+
};
|
|
81
|
+
return client;
|
|
82
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { configure, logLlmCall, newCallId } from './clickhouse';
|
|
2
|
+
export { configure, logLlmCall, newCallId };
|
|
3
|
+
export type { AutovalConfig, LlmCallLog } from './clickhouse';
|
|
4
|
+
/**
|
|
5
|
+
* Instrument an LLM client so every call logs to ClickHouse for Autoval to
|
|
6
|
+
* scan, eval, and PR-fix.
|
|
7
|
+
*
|
|
8
|
+
* import OpenAI from 'openai';
|
|
9
|
+
* import { autoval } from 'autoval';
|
|
10
|
+
*
|
|
11
|
+
* const client = new OpenAI();
|
|
12
|
+
* autoval.instrument(client); // ← that's it
|
|
13
|
+
*
|
|
14
|
+
* // ... your existing calls log automatically:
|
|
15
|
+
* await client.chat.completions.create({ model: 'gpt-4o', messages: [...] });
|
|
16
|
+
*
|
|
17
|
+
* Auto-detects OpenAI, Anthropic (@anthropic-ai/sdk), and Google
|
|
18
|
+
* (@google/generative-ai). Mutates the passed client in-place and returns
|
|
19
|
+
* it for chaining. Pass-through (no-op) for unknown clients with a warning.
|
|
20
|
+
*
|
|
21
|
+
* Configure once at startup if you don't use env vars:
|
|
22
|
+
*
|
|
23
|
+
* autoval.configure({
|
|
24
|
+
* clickhouseUrl: '...',
|
|
25
|
+
* clickhouseUser: 'default',
|
|
26
|
+
* clickhousePassword: '...',
|
|
27
|
+
* clickhouseDatabase: 'autoval',
|
|
28
|
+
* })
|
|
29
|
+
*/
|
|
30
|
+
declare function instrument<T>(client: T): T;
|
|
31
|
+
/** Public surface. Use as `autoval.instrument(client)` or named import. */
|
|
32
|
+
export declare const autoval: {
|
|
33
|
+
instrument: typeof instrument;
|
|
34
|
+
configure: typeof configure;
|
|
35
|
+
newCallId: typeof newCallId;
|
|
36
|
+
};
|
|
37
|
+
export { instrument };
|
|
38
|
+
export default autoval;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.autoval = exports.newCallId = exports.logLlmCall = exports.configure = void 0;
|
|
4
|
+
exports.instrument = instrument;
|
|
5
|
+
const openai_1 = require("./openai");
|
|
6
|
+
const google_1 = require("./google");
|
|
7
|
+
const anthropic_1 = require("./anthropic");
|
|
8
|
+
const clickhouse_1 = require("./clickhouse");
|
|
9
|
+
Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return clickhouse_1.configure; } });
|
|
10
|
+
Object.defineProperty(exports, "logLlmCall", { enumerable: true, get: function () { return clickhouse_1.logLlmCall; } });
|
|
11
|
+
Object.defineProperty(exports, "newCallId", { enumerable: true, get: function () { return clickhouse_1.newCallId; } });
|
|
12
|
+
/**
|
|
13
|
+
* Instrument an LLM client so every call logs to ClickHouse for Autoval to
|
|
14
|
+
* scan, eval, and PR-fix.
|
|
15
|
+
*
|
|
16
|
+
* import OpenAI from 'openai';
|
|
17
|
+
* import { autoval } from 'autoval';
|
|
18
|
+
*
|
|
19
|
+
* const client = new OpenAI();
|
|
20
|
+
* autoval.instrument(client); // ← that's it
|
|
21
|
+
*
|
|
22
|
+
* // ... your existing calls log automatically:
|
|
23
|
+
* await client.chat.completions.create({ model: 'gpt-4o', messages: [...] });
|
|
24
|
+
*
|
|
25
|
+
* Auto-detects OpenAI, Anthropic (@anthropic-ai/sdk), and Google
|
|
26
|
+
* (@google/generative-ai). Mutates the passed client in-place and returns
|
|
27
|
+
* it for chaining. Pass-through (no-op) for unknown clients with a warning.
|
|
28
|
+
*
|
|
29
|
+
* Configure once at startup if you don't use env vars:
|
|
30
|
+
*
|
|
31
|
+
* autoval.configure({
|
|
32
|
+
* clickhouseUrl: '...',
|
|
33
|
+
* clickhouseUser: 'default',
|
|
34
|
+
* clickhousePassword: '...',
|
|
35
|
+
* clickhouseDatabase: 'autoval',
|
|
36
|
+
* })
|
|
37
|
+
*/
|
|
38
|
+
function instrument(client) {
|
|
39
|
+
if (!client || typeof client !== 'object')
|
|
40
|
+
return client;
|
|
41
|
+
const c = client;
|
|
42
|
+
// Sniff SDK shape — order matters: OpenAI's `messages` lives on a different
|
|
43
|
+
// path than Anthropic's, so we check signatures, not just key presence.
|
|
44
|
+
const hasOpenAIShape = 'chat' in c &&
|
|
45
|
+
typeof c.chat === 'object' &&
|
|
46
|
+
c.chat !== null &&
|
|
47
|
+
'completions' in c.chat;
|
|
48
|
+
if (hasOpenAIShape)
|
|
49
|
+
return (0, openai_1.instrumentOpenAI)(client);
|
|
50
|
+
const hasGoogleShape = 'getGenerativeModel' in c && typeof c.getGenerativeModel === 'function';
|
|
51
|
+
if (hasGoogleShape)
|
|
52
|
+
return (0, google_1.instrumentGoogle)(client);
|
|
53
|
+
const hasAnthropicShape = 'messages' in c &&
|
|
54
|
+
typeof c.messages === 'object' &&
|
|
55
|
+
c.messages !== null &&
|
|
56
|
+
'create' in c.messages;
|
|
57
|
+
if (hasAnthropicShape)
|
|
58
|
+
return (0, anthropic_1.instrumentAnthropic)(client);
|
|
59
|
+
console.warn('[autoval] Unrecognized client shape — pass-through (not instrumented). ' +
|
|
60
|
+
'Supported: OpenAI, Anthropic, Google GenerativeAI.');
|
|
61
|
+
return client;
|
|
62
|
+
}
|
|
63
|
+
/** Public surface. Use as `autoval.instrument(client)` or named import. */
|
|
64
|
+
exports.autoval = {
|
|
65
|
+
instrument,
|
|
66
|
+
configure: clickhouse_1.configure,
|
|
67
|
+
newCallId: clickhouse_1.newCallId,
|
|
68
|
+
};
|
|
69
|
+
exports.default = exports.autoval;
|
package/dist/openai.d.ts
ADDED
package/dist/openai.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentOpenAI = instrumentOpenAI;
|
|
4
|
+
const clickhouse_1 = require("./clickhouse");
|
|
5
|
+
function isOpenAIClient(client) {
|
|
6
|
+
if (typeof client !== 'object' || client === null)
|
|
7
|
+
return false;
|
|
8
|
+
const chat = client.chat;
|
|
9
|
+
if (typeof chat !== 'object' || chat === null)
|
|
10
|
+
return false;
|
|
11
|
+
const completions = chat.completions;
|
|
12
|
+
if (typeof completions !== 'object' || completions === null)
|
|
13
|
+
return false;
|
|
14
|
+
return typeof completions.create === 'function';
|
|
15
|
+
}
|
|
16
|
+
function summarizeInput(params) {
|
|
17
|
+
if (!params.messages || !Array.isArray(params.messages))
|
|
18
|
+
return '';
|
|
19
|
+
return params.messages
|
|
20
|
+
.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`)
|
|
21
|
+
.join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
function extractOutput(res) {
|
|
24
|
+
return res.choices?.[0]?.message?.content ?? '';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wraps `client.chat.completions.create` so every call logs input+output
|
|
28
|
+
* to ClickHouse. Mutates the client in-place and returns it for chaining.
|
|
29
|
+
*/
|
|
30
|
+
function instrumentOpenAI(client) {
|
|
31
|
+
if (!isOpenAIClient(client))
|
|
32
|
+
return client;
|
|
33
|
+
const completions = client.chat.completions;
|
|
34
|
+
const original = completions.create.bind(completions);
|
|
35
|
+
completions.create = async (params, ...rest) => {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
const p = (params ?? {});
|
|
38
|
+
const result = await original(params, ...rest);
|
|
39
|
+
const latencyMs = Date.now() - start;
|
|
40
|
+
try {
|
|
41
|
+
const res = result;
|
|
42
|
+
await (0, clickhouse_1.logLlmCall)({
|
|
43
|
+
input: summarizeInput(p),
|
|
44
|
+
output: extractOutput(res),
|
|
45
|
+
model: p.model ?? res.model ?? 'unknown',
|
|
46
|
+
latency_ms: latencyMs,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('[autoval] OpenAI log wrapper failed:', err);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
return client;
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autoval",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Two lines, any codebase. Instrument your LLM client so Autoval can scan, eval, and PR-fix your prompts.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"llm",
|
|
17
|
+
"eval",
|
|
18
|
+
"observability",
|
|
19
|
+
"openai",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"gemini",
|
|
22
|
+
"clickhouse",
|
|
23
|
+
"autoval"
|
|
24
|
+
],
|
|
25
|
+
"author": "Autoval team",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@clickhouse/client": "^1.18.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@anthropic-ai/sdk": ">=0.20.0",
|
|
32
|
+
"@google/generative-ai": ">=0.20.0",
|
|
33
|
+
"openai": ">=4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@anthropic-ai/sdk": { "optional": true },
|
|
37
|
+
"@google/generative-ai": { "optional": true },
|
|
38
|
+
"openai": { "optional": true }
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|