@f5-sales-demo/pi-ai 21.6.4 → 21.8.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "21.6.4",
4
+ "version": "21.8.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "21.6.4",
49
+ "@f5-sales-demo/pi-utils": "21.8.0",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -17,6 +17,18 @@ export const enum Effort {
17
17
  Max = "max",
18
18
  }
19
19
 
20
+ /** Public reasoning preset enum. `none` is distinct from inheriting the provider default. */
21
+ export const ReasoningEffort = {
22
+ None: "none",
23
+ Minimal: Effort.Minimal,
24
+ Low: Effort.Low,
25
+ Medium: Effort.Medium,
26
+ High: Effort.High,
27
+ XHigh: Effort.XHigh,
28
+ Max: Effort.Max,
29
+ } as const;
30
+ export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
31
+
20
32
  /**
21
33
  * Anthropic's `output_config.effort` enum, verbatim. Authority is the API's own
22
34
  * validation error: `Input should be 'low', 'medium', 'high', 'xhigh' or 'max'`.
@@ -48,6 +60,36 @@ export const THINKING_EFFORTS: readonly Effort[] = [
48
60
  Effort.Max,
49
61
  ];
50
62
 
63
+ export const REASONING_EFFORTS: readonly ReasoningEffort[] = [ReasoningEffort.None, ...THINKING_EFFORTS];
64
+
65
+ const EFFORT_DESCRIPTIONS: Readonly<Record<ReasoningEffort, string>> = {
66
+ [ReasoningEffort.None]: "No reasoning",
67
+ [ReasoningEffort.Minimal]: "Very brief reasoning",
68
+ [ReasoningEffort.Low]: "Light reasoning",
69
+ [ReasoningEffort.Medium]: "Balanced reasoning",
70
+ [ReasoningEffort.High]: "Deep reasoning",
71
+ [ReasoningEffort.XHigh]: "Very deep reasoning",
72
+ [ReasoningEffort.Max]: "Maximum reasoning",
73
+ };
74
+
75
+ /** Build explicit metadata for custom models and programmatic registries. */
76
+ export function createThinkingConfig(
77
+ supportedLevels: readonly ReasoningEffort[],
78
+ mode: ThinkingConfig["mode"] = "effort",
79
+ defaultLevel: ReasoningEffort = supportedLevels.includes(ReasoningEffort.Medium)
80
+ ? ReasoningEffort.Medium
81
+ : supportedLevels[0]!,
82
+ ): ThinkingConfig {
83
+ if (supportedLevels.length === 0 || !supportedLevels.includes(defaultLevel)) {
84
+ throw new Error("Thinking metadata requires supported levels and an exact supported default");
85
+ }
86
+ return {
87
+ mode,
88
+ defaultLevel,
89
+ supportedLevels: supportedLevels.map(effort => ({ effort, description: EFFORT_DESCRIPTIONS[effort] })),
90
+ };
91
+ }
92
+
51
93
  const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
