@almadar/llm 2.50.0 → 2.52.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/dist/{chunk-RBT22YSX.js → chunk-DLEZ7FGQ.js} +13 -4
- package/dist/chunk-DLEZ7FGQ.js.map +1 -0
- package/dist/chunk-MOIECDMB.js +172 -0
- package/dist/chunk-MOIECDMB.js.map +1 -0
- package/dist/chunk-OXFWONZP.js +369 -0
- package/dist/chunk-OXFWONZP.js.map +1 -0
- package/dist/{chunk-5AM54OAM.js → chunk-SJE3GTGZ.js} +5 -3
- package/dist/{chunk-5AM54OAM.js.map → chunk-SJE3GTGZ.js.map} +1 -1
- package/dist/{chunk-RPG3SUIB.js → chunk-T6AKOBX3.js} +25 -177
- package/dist/chunk-T6AKOBX3.js.map +1 -0
- package/dist/{client-SYMpnw-w.d.ts → client-DfzMDgkm.d.ts} +10 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +3 -2
- package/dist/index.d.ts +12 -6
- package/dist/index.js +49 -12
- package/dist/index.js.map +1 -1
- package/dist/providers/index.d.ts +138 -1
- package/dist/providers/index.js +16 -1
- package/dist/{rate-limiter-CXaf8aAy.d.ts → rate-limiter-Bz0iSJgZ.d.ts} +20 -1
- package/dist/structured-output.d.ts +1 -1
- package/dist/structured-output.js +3 -2
- package/package.json +6 -4
- package/src/client.ts +22 -1
- package/src/image-client.ts +27 -4
- package/src/index.ts +25 -0
- package/src/providers/index.ts +26 -0
- package/src/providers/jev.ts +456 -0
- package/src/token-tracker.ts +67 -12
- package/dist/chunk-MUTXGY6D.js +0 -133
- package/dist/chunk-MUTXGY6D.js.map +0 -1
- package/dist/chunk-RBT22YSX.js.map +0 -1
- package/dist/chunk-RPG3SUIB.js.map +0 -1
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getGlobalTokenTracker
|
|
3
|
+
} from "./chunk-T6AKOBX3.js";
|
|
4
|
+
|
|
5
|
+
// src/providers/masar.ts
|
|
6
|
+
var MasarError = class extends Error {
|
|
7
|
+
constructor(message, statusCode, responseBody) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.responseBody = responseBody;
|
|
11
|
+
this.name = "MasarError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_BASE_URL = "https://masar-345008351456.europe-west4.run.app";
|
|
15
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
16
|
+
var MasarProvider = class {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.baseUrl = (options?.baseUrl ?? process.env.MASAR_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
19
|
+
this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20
|
+
}
|
|
21
|
+
// --------------------------------------------------------------------------
|
|
22
|
+
// Public API
|
|
23
|
+
// --------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Generate text from a prompt.
|
|
26
|
+
*
|
|
27
|
+
* POST /generate
|
|
28
|
+
*/
|
|
29
|
+
async generate(prompt, options) {
|
|
30
|
+
return this.post("/generate", {
|
|
31
|
+
prompt,
|
|
32
|
+
...options
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate a .orb schema via GFlowNet sampling.
|
|
37
|
+
*
|
|
38
|
+
* POST /generate/gflownet
|
|
39
|
+
*/
|
|
40
|
+
async generateGFlowNet(goal) {
|
|
41
|
+
return this.post("/generate/gflownet", goal);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Predict validation errors in a .orb schema before compilation.
|
|
45
|
+
*
|
|
46
|
+
* POST /predict-errors
|
|
47
|
+
*/
|
|
48
|
+
async predictErrors(schema) {
|
|
49
|
+
return this.post("/predict-errors", { schema });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Rank candidate edits for fixing errors in a .orb schema.
|
|
53
|
+
*
|
|
54
|
+
* POST /rank-edits
|
|
55
|
+
*/
|
|
56
|
+
async rankEdits(schema, errors) {
|
|
57
|
+
return this.post("/rank-edits", { schema, errors });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check server health.
|
|
61
|
+
*
|
|
62
|
+
* GET /health
|
|
63
|
+
*/
|
|
64
|
+
async health() {
|
|
65
|
+
return this.get("/health");
|
|
66
|
+
}
|
|
67
|
+
// --------------------------------------------------------------------------
|
|
68
|
+
// Internal helpers
|
|
69
|
+
// --------------------------------------------------------------------------
|
|
70
|
+
async post(path, body) {
|
|
71
|
+
return this.request(path, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify(body)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async get(path) {
|
|
78
|
+
return this.request(path, { method: "GET" });
|
|
79
|
+
}
|
|
80
|
+
async request(path, init) {
|
|
81
|
+
const url = `${this.baseUrl}${path}`;
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
...init,
|
|
87
|
+
signal: controller.signal
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const text = await response.text().catch(() => "");
|
|
91
|
+
throw new MasarError(
|
|
92
|
+
`Masar ${init.method} ${path} failed with status ${response.status}`,
|
|
93
|
+
response.status,
|
|
94
|
+
text
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return await response.json();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof MasarError) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
103
|
+
throw new MasarError(
|
|
104
|
+
`Masar ${init.method} ${path} timed out after ${this.timeoutMs}ms`,
|
|
105
|
+
0,
|
|
106
|
+
""
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
throw new MasarError(
|
|
111
|
+
`Masar ${init.method} ${path} failed: ${message}`,
|
|
112
|
+
0,
|
|
113
|
+
""
|
|
114
|
+
);
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var sharedInstance = null;
|
|
121
|
+
function getMasarProvider(options) {
|
|
122
|
+
if (!sharedInstance) {
|
|
123
|
+
sharedInstance = new MasarProvider(options);
|
|
124
|
+
}
|
|
125
|
+
return sharedInstance;
|
|
126
|
+
}
|
|
127
|
+
function resetMasarProvider() {
|
|
128
|
+
sharedInstance = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/providers/jev.ts
|
|
132
|
+
var JEV_MODELS = {
|
|
133
|
+
JEV_1_13: "typesafe/jev-1.13",
|
|
134
|
+
JEV_LATEST: "~typesafe/jev-latest"
|
|
135
|
+
};
|
|
136
|
+
var JEV_DECISIONS_URL = "https://openrouter.ai/api/alpha/decisions";
|
|
137
|
+
var JEV_MODELS_BASE_URL = "https://openrouter.ai/api/v1/models";
|
|
138
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
139
|
+
var JevError = class extends Error {
|
|
140
|
+
constructor(message, status, body) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.status = status;
|
|
143
|
+
this.body = body;
|
|
144
|
+
this.name = "JevError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function isRecord(value) {
|
|
148
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
149
|
+
}
|
|
150
|
+
function isNumberRecord(value) {
|
|
151
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "number");
|
|
152
|
+
}
|
|
153
|
+
function isStringRecord(value) {
|
|
154
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
155
|
+
}
|
|
156
|
+
function isNoulAnswer(value) {
|
|
157
|
+
return isRecord(value) && value.type === "noul" && typeof value.noul === "number";
|
|
158
|
+
}
|
|
159
|
+
function isChoiceAnswer(value) {
|
|
160
|
+
return isRecord(value) && value.type === "choice" && typeof value.choice === "string" && typeof value.confidence === "number" && isNumberRecord(value.probabilities);
|
|
161
|
+
}
|
|
162
|
+
function isScoreAnswer(value) {
|
|
163
|
+
return isRecord(value) && value.type === "score" && typeof value.score === "number" && typeof value.confidence === "number" && isStringRecord(value.legend) && isNumberRecord(value.probabilities);
|
|
164
|
+
}
|
|
165
|
+
function isJevAnswer(value) {
|
|
166
|
+
return isNoulAnswer(value) || isChoiceAnswer(value) || isScoreAnswer(value);
|
|
167
|
+
}
|
|
168
|
+
function parseWireBody(value, status, rawBody) {
|
|
169
|
+
if (!isRecord(value)) {
|
|
170
|
+
throw new JevError("Jev decide: response body is not a JSON object", status, rawBody);
|
|
171
|
+
}
|
|
172
|
+
const { model, id, provider, answers, usage } = value;
|
|
173
|
+
if (typeof model !== "string") {
|
|
174
|
+
throw new JevError('Jev decide: response is missing string "model"', status, rawBody);
|
|
175
|
+
}
|
|
176
|
+
if (typeof id !== "string") {
|
|
177
|
+
throw new JevError('Jev decide: response is missing string "id"', status, rawBody);
|
|
178
|
+
}
|
|
179
|
+
if (typeof provider !== "string") {
|
|
180
|
+
throw new JevError('Jev decide: response is missing string "provider"', status, rawBody);
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(answers)) {
|
|
183
|
+
throw new JevError('Jev decide: response is missing an "answers" object', status, rawBody);
|
|
184
|
+
}
|
|
185
|
+
if (!isRecord(usage)) {
|
|
186
|
+
throw new JevError('Jev decide: response is missing a "usage" object', status, rawBody);
|
|
187
|
+
}
|
|
188
|
+
const { input_tokens: inputTokens, output_tokens: outputTokens, cost } = usage;
|
|
189
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number" || typeof cost !== "number") {
|
|
190
|
+
throw new JevError('Jev decide: "usage" is missing input_tokens/output_tokens/cost', status, rawBody);
|
|
191
|
+
}
|
|
192
|
+
const parsedAnswers = {};
|
|
193
|
+
for (const [key, answer] of Object.entries(answers)) {
|
|
194
|
+
if (!isJevAnswer(answer)) {
|
|
195
|
+
throw new JevError(`Jev decide: answer "${key}" has an invalid or unrecognized shape`, status, rawBody);
|
|
196
|
+
}
|
|
197
|
+
parsedAnswers[key] = answer;
|
|
198
|
+
}
|
|
199
|
+
return { model, id, provider, answers: parsedAnswers, usage: { inputTokens, outputTokens, cost } };
|
|
200
|
+
}
|
|
201
|
+
var JevProvider = class {
|
|
202
|
+
constructor(options) {
|
|
203
|
+
this.apiKeyOverride = options?.apiKey;
|
|
204
|
+
this.defaultModel = options?.model ?? JEV_MODELS.JEV_1_13;
|
|
205
|
+
this.baseUrl = options?.baseUrl ?? JEV_DECISIONS_URL;
|
|
206
|
+
this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
207
|
+
this.fetchImpl = options?.fetchImpl ?? fetch;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Answer one or more `noul` / `choice` / `score` questions about `state`
|
|
211
|
+
* in a single round-trip.
|
|
212
|
+
*
|
|
213
|
+
* POST /api/alpha/decisions
|
|
214
|
+
*/
|
|
215
|
+
async decide(req) {
|
|
216
|
+
const apiKey = this.resolveApiKey();
|
|
217
|
+
const model = req.model ?? this.defaultModel;
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
220
|
+
const startedAt = Date.now();
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await this.fetchImpl(this.baseUrl, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${apiKey}`,
|
|
227
|
+
"Content-Type": "application/json"
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({ model, state: req.state, questions: req.questions }),
|
|
230
|
+
signal: controller.signal
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
235
|
+
throw new JevError(`Jev decide timed out after ${this.timeoutMs}ms`);
|
|
236
|
+
}
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
throw new JevError(`Jev decide request failed: ${message}`);
|
|
239
|
+
}
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
const rawBody = await response.text();
|
|
242
|
+
const durationMs = Date.now() - startedAt;
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw new JevError(`Jev decide failed with status ${response.status}`, response.status, rawBody);
|
|
245
|
+
}
|
|
246
|
+
let parsedJson;
|
|
247
|
+
try {
|
|
248
|
+
parsedJson = JSON.parse(rawBody);
|
|
249
|
+
} catch {
|
|
250
|
+
throw new JevError("Jev decide: response body is not valid JSON", response.status, rawBody);
|
|
251
|
+
}
|
|
252
|
+
const wire = parseWireBody(parsedJson, response.status, rawBody);
|
|
253
|
+
const answers = {};
|
|
254
|
+
for (const key of Object.keys(req.questions)) {
|
|
255
|
+
const question = req.questions[key];
|
|
256
|
+
const answer = wire.answers[key];
|
|
257
|
+
if (!answer) {
|
|
258
|
+
throw new JevError(`Jev decide: missing answer for question "${key}"`, response.status, rawBody);
|
|
259
|
+
}
|
|
260
|
+
if (answer.type !== question.type) {
|
|
261
|
+
throw new JevError(
|
|
262
|
+
`Jev decide: answer "${key}" has type "${answer.type}", expected "${question.type}"`,
|
|
263
|
+
response.status,
|
|
264
|
+
rawBody
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (answer.type === "choice" && question.type === "choice" && !(answer.choice in question.criteria)) {
|
|
268
|
+
throw new JevError(
|
|
269
|
+
`Jev decide: answer "${key}" chose "${answer.choice}", which is not one of the declared criteria`,
|
|
270
|
+
response.status,
|
|
271
|
+
rawBody
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
answers[key] = answer;
|
|
275
|
+
}
|
|
276
|
+
getGlobalTokenTracker(model).addUsage(wire.usage.inputTokens, wire.usage.outputTokens, {
|
|
277
|
+
provider: "jev",
|
|
278
|
+
durationMs,
|
|
279
|
+
// Jev is absent from OpenRouter's /api/v1/models catalog, so cost must
|
|
280
|
+
// come from the decisions endpoint's own authoritative usage.cost.
|
|
281
|
+
costUSD: wire.usage.cost
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
// Unavoidable: Object.keys(req.questions) erases each key to plain
|
|
285
|
+
// `string`, so TS can't prove a loop-built object satisfies the
|
|
286
|
+
// generic mapped type JevAnswersOf<Q> per key — this one assertion
|
|
287
|
+
// follows the per-key runtime validation (present, type, criteria) above.
|
|
288
|
+
answers,
|
|
289
|
+
model: wire.model,
|
|
290
|
+
id: wire.id,
|
|
291
|
+
provider: wire.provider,
|
|
292
|
+
usage: {
|
|
293
|
+
inputTokens: wire.usage.inputTokens,
|
|
294
|
+
outputTokens: wire.usage.outputTokens,
|
|
295
|
+
costUSD: wire.usage.cost
|
|
296
|
+
},
|
|
297
|
+
durationMs
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check the model is servable. `~typesafe/jev-latest` legitimately reports
|
|
302
|
+
* `endpoints: []` yet still works as a `model` value, so `ok` tracks HTTP
|
|
303
|
+
* 200 only — an empty endpoint list is never treated as "down".
|
|
304
|
+
*
|
|
305
|
+
* GET /api/v1/models/<model>/endpoints
|
|
306
|
+
*/
|
|
307
|
+
async health() {
|
|
308
|
+
const apiKey = this.resolveApiKey();
|
|
309
|
+
const model = this.defaultModel;
|
|
310
|
+
const url = `${JEV_MODELS_BASE_URL}/${model}/endpoints`;
|
|
311
|
+
let response;
|
|
312
|
+
try {
|
|
313
|
+
response = await this.fetchImpl(url, {
|
|
314
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
315
|
+
});
|
|
316
|
+
} catch {
|
|
317
|
+
return { ok: false, model, endpoints: 0 };
|
|
318
|
+
}
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
return { ok: false, model, endpoints: 0 };
|
|
321
|
+
}
|
|
322
|
+
let endpoints = 0;
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(await response.text());
|
|
325
|
+
if (isRecord(parsed) && isRecord(parsed.data) && Array.isArray(parsed.data.endpoints)) {
|
|
326
|
+
endpoints = parsed.data.endpoints.length;
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
return { ok: true, model, endpoints };
|
|
331
|
+
}
|
|
332
|
+
resolveApiKey() {
|
|
333
|
+
const apiKey = this.apiKeyOverride ?? process.env.OPENROUTER_API_KEY ?? process.env.OPEN_ROUTER_API_KEY;
|
|
334
|
+
if (!apiKey) {
|
|
335
|
+
throw new JevError(
|
|
336
|
+
"Jev: no API key. Set OPENROUTER_API_KEY (or OPEN_ROUTER_API_KEY) in the environment, or pass { apiKey } to JevProvider."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return apiKey;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var sharedInstance2 = null;
|
|
343
|
+
function getJevProvider(options) {
|
|
344
|
+
if (!sharedInstance2) {
|
|
345
|
+
sharedInstance2 = new JevProvider(options);
|
|
346
|
+
}
|
|
347
|
+
return sharedInstance2;
|
|
348
|
+
}
|
|
349
|
+
function resetJevProvider() {
|
|
350
|
+
sharedInstance2 = null;
|
|
351
|
+
}
|
|
352
|
+
function isJevAvailable() {
|
|
353
|
+
return Boolean(process.env.OPENROUTER_API_KEY ?? process.env.OPEN_ROUTER_API_KEY);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
MasarError,
|
|
358
|
+
MasarProvider,
|
|
359
|
+
getMasarProvider,
|
|
360
|
+
resetMasarProvider,
|
|
361
|
+
JEV_MODELS,
|
|
362
|
+
JEV_DECISIONS_URL,
|
|
363
|
+
JevError,
|
|
364
|
+
JevProvider,
|
|
365
|
+
getJevProvider,
|
|
366
|
+
resetJevProvider,
|
|
367
|
+
isJevAvailable
|
|
368
|
+
};
|
|
369
|
+
//# sourceMappingURL=chunk-OXFWONZP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/masar.ts","../src/providers/jev.ts"],"sourcesContent":["/**\n * Masar Provider\n *\n * Thin HTTP client for the Masar neural pipeline server.\n * Exposes generate, GFlowNet generation, error prediction,\n * edit ranking, and health-check endpoints.\n *\n * Reads `MASAR_URL` from environment (default: http://localhost:8080).\n *\n * @packageDocumentation\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MasarGenerateOptions {\n /** Model override (server decides default if omitted). */\n model?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Maximum tokens to generate. */\n maxTokens?: number;\n}\n\nexport interface MasarGenerateResult {\n text: string;\n usage: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n}\n\n/** GFlowNet sampling constraint value: primitives, arrays, or nested constraint maps. */\ntype ConstraintValue = string | number | boolean | null | ConstraintValue[] | { [key: string]: ConstraintValue };\n\nexport interface GoalSpec {\n /** Natural-language description of the desired application. */\n description: string;\n /** Target entities (e.g. [\"User\", \"Product\", \"Order\"]). */\n entities?: string[];\n /** Domain hint (e.g. \"e-commerce\", \"healthcare\"). */\n domain?: string;\n /** Additional constraints passed to the GFlowNet sampler. */\n constraints?: Record<string, ConstraintValue>;\n}\n\nexport interface GFlowNetResult {\n /** Generated .orb schema text. */\n schema: string;\n /** Log-probability of the sampled trajectory. */\n logProb: number;\n /** Number of sampling steps taken. */\n steps: number;\n}\n\nexport interface ErrorPrediction {\n /** Line number (1-based) where the error is predicted. */\n line: number;\n /** Predicted error category. */\n category: string;\n /** Human-readable description. */\n message: string;\n /** Confidence score in [0, 1]. */\n confidence: number;\n}\n\nexport interface PredictErrorsResult {\n errors: ErrorPrediction[];\n}\n\nexport interface RankedEdit {\n /** The proposed replacement text. */\n edit: string;\n /** Score assigned by the ranker (higher is better). */\n score: number;\n /** Which error this edit addresses. */\n targetError: string;\n}\n\nexport interface RankEditsResult {\n edits: RankedEdit[];\n}\n\nexport interface MasarHealthResult {\n status: string;\n version?: string;\n uptime?: number;\n}\n\nexport interface MasarProviderOptions {\n /** Base URL of the Masar server. Overrides MASAR_URL env var. */\n baseUrl?: string;\n /** Request timeout in milliseconds (default: 30 000). */\n timeoutMs?: number;\n}\n\n// ============================================================================\n// Error\n// ============================================================================\n\nexport class MasarError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly responseBody: string,\n ) {\n super(message);\n this.name = 'MasarError';\n }\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\nconst DEFAULT_BASE_URL = 'https://masar-345008351456.europe-west4.run.app';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport class MasarProvider {\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: MasarProviderOptions) {\n this.baseUrl = (\n options?.baseUrl ??\n process.env.MASAR_URL ??\n DEFAULT_BASE_URL\n ).replace(/\\/+$/, '');\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n /**\n * Generate text from a prompt.\n *\n * POST /generate\n */\n async generate(\n prompt: string,\n options?: MasarGenerateOptions,\n ): Promise<MasarGenerateResult> {\n return this.post<MasarGenerateResult>('/generate', {\n prompt,\n ...options,\n });\n }\n\n /**\n * Generate a .orb schema via GFlowNet sampling.\n *\n * POST /generate/gflownet\n */\n async generateGFlowNet(goal: GoalSpec): Promise<GFlowNetResult> {\n return this.post<GFlowNetResult>('/generate/gflownet', goal);\n }\n\n /**\n * Predict validation errors in a .orb schema before compilation.\n *\n * POST /predict-errors\n */\n async predictErrors(schema: string): Promise<PredictErrorsResult> {\n return this.post<PredictErrorsResult>('/predict-errors', { schema });\n }\n\n /**\n * Rank candidate edits for fixing errors in a .orb schema.\n *\n * POST /rank-edits\n */\n async rankEdits(\n schema: string,\n errors: string[],\n ): Promise<RankEditsResult> {\n return this.post<RankEditsResult>('/rank-edits', { schema, errors });\n }\n\n /**\n * Check server health.\n *\n * GET /health\n */\n async health(): Promise<MasarHealthResult> {\n return this.get<MasarHealthResult>('/health');\n }\n\n // --------------------------------------------------------------------------\n // Internal helpers\n // --------------------------------------------------------------------------\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n }\n\n private async get<T>(path: string): Promise<T> {\n return this.request<T>(path, { method: 'GET' });\n }\n\n private async request<T>(\n path: string,\n init: RequestInit,\n ): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...init,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new MasarError(\n `Masar ${init.method} ${path} failed with status ${response.status}`,\n response.status,\n text,\n );\n }\n\n return (await response.json()) as T;\n } catch (error) {\n if (error instanceof MasarError) {\n throw error;\n }\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new MasarError(\n `Masar ${init.method} ${path} timed out after ${this.timeoutMs}ms`,\n 0,\n '',\n );\n }\n\n const message =\n error instanceof Error ? error.message : String(error);\n throw new MasarError(\n `Masar ${init.method} ${path} failed: ${message}`,\n 0,\n '',\n );\n } finally {\n clearTimeout(timer);\n }\n }\n}\n\n// ============================================================================\n// Singleton\n// ============================================================================\n\nlet sharedInstance: MasarProvider | null = null;\n\n/**\n * Get the singleton Masar provider instance.\n *\n * Creates the instance on first call, returns cached instance thereafter.\n *\n * @param {MasarProviderOptions} [options] - Provider configuration options\n * @returns {MasarProvider} The Masar provider instance\n */\nexport function getMasarProvider(\n options?: MasarProviderOptions,\n): MasarProvider {\n if (!sharedInstance) {\n sharedInstance = new MasarProvider(options);\n }\n return sharedInstance;\n}\n\nexport function resetMasarProvider(): void {\n sharedInstance = null;\n}\n","/**\n * Jev Provider\n *\n * Thin HTTP client for TypeSafe's \"System One\" decision model on OpenRouter.\n * Not a chat model — `POST /api/alpha/decisions`, one or more `noul` / `choice`\n * / `score` questions batched into a single round-trip. No temperature/top_p/\n * max_tokens, no streaming, no system prompt.\n *\n * See docs/Almadar_Rabit.md § \"Jev decision provider\" for the verified contract.\n *\n * @packageDocumentation\n */\n\nimport { getGlobalTokenTracker } from '../token-tracker.js';\n\n// ============================================================================\n// Models\n// ============================================================================\n\nexport const JEV_MODELS = {\n JEV_1_13: 'typesafe/jev-1.13',\n JEV_LATEST: '~typesafe/jev-latest',\n} as const;\n\nexport type JevModelId = (typeof JEV_MODELS)[keyof typeof JEV_MODELS];\n\nexport const JEV_DECISIONS_URL = 'https://openrouter.ai/api/alpha/decisions';\n\nconst JEV_MODELS_BASE_URL = 'https://openrouter.ai/api/v1/models';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n// ============================================================================\n// JSON value (recursive, not Record<string, unknown> — repo lint bans that)\n// ============================================================================\n\nexport type JevJsonValue =\n | string\n | number\n | boolean\n | null\n | JevJsonValue[]\n | { [key: string]: JevJsonValue };\n\nexport type JevState = string | Record<string, JevJsonValue>;\n\n// ============================================================================\n// Questions\n// ============================================================================\n\nexport interface JevNoulQuestion {\n type: 'noul';\n /** Must pose the yes/no question directly (docs.typesafe.ai/api, e.g. \"Does this convey urgency?\") — the noul answer is P(yes) TO THIS TEXT, never a policy paragraph. */\n instructions: string;\n /** Optional descriptions of what a yes and a no mean (docs-verified request shape); passed through verbatim to `/api/alpha/decisions`. */\n criteria?: { true: string; false: string };\n}\n\nexport interface JevChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\nexport interface JevScoreQuestion {\n type: 'score';\n instructions: string;\n /** Ordered level descriptions, 2–10 entries (verified docs.typesafe.ai/api). */\n criteria: string[];\n}\n\nexport type JevQuestion = JevNoulQuestion | JevChoiceQuestion | JevScoreQuestion;\n\n// ============================================================================\n// Answers\n// ============================================================================\n\nexport interface JevNoulAnswer {\n type: 'noul';\n noul: number;\n}\n\nexport interface JevChoiceAnswer {\n type: 'choice';\n choice: string;\n probabilities: Record<string, number>;\n confidence: number;\n}\n\nexport interface JevScoreAnswer {\n type: 'score';\n score: number;\n legend: Record<string, string>;\n probabilities: Record<string, number>;\n confidence: number;\n}\n\nexport type JevAnswer = JevNoulAnswer | JevChoiceAnswer | JevScoreAnswer;\n\n// ============================================================================\n// Request / Result\n// ============================================================================\n\nexport interface JevDecideRequest<Q extends Record<string, JevQuestion>> {\n state: JevState;\n questions: Q;\n model?: JevModelId;\n}\n\nexport interface JevUsage {\n inputTokens: number;\n outputTokens: number;\n costUSD: number;\n}\n\n// The extends-check must run on JevAnswerFor's OWN naked type parameter, not\n// inline on the indexed access `Q[K]` — a conditional keyed off an indexed\n// access type isn't distributive, so a wide Q (e.g. Record<string,\n// JevQuestion>) would collapse every key to the final `never`/last branch\n// instead of distributing per union member.\ntype JevAnswerFor<TQ extends JevQuestion> = TQ extends JevNoulQuestion\n ? JevNoulAnswer\n : TQ extends JevChoiceQuestion\n ? JevChoiceAnswer\n : TQ extends JevScoreQuestion\n ? JevScoreAnswer\n : never;\n\ntype JevAnswersOf<Q extends Record<string, JevQuestion>> = {\n [K in keyof Q]: JevAnswerFor<Q[K]>;\n};\n\nexport interface JevDecideResult<Q extends Record<string, JevQuestion>> {\n answers: JevAnswersOf<Q>;\n model: string;\n id: string;\n provider: string;\n usage: JevUsage;\n durationMs: number;\n}\n\nexport interface JevProviderOptions {\n apiKey?: string;\n model?: JevModelId;\n baseUrl?: string;\n timeoutMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport interface JevHealthResult {\n ok: boolean;\n model: string;\n endpoints: number;\n}\n\n// ============================================================================\n// Error\n// ============================================================================\n\nexport class JevError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly body?: string,\n ) {\n super(message);\n this.name = 'JevError';\n }\n}\n\n// ============================================================================\n// Runtime validation — parse untrusted JSON into narrow local types via\n// explicit checks, never `as any` / `as unknown as X`.\n// ============================================================================\n\nfunction isRecord(value: unknown): value is { [key: string]: unknown } {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNumberRecord(value: unknown): value is Record<string, number> {\n return isRecord(value) && Object.values(value).every((v) => typeof v === 'number');\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n return isRecord(value) && Object.values(value).every((v) => typeof v === 'string');\n}\n\nfunction isNoulAnswer(value: unknown): value is JevNoulAnswer {\n return isRecord(value) && value.type === 'noul' && typeof value.noul === 'number';\n}\n\nfunction isChoiceAnswer(value: unknown): value is JevChoiceAnswer {\n return (\n isRecord(value) &&\n value.type === 'choice' &&\n typeof value.choice === 'string' &&\n typeof value.confidence === 'number' &&\n isNumberRecord(value.probabilities)\n );\n}\n\nfunction isScoreAnswer(value: unknown): value is JevScoreAnswer {\n return (\n isRecord(value) &&\n value.type === 'score' &&\n typeof value.score === 'number' &&\n typeof value.confidence === 'number' &&\n isStringRecord(value.legend) &&\n isNumberRecord(value.probabilities)\n );\n}\n\nfunction isJevAnswer(value: unknown): value is JevAnswer {\n return isNoulAnswer(value) || isChoiceAnswer(value) || isScoreAnswer(value);\n}\n\ninterface JevWireBody {\n model: string;\n id: string;\n provider: string;\n answers: Record<string, JevAnswer>;\n usage: { inputTokens: number; outputTokens: number; cost: number };\n}\n\nfunction parseWireBody(value: unknown, status: number, rawBody: string): JevWireBody {\n if (!isRecord(value)) {\n throw new JevError('Jev decide: response body is not a JSON object', status, rawBody);\n }\n const { model, id, provider, answers, usage } = value;\n if (typeof model !== 'string') {\n throw new JevError('Jev decide: response is missing string \"model\"', status, rawBody);\n }\n if (typeof id !== 'string') {\n throw new JevError('Jev decide: response is missing string \"id\"', status, rawBody);\n }\n if (typeof provider !== 'string') {\n throw new JevError('Jev decide: response is missing string \"provider\"', status, rawBody);\n }\n if (!isRecord(answers)) {\n throw new JevError('Jev decide: response is missing an \"answers\" object', status, rawBody);\n }\n if (!isRecord(usage)) {\n throw new JevError('Jev decide: response is missing a \"usage\" object', status, rawBody);\n }\n const { input_tokens: inputTokens, output_tokens: outputTokens, cost } = usage;\n if (typeof inputTokens !== 'number' || typeof outputTokens !== 'number' || typeof cost !== 'number') {\n throw new JevError('Jev decide: \"usage\" is missing input_tokens/output_tokens/cost', status, rawBody);\n }\n\n const parsedAnswers: Record<string, JevAnswer> = {};\n for (const [key, answer] of Object.entries(answers)) {\n if (!isJevAnswer(answer)) {\n throw new JevError(`Jev decide: answer \"${key}\" has an invalid or unrecognized shape`, status, rawBody);\n }\n parsedAnswers[key] = answer;\n }\n\n return { model, id, provider, answers: parsedAnswers, usage: { inputTokens, outputTokens, cost } };\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\nexport class JevProvider {\n private readonly apiKeyOverride: string | undefined;\n private readonly defaultModel: JevModelId;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options?: JevProviderOptions) {\n this.apiKeyOverride = options?.apiKey;\n this.defaultModel = options?.model ?? JEV_MODELS.JEV_1_13;\n this.baseUrl = options?.baseUrl ?? JEV_DECISIONS_URL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.fetchImpl = options?.fetchImpl ?? fetch;\n }\n\n /**\n * Answer one or more `noul` / `choice` / `score` questions about `state`\n * in a single round-trip.\n *\n * POST /api/alpha/decisions\n */\n async decide<Q extends Record<string, JevQuestion>>(\n req: JevDecideRequest<Q>,\n ): Promise<JevDecideResult<Q>> {\n const apiKey = this.resolveApiKey();\n const model = req.model ?? this.defaultModel;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n const startedAt = Date.now();\n\n let response: Response;\n try {\n response = await this.fetchImpl(this.baseUrl, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ model, state: req.state, questions: req.questions }),\n signal: controller.signal,\n });\n } catch (error) {\n clearTimeout(timer);\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new JevError(`Jev decide timed out after ${this.timeoutMs}ms`);\n }\n const message = error instanceof Error ? error.message : String(error);\n throw new JevError(`Jev decide request failed: ${message}`);\n }\n clearTimeout(timer);\n\n const rawBody = await response.text();\n const durationMs = Date.now() - startedAt;\n\n if (!response.ok) {\n throw new JevError(`Jev decide failed with status ${response.status}`, response.status, rawBody);\n }\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(rawBody);\n } catch {\n throw new JevError('Jev decide: response body is not valid JSON', response.status, rawBody);\n }\n\n const wire = parseWireBody(parsedJson, response.status, rawBody);\n\n const answers: Record<string, JevAnswer> = {};\n for (const key of Object.keys(req.questions)) {\n const question = req.questions[key];\n const answer = wire.answers[key];\n if (!answer) {\n throw new JevError(`Jev decide: missing answer for question \"${key}\"`, response.status, rawBody);\n }\n if (answer.type !== question.type) {\n throw new JevError(\n `Jev decide: answer \"${key}\" has type \"${answer.type}\", expected \"${question.type}\"`,\n response.status,\n rawBody,\n );\n }\n if (answer.type === 'choice' && question.type === 'choice' && !(answer.choice in question.criteria)) {\n throw new JevError(\n `Jev decide: answer \"${key}\" chose \"${answer.choice}\", which is not one of the declared criteria`,\n response.status,\n rawBody,\n );\n }\n answers[key] = answer;\n }\n\n getGlobalTokenTracker(model).addUsage(wire.usage.inputTokens, wire.usage.outputTokens, {\n provider: 'jev',\n durationMs,\n // Jev is absent from OpenRouter's /api/v1/models catalog, so cost must\n // come from the decisions endpoint's own authoritative usage.cost.\n costUSD: wire.usage.cost,\n });\n\n return {\n // Unavoidable: Object.keys(req.questions) erases each key to plain\n // `string`, so TS can't prove a loop-built object satisfies the\n // generic mapped type JevAnswersOf<Q> per key — this one assertion\n // follows the per-key runtime validation (present, type, criteria) above.\n answers: answers as JevAnswersOf<Q>,\n model: wire.model,\n id: wire.id,\n provider: wire.provider,\n usage: {\n inputTokens: wire.usage.inputTokens,\n outputTokens: wire.usage.outputTokens,\n costUSD: wire.usage.cost,\n },\n durationMs,\n };\n }\n\n /**\n * Check the model is servable. `~typesafe/jev-latest` legitimately reports\n * `endpoints: []` yet still works as a `model` value, so `ok` tracks HTTP\n * 200 only — an empty endpoint list is never treated as \"down\".\n *\n * GET /api/v1/models/<model>/endpoints\n */\n async health(): Promise<JevHealthResult> {\n const apiKey = this.resolveApiKey();\n const model = this.defaultModel;\n const url = `${JEV_MODELS_BASE_URL}/${model}/endpoints`;\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n } catch {\n return { ok: false, model, endpoints: 0 };\n }\n if (!response.ok) {\n return { ok: false, model, endpoints: 0 };\n }\n\n let endpoints = 0;\n try {\n const parsed: unknown = JSON.parse(await response.text());\n if (isRecord(parsed) && isRecord(parsed.data) && Array.isArray(parsed.data.endpoints)) {\n endpoints = parsed.data.endpoints.length;\n }\n } catch {\n // Non-JSON body: still HTTP 200, so `ok` stays true with endpoints 0.\n }\n return { ok: true, model, endpoints };\n }\n\n private resolveApiKey(): string {\n const apiKey = this.apiKeyOverride ?? process.env.OPENROUTER_API_KEY ?? process.env.OPEN_ROUTER_API_KEY;\n if (!apiKey) {\n throw new JevError(\n 'Jev: no API key. Set OPENROUTER_API_KEY (or OPEN_ROUTER_API_KEY) in the environment, or pass { apiKey } to JevProvider.',\n );\n }\n return apiKey;\n }\n}\n\n// ============================================================================\n// Singleton\n// ============================================================================\n\nlet sharedInstance: JevProvider | null = null;\n\n/**\n * Get the singleton Jev provider instance.\n *\n * Creates the instance on first call, returns cached instance thereafter.\n *\n * @param {JevProviderOptions} [options] - Provider configuration options\n * @returns {JevProvider} The Jev provider instance\n */\nexport function getJevProvider(options?: JevProviderOptions): JevProvider {\n if (!sharedInstance) {\n sharedInstance = new JevProvider(options);\n }\n return sharedInstance;\n}\n\nexport function resetJevProvider(): void {\n sharedInstance = null;\n}\n\nexport function isJevAvailable(): boolean {\n return Boolean(process.env.OPENROUTER_API_KEY ?? process.env.OPEN_ROUTER_API_KEY);\n}\n"],"mappings":"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","names":["DEFAULT_TIMEOUT_MS","sharedInstance"]}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RateLimiter,
|
|
3
|
-
getGlobalRateLimiter
|
|
3
|
+
getGlobalRateLimiter
|
|
4
|
+
} from "./chunk-MOIECDMB.js";
|
|
5
|
+
import {
|
|
4
6
|
getGlobalTokenTracker
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-T6AKOBX3.js";
|
|
6
8
|
|
|
7
9
|
// src/structured-output.ts
|
|
8
10
|
import OpenAI from "openai";
|
|
@@ -175,4 +177,4 @@ export {
|
|
|
175
177
|
resetStructuredOutputClient,
|
|
176
178
|
isStructuredOutputAvailable
|
|
177
179
|
};
|
|
178
|
-
//# sourceMappingURL=chunk-
|
|
180
|
+
//# sourceMappingURL=chunk-SJE3GTGZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/structured-output.ts"],"sourcesContent":["/**\n * Structured Output Client for OpenAI\n *\n * Uses OpenAI's structured outputs feature (json_schema response_format)\n * to guarantee schema compliance at generation time.\n *\n * The system prompt builder is injectable so consumers can provide\n * domain-specific prompts (e.g., orbital schema references).\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions';\nimport type { ResponseFormatJSONSchema } from 'openai/resources/shared';\nimport { z } from 'zod';\nimport {\n RateLimiter,\n getGlobalRateLimiter,\n type RateLimiterOptions,\n} from './rate-limiter.js';\nimport { TokenTracker, getGlobalTokenTracker } from './token-tracker.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * JSON Schema type used for OpenAI structured outputs.\n */\nexport interface JsonSchema {\n type?: string | string[];\n properties?: Record<string, JsonSchema>;\n required?: string[];\n items?: JsonSchema;\n enum?: unknown[];\n const?: unknown;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n allOf?: JsonSchema[];\n $ref?: string;\n $defs?: Record<string, JsonSchema>;\n definitions?: Record<string, JsonSchema>;\n additionalProperties?: boolean | JsonSchema;\n description?: string;\n default?: unknown;\n minItems?: number;\n maxItems?: number;\n minLength?: number;\n}\n\nexport interface StructuredOutputOptions {\n model?: string;\n temperature?: number;\n maxTokens?: number;\n rateLimiter?: RateLimiterOptions;\n useGlobalRateLimiter?: boolean;\n trackTokens?: boolean;\n}\n\nexport interface StructuredGenerationOptions {\n /** User's natural language request */\n userRequest: string;\n /** Model to use (overrides client default) */\n model?: string;\n /** Temperature (overrides client default) */\n temperature?: number;\n /** Maximum tokens (overrides client default) */\n maxTokens?: number;\n /** JSON Schema for structured output */\n jsonSchema?: JsonSchema;\n /** Schema name for the json_schema response format */\n schemaName?: string;\n /** System prompt override */\n systemPrompt?: string;\n /** System prompt builder function (called dynamically) */\n buildSystemPrompt?: () => string;\n /** Additional system prompt instructions */\n additionalInstructions?: string;\n /** Existing context for updates (e.g., existing schema JSON) */\n existingContext?: string;\n /** Skip post-generation validation (default: false) */\n skipValidation?: boolean;\n}\n\nexport interface StructuredGenerationResult<T = unknown> {\n /** Generated data (guaranteed to match JSON Schema structure) */\n data: T;\n /** Raw JSON string from API */\n raw: string;\n /** Token usage statistics */\n usage: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n /** Generation latency in milliseconds */\n latencyMs: number;\n /** Model used for generation */\n model: string;\n /** Zod validation result (if not skipped) */\n zodValidation?: {\n success: boolean;\n errors?: z.ZodError['issues'];\n };\n}\n\nexport const STRUCTURED_OUTPUT_MODELS = {\n GPT5_MINI: 'gpt-5-mini',\n GPT4O_MINI: 'gpt-4o-mini',\n GPT4O: 'gpt-4o',\n GPT4O_2024_08_06: 'gpt-4o-2024-08-06',\n} as const;\n\n// ============================================================================\n// Default System Prompt\n// ============================================================================\n\nconst DEFAULT_SYSTEM_PROMPT = `You are an expert application architect that generates structured schemas from natural language requirements.\n\nGenerate a complete, well-structured schema based on the user's requirements. Follow the JSON Schema structure exactly.`;\n\n// ============================================================================\n// Structured Output Client\n// ============================================================================\n\nexport class StructuredOutputClient {\n private openai: OpenAI;\n private rateLimiter: RateLimiter;\n private tokenTracker: TokenTracker | null;\n private defaultModel: string;\n private defaultTemperature: number;\n private defaultMaxTokens: number;\n\n constructor(options: StructuredOutputOptions = {}) {\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPENAI_API_KEY environment variable is required for StructuredOutputClient',\n );\n }\n\n this.openai = new OpenAI({ apiKey });\n this.defaultModel = options.model || STRUCTURED_OUTPUT_MODELS.GPT5_MINI;\n this.defaultTemperature = options.temperature ?? 0.3;\n this.defaultMaxTokens = options.maxTokens ?? 16384;\n\n this.rateLimiter =\n options.useGlobalRateLimiter !== false\n ? getGlobalRateLimiter(options.rateLimiter)\n : new RateLimiter(options.rateLimiter);\n\n this.tokenTracker =\n options.trackTokens !== false\n ? getGlobalTokenTracker(this.defaultModel)\n : null;\n\n console.log(\n `[StructuredOutputClient] Initialized with model: ${this.defaultModel}`,\n );\n }\n\n private usesMaxCompletionTokens(model: string): boolean {\n const m = model.toLowerCase();\n return (\n m.startsWith('o1') ||\n m.startsWith('gpt-5') ||\n m.includes('o1-') ||\n m.includes('o3')\n );\n }\n\n /**\n * Generate structured output with guaranteed JSON Schema compliance.\n */\n async generate<T = unknown>(\n options: StructuredGenerationOptions,\n ): Promise<StructuredGenerationResult<T>> {\n const model = options.model || this.defaultModel;\n const temperature = options.temperature ?? this.defaultTemperature;\n const maxTokens = options.maxTokens ?? this.defaultMaxTokens;\n const startTime = Date.now();\n\n const jsonSchema: JsonSchema = options.jsonSchema || {\n type: 'object',\n properties: {},\n required: [],\n additionalProperties: false,\n };\n\n // Build system prompt\n let systemPrompt: string;\n if (options.systemPrompt) {\n systemPrompt = options.systemPrompt;\n } else if (options.buildSystemPrompt) {\n systemPrompt = options.buildSystemPrompt();\n } else {\n systemPrompt = DEFAULT_SYSTEM_PROMPT;\n }\n\n if (options.additionalInstructions) {\n systemPrompt += `\\n\\n## Additional Instructions\\n${options.additionalInstructions}`;\n }\n\n // Build user prompt\n let userPrompt = options.userRequest;\n if (options.existingContext) {\n userPrompt += `\\n\\n## Existing Context\\nUpdate based on the above request:\\n\\`\\`\\`json\\n${options.existingContext}\\n\\`\\`\\``;\n }\n\n const schemaName = options.schemaName || 'structured_output';\n\n console.log(\n `[StructuredOutputClient] Generating with ${model}...`,\n );\n console.log(\n `[StructuredOutputClient] Request: \"${options.userRequest.slice(0, 80)}...\"`,\n );\n\n const response = await this.rateLimiter.execute(async () => {\n const isReasoningModel = this.usesMaxCompletionTokens(model);\n\n const tokenParam = isReasoningModel\n ? { max_completion_tokens: maxTokens }\n : { max_tokens: maxTokens };\n\n const tempParam = isReasoningModel ? {} : { temperature };\n\n const params: ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt },\n ],\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: schemaName,\n strict: true,\n schema: jsonSchema as ResponseFormatJSONSchema.JSONSchema['schema'],\n },\n },\n ...tempParam,\n ...tokenParam,\n };\n\n return this.openai.chat.completions.create(params);\n });\n\n const latencyMs = Date.now() - startTime;\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error('No content in OpenAI response');\n }\n\n let data: T;\n try {\n data = JSON.parse(content) as T;\n } catch (error) {\n throw new Error(`Failed to parse response JSON: ${error}`);\n }\n\n const usage = {\n promptTokens: response.usage?.prompt_tokens || 0,\n completionTokens: response.usage?.completion_tokens || 0,\n totalTokens: response.usage?.total_tokens || 0,\n };\n\n if (this.tokenTracker) {\n const cachedTokens =\n (response.usage as { prompt_tokens_details?: { cached_tokens?: number } } | undefined)\n ?.prompt_tokens_details?.cached_tokens ?? 0;\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: 'structured-output',\n cachedPromptTokens: cachedTokens,\n });\n }\n\n console.log(\n `[StructuredOutputClient] Generated in ${latencyMs}ms, ${usage.totalTokens} tokens`,\n );\n\n let zodValidation: StructuredGenerationResult['zodValidation'];\n if (!options.skipValidation) {\n zodValidation = { success: true };\n }\n\n return {\n data,\n raw: content,\n usage,\n latencyMs,\n model,\n zodValidation,\n };\n }\n\n getModel(): string {\n return this.defaultModel;\n }\n\n getRateLimiterStatus() {\n return this.rateLimiter.getStatus();\n }\n\n getTokenUsage() {\n return this.tokenTracker?.getSummary() ?? null;\n }\n}\n\n// ============================================================================\n// Singleton Instance\n// ============================================================================\n\nlet sharedClient: StructuredOutputClient | null = null;\n\n/**\n * Get the singleton structured output client instance.\n *\n * Creates the instance on first call, returns cached instance thereafter.\n *\n * @param {StructuredOutputOptions} [options] - Client configuration options\n * @returns {StructuredOutputClient} The structured output client instance\n */\nexport function getStructuredOutputClient(\n options?: StructuredOutputOptions,\n): StructuredOutputClient {\n if (!sharedClient) {\n sharedClient = new StructuredOutputClient(options);\n }\n return sharedClient;\n}\n\nexport function resetStructuredOutputClient(): void {\n sharedClient = null;\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\nexport function isStructuredOutputAvailable(): boolean {\n return !!process.env.OPENAI_API_KEY;\n}\n"],"mappings":";;;;;;;AAYA,OAAO,YAAY;AA+FZ,IAAM,2BAA2B;AAAA,EACtC,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,kBAAkB;AACpB;AAMA,IAAM,wBAAwB;AAAA;AAAA;AAQvB,IAAM,yBAAN,MAA6B;AAAA,EAQlC,YAAY,UAAmC,CAAC,GAAG;AACjD,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,SAAS,IAAI,OAAO,EAAE,OAAO,CAAC;AACnC,SAAK,eAAe,QAAQ,SAAS,yBAAyB;AAC9D,SAAK,qBAAqB,QAAQ,eAAe;AACjD,SAAK,mBAAmB,QAAQ,aAAa;AAE7C,SAAK,cACH,QAAQ,yBAAyB,QAC7B,qBAAqB,QAAQ,WAAW,IACxC,IAAI,YAAY,QAAQ,WAAW;AAEzC,SAAK,eACH,QAAQ,gBAAgB,QACpB,sBAAsB,KAAK,YAAY,IACvC;AAEN,YAAQ;AAAA,MACN,oDAAoD,KAAK,YAAY;AAAA,IACvE;AAAA,EACF;AAAA,EAEQ,wBAAwB,OAAwB;AACtD,UAAM,IAAI,MAAM,YAAY;AAC5B,WACE,EAAE,WAAW,IAAI,KACjB,EAAE,WAAW,OAAO,KACpB,EAAE,SAAS,KAAK,KAChB,EAAE,SAAS,IAAI;AAAA,EAEnB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SACJ,SACwC;AACxC,UAAM,QAAQ,QAAQ,SAAS,KAAK;AACpC,UAAM,cAAc,QAAQ,eAAe,KAAK;AAChD,UAAM,YAAY,QAAQ,aAAa,KAAK;AAC5C,UAAM,YAAY,KAAK,IAAI;AAE3B,UAAM,aAAyB,QAAQ,cAAc;AAAA,MACnD,MAAM;AAAA,MACN,YAAY,CAAC;AAAA,MACb,UAAU,CAAC;AAAA,MACX,sBAAsB;AAAA,IACxB;AAGA,QAAI;AACJ,QAAI,QAAQ,cAAc;AACxB,qBAAe,QAAQ;AAAA,IACzB,WAAW,QAAQ,mBAAmB;AACpC,qBAAe,QAAQ,kBAAkB;AAAA,IAC3C,OAAO;AACL,qBAAe;AAAA,IACjB;AAEA,QAAI,QAAQ,wBAAwB;AAClC,sBAAgB;AAAA;AAAA;AAAA,EAAmC,QAAQ,sBAAsB;AAAA,IACnF;AAGA,QAAI,aAAa,QAAQ;AACzB,QAAI,QAAQ,iBAAiB;AAC3B,oBAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAA4E,QAAQ,eAAe;AAAA;AAAA,IACnH;AAEA,UAAM,aAAa,QAAQ,cAAc;AAEzC,YAAQ;AAAA,MACN,4CAA4C,KAAK;AAAA,IACnD;AACA,YAAQ;AAAA,MACN,sCAAsC,QAAQ,YAAY,MAAM,GAAG,EAAE,CAAC;AAAA,IACxE;AAEA,UAAM,WAAW,MAAM,KAAK,YAAY,QAAQ,YAAY;AAC1D,YAAM,mBAAmB,KAAK,wBAAwB,KAAK;AAE3D,YAAM,aAAa,mBACf,EAAE,uBAAuB,UAAU,IACnC,EAAE,YAAY,UAAU;AAE5B,YAAM,YAAY,mBAAmB,CAAC,IAAI,EAAE,YAAY;AAExD,YAAM,SAAiD;AAAA,QACrD;AAAA,QACA,UAAU;AAAA,UACR,EAAE,MAAM,UAAU,SAAS,aAAa;AAAA,UACxC,EAAE,MAAM,QAAQ,SAAS,WAAW;AAAA,QACtC;AAAA,QACA,iBAAiB;AAAA,UACf,MAAM;AAAA,UACN,aAAa;AAAA,YACX,MAAM;AAAA,YACN,QAAQ;AAAA,YACR,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,QACA,GAAG;AAAA,QACH,GAAG;AAAA,MACL;AAEA,aAAO,KAAK,OAAO,KAAK,YAAY,OAAO,MAAM;AAAA,IACnD,CAAC;AAED,UAAM,YAAY,KAAK,IAAI,IAAI;AAE/B,UAAM,UAAU,SAAS,QAAQ,CAAC,GAAG,SAAS;AAC9C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,IAAI,MAAM,kCAAkC,KAAK,EAAE;AAAA,IAC3D;AAEA,UAAM,QAAQ;AAAA,MACZ,cAAc,SAAS,OAAO,iBAAiB;AAAA,MAC/C,kBAAkB,SAAS,OAAO,qBAAqB;AAAA,MACvD,aAAa,SAAS,OAAO,gBAAgB;AAAA,IAC/C;AAEA,QAAI,KAAK,cAAc;AACrB,YAAM,eACH,SAAS,OACN,uBAAuB,iBAAiB;AAC9C,WAAK,aAAa,SAAS,MAAM,cAAc,MAAM,kBAAkB;AAAA,QACrE,UAAU;AAAA,QACV,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,YAAQ;AAAA,MACN,yCAAyC,SAAS,OAAO,MAAM,WAAW;AAAA,IAC5E;AAEA,QAAI;AACJ,QAAI,CAAC,QAAQ,gBAAgB;AAC3B,sBAAgB,EAAE,SAAS,KAAK;AAAA,IAClC;AAEA,WAAO;AAAA,MACL;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,WAAmB;AACjB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,uBAAuB;AACrB,WAAO,KAAK,YAAY,UAAU;AAAA,EACpC;AAAA,EAEA,gBAAgB;AACd,WAAO,KAAK,cAAc,WAAW,KAAK;AAAA,EAC5C;AACF;AAMA,IAAI,eAA8C;AAU3C,SAAS,0BACd,SACwB;AACxB,MAAI,CAAC,cAAc;AACjB,mBAAe,IAAI,uBAAuB,OAAO;AAAA,EACnD;AACA,SAAO;AACT;AAEO,SAAS,8BAAoC;AAClD,iBAAe;AACjB;AAMO,SAAS,8BAAuC;AACrD,SAAO,CAAC,CAAC,QAAQ,IAAI;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/structured-output.ts"],"sourcesContent":["/**\n * Structured Output Client for OpenAI\n *\n * Uses OpenAI's structured outputs feature (json_schema response_format)\n * to guarantee schema compliance at generation time.\n *\n * The system prompt builder is injectable so consumers can provide\n * domain-specific prompts (e.g., orbital schema references).\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions';\nimport type { ResponseFormatJSONSchema } from 'openai/resources/shared';\nimport { z } from 'zod';\nimport {\n RateLimiter,\n getGlobalRateLimiter,\n type RateLimiterOptions,\n} from './rate-limiter.js';\nimport { TokenTracker, getGlobalTokenTracker } from './token-tracker.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * JSON Schema type used for OpenAI structured outputs.\n */\nexport interface JsonSchema {\n type?: string | string[];\n properties?: Record<string, JsonSchema>;\n required?: string[];\n items?: JsonSchema;\n enum?: unknown[];\n const?: unknown;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n allOf?: JsonSchema[];\n $ref?: string;\n $defs?: Record<string, JsonSchema>;\n definitions?: Record<string, JsonSchema>;\n additionalProperties?: boolean | JsonSchema;\n description?: string;\n default?: unknown;\n minItems?: number;\n maxItems?: number;\n minLength?: number;\n}\n\nexport interface StructuredOutputOptions {\n model?: string;\n temperature?: number;\n maxTokens?: number;\n rateLimiter?: RateLimiterOptions;\n useGlobalRateLimiter?: boolean;\n trackTokens?: boolean;\n}\n\nexport interface StructuredGenerationOptions {\n /** User's natural language request */\n userRequest: string;\n /** Model to use (overrides client default) */\n model?: string;\n /** Temperature (overrides client default) */\n temperature?: number;\n /** Maximum tokens (overrides client default) */\n maxTokens?: number;\n /** JSON Schema for structured output */\n jsonSchema?: JsonSchema;\n /** Schema name for the json_schema response format */\n schemaName?: string;\n /** System prompt override */\n systemPrompt?: string;\n /** System prompt builder function (called dynamically) */\n buildSystemPrompt?: () => string;\n /** Additional system prompt instructions */\n additionalInstructions?: string;\n /** Existing context for updates (e.g., existing schema JSON) */\n existingContext?: string;\n /** Skip post-generation validation (default: false) */\n skipValidation?: boolean;\n}\n\nexport interface StructuredGenerationResult<T = unknown> {\n /** Generated data (guaranteed to match JSON Schema structure) */\n data: T;\n /** Raw JSON string from API */\n raw: string;\n /** Token usage statistics */\n usage: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n /** Generation latency in milliseconds */\n latencyMs: number;\n /** Model used for generation */\n model: string;\n /** Zod validation result (if not skipped) */\n zodValidation?: {\n success: boolean;\n errors?: z.ZodError['issues'];\n };\n}\n\nexport const STRUCTURED_OUTPUT_MODELS = {\n GPT5_MINI: 'gpt-5-mini',\n GPT4O_MINI: 'gpt-4o-mini',\n GPT4O: 'gpt-4o',\n GPT4O_2024_08_06: 'gpt-4o-2024-08-06',\n} as const;\n\n// ============================================================================\n// Default System Prompt\n// ============================================================================\n\nconst DEFAULT_SYSTEM_PROMPT = `You are an expert application architect that generates structured schemas from natural language requirements.\n\nGenerate a complete, well-structured schema based on the user's requirements. Follow the JSON Schema structure exactly.`;\n\n// ============================================================================\n// Structured Output Client\n// ============================================================================\n\nexport class StructuredOutputClient {\n private openai: OpenAI;\n private rateLimiter: RateLimiter;\n private tokenTracker: TokenTracker | null;\n private defaultModel: string;\n private defaultTemperature: number;\n private defaultMaxTokens: number;\n\n constructor(options: StructuredOutputOptions = {}) {\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPENAI_API_KEY environment variable is required for StructuredOutputClient',\n );\n }\n\n this.openai = new OpenAI({ apiKey });\n this.defaultModel = options.model || STRUCTURED_OUTPUT_MODELS.GPT5_MINI;\n this.defaultTemperature = options.temperature ?? 0.3;\n this.defaultMaxTokens = options.maxTokens ?? 16384;\n\n this.rateLimiter =\n options.useGlobalRateLimiter !== false\n ? getGlobalRateLimiter(options.rateLimiter)\n : new RateLimiter(options.rateLimiter);\n\n this.tokenTracker =\n options.trackTokens !== false\n ? getGlobalTokenTracker(this.defaultModel)\n : null;\n\n console.log(\n `[StructuredOutputClient] Initialized with model: ${this.defaultModel}`,\n );\n }\n\n private usesMaxCompletionTokens(model: string): boolean {\n const m = model.toLowerCase();\n return (\n m.startsWith('o1') ||\n m.startsWith('gpt-5') ||\n m.includes('o1-') ||\n m.includes('o3')\n );\n }\n\n /**\n * Generate structured output with guaranteed JSON Schema compliance.\n */\n async generate<T = unknown>(\n options: StructuredGenerationOptions,\n ): Promise<StructuredGenerationResult<T>> {\n const model = options.model || this.defaultModel;\n const temperature = options.temperature ?? this.defaultTemperature;\n const maxTokens = options.maxTokens ?? this.defaultMaxTokens;\n const startTime = Date.now();\n\n const jsonSchema: JsonSchema = options.jsonSchema || {\n type: 'object',\n properties: {},\n required: [],\n additionalProperties: false,\n };\n\n // Build system prompt\n let systemPrompt: string;\n if (options.systemPrompt) {\n systemPrompt = options.systemPrompt;\n } else if (options.buildSystemPrompt) {\n systemPrompt = options.buildSystemPrompt();\n } else {\n systemPrompt = DEFAULT_SYSTEM_PROMPT;\n }\n\n if (options.additionalInstructions) {\n systemPrompt += `\\n\\n## Additional Instructions\\n${options.additionalInstructions}`;\n }\n\n // Build user prompt\n let userPrompt = options.userRequest;\n if (options.existingContext) {\n userPrompt += `\\n\\n## Existing Context\\nUpdate based on the above request:\\n\\`\\`\\`json\\n${options.existingContext}\\n\\`\\`\\``;\n }\n\n const schemaName = options.schemaName || 'structured_output';\n\n console.log(\n `[StructuredOutputClient] Generating with ${model}...`,\n );\n console.log(\n `[StructuredOutputClient] Request: \"${options.userRequest.slice(0, 80)}...\"`,\n );\n\n const response = await this.rateLimiter.execute(async () => {\n const isReasoningModel = this.usesMaxCompletionTokens(model);\n\n const tokenParam = isReasoningModel\n ? { max_completion_tokens: maxTokens }\n : { max_tokens: maxTokens };\n\n const tempParam = isReasoningModel ? {} : { temperature };\n\n const params: ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt },\n ],\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: schemaName,\n strict: true,\n schema: jsonSchema as ResponseFormatJSONSchema.JSONSchema['schema'],\n },\n },\n ...tempParam,\n ...tokenParam,\n };\n\n return this.openai.chat.completions.create(params);\n });\n\n const latencyMs = Date.now() - startTime;\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error('No content in OpenAI response');\n }\n\n let data: T;\n try {\n data = JSON.parse(content) as T;\n } catch (error) {\n throw new Error(`Failed to parse response JSON: ${error}`);\n }\n\n const usage = {\n promptTokens: response.usage?.prompt_tokens || 0,\n completionTokens: response.usage?.completion_tokens || 0,\n totalTokens: response.usage?.total_tokens || 0,\n };\n\n if (this.tokenTracker) {\n const cachedTokens =\n (response.usage as { prompt_tokens_details?: { cached_tokens?: number } } | undefined)\n ?.prompt_tokens_details?.cached_tokens ?? 0;\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: 'structured-output',\n cachedPromptTokens: cachedTokens,\n });\n }\n\n console.log(\n `[StructuredOutputClient] Generated in ${latencyMs}ms, ${usage.totalTokens} tokens`,\n );\n\n let zodValidation: StructuredGenerationResult['zodValidation'];\n if (!options.skipValidation) {\n zodValidation = { success: true };\n }\n\n return {\n data,\n raw: content,\n usage,\n latencyMs,\n model,\n zodValidation,\n };\n }\n\n getModel(): string {\n return this.defaultModel;\n }\n\n getRateLimiterStatus() {\n return this.rateLimiter.getStatus();\n }\n\n getTokenUsage() {\n return this.tokenTracker?.getSummary() ?? null;\n }\n}\n\n// ============================================================================\n// Singleton Instance\n// ============================================================================\n\nlet sharedClient: StructuredOutputClient | null = null;\n\n/**\n * Get the singleton structured output client instance.\n *\n * Creates the instance on first call, returns cached instance thereafter.\n *\n * @param {StructuredOutputOptions} [options] - Client configuration options\n * @returns {StructuredOutputClient} The structured output client instance\n */\nexport function getStructuredOutputClient(\n options?: StructuredOutputOptions,\n): StructuredOutputClient {\n if (!sharedClient) {\n sharedClient = new StructuredOutputClient(options);\n }\n return sharedClient;\n}\n\nexport function resetStructuredOutputClient(): void {\n sharedClient = null;\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\nexport function isStructuredOutputAvailable(): boolean {\n return !!process.env.OPENAI_API_KEY;\n}\n"],"mappings":"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","names":[]}
|