@rulvar/openai 1.17.0 → 1.19.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/index.d.ts CHANGED
@@ -7,6 +7,14 @@ interface OpenAiModelInfo {
7
7
  api: "responses" | "chat";
8
8
  /** Reasoning models reject non-default sampling parameters. */
9
9
  reasoning: boolean;
10
+ /**
11
+ * The model accepts wire `reasoning.effort: "max"` (GPT-5.6 Sol per
12
+ * the official model docs). When false, canonical max downmaps to
13
+ * wire xhigh; the downmap is recorded in providerMetadata and the
14
+ * journal identity keeps max, so caps accept the full canonical set
15
+ * either way.
16
+ */
17
+ wireMaxEffort: boolean;
10
18
  }
11
19
  /** Static seed table of the current model set. */
12
20
  declare const OPENAI_MODELS: Record<string, OpenAiModelInfo>;
@@ -111,12 +119,15 @@ declare class OpenAiIdMap {
111
119
  wireFor(canonicalId: CanonicalId): string;
112
120
  }
113
121
  /**
114
- * Canonical-to-wire effort: low through
115
- * xhigh pass through; canonical max downmaps to xhigh (documented lossy;
116
- * recorded in providerMetadata); provider 'none' is reachable only via
117
- * providerOptions.openai.reasoningEffort.
122
+ * Canonical-to-wire effort: low through xhigh pass through. Canonical
123
+ * max passes through unchanged on models whose caps declare wire max
124
+ * support (GPT-5.6 Sol); elsewhere it downmaps to xhigh (documented
125
+ * lossy; recorded in providerMetadata). Provider 'none' is reachable
126
+ * only via providerOptions.openai.reasoningEffort.
118
127
  */