52
94
  const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [
53
95
  Effort.Minimal,
@@ -226,18 +268,31 @@ export function linkSparkPromotionTargets(models: ApiModel<Api>[]): void {
226
268
  * @throws Error when a reasoning-capable model is missing thinking metadata
227
269
  */
228
270
  export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
271
+ return getSupportedReasoningEfforts(model).filter((effort): effort is Effort => effort !== ReasoningEffort.None);
272
+ }
273
+
274
+ /** Returns the exact service-advertised list, including an explicit `none` preset. */
275
+ export function getSupportedReasoningEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly ReasoningEffort[] {
229
276
  if (!model.reasoning) {
230
277
  return [];
231
278
  }
232
279
  if (!model.thinking) {
233
280
  throw new Error(`Model ${model.provider}/${model.id} is missing thinking metadata`);
234
281
  }
235
- const configuredEfforts = expandEffortRange(model.thinking);
236
- const parsedModel = parseKnownModel(model.id);
237
- if (parsedModel.family === "unknown") {
238
- return configuredEfforts;
282
+ return model.thinking.supportedLevels.map(level => level.effort);
283
+ }
284
+
285
+ export function requireSupportedReasoningEffort<TApi extends Api>(
286
+ model: ApiModel<TApi>,
287
+ effort: ReasoningEffort,
288
+ ): ReasoningEffort {
289
+ const levels = getSupportedReasoningEfforts(model);
290
+ if (!levels.includes(effort)) {
291
+ throw new Error(
292
+ `Reasoning effort ${effort} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`,
293
+ );
239
294
  }
240
- return intersectEfforts(configuredEfforts, inferSupportedEfforts(parsedModel, model));
295
+ return effort;
241
296
  }
242
297
 
243
298
  /**
@@ -261,20 +316,9 @@ export function clampThinkingLevelForModel<TApi extends Api>(
261
316
  return requested;
262
317
  }
263
318
 
264
- const requestedIndex = THINKING_EFFORTS.indexOf(requested);
265
- if (requestedIndex === -1) {
266
- return undefined;
267
- }
268
-
269
- let clamped: Effort | undefined;
270
- for (const effort of levels) {
271
- if (THINKING_EFFORTS.indexOf(effort) > requestedIndex) {
272
- break;
273
- }
274
- clamped = effort;
275
- }
276
-
277
- return clamped ?? levels[0];
319
+ throw new Error(
320
+ `Thinking effort ${requested} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`,
321
+ );
278
322
  }
279
323
 
280
324
  export function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort {
@@ -392,15 +436,13 @@ function applyOpenAICatalogPolicy(model: ApiModel<Api>, parsedModel: OpenAIModel
392
436
  function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingConfig {
393
437
  const parsedModel = parseKnownModel(model.id);
394
438
  const efforts = inferSupportedEfforts(parsedModel, model);
395
- const minLevel = efforts[0];
396
- const maxLevel = efforts.at(-1);
397
- if (!minLevel || !maxLevel) {
398
- throw new Error(`Model ${model.provider}/${model.id} resolved to an empty thinking range`);
439
+ if (efforts.length === 0) {
440
+ throw new Error(`Model ${model.provider}/${model.id} resolved to no thinking presets`);
399
441
  }
400
442
  const thinking: ThinkingConfig = {
401
443
  mode: inferThinkingControlMode(model, parsedModel),
402
- minLevel,
403
- maxLevel,
444
+ supportedLevels: efforts.map(effort => ({ effort, description: EFFORT_DESCRIPTIONS[effort] })),
445
+ defaultLevel: efforts.includes(ReasoningEffort.Medium) ? ReasoningEffort.Medium : efforts[0]!,
404
446
  };
405
447
  if (
406
448
  model.provider === "google-vertex" &&
@@ -408,18 +450,22 @@ function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingCo
408
450
  !model.id.toLowerCase().includes("lite")
409
451
  ) {
410
452
  thinking.defaultLevel = Effort.High;
411
- if (parsedModel.version.major === 3) {
412
- thinking.canDisable = false;
413
- }
414
453
  }
415
454
  return thinking;
416
455
  }
417
456
 
418
457
  function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): ThinkingConfig | undefined {
419
- if (!thinking || expandEffortRange(thinking).length === 0) {
458
+ if (!thinking || !Array.isArray(thinking.supportedLevels) || thinking.supportedLevels.length === 0) {
420
459
  return undefined;
421
460
  }
422
- return thinking;
461
+ const supportedLevels = thinking.supportedLevels.filter(
462
+ (level, index, levels) =>
463
+ REASONING_EFFORTS.includes(level.effort) && levels.findIndex(value => value.effort === level.effort) === index,
464
+ );
465
+ if (supportedLevels.length === 0 || !supportedLevels.some(level => level.effort === thinking.defaultLevel)) {
466
+ return undefined;
467
+ }
468
+ return { ...thinking, supportedLevels };
423
469
  }
424
470
 
425
471
  function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig | undefined): boolean {
@@ -427,30 +473,23 @@ function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig
427
473
  if (!left || !right) return false;
428
474
  return (
429
475
  left.mode === right.mode &&
430
- left.minLevel === right.minLevel &&
431
- left.maxLevel === right.maxLevel &&
432
476
  left.defaultLevel === right.defaultLevel &&
433
- left.canDisable === right.canDisable
477
+ left.supportedLevels.length === right.supportedLevels.length &&
478
+ left.supportedLevels.every(
479
+ (level, index) =>
480
+ level.effort === right.supportedLevels[index]?.effort &&
481
+ level.description === right.supportedLevels[index]?.description,
482
+ )
434
483
  );
435
484
  }
436
485
 
437
- function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] {
438
- const minIndex = THINKING_EFFORTS.indexOf(thinking.minLevel);
439
- const maxIndex = THINKING_EFFORTS.indexOf(thinking.maxLevel);
440
- if (minIndex === -1 || maxIndex === -1 || minIndex > maxIndex) {
441
- return [];
442
- }
443
- return THINKING_EFFORTS.slice(minIndex, maxIndex + 1);
444
- }
445
-
446
- function intersectEfforts(left: readonly Effort[], right: readonly Effort[]): readonly Effort[] {
447
- return left.filter(effort => right.includes(effort));
448
- }
449
-
450
- function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model: ApiModel<TApi>): readonly Effort[] {
486
+ function inferSupportedEfforts<TApi extends Api>(
487
+ parsedModel: ParsedModel,
488
+ model: ApiModel<TApi>,
489
+ ): readonly ReasoningEffort[] {
451
490
  switch (parsedModel.family) {
452
491
  case "openai":
453
- return inferOpenAISupportedEfforts(parsedModel);
492
+ return inferOpenAISupportedEfforts(parsedModel, model);
454
493
  case "gemini":
455
494
  return inferGeminiSupportedEfforts(parsedModel);
456
495
  case "anthropic":
@@ -460,7 +499,13 @@ function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model
460
499
  }
461
500
  }
462
501
 
463
- function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] {
502
+ function inferOpenAISupportedEfforts<TApi extends Api>(
503
+ model: OpenAIModel,
504
+ catalogModel: ApiModel<TApi>,
505
+ ): readonly ReasoningEffort[] {
506
+ if (semverGte(model.version, "5.6") && catalogModel.api === "openai-codex-responses") {
507
+ return [ReasoningEffort.None, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
508
+ }
464
509
  if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) {
465
510
  return GPT_5_1_CODEX_MINI_EFFORTS;
466
511
  }