@rulvar/openai 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ points the same adapter at any OpenAI-compatible endpoint (Ollama, vLLM,
6
6
  gateways) with an explicit id and baseURL. Models are addressed as
7
7
  `'openai:<model>'` in routing.
8
8
 
9
- Part of [rulvar](https://rulvar.com), an embeddable TypeScript engine
9
+ Part of [Rulvar](https://rulvar.com), an embeddable TypeScript engine
10
10
  for durable, budget-bounded multi-agent LLM workflows, where a completed
11
11
  LLM call is never paid for twice. Full documentation:
12
12
  [docs.rulvar.com](https://docs.rulvar.com).
package/dist/index.d.ts CHANGED
@@ -9,7 +9,15 @@ interface OpenAiModelInfo {
9
9
  }
10
10
  /** Static seed table of the current model set. */
11
11
  declare const OPENAI_MODELS: Record<string, OpenAiModelInfo>;
12
- /** Unknown OpenAI models are assumed current-generation Responses models. */
12
+ /**
13
+ * Unknown OpenAI models are assumed current-generation Responses models
14
+ * with conservative transport caps and NO pricing: a fabricated price row
15
+ * silently misprices every model newer than this table (it priced
16
+ * gpt-5.6-sol as gpt-5.4 before the 5.6 entries landed). Hosts price an
17
+ * unrecognized hosted model via a versioned createEngine({ pricing }) row;
18
+ * until then its usage surfaces in CostReport.unpriced and a run ceiling
19
+ * warns that it cannot bound the model.
20
+ */
13
21
  declare function openAiModelInfo(model: string): OpenAiModelInfo;
14
22
  //#endregion
15
23
  //#region src/adapter.d.ts
@@ -98,15 +106,17 @@ type ResponsesStreamEvent = Record<string, unknown> & {
98
106
  /** Normalizes Responses usage: input_tokens already includes cached reads. */
99
107
  declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined): Usage;
100
108
  /**
101
- * Maps the typed Responses SSE stream to ChatEvents.
102
- * Canonical parts come from the typed output array,
103
- * never the output_text aggregate. Raw output items ride
109
+ * Maps the typed Responses SSE stream to ChatEvents, yielding each
110
+ * canonical event AS the corresponding provider event is consumed: the
111
+ * consumer's pull drives the provider read (natural backpressure, no
112
+ * buffering, no detached work). Canonical parts come from the typed
113
+ * output array, never the output_text aggregate. Raw output items ride
104
114
  * finish.providerMetadata.openai.outputItems so the runtime can retain
105
115
  * reasoning items as provider-raw parts.
106
116
  */
107
- declare function mapResponsesStream(stream: AsyncIterable<ResponsesStreamEvent>, ids: OpenAiIdMap, emit: (event: ChatEvent) => void, options?: {
117
+ declare function mapResponsesStream(stream: AsyncIterable<ResponsesStreamEvent>, ids: OpenAiIdMap, options?: {
108
118
  effortDownmapped?: boolean;
109
- }): Promise<void>;
119
+ }): AsyncGenerator<ChatEvent, void>;
110
120
  /** Projects SDK/API errors into the retryable WireError vocabulary. */
111
121
  declare function openAiErrorToWire(error: unknown): WireError;
112
122
  /**
@@ -117,7 +127,11 @@ declare function openAiErrorToWire(error: unknown): WireError;
117
127
  * events, never silent.
118
128
  */
119
129
  declare function buildChatCompletionsParams(req: ChatRequest, ids: OpenAiIdMap): Record<string, unknown>;
120
- /** Delta-patched chunk assembly for the degraded path. */
121
- declare function mapChatCompletionsStream(stream: AsyncIterable<Record<string, unknown>>, ids: OpenAiIdMap, emit: (event: ChatEvent) => void): Promise<void>;
130
+ /**
131
+ * Delta-patched chunk assembly for the degraded path; yields each
132
+ * canonical event as its chunk is consumed (same live-streaming contract
133
+ * as mapResponsesStream).
134
+ */
135
+ declare function mapChatCompletionsStream(stream: AsyncIterable<Record<string, unknown>>, ids: OpenAiIdMap): AsyncGenerator<ChatEvent, void>;
122
136
  //#endregion
123
137
  export { CONSERVATIVE_COMPATIBLE_CAPS, OPENAI_MODELS, type OpenAiAdapterOptions, type OpenAiClientLike, type OpenAiCompatibleConfig, OpenAiIdMap, type OpenAiModelInfo, type ResponsesStreamEvent, buildChatCompletionsParams, buildResponsesParams, mapChatCompletionsStream, mapOpenAiEffort, mapResponsesStream, normalizeOpenAiUsage, openAiErrorToWire, openAiModelInfo, openai, openaiCompatible };
package/dist/index.js CHANGED
@@ -16,49 +16,72 @@ function responses(contextWindow, maxOutputTokens, pricing) {
16
16
  reasoningEfforts: [...REASONING_EFFORTS, "max"],
17
17
  contextWindow,
18
18
  maxOutputTokens,
19
- pricing: {
20
- inputUsdPerMTok: pricing.in,
21
- outputUsdPerMTok: pricing.out,
22
- cacheReadUsdPerMTok: pricing.cacheRead
23
- }
19
+ ...pricing === void 0 ? {} : { pricing }
24
20
  },
25
21
  api: "responses",
26
22
  reasoning: true
27
23
  };
28
24
  }
25
+ /**
26
+ * GPT-5.6 Sol (developers.openai.com/api/docs/models/gpt-5.6-sol):
27
+ * prompts strictly above 272K input tokens price the FULL request at
28
+ * 2x input and 1.5x output; cache writes bill at 1.25x uncached input.
29
+ */
30
+ const GPT_56_SOL = responses(105e4, 128e3, {
31
+ inputUsdPerMTok: 5,
32
+ outputUsdPerMTok: 30,
33
+ cacheReadUsdPerMTok: .5,
34
+ cacheWriteUsdPerMTok: 6.25,
35
+ tiers: [{
36
+ aboveInputTokens: 272e3,
37
+ inputMultiplier: 2,
38
+ outputMultiplier: 1.5
39
+ }]
40
+ });
29
41
  /** Static seed table of the current model set. */
30
42
  const OPENAI_MODELS = {
43
+ "gpt-5.6-sol": GPT_56_SOL,
44
+ "gpt-5.6": GPT_56_SOL,
31
45
  "gpt-5.5": responses(4e5, 128e3, {
32
- in: 10,
33
- out: 40,
34
- cacheRead: 1
46
+ inputUsdPerMTok: 10,
47
+ outputUsdPerMTok: 40,
48
+ cacheReadUsdPerMTok: 1
35
49
  }),
36
50
  "gpt-5.5-pro": responses(4e5, 128e3, {
37
- in: 40,
38
- out: 160,
39
- cacheRead: 4
51
+ inputUsdPerMTok: 40,
52
+ outputUsdPerMTok: 160,
53
+ cacheReadUsdPerMTok: 4
40
54
  }),
41
55
  "gpt-5.4": responses(272e3, 1e5, {
42
- in: 6,
43
- out: 24,
44
- cacheRead: .6
56
+ inputUsdPerMTok: 6,
57
+ outputUsdPerMTok: 24,
58
+ cacheReadUsdPerMTok: .6
45
59
  }),
46
60
  "gpt-5.4-mini": responses(272e3, 1e5, {
47
- in: 1.2,
48
- out: 4.8,
49
- cacheRead: .12
61
+ inputUsdPerMTok: 1.2,
62
+ outputUsdPerMTok: 4.8,
63
+ cacheReadUsdPerMTok: .12
50
64
  })
51
65
  };
52
- /** Unknown OpenAI models are assumed current-generation Responses models. */
66
+ /**
67
+ * Unknown OpenAI models are assumed current-generation Responses models
68
+ * with conservative transport caps and NO pricing: a fabricated price row
69
+ * silently misprices every model newer than this table (it priced
70
+ * gpt-5.6-sol as gpt-5.4 before the 5.6 entries landed). Hosts price an
71
+ * unrecognized hosted model via a versioned createEngine({ pricing }) row;
72
+ * until then its usage surfaces in CostReport.unpriced and a run ceiling
73
+ * warns that it cannot bound the model.
74
+ */
53
75
  function openAiModelInfo(model) {
54
76
  const exact = OPENAI_MODELS[model];
55
77
  if (exact !== void 0) return exact;
56
- for (const [name, info] of Object.entries(OPENAI_MODELS)) if (model.startsWith(`${name}-`)) return info;
57
- return responses(272e3, 1e5, {
58
- in: 6,
59
- out: 24,
60
- cacheRead: .6
61
- });
78
+ let best;
79
+ for (const [name, info] of Object.entries(OPENAI_MODELS)) if (model.startsWith(`${name}-`) && (best === void 0 || name.length > best.name.length)) best = {
80
+ name,
81
+ info
82
+ };
83
+ if (best !== void 0) return best.info;
84
+ return responses(272e3, 1e5);
62
85
  }
63
86
  //#endregion
64
87
  //#region src/wire.ts
@@ -244,13 +267,15 @@ function normalizeOpenAiUsage(raw) {
244
267
  return usage;
245
268
  }
246
269
  /**
247
- * Maps the typed Responses SSE stream to ChatEvents.
248
- * Canonical parts come from the typed output array,
249
- * never the output_text aggregate. Raw output items ride
270
+ * Maps the typed Responses SSE stream to ChatEvents, yielding each
271
+ * canonical event AS the corresponding provider event is consumed: the
272
+ * consumer's pull drives the provider read (natural backpressure, no
273
+ * buffering, no detached work). Canonical parts come from the typed
274
+ * output array, never the output_text aggregate. Raw output items ride
250
275
  * finish.providerMetadata.openai.outputItems so the runtime can retain
251
276
  * reasoning items as provider-raw parts.
252
277
  */
253
- async function mapResponsesStream(stream, ids, emit, options) {
278
+ async function* mapResponsesStream(stream, ids, options) {
254
279
  const callIdByItemId = /* @__PURE__ */ new Map();
255
280
  for await (const event of stream) switch (event.type) {
256
281
  case "response.output_item.added": {
@@ -258,22 +283,22 @@ async function mapResponsesStream(stream, ids, emit, options) {
258
283
  if (item?.type === "function_call") {
259
284
  const callId = item.call_id ?? item.id;
260
285
  if (typeof item.id === "string") callIdByItemId.set(item.id, callId);
261
- emit({
286
+ yield {
262
287
  type: "tool-call-start",
263
288
  id: ids.canonicalFor(callId),
264
289
  name: item.name ?? ""
265
- });
290
+ };
266
291
  }
267
292
  break;
268
293
  }
269
294
  case "response.function_call_arguments.delta": {
270
295
  const itemId = event.item_id;
271
296
  const callId = itemId === void 0 ? void 0 : callIdByItemId.get(itemId);
272
- if (callId !== void 0) emit({
297
+ if (callId !== void 0) yield {
273
298
  type: "tool-call-delta",
274
299
  id: ids.canonicalFor(callId),
275
300
  argsTextDelta: event.delta ?? ""
276
- });
301
+ };
277
302
  break;
278
303
  }
279
304
  case "response.output_item.done": {
@@ -286,35 +311,35 @@ async function mapResponsesStream(stream, ids, emit, options) {
286
311
  } catch {
287
312
  args = { __unparsed: item.arguments };
288
313
  }
289
- emit({
314
+ yield {
290
315
  type: "tool-call-end",
291
316
  id: ids.canonicalFor(callId),
292
317
  args
293
- });
318
+ };
294
319
  }
295
320
  break;
296
321
  }
297
322
  case "response.output_text.delta":
298
- emit({
323
+ yield {
299
324
  type: "text-delta",
300
325
  text: event.delta ?? ""
301
- });
326
+ };
302
327
  break;
303
328
  case "response.reasoning_summary_text.delta":
304
329
  case "response.reasoning_text.delta":
305
- emit({
330
+ yield {
306
331
  type: "reasoning-delta",
307
332
  text: event.delta ?? ""
308
- });
333
+ };
309
334
  break;
310
335
  case "response.completed":
311
336
  case "response.incomplete": {
312
337
  const response = event.response;
313
338
  const usage = normalizeOpenAiUsage(response?.usage);
314
- emit({
339
+ yield {
315
340
  type: "usage",
316
341
  usage
317
- });
342
+ };
318
343
  let finish = { reason: "stop" };
319
344
  if (event.type === "response.incomplete") {
320
345
  const details = response?.incomplete_details;
@@ -336,29 +361,27 @@ async function mapResponsesStream(stream, ids, emit, options) {
336
361
  if (reasoningItems.length > 0) meta.retainedParts = reasoningItems;
337
362
  if (typeof response?.id === "string") meta.responseId = response.id;
338
363
  if (options?.effortDownmapped === true) meta.effortDownmapped = "max->xhigh";
339
- emit({
364
+ yield {
340
365
  type: "finish",
341
366
  finish,
342
367
  usage,
343
368
  providerMetadata: { openai: meta }
344
- });
369
+ };
345
370
  return;
346
371
  }
347
- case "response.failed": {
348
- const error = event.response?.error;
349
- emit({
372
+ case "response.failed":
373
+ yield {
350
374
  type: "error",
351
375
  error: {
352
376
  code: "agent",
353
- message: error?.message ?? "response.failed",
377
+ message: (event.response?.error)?.message ?? "response.failed",
354
378
  retryable: false,
355
379
  data: { kind: "transport" }
356
380
  }
357
- });
381
+ };
358
382
  return;
359
- }
360
383
  case "error":
361
- emit({
384
+ yield {
362
385
  type: "error",
363
386
  error: {
364
387
  code: "agent",
@@ -366,7 +389,7 @@ async function mapResponsesStream(stream, ids, emit, options) {
366
389
  retryable: false,
367
390
  data: { kind: "transport" }
368
391
  }
369
- });
392
+ };
370
393
  return;
371
394
  default: break;
372
395
  }
@@ -484,8 +507,12 @@ function buildChatCompletionsParams(req, ids) {
484
507
  };
485
508
  return params;
486
509
  }
487
- /** Delta-patched chunk assembly for the degraded path. */
488
- async function mapChatCompletionsStream(stream, ids, emit) {
510
+ /**
511
+ * Delta-patched chunk assembly for the degraded path; yields each
512
+ * canonical event as its chunk is consumed (same live-streaming contract
513
+ * as mapResponsesStream).
514
+ */
515
+ async function* mapChatCompletionsStream(stream, ids) {
489
516
  const pendingCalls = /* @__PURE__ */ new Map();
490
517
  let finishReason;
491
518
  let usage = {
@@ -497,10 +524,10 @@ async function mapChatCompletionsStream(stream, ids, emit) {
497
524
  for await (const chunk of stream) {
498
525
  const choice = chunk.choices?.[0];
499
526
  const delta = choice?.delta;
500
- if (typeof delta?.content === "string" && delta.content !== "") emit({
527
+ if (typeof delta?.content === "string" && delta.content !== "") yield {
501
528
  type: "text-delta",
502
529
  text: delta.content
503
- });
530
+ };
504
531
  const toolCalls = delta?.tool_calls;
505
532
  if (toolCalls !== void 0) for (const call of toolCalls) {
506
533
  const index = call.index ?? 0;
@@ -513,20 +540,20 @@ async function mapChatCompletionsStream(stream, ids, emit) {
513
540
  args: ""
514
541
  };
515
542
  pendingCalls.set(index, pending);
516
- emit({
543
+ yield {
517
544
  type: "tool-call-start",
518
545
  id: ids.canonicalFor(pending.id),
519
546
  name: fn?.name ?? ""
520
- });
547
+ };
521
548
  }
522
549
  if (typeof fn?.name === "string") pending.name += fn.name;
523
550
  if (typeof fn?.arguments === "string" && fn.arguments !== "") {
524
551
  pending.args += fn.arguments;
525
- emit({
552
+ yield {
526
553
  type: "tool-call-delta",
527
554
  id: ids.canonicalFor(pending.id),
528
555
  argsTextDelta: fn.arguments
529
- });
556
+ };
530
557
  }
531
558
  }
532
559
  if (typeof choice?.finish_reason === "string") finishReason = choice.finish_reason;
@@ -548,17 +575,17 @@ async function mapChatCompletionsStream(stream, ids, emit) {
548
575
  } catch {
549
576
  args = { __unparsed: pending.args };
550
577
  }
551
- emit({
578
+ yield {
552
579
  type: "tool-call-end",
553
580
  id: ids.canonicalFor(pending.id),
554
581
  args
555
- });
582
+ };
556
583
  }
557
- emit({
584
+ yield {
558
585
  type: "usage",
559
586
  usage
560
- });
561
- emit({
587
+ };
588
+ yield {
562
589
  type: "finish",
563
590
  finish: finishReason === "length" ? { reason: "max-tokens" } : finishReason === "tool_calls" ? { reason: "tool-calls" } : finishReason === "content_filter" ? {
564
591
  reason: "refusal",
@@ -566,7 +593,7 @@ async function mapChatCompletionsStream(stream, ids, emit) {
566
593
  } : { reason: "stop" },
567
594
  usage,
568
595
  providerMetadata: { openai: { degradedPath: "chat" } }
569
- });
596
+ };
570
597
  }
571
598
  //#endregion
572
599
  //#region src/adapter.ts
@@ -594,32 +621,27 @@ function openai(options = {}) {
594
621
  },
595
622
  async *stream(req, signal) {
596
623
  const info = openAiModelInfo(req.model);
597
- const pending = [];
598
- const emit = (event) => {
599
- pending.push(event);
600
- };
601
624
  try {
602
625
  if (info.api === "responses") {
603
626
  const { params, effortDownmapped } = buildResponsesParams(req, ids);
604
- await mapResponsesStream(await client.responses.create({
627
+ yield* mapResponsesStream(await client.responses.create({
605
628
  ...params,
606
629
  stream: true
607
- }, signal === void 0 ? void 0 : { signal }), ids, emit, { effortDownmapped });
630
+ }, signal === void 0 ? void 0 : { signal }), ids, { effortDownmapped });
608
631
  } else {
609
632
  const params = buildChatCompletionsParams(req, ids);
610
- await mapChatCompletionsStream(await client.chat.completions.create({
633
+ yield* mapChatCompletionsStream(await client.chat.completions.create({
611
634
  ...params,
612
635
  stream: true,
613
636
  stream_options: { include_usage: true }
614
- }, signal === void 0 ? void 0 : { signal }), ids, emit);
637
+ }, signal === void 0 ? void 0 : { signal }), ids);
615
638
  }
616
639
  } catch (thrown) {
617
- if (signal?.aborted !== true) pending.push({
640
+ if (signal?.aborted !== true) yield {
618
641
  type: "error",
619
642
  error: openAiErrorToWire(thrown)
620
- });
643
+ };
621
644
  }
622
- for (const event of pending) yield event;
623
645
  }
624
646
  };
625
647
  }
@@ -672,24 +694,19 @@ function openaiCompatible(cfg) {
672
694
  };
673
695
  },
674
696
  async *stream(req, signal) {
675
- const pending = [];
676
- const emit = (event) => {
677
- pending.push(event);
678
- };
679
697
  try {
680
698
  const params = buildChatCompletionsParams(req, ids);
681
- await mapChatCompletionsStream(await client.chat.completions.create({
699
+ yield* mapChatCompletionsStream(await client.chat.completions.create({
682
700
  ...params,
683
701
  stream: true,
684
702
  stream_options: { include_usage: true }
685
- }, signal === void 0 ? void 0 : { signal }), ids, emit);
703
+ }, signal === void 0 ? void 0 : { signal }), ids);
686
704
  } catch (thrown) {
687
- if (signal?.aborted !== true) pending.push({
705
+ if (signal?.aborted !== true) yield {
688
706
  type: "error",
689
707
  error: openAiErrorToWire(thrown)
690
- });
708
+ };
691
709
  }
692
- for (const event of pending) yield event;
693
710
  }
694
711
  };
695
712
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.5.2",
4
- "description": "rulvar first-class provider adapter for the OpenAI Responses API, plus the openaiCompatible factory.",
3
+ "version": "1.6.0",
4
+ "description": "Rulvar first-class provider adapter for the OpenAI Responses API, plus the openaiCompatible factory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "engines": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "openai": "^6.45.0",
26
- "@rulvar/core": "1.5.2"
26
+ "@rulvar/core": "1.6.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",