119
- declare function mapOpenAiEffort(effort: Effort): {
128
+ declare function mapOpenAiEffort(effort: Effort, options?: {
129
+ wireMaxEffort?: boolean;
130
+ }): {
120
131
  wire: string;
121
132
  downmapped: boolean;
122
133
  };
@@ -127,7 +138,9 @@ declare function mapOpenAiEffort(effort: Effort): {
127
138
  * are REJECTED as a typed ConfigError. Role
128
139
  * 'system' messages project into top-level instructions on every request.
129
140
  */
130
- declare function buildResponsesParams(req: ChatRequest, ids: OpenAiIdMap): {
141
+ declare function buildResponsesParams(req: ChatRequest, ids: OpenAiIdMap, options?: {
142
+ wireMaxEffort?: boolean;
143
+ }): {
131
144
  params: Record<string, unknown>;
132
145
  effortDownmapped: boolean;
133
146
  };
@@ -135,7 +148,17 @@ declare function buildResponsesParams(req: ChatRequest, ids: OpenAiIdMap): {
135
148
  type ResponsesStreamEvent = Record<string, unknown> & {
136
149
  type: string;
137
150
  };
138
- /** Normalizes Responses usage: input_tokens already includes cached reads. */
151
+ /**
152
+ * Normalizes Responses usage into the canonical Usage invariant, where
153
+ * `inputTokens` is the FULL prompt: wire `input_tokens` already includes
154
+ * cached READS, while cache WRITE tokens arrive SEPARATELY in
155
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later families;
156
+ * they bill at the 1.25x write premium, and earlier families report no
157
+ * field and pay no premium). Dropping the field lost the whole write
158
+ * charge and weakened the budget guard (v1.18.0 review P1-2), so writes
159
+ * are added into `inputTokens` and surfaced as `cacheWriteTokens` for
160
+ * the premium rate, exactly mirroring the Anthropic adapter's mapping.
161
+ */
139
162
  declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined): Usage;
140
163
  /**
141
164
  * Maps the typed Responses SSE stream to ChatEvents, yielding each
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ const REASONING_EFFORTS = [
7
7
  "high",
8
8
  "xhigh"
9
9
  ];
10
- function responses(contextWindow, maxOutputTokens, pricing) {
10
+ function responses(contextWindow, maxOutputTokens, pricing, options) {
11
11
  return {
12
12
  caps: {
13
13
  structuredOutput: "native",
@@ -19,48 +19,66 @@ function responses(contextWindow, maxOutputTokens, pricing) {
19
19
  ...pricing === void 0 ? {} : { pricing }
20
20
  },
21
21
  api: "responses",
22
- reasoning: true
22
+ reasoning: true,
23
+ wireMaxEffort: options?.wireMaxEffort === true
23
24
  };
24
25
  }
26
+ const GPT_56_TIERS = [{
27
+ aboveInputTokens: 272e3,
28
+ inputMultiplier: 2,
29
+ outputMultiplier: 1.5
30
+ }];
25
31
  /**
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.
32
+ * GPT-5.6 Sol, Terra, and Luna are three sibling models, not snapshots
33
+ * of one model (developers.openai.com/api/docs/models/gpt-5.6-sol,
34
+ * .../gpt-5.6-terra, .../gpt-5.6-luna; rates verified 2026-07-18). All
35
+ * three: prompts strictly above 272K input tokens price the FULL
36
+ * request at 2x input and 1.5x output; cache writes bill at 1.25x
37
+ * uncached input. Only Sol accepts wire reasoning effort `max`.
29
38
  */
30
39
  const GPT_56_SOL = responses(105e4, 128e3, {
31
40
  inputUsdPerMTok: 5,
32
41
  outputUsdPerMTok: 30,
33
42
  cacheReadUsdPerMTok: .5,
34
43
  cacheWriteUsdPerMTok: 6.25,
35
- tiers: [{
36
- aboveInputTokens: 272e3,
37
- inputMultiplier: 2,
38
- outputMultiplier: 1.5
39
- }]
40
- });
44
+ tiers: GPT_56_TIERS
45
+ }, { wireMaxEffort: true });
41
46
  /** Static seed table of the current model set. */
42
47
  const OPENAI_MODELS = {
43
48
  "gpt-5.6-sol": GPT_56_SOL,
49
+ "gpt-5.6-terra": responses(105e4, 128e3, {
50
+ inputUsdPerMTok: 2.5,
51
+ outputUsdPerMTok: 15,
52
+ cacheReadUsdPerMTok: .25,
53
+ cacheWriteUsdPerMTok: 3.125,
54
+ tiers: GPT_56_TIERS
55
+ }),
56
+ "gpt-5.6-luna": responses(105e4, 128e3, {
57
+ inputUsdPerMTok: 1,
58
+ outputUsdPerMTok: 6,
59
+ cacheReadUsdPerMTok: .1,
60
+ cacheWriteUsdPerMTok: 1.25,
61
+ tiers: GPT_56_TIERS
62
+ }),
44
63
  "gpt-5.6": GPT_56_SOL,
45
64
  "gpt-5.5": responses(4e5, 128e3, {
46
- inputUsdPerMTok: 10,
47
- outputUsdPerMTok: 40,
48
- cacheReadUsdPerMTok: 1
65
+ inputUsdPerMTok: 5,
66
+ outputUsdPerMTok: 30,
67
+ cacheReadUsdPerMTok: .5
49
68
  }),
50
69
  "gpt-5.5-pro": responses(4e5, 128e3, {
51
- inputUsdPerMTok: 40,
52
- outputUsdPerMTok: 160,
53
- cacheReadUsdPerMTok: 4
70
+ inputUsdPerMTok: 30,
71
+ outputUsdPerMTok: 180
54
72
  }),
55
73
  "gpt-5.4": responses(272e3, 1e5, {
56
- inputUsdPerMTok: 6,
57
- outputUsdPerMTok: 24,
58
- cacheReadUsdPerMTok: .6
74
+ inputUsdPerMTok: 2.5,
75
+ outputUsdPerMTok: 15,
76
+ cacheReadUsdPerMTok: .25
59
77
  }),
60
78
  "gpt-5.4-mini": responses(272e3, 1e5, {
61
- inputUsdPerMTok: 1.2,
62
- outputUsdPerMTok: 4.8,
63
- cacheReadUsdPerMTok: .12
79
+ inputUsdPerMTok: .75,
80
+ outputUsdPerMTok: 4.5,
81
+ cacheReadUsdPerMTok: .075
64
82
  })
65
83
  };
66
84
  /**
@@ -83,22 +101,30 @@ const OPENAI_MODELS = {
83
101
  * silent reinterpretation.
84
102
  */
85
103
  const OPENAI_PRICING = {
86
- pricingVersion: "openai-2026-07-16",
104
+ pricingVersion: "openai-2026-07-18-r2",
87
105
  models: (() => {
88
106
  const models = {};
89
107
  for (const [name, info] of Object.entries(OPENAI_MODELS)) if (info.caps.pricing !== void 0) models[`openai:${name}`] = info.caps.pricing;
90
108
  return models;
91
109
  })()
92
110
  };
111
+ /**
112
+ * The documented snapshot grammar: `<exact model>-YYYY-MM-DD`. Nothing
113
+ * else inherits a table row (v1.17.0 review P1-1): a general prefix
114
+ * matcher let the 'gpt-5.6' family alias capture the SIBLING models
115
+ * 'gpt-5.6-terra' and 'gpt-5.6-luna' and price them as Sol, which is
116
+ * worse than no price at all. An unknown sibling or preview suffix now
117
+ * falls through to conservative unpriced caps.
118
+ */
119
+ const DATED_SNAPSHOT = /^(?<base>.+)-\d{4}-\d{2}-\d{2}$/u;
93
120
  function openAiModelInfo(model) {
94
121
  const exact = OPENAI_MODELS[model];
95
122
  if (exact !== void 0) return exact;
96
- let best;
97
- for (const [name, info] of Object.entries(OPENAI_MODELS)) if (model.startsWith(`${name}-`) && (best === void 0 || name.length > best.name.length)) best = {
98
- name,
99
- info
100
- };
101
- if (best !== void 0) return best.info;
123
+ const snapshot = DATED_SNAPSHOT.exec(model)?.groups?.base;
124
+ if (snapshot !== void 0) {
125
+ const base = OPENAI_MODELS[snapshot];
126
+ if (base !== void 0) return base;
127
+ }
102
128
  return responses(272e3, 1e5);
103
129
  }
104
130
  //#endregion
@@ -140,13 +166,14 @@ var OpenAiIdMap = class {
140
166
  }
141
167
  };
