llm-fns 1.0.22 → 1.0.24
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/dist/IterativeRefiner.d.ts +24 -0
- package/dist/IterativeRefiner.js +61 -0
- package/dist/completionToAssistantMessage.d.ts +10 -0
- package/dist/completionToAssistantMessage.js +77 -0
- package/dist/createAiLoggingFetcher.d.ts +3 -0
- package/dist/createAiLoggingFetcher.js +79 -0
- package/dist/createCachedFetcher.js +5 -13
- package/dist/createCandidateSelector.d.ts +41 -0
- package/dist/createCandidateSelector.js +52 -0
- package/dist/createConversation.d.ts +41 -0
- package/dist/createConversation.js +134 -0
- package/dist/createDnsFetcher.d.ts +12 -0
- package/dist/createDnsFetcher.js +49 -0
- package/dist/createGoogleDnsFetcher.d.ts +1 -0
- package/dist/createGoogleDnsFetcher.js +1 -0
- package/dist/createIterativeRefiner.d.ts +39 -0
- package/dist/createIterativeRefiner.js +51 -0
- package/dist/createJsonSchemaLlmClient.d.ts +1 -0
- package/dist/createJsonSchemaLlmClient.js +15 -19
- package/dist/createLlmClient.d.ts +10 -3
- package/dist/createLlmClient.js +35 -178
- package/dist/createLlmClient.spec.js +37 -39
- package/dist/createLlmConversation.d.ts +1 -0
- package/dist/createLlmConversation.js +1 -0
- package/dist/createLlmRetryClient.d.ts +11 -1
- package/dist/createLlmRetryClient.js +114 -70
- package/dist/createMockOpenAi.d.ts +3 -0
- package/dist/createMockOpenAi.js +60 -0
- package/dist/createZodLlmClient.js +5 -42
- package/dist/createZodLlmClient.spec.js +60 -62
- package/dist/extractBinary.d.ts +21 -0
- package/dist/extractBinary.js +76 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +16 -23
- package/dist/llmFactory.d.ts +23 -9
- package/dist/llmFactory.js +25 -15
- package/dist/retryUtils.d.ts +2 -1
- package/dist/retryUtils.js +6 -7
- package/dist/util.d.ts +18 -0
- package/dist/util.js +200 -0
- package/package.json +8 -6
- package/readme.md +117 -43
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RefinerOptions {
|
|
2
|
+
maxRetries: number;
|
|
3
|
+
}
|
|
4
|
+
export interface EvaluationResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
feedback?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IterationHistory<TConfig> {
|
|
9
|
+
config?: TConfig;
|
|
10
|
+
feedback?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare abstract class IterativeRefiner<TInput, TConfig, TOutput> {
|
|
14
|
+
protected options: RefinerOptions;
|
|
15
|
+
constructor(options: RefinerOptions);
|
|
16
|
+
protected abstract generate(input: TInput, history: IterationHistory<TConfig>[]): Promise<TConfig>;
|
|
17
|
+
protected abstract execute(config: TConfig, input: TInput): Promise<TOutput>;
|
|
18
|
+
protected abstract evaluate(input: TInput, config: TConfig, output: TOutput): Promise<EvaluationResult>;
|
|
19
|
+
run(input: TInput): Promise<{
|
|
20
|
+
config: TConfig;
|
|
21
|
+
output?: TOutput;
|
|
22
|
+
iterations: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IterativeRefiner = void 0;
|
|
4
|
+
class IterativeRefiner {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
async run(input) {
|
|
10
|
+
const history = [];
|
|
11
|
+
let currentConfig;
|
|
12
|
+
let lastOutput;
|
|
13
|
+
for (let i = 0; i < this.options.maxRetries; i++) {
|
|
14
|
+
console.log(`[IterativeRefiner] Iteration ${i + 1}/${this.options.maxRetries}`);
|
|
15
|
+
// 1. Generate
|
|
16
|
+
try {
|
|
17
|
+
currentConfig = await this.generate(input, history);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.error(`[IterativeRefiner] Generation failed: ${e.message}`);
|
|
21
|
+
// If generation fails, we record it and try again
|
|
22
|
+
history.push({
|
|
23
|
+
error: e.message,
|
|
24
|
+
feedback: `Previous generation failed with error: ${e.message}. Please fix the configuration structure.`
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// 2. Execute
|
|
29
|
+
try {
|
|
30
|
+
lastOutput = await this.execute(currentConfig, input);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error(`[IterativeRefiner] Execution failed: ${e.message}`);
|
|
34
|
+
// Execution errors are valid feedback for the LLM
|
|
35
|
+
history.push({
|
|
36
|
+
config: currentConfig,
|
|
37
|
+
error: e.message,
|
|
38
|
+
feedback: `The configuration caused an execution error: ${e.message}. Please fix the configuration to avoid this error.`
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// 3. Evaluate
|
|
43
|
+
const evaluation = await this.evaluate(input, currentConfig, lastOutput);
|
|
44
|
+
if (evaluation.success) {
|
|
45
|
+
console.log(`[IterativeRefiner] Success on iteration ${i + 1}`);
|
|
46
|
+
return { config: currentConfig, output: lastOutput, iterations: i + 1 };
|
|
47
|
+
}
|
|
48
|
+
console.log(`[IterativeRefiner] Feedback: ${evaluation.feedback}`);
|
|
49
|
+
history.push({
|
|
50
|
+
config: currentConfig,
|
|
51
|
+
feedback: evaluation.feedback
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!currentConfig) {
|
|
55
|
+
throw new Error("Failed to generate any valid configuration.");
|
|
56
|
+
}
|
|
57
|
+
console.warn(`[IterativeRefiner] Max retries reached. Returning last result.`);
|
|
58
|
+
return { config: currentConfig, output: lastOutput, iterations: this.options.maxRetries };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.IterativeRefiner = IterativeRefiner;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
/**
|
|
3
|
+
* Converts an OpenAI ChatCompletion object into an assistant message parameter.
|
|
4
|
+
* Handles text, audio, refusals, tool calls, and custom image attachments.
|
|
5
|
+
*
|
|
6
|
+
* @param completion The ChatCompletion object.
|
|
7
|
+
* @returns An OpenAI ChatCompletionMessageParam with role 'assistant'.
|
|
8
|
+
* @throws Error if the input is not a valid ChatCompletion object.
|
|
9
|
+
*/
|
|
10
|
+
export declare function completionToMessage(completion: OpenAI.Chat.Completions.ChatCompletion): OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an OpenAI ChatCompletion object into an assistant message parameter.
|
|
3
|
+
* Handles text, audio, refusals, tool calls, and custom image attachments.
|
|
4
|
+
*
|
|
5
|
+
* @param completion The ChatCompletion object.
|
|
6
|
+
* @returns An OpenAI ChatCompletionMessageParam with role 'assistant'.
|
|
7
|
+
* @throws Error if the input is not a valid ChatCompletion object.
|
|
8
|
+
*/
|
|
9
|
+
export function completionToMessage(completion) {
|
|
10
|
+
if (!completion.choices || completion.choices.length === 0) {
|
|
11
|
+
throw new Error("Invalid completion object: No choices found.");
|
|
12
|
+
}
|
|
13
|
+
const message = completion.choices[0].message;
|
|
14
|
+
// Base message structure
|
|
15
|
+
const messageParam = {
|
|
16
|
+
role: 'assistant',
|
|
17
|
+
};
|
|
18
|
+
// Preserve standard fields
|
|
19
|
+
if (message.content !== undefined)
|
|
20
|
+
messageParam.content = message.content;
|
|
21
|
+
if (message.refusal !== undefined)
|
|
22
|
+
messageParam.refusal = message.refusal;
|
|
23
|
+
if (message.tool_calls !== undefined)
|
|
24
|
+
messageParam.tool_calls = message.tool_calls;
|
|
25
|
+
if (message.function_call !== undefined)
|
|
26
|
+
messageParam.function_call = message.function_call;
|
|
27
|
+
// Handle Content Normalization for Extensions (Images/Audio)
|
|
28
|
+
// If the provider returned images or audio in top-level fields (like OpenRouter or OpenAI Audio),
|
|
29
|
+
// we merge them into the content parts array to ensure they are preserved in history.
|
|
30
|
+
const images = message.images;
|
|
31
|
+
const audio = message.audio;
|
|
32
|
+
if ((images && Array.isArray(images) && images.length > 0) || (audio && audio.data)) {
|
|
33
|
+
let parts = [];
|
|
34
|
+
// Convert existing content to parts if it's a string
|
|
35
|
+
if (typeof messageParam.content === 'string') {
|
|
36
|
+
parts.push({ type: 'text', text: messageParam.content });
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(messageParam.content)) {
|
|
39
|
+
parts = [...messageParam.content];
|
|
40
|
+
}
|
|
41
|
+
// Add images from extension field if not already present in parts
|
|
42
|
+
if (images && Array.isArray(images)) {
|
|
43
|
+
for (const img of images) {
|
|
44
|
+
const url = img.image_url?.url || img.url;
|
|
45
|
+
if (url && !parts.some(p => p.type === 'image_url' && p.image_url?.url === url)) {
|
|
46
|
+
parts.push({
|
|
47
|
+
type: 'image_url',
|
|
48
|
+
image_url: { url }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Add audio from extension field if not already present in parts
|
|
54
|
+
if (audio && audio.data) {
|
|
55
|
+
if (!parts.some(p => p.type === 'input_audio' && p.input_audio?.data === audio.data)) {
|
|
56
|
+
parts.push({
|
|
57
|
+
type: 'input_audio',
|
|
58
|
+
input_audio: {
|
|
59
|
+
data: audio.data,
|
|
60
|
+
format: audio.format || undefined
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (parts.length > 0) {
|
|
66
|
+
// If we only have one text part, we can keep it as a string for simplicity,
|
|
67
|
+
// but if we have media, we must use the array format.
|
|
68
|
+
if (parts.length === 1 && parts[0].type === 'text') {
|
|
69
|
+
messageParam.content = parts[0].text;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
messageParam.content = parts;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return messageParam;
|
|
77
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function isAiRequest(url: string | URL | Request, init?: RequestInit): boolean;
|
|
2
|
+
export declare function handleAiRequest(url: RequestInfo | URL, init: RequestInit | undefined, originalFetch: (url: RequestInfo | URL, init?: RequestInit) => Promise<Response>): Promise<Response>;
|
|
3
|
+
export declare function createAiLoggingFetcher(fetcher?: (url: RequestInfo | URL, init?: RequestInit) => Promise<Response>): (url: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getPromptSummary } from "./util.js";
|
|
2
|
+
export function isAiRequest(url, init) {
|
|
3
|
+
const urlString = typeof url === 'string' ? url : url.toString();
|
|
4
|
+
// Check for chat completions endpoint pattern
|
|
5
|
+
if (urlString.includes('/chat/completions') ||
|
|
6
|
+
urlString.includes('/v1/completions') ||
|
|
7
|
+
urlString.includes('/v1/images/generations')) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
// Check if body looks like an AI request
|
|
11
|
+
if (init?.body && typeof init.body === 'string') {
|
|
12
|
+
try {
|
|
13
|
+
if (init.body.trim().startsWith('{')) {
|
|
14
|
+
const body = JSON.parse(init.body);
|
|
15
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (body.model && (body.prompt || body.messages)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Not JSON, not an AI request
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
export async function handleAiRequest(url, init, originalFetch) {
|
|
30
|
+
let requestModel = 'unknown';
|
|
31
|
+
// Log Request
|
|
32
|
+
try {
|
|
33
|
+
if (init?.body && typeof init.body === 'string') {
|
|
34
|
+
const body = JSON.parse(init.body);
|
|
35
|
+
if (body.model) {
|
|
36
|
+
requestModel = body.model;
|
|
37
|
+
}
|
|
38
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
39
|
+
console.log(`[LLM] [${requestModel}] Executing: ${getPromptSummary(body.messages)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Ignore logging errors
|
|
45
|
+
}
|
|
46
|
+
// Execute
|
|
47
|
+
const response = await originalFetch(url, init);
|
|
48
|
+
// Log Response
|
|
49
|
+
const clone = response.clone();
|
|
50
|
+
try {
|
|
51
|
+
const contentType = clone.headers.get('content-type');
|
|
52
|
+
if (contentType && contentType.includes('application/json')) {
|
|
53
|
+
const data = await clone.json();
|
|
54
|
+
const responseModel = data.model || requestModel;
|
|
55
|
+
if (data.choices && data.choices[0]?.message?.content) {
|
|
56
|
+
console.log(`[LLM] [${responseModel}] DONE ${response.status}: ${getPromptSummary([{ role: 'assistant', content: data.choices[0].message.content }])}`);
|
|
57
|
+
}
|
|
58
|
+
else if (data.error) {
|
|
59
|
+
console.error(`[LLM] [${responseModel}] ERROR ${response.status}:`, data.error);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(`[LLM] [${responseModel}] DONE ${response.status}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore logging errors
|
|
68
|
+
}
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
export function createAiLoggingFetcher(fetcher) {
|
|
72
|
+
const originalFetch = fetcher || globalThis.fetch;
|
|
73
|
+
return async (url, init) => {
|
|
74
|
+
if (isAiRequest(url, init)) {
|
|
75
|
+
return handleAiRequest(url, init, originalFetch);
|
|
76
|
+
}
|
|
77
|
+
return originalFetch(url, init);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CachedResponse = void 0;
|
|
7
|
-
exports.createCachedFetcher = createCachedFetcher;
|
|
8
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
-
class CachedResponse extends Response {
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export class CachedResponse extends Response {
|
|
10
3
|
#finalUrl;
|
|
11
4
|
constructor(body, init, finalUrl) {
|
|
12
5
|
super(body, init);
|
|
@@ -16,7 +9,6 @@ class CachedResponse extends Response {
|
|
|
16
9
|
return this.#finalUrl;
|
|
17
10
|
}
|
|
18
11
|
}
|
|
19
|
-
exports.CachedResponse = CachedResponse;
|
|
20
12
|
/**
|
|
21
13
|
* Creates a deterministic hash of headers for cache key generation.
|
|
22
14
|
* Headers are sorted alphabetically to ensure consistency.
|
|
@@ -41,14 +33,14 @@ function hashHeaders(headers) {
|
|
|
41
33
|
const headerString = headerEntries
|
|
42
34
|
.map(([key, value]) => `${key}:${value}`)
|
|
43
35
|
.join('|');
|
|
44
|
-
return
|
|
36
|
+
return crypto.createHash('md5').update(headerString).digest('hex');
|
|
45
37
|
}
|
|
46
38
|
/**
|
|
47
39
|
* Factory function that creates a `fetch` replacement with a caching layer.
|
|
48
40
|
* @param deps - Dependencies including the cache instance, prefix, TTL, and timeout.
|
|
49
41
|
* @returns A function with the same signature as native `fetch`.
|
|
50
42
|
*/
|
|
51
|
-
function createCachedFetcher(deps) {
|
|
43
|
+
export function createCachedFetcher(deps) {
|
|
52
44
|
const { cache, prefix = 'http-cache', ttl, timeout, userAgent, fetch: customFetch, shouldCache } = deps;
|
|
53
45
|
const fetchImpl = customFetch ?? fetch;
|
|
54
46
|
const fetchWithTimeout = async (url, options) => {
|
|
@@ -121,7 +113,7 @@ function createCachedFetcher(deps) {
|
|
|
121
113
|
bodyStr = 'unserializable';
|
|
122
114
|
}
|
|
123
115
|
}
|
|
124
|
-
const bodyHash =
|
|
116
|
+
const bodyHash = crypto.createHash('md5').update(bodyStr).digest('hex');
|
|
125
117
|
cacheKey += `:body:${bodyHash}`;
|
|
126
118
|
}
|
|
127
119
|
// Hash all request headers into cache key
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SelectionResult {
|
|
2
|
+
bestCandidateIndex: number;
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CandidateResult<TCandidate> {
|
|
6
|
+
candidate: TCandidate;
|
|
7
|
+
originalIndex: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CreateCandidateSelectorParams<TInput, TCandidate> {
|
|
10
|
+
/**
|
|
11
|
+
* Number of candidates to generate.
|
|
12
|
+
*/
|
|
13
|
+
candidateCount: number;
|
|
14
|
+
/**
|
|
15
|
+
* Function to generate a single candidate.
|
|
16
|
+
* @param input The input data.
|
|
17
|
+
* @param index The index of the candidate being generated (0 to candidateCount-1).
|
|
18
|
+
* @param salt A unique salt string for this candidate.
|
|
19
|
+
*/
|
|
20
|
+
generate: (input: TInput, index: number, salt: string) => Promise<TCandidate>;
|
|
21
|
+
/**
|
|
22
|
+
* Function to select the best candidate.
|
|
23
|
+
* @param input The original input data.
|
|
24
|
+
* @param candidates The list of successfully generated candidates.
|
|
25
|
+
*/
|
|
26
|
+
judge: (input: TInput, candidates: TCandidate[]) => Promise<SelectionResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Optional callback for when a candidate generation fails.
|
|
29
|
+
*/
|
|
30
|
+
onCandidateError?: (error: any, index: number) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createCandidateSelector<TInput, TCandidate>(params: CreateCandidateSelectorParams<TInput, TCandidate>): {
|
|
33
|
+
run: (input: TInput, baseSalt?: string | number) => Promise<{
|
|
34
|
+
winner: TCandidate;
|
|
35
|
+
winnerIndex: number;
|
|
36
|
+
candidates: TCandidate[];
|
|
37
|
+
reason: string;
|
|
38
|
+
skippedJudge: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
export type CandidateSelector = ReturnType<typeof createCandidateSelector>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export function createCandidateSelector(params) {
|
|
3
|
+
const { candidateCount, generate, judge, onCandidateError } = params;
|
|
4
|
+
async function run(input, baseSalt = crypto.randomUUID()) {
|
|
5
|
+
// 1. Generate Candidates in Parallel
|
|
6
|
+
const promises = [];
|
|
7
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
8
|
+
const salt = `${baseSalt}:${i}`;
|
|
9
|
+
promises.push(generate(input, i, salt)
|
|
10
|
+
.then(candidate => ({ candidate, originalIndex: i }))
|
|
11
|
+
.catch(err => {
|
|
12
|
+
if (onCandidateError) {
|
|
13
|
+
onCandidateError(err, i);
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
const results = await Promise.all(promises);
|
|
19
|
+
const successfulCandidates = results.filter((r) => r !== null);
|
|
20
|
+
// 2. Handle Failure Scenarios
|
|
21
|
+
if (successfulCandidates.length === 0) {
|
|
22
|
+
throw new Error(`All ${candidateCount} candidates failed to generate.`);
|
|
23
|
+
}
|
|
24
|
+
// 3. Short-circuit if only one candidate succeeded
|
|
25
|
+
if (successfulCandidates.length === 1) {
|
|
26
|
+
return {
|
|
27
|
+
winner: successfulCandidates[0].candidate,
|
|
28
|
+
winnerIndex: successfulCandidates[0].originalIndex,
|
|
29
|
+
candidates: successfulCandidates.map(c => c.candidate),
|
|
30
|
+
reason: "Only one candidate succeeded; skipping judge.",
|
|
31
|
+
skippedJudge: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// 4. Judge
|
|
35
|
+
// We pass only the candidate objects to the judge, stripping metadata
|
|
36
|
+
const candidatesForJudge = successfulCandidates.map(c => c.candidate);
|
|
37
|
+
const selection = await judge(input, candidatesForJudge);
|
|
38
|
+
// Validate judge output
|
|
39
|
+
if (selection.bestCandidateIndex < 0 || selection.bestCandidateIndex >= successfulCandidates.length) {
|
|
40
|
+
throw new Error(`Judge returned invalid index ${selection.bestCandidateIndex}. Must be between 0 and ${successfulCandidates.length - 1}.`);
|
|
41
|
+
}
|
|
42
|
+
const winnerResult = successfulCandidates[selection.bestCandidateIndex];
|
|
43
|
+
return {
|
|
44
|
+
winner: winnerResult.candidate,
|
|
45
|
+
winnerIndex: winnerResult.originalIndex,
|
|
46
|
+
candidates: candidatesForJudge,
|
|
47
|
+
reason: selection.reason,
|
|
48
|
+
skippedJudge: false
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { run };
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { CreateLlmClientParams } from './createLlmClient.js';
|
|
3
|
+
/**
|
|
4
|
+
* Abstract interface for managing conversation history.
|
|
5
|
+
* System messages are explicitly excluded from the conversation history.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConversationState {
|
|
8
|
+
/** Returns a read-only copy of the current message history (excluding system messages) */
|
|
9
|
+
getMessages(): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
10
|
+
/** Returns the initial system messages */
|
|
11
|
+
getSystemMessages(): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
12
|
+
/** Adds a raw message parameter to the history. System messages are ignored. */
|
|
13
|
+
add(message: OpenAI.Chat.Completions.ChatCompletionMessageParam): void;
|
|
14
|
+
/** Normalizes and adds a full completion to the history as an assistant message */
|
|
15
|
+
addCompletion(completion: OpenAI.Chat.Completions.ChatCompletion): void;
|
|
16
|
+
/** Shorthand to add a user message. Supports strings or content parts. */
|
|
17
|
+
addUserMessage(content: string | OpenAI.Chat.Completions.ChatCompletionContentPart[]): void;
|
|
18
|
+
/** Shorthand to add an assistant message. Supports strings, content parts, or null. */
|
|
19
|
+
addAssistantMessage(content: string | OpenAI.Chat.Completions.ChatCompletionContentPart[] | null): void;
|
|
20
|
+
/** Removes the last N messages from the history */
|
|
21
|
+
pop(count?: number): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
22
|
+
/** Clears all messages */
|
|
23
|
+
clear(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a simple in-memory implementation of ConversationState.
|
|
27
|
+
* Filters out system messages from initial input and prevents them from being added.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createConversationState(initialMessages?: OpenAI.Chat.Completions.ChatCompletionMessageParam[]): ConversationState;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a stateful conversation client.
|
|
32
|
+
* Every call to a prompt method will:
|
|
33
|
+
* 1. Capture the normalized user message(s) from the first call of the turn.
|
|
34
|
+
* 2. Execute the call using the full history from the state.
|
|
35
|
+
* 3. Capture the final assistant response and append it to the state.
|
|
36
|
+
*
|
|
37
|
+
* System messages are used for the duration of a call but are never stored in history.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createConversation(params: CreateLlmClientParams, initialMessages?: OpenAI.Chat.Completions.ChatCompletionMessageParam[]): ConversationState & {
|
|
40
|
+
[k: string]: any;
|
|
41
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { completionToMessage } from './completionToAssistantMessage.js';
|
|
2
|
+
import { createLlm } from './llmFactory.js';
|
|
3
|
+
import { concatSystemContent } from './util.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a simple in-memory implementation of ConversationState.
|
|
6
|
+
* Filters out system messages from initial input and prevents them from being added.
|
|
7
|
+
*/
|
|
8
|
+
export function createConversationState(initialMessages = []) {
|
|
9
|
+
const systemMessages = initialMessages.filter(m => m.role === 'system');
|
|
10
|
+
let messages = initialMessages.filter(m => m.role !== 'system');
|
|
11
|
+
const add = (m) => {
|
|
12
|
+
if (m.role !== 'system') {
|
|
13
|
+
messages.push(m);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const addCompletion = (completion) => {
|
|
17
|
+
add(completionToMessage(completion));
|
|
18
|
+
};
|
|
19
|
+
const addUserMessage = (content) => {
|
|
20
|
+
add({ role: 'user', content: content });
|
|
21
|
+
};
|
|
22
|
+
const addAssistantMessage = (content) => {
|
|
23
|
+
add({ role: 'assistant', content: content });
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
getMessages: () => [...messages],
|
|
27
|
+
getSystemMessages: () => [...systemMessages],
|
|
28
|
+
add,
|
|
29
|
+
addCompletion,
|
|
30
|
+
addUserMessage,
|
|
31
|
+
addAssistantMessage,
|
|
32
|
+
pop: (count = 1) => messages.splice(-count),
|
|
33
|
+
clear: () => { messages = []; }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a stateful conversation client.
|
|
38
|
+
* Every call to a prompt method will:
|
|
39
|
+
* 1. Capture the normalized user message(s) from the first call of the turn.
|
|
40
|
+
* 2. Execute the call using the full history from the state.
|
|
41
|
+
* 3. Capture the final assistant response and append it to the state.
|
|
42
|
+
*
|
|
43
|
+
* System messages are used for the duration of a call but are never stored in history.
|
|
44
|
+
*/
|
|
45
|
+
export function createConversation(params, initialMessages) {
|
|
46
|
+
const state = createConversationState(initialMessages);
|
|
47
|
+
// Turn-specific tracking state
|
|
48
|
+
let isFirstCallInTurn = true;
|
|
49
|
+
let turnInitialMessages = [];
|
|
50
|
+
let lastCompletionInTurn;
|
|
51
|
+
/**
|
|
52
|
+
* Low-level SDK Spy.
|
|
53
|
+
* Intercepts every call to OpenAI to inject history and manage system messages.
|
|
54
|
+
*/
|
|
55
|
+
const spiedOpenAi = {
|
|
56
|
+
...params.openai,
|
|
57
|
+
chat: {
|
|
58
|
+
...params.openai.chat,
|
|
59
|
+
completions: {
|
|
60
|
+
...params.openai.chat.completions,
|
|
61
|
+
create: async (createParams, createOptions) => {
|
|
62
|
+
const incomingMessages = createParams.messages;
|
|
63
|
+
if (isFirstCallInTurn) {
|
|
64
|
+
// Capture the "clean" prompt messages from the start of the turn (excluding system)
|
|
65
|
+
turnInitialMessages = incomingMessages.filter(m => m.role !== 'system');
|
|
66
|
+
isFirstCallInTurn = false;
|
|
67
|
+
}
|
|
68
|
+
const historyMessages = state.getMessages();
|
|
69
|
+
const baseSystemMessages = state.getSystemMessages();
|
|
70
|
+
const callSystemMessages = incomingMessages.filter(m => m.role === 'system');
|
|
71
|
+
const currentWithoutSystem = incomingMessages.filter(m => m.role !== 'system');
|
|
72
|
+
// Combine all system messages (initial + call-specific)
|
|
73
|
+
// We cast to the expected type because TS infers a wider type for content (including Refusal parts)
|
|
74
|
+
// from the generic ChatCompletionMessageParam, but we know system messages are compatible.
|
|
75
|
+
const finalSystemContent = concatSystemContent([
|
|
76
|
+
...baseSystemMessages.map(m => m.content),
|
|
77
|
+
...callSystemMessages.map(m => m.content)
|
|
78
|
+
]);
|
|
79
|
+
let finalSystemMessage;
|
|
80
|
+
if (finalSystemContent) {
|
|
81
|
+
finalSystemMessage = { role: 'system', content: finalSystemContent };
|
|
82
|
+
}
|
|
83
|
+
// Rebuild the full message array for the actual SDK call
|
|
84
|
+
const finalMessages = finalSystemMessage
|
|
85
|
+
? [finalSystemMessage, ...historyMessages, ...currentWithoutSystem]
|
|
86
|
+
: [...historyMessages, ...currentWithoutSystem];
|
|
87
|
+
const result = await params.openai.chat.completions.create({
|
|
88
|
+
...createParams,
|
|
89
|
+
messages: finalMessages
|
|
90
|
+
}, createOptions);
|
|
91
|
+
lastCompletionInTurn = result;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Create a standard LLM client using the spied OpenAI instance
|
|
98
|
+
const client = createLlm({
|
|
99
|
+
...params,
|
|
100
|
+
openai: spiedOpenAi
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* High-level Turn Wrapper.
|
|
104
|
+
* Defines the boundaries of a single user interaction.
|
|
105
|
+
*/
|
|
106
|
+
const wrapMethod = (methodName) => {
|
|
107
|
+
const originalMethod = client[methodName];
|
|
108
|
+
if (typeof originalMethod !== 'function')
|
|
109
|
+
return originalMethod;
|
|
110
|
+
return async (...args) => {
|
|
111
|
+
// Reset turn context
|
|
112
|
+
isFirstCallInTurn = true;
|
|
113
|
+
turnInitialMessages = [];
|
|
114
|
+
lastCompletionInTurn = undefined;
|
|
115
|
+
const result = await originalMethod.apply(client, args);
|
|
116
|
+
// Turn finished successfully. Commit the turn to the long-term history.
|
|
117
|
+
for (const m of turnInitialMessages) {
|
|
118
|
+
state.add(m);
|
|
119
|
+
}
|
|
120
|
+
if (lastCompletionInTurn) {
|
|
121
|
+
state.addCompletion(lastCompletionInTurn);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
// Wrap all high-level methods to ensure they are treated as stateful turns
|
|
127
|
+
const wrappedMethods = Object.fromEntries(Object.keys(client)
|
|
128
|
+
.filter(key => key !== 'createConversation')
|
|
129
|
+
.map(key => [key, wrapMethod(key)]));
|
|
130
|
+
return {
|
|
131
|
+
...state,
|
|
132
|
+
...wrappedMethods
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a fetch-like function that uses specific DNS servers for hostname resolution.
|
|
3
|
+
* Defaults to Google's DNS servers (8.8.8.8, 8.8.4.4).
|
|
4
|
+
*
|
|
5
|
+
* This implementation uses Node.js's http/https agents with a custom lookup function.
|
|
6
|
+
* It is primarily compatible with fetch implementations that respect the `agent` option
|
|
7
|
+
* (like node-fetch or the built-in fetch in Node.js 18+).
|
|
8
|
+
*
|
|
9
|
+
* @param dnsServers Array of DNS server IP addresses. Defaults to Google DNS.
|
|
10
|
+
* @param innerFetch Optional fetch implementation to wrap. Defaults to global fetch.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createDnsFetcher(dnsServers?: string[], innerFetch?: typeof fetch): (url: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import dns from 'dns';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a fetch-like function that uses specific DNS servers for hostname resolution.
|
|
6
|
+
* Defaults to Google's DNS servers (8.8.8.8, 8.8.4.4).
|
|
7
|
+
*
|
|
8
|
+
* This implementation uses Node.js's http/https agents with a custom lookup function.
|
|
9
|
+
* It is primarily compatible with fetch implementations that respect the `agent` option
|
|
10
|
+
* (like node-fetch or the built-in fetch in Node.js 18+).
|
|
11
|
+
*
|
|
12
|
+
* @param dnsServers Array of DNS server IP addresses. Defaults to Google DNS.
|
|
13
|
+
* @param innerFetch Optional fetch implementation to wrap. Defaults to global fetch.
|
|
14
|
+
*/
|
|
15
|
+
export function createDnsFetcher(dnsServers = ['8.8.8.8', '8.8.4.4'], innerFetch) {
|
|
16
|
+
const resolver = new dns.Resolver();
|
|
17
|
+
resolver.setServers(dnsServers);
|
|
18
|
+
const lookup = (hostname, options, callback) => {
|
|
19
|
+
const cb = typeof options === 'function' ? options : callback;
|
|
20
|
+
const opts = typeof options === 'object' ? options : {};
|
|
21
|
+
// If the caller requested all addresses, fallback to system lookup for simplicity
|
|
22
|
+
if (opts.all) {
|
|
23
|
+
return dns.lookup(hostname, opts, cb);
|
|
24
|
+
}
|
|
25
|
+
// Attempt to resolve IPv4 via configured DNS servers
|
|
26
|
+
resolver.resolve4(hostname, (err, addresses) => {
|
|
27
|
+
if (err || !addresses || addresses.length === 0) {
|
|
28
|
+
// Fallback to system DNS lookup if custom DNS fails or doesn't return IPv4
|
|
29
|
+
return dns.lookup(hostname, opts, cb);
|
|
30
|
+
}
|
|
31
|
+
// Return the first resolved address
|
|
32
|
+
cb(null, addresses[0], 4);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const httpAgent = new http.Agent({ lookup });
|
|
36
|
+
const httpsAgent = new https.Agent({ lookup });
|
|
37
|
+
const fetchImpl = innerFetch || globalThis.fetch;
|
|
38
|
+
return (url, init) => {
|
|
39
|
+
const urlString = typeof url === 'string'
|
|
40
|
+
? url
|
|
41
|
+
: (url instanceof Request ? url.url : url.toString());
|
|
42
|
+
const isHttps = urlString.startsWith('https');
|
|
43
|
+
return fetchImpl(url, {
|
|
44
|
+
...init,
|
|
45
|
+
// Pass agents for libraries like node-fetch
|
|
46
|
+
agent: isHttps ? httpsAgent : httpAgent,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|