142
168
  /**
143
- * Canonical-to-wire effort: low through
144
- * xhigh pass through; canonical max downmaps to xhigh (documented lossy;
145
- * recorded in providerMetadata); provider 'none' is reachable only via
146
- * providerOptions.openai.reasoningEffort.
169
+ * Canonical-to-wire effort: low through xhigh pass through. Canonical
170
+ * max passes through unchanged on models whose caps declare wire max
171
+ * support (GPT-5.6 Sol); elsewhere it downmaps to xhigh (documented
172
+ * lossy; recorded in providerMetadata). Provider 'none' is reachable
173
+ * only via providerOptions.openai.reasoningEffort.
147
174
  */
148
- function mapOpenAiEffort(effort) {
149
- if (effort === "max") return {
175
+ function mapOpenAiEffort(effort, options) {
176
+ if (effort === "max" && options?.wireMaxEffort !== true) return {
150
177
  wire: "xhigh",
151
178
  downmapped: true
152
179
  };
@@ -162,7 +189,7 @@ function mapOpenAiEffort(effort) {
162
189
  * are REJECTED as a typed ConfigError. Role
163
190
  * 'system' messages project into top-level instructions on every request.
164
191
  */
165
- function buildResponsesParams(req, ids) {
192
+ function buildResponsesParams(req, ids, options) {
166
193
  const openaiOptions = req.providerOptions?.openai ?? {};
167
194
  for (const forbidden of [
168
195
  "previous_response_id",
@@ -260,7 +287,7 @@ function buildResponsesParams(req, ids) {
260
287
  const explicitEffort = openaiOptions.reasoningEffort;
261
288
  if (typeof explicitEffort === "string") params.reasoning = { effort: explicitEffort };
262
289
  else if (req.effort !== void 0) {
263
- const mapped = mapOpenAiEffort(req.effort);
290
+ const mapped = mapOpenAiEffort(req.effort, options);
264
291
  effortDownmapped = mapped.downmapped;
265
292
  params.reasoning = { effort: mapped.wire };
266
293
  }
@@ -270,15 +297,26 @@ function buildResponsesParams(req, ids) {
270
297
  effortDownmapped
271
298
  };
272
299
  }
273
- /** Normalizes Responses usage: input_tokens already includes cached reads. */
300
+ /**
301
+ * Normalizes Responses usage into the canonical Usage invariant, where
302
+ * `inputTokens` is the FULL prompt: wire `input_tokens` already includes
303
+ * cached READS, while cache WRITE tokens arrive SEPARATELY in
304
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later families;
305
+ * they bill at the 1.25x write premium, and earlier families report no
306
+ * field and pay no premium). Dropping the field lost the whole write
307
+ * charge and weakened the budget guard (v1.18.0 review P1-2), so writes
308
+ * are added into `inputTokens` and surfaced as `cacheWriteTokens` for
309
+ * the premium rate, exactly mirroring the Anthropic adapter's mapping.
310
+ */
274
311
  function normalizeOpenAiUsage(raw) {
275
312
  const inputDetails = raw?.input_tokens_details;
276
313
  const outputDetails = raw?.output_tokens_details;
314
+ const cacheWrite = typeof inputDetails?.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : 0;
277
315
  const usage = {
278
- inputTokens: typeof raw?.input_tokens === "number" ? raw.input_tokens : 0,
316
+ inputTokens: (typeof raw?.input_tokens === "number" ? raw.input_tokens : 0) + cacheWrite,
279
317
  outputTokens: typeof raw?.output_tokens === "number" ? raw.output_tokens : 0,
280
318
  cacheReadTokens: typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0,
281
- cacheWriteTokens: 0
319
+ cacheWriteTokens: cacheWrite
282
320
  };
283
321
  const reasoning = outputDetails?.reasoning_tokens;
284
322
  if (typeof reasoning === "number" && reasoning > 0) usage.reasoningTokens = reasoning;
@@ -387,31 +425,59 @@ async function* mapResponsesStream(stream, ids, options) {
387
425
  };
388
426
  return;
389
427
  }
390
- case "response.failed":
428
+ case "response.failed": {
429
+ const response = event.response;
430
+ const rawUsage = response?.usage;
431
+ if (rawUsage !== void 0) yield {
432
+ type: "usage",
433
+ usage: normalizeOpenAiUsage(rawUsage)
434
+ };
435
+ const error = response?.error;
391
436
  yield {
392
437
  type: "error",
393
- error: {
394
- code: "agent",
395
- message: (event.response?.error)?.message ?? "response.failed",
396
- retryable: false,
397
- data: { kind: "transport" }
398
- }
438
+ error: failedResponseError(typeof error?.code === "string" ? error.code : void 0, error?.message ?? "response.failed")
399
439
  };
400
440
  return;
441
+ }
401
442
  case "error":
402
443
  yield {
403
444
  type: "error",
404
- error: {
405
- code: "agent",
406
- message: event.message ?? "stream error",
407
- retryable: false,
408
- data: { kind: "transport" }
409
- }
445
+ error: failedResponseError(typeof event.code === "string" ? event.code : void 0, event.message ?? "stream error")
410
446
  };
411
447
  return;
412
448
  default: break;
413
449
  }
414
450
  }
451
+ /**
452
+ * Classifies a terminal stream failure (`response.failed` /
453
+ * SSE `error`) into the retryable WireError vocabulary by the
454
+ * documented `response.error.code` enum (Responses API reference):
455
+ * `rate_limit_exceeded` retries as a rate limit, `server_error` and
456
+ * timeout-class codes retry as transport faults, and everything else
457
+ * (validation such as `invalid_prompt`, policy, auth) stays
458
+ * non-retryable. Unknown codes fail closed as non-retryable: retrying a
459
+ * permanent failure would loop-bill it (v1.18.0 review P1-3).
460
+ */
461
+ function failedResponseError(code, message) {
462
+ if (code === "rate_limit_exceeded") return {
463
+ code: "agent",
464
+ message,
465
+ retryable: true,
466
+ data: {
467
+ kind: "rate-limit",
468
+ providerCode: code
469
+ }
470
+ };
471
+ return {
472
+ code: "agent",
473
+ message,
474
+ retryable: code === "server_error" || code !== void 0 && code.endsWith("_timeout"),
475
+ data: {
476
+ kind: "transport",
477
+ ...code === void 0 ? {} : { providerCode: code }
478
+ }
479
+ };
480
+ }
415
481
  /** Projects SDK/API errors into the retryable WireError vocabulary. */
416
482
  function openAiErrorToWire(error) {
417
483
  const record = error;
@@ -578,11 +644,12 @@ async function* mapChatCompletionsStream(stream, ids) {
578
644
  const chunkUsage = chunk.usage;
579
645
  if (chunkUsage !== void 0 && chunkUsage !== null) {
580
646
  const promptDetails = chunkUsage.prompt_tokens_details;
647
+ const cacheWrite = typeof promptDetails?.cache_write_tokens === "number" ? promptDetails.cache_write_tokens : 0;
581
648
  usage = {
582
- inputTokens: typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0,
649
+ inputTokens: (typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0) + cacheWrite,
583
650
  outputTokens: typeof chunkUsage.completion_tokens === "number" ? chunkUsage.completion_tokens : 0,
584
651
  cacheReadTokens: typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0,
585
- cacheWriteTokens: 0
652
+ cacheWriteTokens: cacheWrite
586
653
  };
587
654
  }
588
655
  }
@@ -655,7 +722,7 @@ function openai(options = {}) {
655
722
  const info = openAiModelInfo(req.model);
656
723
  try {
657
724
  if (info.api === "responses") {
658
- const { params, effortDownmapped } = buildResponsesParams(req, ids);
725
+ const { params, effortDownmapped } = buildResponsesParams(req, ids, { wireMaxEffort: info.wireMaxEffort });
659
726
  yield* mapResponsesStream(await client.responses.create({
660
727
  ...params,
661
728
  stream: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
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",
@@ -23,13 +23,13 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "openai": "^6.45.0",
26
- "@rulvar/core": "1.17.0"
26
+ "@rulvar/core": "1.19.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",
30
30
  "tsdown": "^0.22.3",
31
31
  "typescript": "~6.0.3",
32
- "@rulvar/testing": "1.17.0"
32
+ "@rulvar/testing": "1.19.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",