@librechat/agents 3.7.19 → 3.7.20

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.
@@ -122,6 +122,7 @@ type OpenAIClientConfig = NonNullable<
122
122
  >;
123
123
  type LibreChatOpenAIFields = t.ChatOpenAIFields & {
124
124
  _lc_stream_delay?: number;
125
+ firstPartyEndpoint?: boolean;
125
126
  includeReasoningContent?: boolean;
126
127
  includeReasoningDetails?: boolean;
127
128
  convertReasoningDetailsToContent?: boolean;
@@ -133,6 +134,7 @@ type LibreChatOpenAIFields = t.ChatOpenAIFields & {
133
134
  };
134
135
  type LibreChatAzureOpenAIFields = t.AzureOpenAIInput & {
135
136
  _lc_stream_delay?: number;
137
+ firstPartyEndpoint?: boolean;
136
138
  promptCacheExplicit?: boolean;
137
139
  safety_identifier?: string;
138
140
  };
@@ -503,13 +505,127 @@ export function addResponseCacheBreakpoints(
503
505
  );
504
506
  }
505
507
 
508
+ /**
509
+ * GPT-6 Astra, matched on the model id rather than a `gpt-6` family cutoff.
510
+ *
511
+ * OpenAI documents these constraints for Astra specifically, and every gate
512
+ * keyed off this helper *removes* capability — it forces the Responses API,
513
+ * drops sampling parameters, and lowers reasoning effort. A false positive
514
+ * therefore silently degrades a sibling model that never needed it, so the
515
+ * match stays narrow until OpenAI documents the same rules more widely.
516
+ *
517
+ * Deliberately does NOT match a `provider/` prefixed id such as
518
+ * `openai/gpt-6-astra`. A slash means a proxy is doing the routing, and the
519
+ * proxy's contract is not OpenAI's: `ChatOpenRouter` extends this class, and on
520
+ * OpenRouter `effort: 'none'` is a *supported* value meaning "disable
521
+ * reasoning" (it maps to `include_reasoning: false`). Substituting it with
522
+ * `low` there would silently turn reasoning off into reasoning on, and forcing
523
+ * the Responses API would change the endpoint shape a proxy may not serve. Bare
524
+ * ids reach the first-party OpenAI and Azure surfaces these rules describe.
525
+ * @see https://developers.openai.com/api/docs/models/gpt-6-astra
526
+ * @see https://developers.openai.com/api/docs/guides/latest-model
527
+ */
528
+ const GPT_6_ASTRA_PATTERN = /^gpt-6-astra(?:-|$)/i;
529
+
530
+ /** @internal */
531
+ export function isGpt6AstraModel(model?: string): boolean {
532
+ return model != null && GPT_6_ASTRA_PATTERN.test(model.toLowerCase());
533
+ }
534
+
535
+ /**
536
+ * Reasoning efforts GPT-6 Astra does not accept, mapped to the level OpenAI
537
+ * recommends migrating to. `none` is rejected outright and `minimal` is not
538
+ * offered; the migration guide says to "start with `low` and compare results".
539
+ * Substituting keeps a stored agent configuration usable instead of failing
540
+ * the turn on a value the previous model accepted.
541
+ */
542
+ const GPT_6_ASTRA_UNSUPPORTED_EFFORTS = new Set(['none', 'minimal']);
543
+ const GPT_6_ASTRA_EFFORT_FALLBACK = 'low' as const;
544
+
545
+ function substituteUnsupportedAstraEffort(
546
+ astraRulesApply: boolean,
547
+ reasoning: OpenAIClient.Reasoning | undefined
548
+ ): OpenAIClient.Reasoning | undefined {
549
+ const effort = reasoning?.effort;
550
+ if (
551
+ effort == null ||
552
+ !astraRulesApply ||
553
+ !GPT_6_ASTRA_UNSUPPORTED_EFFORTS.has(effort)
554
+ ) {
555
+ return reasoning;
556
+ }
557
+ return { ...reasoning, effort: GPT_6_ASTRA_EFFORT_FALLBACK };
558
+ }
559
+
560
+ /** Rejected inside the Responses `include` array. */
561
+ const GPT_6_ASTRA_UNSUPPORTED_INCLUDE: OpenAIClient.Responses.ResponseIncludable =
562
+ 'message.output_text.logprobs';
563
+
564
+ /**
565
+ * The subset of a request GPT-6 Astra rejects, taken from the SDK's own request
566
+ * types so the stripping boundary keeps their constraints. Every member is
567
+ * optional so a key can be deleted from a request object whose concrete type
568
+ * marks it required.
569
+ */
570
+ type AstraStrippableParams = Partial<
571
+ Pick<
572
+ OpenAIClient.Chat.Completions.ChatCompletionCreateParams,
573
+ 'temperature' | 'top_p' | 'logprobs' | 'top_logprobs'
574
+ >
575
+ > &
576
+ Partial<Pick<OpenAIClient.Responses.ResponseCreateParams, 'include'>>;
577
+
578
+ /**
579
+ * Removes the sampling and logprob parameters GPT-6 Astra rejects.
580
+ *
581
+ * LangChain's request builders emit `temperature` and `top_p` from instance
582
+ * fields unconditionally, so leaving them unset upstream is not enough — they
583
+ * are stripped after `super.invocationParams`. `logprobs` is Chat Completions
584
+ * only; the Responses equivalent rides inside `include`.
585
+ */
586
+ function stripUnsupportedAstraParams<T extends object>(
587
+ astraRulesApply: boolean,
588
+ params: T,
589
+ endpoint: 'completions' | 'responses'
590
+ ): T {
591
+ if (!astraRulesApply) {
592
+ return params;
593
+ }
594
+ const next = { ...params };
595
+ const record = next as AstraStrippableParams;
596
+ delete record.temperature;
597
+ delete record.top_p;
598
+ delete record.top_logprobs;
599
+ if (endpoint === 'completions') {
600
+ delete record.logprobs;
601
+ return next;
602
+ }
603
+ const include = record.include;
604
+ if (!Array.isArray(include)) {
605
+ return next;
606
+ }
607
+ const filtered = include.filter(
608
+ (entry) => entry !== GPT_6_ASTRA_UNSUPPORTED_INCLUDE
609
+ );
610
+ if (filtered.length === include.length) {
611
+ return next;
612
+ }
613
+ if (filtered.length === 0) {
614
+ delete record.include;
615
+ } else {
616
+ record.include = filtered;
617
+ }
618
+ return next;
619
+ }
620
+
506
621
  /** @internal */
507
622
  export function shouldIncludeEncryptedReasoning(
508
623
  model: string,
509
624
  params: {
510
625
  store?: boolean | null;
511
626
  reasoning?: unknown;
512
- }
627
+ },
628
+ astraRulesApply = false
513
629
  ): boolean {
514
630
  const reasoningContext = (
515
631
  params.reasoning as
@@ -517,7 +633,7 @@ export function shouldIncludeEncryptedReasoning(
517
633
  | undefined
518
634
  )?.context;
519
635
  return (
520
- /^gpt-5\.6(?:-|$)/i.test(model) &&
636
+ (/^gpt-5\.6(?:-|$)/i.test(model) || astraRulesApply) &&
521
637
  (params.store === false || reasoningContext !== 'current_turn')
522
638
  );
523
639
  }
@@ -1109,6 +1225,7 @@ function getExposedOpenAIClient(
1109
1225
  }
1110
1226
 
1111
1227
  function getReasoningParams(
1228
+ astraRulesApply: boolean,
1112
1229
  baseReasoning: OpenAIClient.Reasoning | undefined,
1113
1230
  options?: ReasoningCallOptions
1114
1231
  ): OpenAIClient.Reasoning | undefined {
@@ -1134,18 +1251,19 @@ function getReasoningParams(
1134
1251
  effort: options.reasoningEffort,
1135
1252
  };
1136
1253
  }
1137
- return reasoning;
1254
+ return substituteUnsupportedAstraEffort(astraRulesApply, reasoning);
1138
1255
  }
1139
1256
 
1140
1257
  function getGatedReasoningParams(
1141
1258
  model: string,
1259
+ astraRulesApply: boolean,
1142
1260
  baseReasoning: OpenAIClient.Reasoning | undefined,
1143
1261
  options?: ReasoningCallOptions
1144
1262
  ): OpenAIClient.Reasoning | undefined {
1145
1263
  if (!isReasoningModel(model)) {
1146
1264
  return;
1147
1265
  }
1148
- return getReasoningParams(baseReasoning, options);
1266
+ return getReasoningParams(astraRulesApply, baseReasoning, options);
1149
1267
  }
1150
1268
 
1151
1269
  function isObject(value: unknown): value is object {
@@ -1610,7 +1728,7 @@ export class CustomAzureOpenAIClient extends AzureOpenAIClient {
1610
1728
  }
1611
1729
  }
1612
1730
 
1613
- const OFFICIAL_OPENAI_BASE_URL_PATTERN = /^https:\/\/api\.openai\.com(\/|$)/;
1731
+ const OFFICIAL_OPENAI_HOSTNAME = 'api.openai.com';
1614
1732
 
1615
1733
  /**
1616
1734
  * Official OpenAI (api.openai.com) and Azure OpenAI Chat Completions streams
@@ -1646,11 +1764,29 @@ function isOfficialOpenAIBaseURL(baseURL: string | null | undefined): boolean {
1646
1764
  if (effectiveBaseURL == null || effectiveBaseURL === '') {
1647
1765
  return true;
1648
1766
  }
1649
- return OFFICIAL_OPENAI_BASE_URL_PATTERN.test(effectiveBaseURL);
1767
+ // Compared through the URL parser rather than textually: it normalizes the
1768
+ // host case and drops the default :443, both of which spell the same
1769
+ // first-party endpoint, while keeping a lookalike host such as
1770
+ // `api.openai.com.example.net` a distinct hostname. A non-default port is
1771
+ // someone else's listener, so it stays proxied.
1772
+ let parsed: URL;
1773
+ try {
1774
+ parsed = new URL(effectiveBaseURL);
1775
+ } catch {
1776
+ return false;
1777
+ }
1778
+ return (
1779
+ parsed.protocol === 'https:' &&
1780
+ parsed.hostname === OFFICIAL_OPENAI_HOSTNAME &&
1781
+ parsed.port === ''
1782
+ );
1650
1783
  }
1651
1784
 
1652
- const AZURE_FIRST_PARTY_BASE_PATH_PATTERN =
1653
- /^https:\/\/[^/]+\.(openai\.azure\.com|cognitiveservices\.azure\.com|api\.cognitive\.microsoft\.com)(:\d+)?(\/|$)/;
1785
+ const AZURE_FIRST_PARTY_HOST_SUFFIXES = [
1786
+ '.openai.azure.com',
1787
+ '.cognitiveservices.azure.com',
1788
+ '.api.cognitive.microsoft.com',
1789
+ ] as const;
1654
1790
 
1655
1791
  /**
1656
1792
  * Azure OpenAI is first-party when requests resolve to an instance-name
@@ -1670,10 +1806,59 @@ function isFirstPartyAzureEndpoint(args: {
1670
1806
  if (args.azureOpenAIBasePath == null || args.azureOpenAIBasePath === '') {
1671
1807
  return true;
1672
1808
  }
1673
- return AZURE_FIRST_PARTY_BASE_PATH_PATTERN.test(args.azureOpenAIBasePath);
1809
+ // Parsed rather than matched textually, for the reason given on
1810
+ // `isOfficialOpenAIBaseURL`: the host case carries no meaning, so an
1811
+ // equivalent mixed-case spelling must not read as a proxy. Any port is
1812
+ // accepted here, as the previous pattern did.
1813
+ let parsed: URL;
1814
+ try {
1815
+ parsed = new URL(args.azureOpenAIBasePath);
1816
+ } catch {
1817
+ return false;
1818
+ }
1819
+ if (parsed.protocol !== 'https:') {
1820
+ return false;
1821
+ }
1822
+ return AZURE_FIRST_PARTY_HOST_SUFFIXES.some((suffix) =>
1823
+ parsed.hostname.endsWith(suffix)
1824
+ );
1825
+ }
1826
+
1827
+ /**
1828
+ * Whether the GPT-6 Astra request-shaping rules apply to this request.
1829
+ *
1830
+ * Shaping only: which API serves the turn is the caller's decision, made where
1831
+ * the rest of the request is shaped. These rules cover what the model rejects
1832
+ * on either API — sampling and logprob parameters, unsupported reasoning
1833
+ * efforts — plus the encrypted reasoning it supports.
1834
+ *
1835
+ * Both halves must hold: the SDK knows the model is Astra, and the caller has
1836
+ * declared that this client talks to the first-party endpoint those rules
1837
+ * describe. The endpoint half is *declared* rather than inferred from a base
1838
+ * URL: only the caller knows whether a given URL is a faithful first-party
1839
+ * route, a gateway, or a proxy with its own semantics, and every gate here
1840
+ * removes capability — forcing Responses, dropping parameters, lowering effort
1841
+ * — so guessing wrong silently degrades an endpoint the SDK cannot see.
1842
+ *
1843
+ * Defaults to off. An undeclared client keeps its existing behavior and a
1844
+ * misconfigured Astra call fails with the provider's own error, which names the
1845
+ * remedy, rather than being silently rewritten.
1846
+ */
1847
+ function astraRulesApply(
1848
+ model: string,
1849
+ firstPartyEndpoint: boolean | undefined
1850
+ ): boolean {
1851
+ return firstPartyEndpoint === true && isGpt6AstraModel(model);
1674
1852
  }
1675
1853
 
1676
1854
  class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1855
+ protected firstPartyEndpoint?: boolean;
1856
+
1857
+ /** @see {@link astraRulesApply} */
1858
+ protected get astraRulesApply(): boolean {
1859
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
1860
+ }
1861
+
1677
1862
  private includeReasoningContent?: boolean;
1678
1863
  private includeReasoningDetails?: boolean;
1679
1864
  private convertReasoningDetailsToContent?: boolean;
@@ -1689,6 +1874,7 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1689
1874
  fields?.convertReasoningDetailsToContent;
1690
1875
  this.preserveToolCacheControl = fields?.preserveToolCacheControl;
1691
1876
  this.promptCacheExplicit = fields?.promptCacheExplicit;
1877
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
1692
1878
  this.safetyIdentifier = fields?.safety_identifier;
1693
1879
  }
1694
1880
 
@@ -1697,17 +1883,21 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1697
1883
  extra?: { streaming?: boolean }
1698
1884
  ): ReturnType<OriginalChatOpenAICompletions['invocationParams']> {
1699
1885
  return stripIntentFromStrictTools(
1700
- applyManagedRequestParams(super.invocationParams(options, extra), {
1701
- promptCacheExplicit: this.promptCacheExplicit,
1702
- safetyIdentifier: this.safetyIdentifier,
1703
- })
1886
+ stripUnsupportedAstraParams(
1887
+ this.astraRulesApply,
1888
+ applyManagedRequestParams(super.invocationParams(options, extra), {
1889
+ promptCacheExplicit: this.promptCacheExplicit,
1890
+ safetyIdentifier: this.safetyIdentifier,
1891
+ }),
1892
+ 'completions'
1893
+ )
1704
1894
  );
1705
1895
  }
1706
1896
 
1707
1897
  protected _getReasoningParams(
1708
1898
  options?: this['ParsedCallOptions']
1709
1899
  ): OpenAIClient.Reasoning | undefined {
1710
- return getReasoningParams(this.reasoning, options);
1900
+ return getReasoningParams(this.astraRulesApply, this.reasoning, options);
1711
1901
  }
1712
1902
 
1713
1903
  _getClientOptions(
@@ -2099,6 +2289,13 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
2099
2289
  }
2100
2290
 
2101
2291
  class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2292
+ protected firstPartyEndpoint?: boolean;
2293
+
2294
+ /** @see {@link astraRulesApply} */
2295
+ protected get astraRulesApply(): boolean {
2296
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
2297
+ }
2298
+
2102
2299
  private promptCacheExplicit?: boolean;
2103
2300
  private responsesPromptCache?: boolean;
2104
2301
  private responsesPromptCacheTtl?: PromptCacheTtl;
@@ -2107,6 +2304,7 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2107
2304
  constructor(fields?: LibreChatOpenAIFields) {
2108
2305
  super(fields);
2109
2306
  this.promptCacheExplicit = fields?.promptCacheExplicit;
2307
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
2110
2308
  this.responsesPromptCache = fields?.responsesPromptCache;
2111
2309
  this.responsesPromptCacheTtl = fields?.responsesPromptCacheTtl;
2112
2310
  this.safetyIdentifier = fields?.safety_identifier;
@@ -2146,7 +2344,7 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2146
2344
  cache_control: cacheControl,
2147
2345
  }),
2148
2346
  };
2149
- if (shouldIncludeEncryptedReasoning(this.model, params)) {
2347
+ if (shouldIncludeEncryptedReasoning(this.model, params, this.astraRulesApply)) {
2150
2348
  params.include = [
2151
2349
  ...new Set([
2152
2350
  ...(params.include ?? []),
@@ -2154,7 +2352,9 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2154
2352
  ]),
2155
2353
  ];
2156
2354
  }
2157
- return stripIntentFromStrictTools(params);
2355
+ return stripIntentFromStrictTools(
2356
+ stripUnsupportedAstraParams(this.astraRulesApply, params, 'responses')
2357
+ );
2158
2358
  }
2159
2359
 
2160
2360
  async completionWithRetry(
@@ -2237,7 +2437,7 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2237
2437
  protected _getReasoningParams(
2238
2438
  options?: this['ParsedCallOptions']
2239
2439
  ): OpenAIClient.Reasoning | undefined {
2240
- return getReasoningParams(this.reasoning, options);
2440
+ return getReasoningParams(this.astraRulesApply, this.reasoning, options);
2241
2441
  }
2242
2442
 
2243
2443
  _getClientOptions(
@@ -2248,12 +2448,20 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
2248
2448
  }
2249
2449
 
2250
2450
  class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions {
2451
+ protected firstPartyEndpoint?: boolean;
2452
+
2453
+ /** @see {@link astraRulesApply} */
2454
+ protected get astraRulesApply(): boolean {
2455
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
2456
+ }
2457
+
2251
2458
  private promptCacheExplicit?: boolean;
2252
2459
  private safetyIdentifier?: string;
2253
2460
 
2254
2461
  constructor(fields?: LibreChatAzureOpenAIFields) {
2255
2462
  super(fields);
2256
2463
  this.promptCacheExplicit = fields?.promptCacheExplicit;
2464
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
2257
2465
  this.safetyIdentifier = fields?.safety_identifier;
2258
2466
  }
2259
2467
 
@@ -2262,17 +2470,26 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
2262
2470
  extra?: { streaming?: boolean }
2263
2471
  ): ReturnType<OriginalAzureChatOpenAICompletions['invocationParams']> {
2264
2472
  return stripIntentFromStrictTools(
2265
- applyManagedRequestParams(super.invocationParams(options, extra), {
2266
- promptCacheExplicit: this.promptCacheExplicit,
2267
- safetyIdentifier: this.safetyIdentifier,
2268
- })
2473
+ stripUnsupportedAstraParams(
2474
+ this.astraRulesApply,
2475
+ applyManagedRequestParams(super.invocationParams(options, extra), {
2476
+ promptCacheExplicit: this.promptCacheExplicit,
2477
+ safetyIdentifier: this.safetyIdentifier,
2478
+ }),
2479
+ 'completions'
2480
+ )
2269
2481
  );
2270
2482
  }
2271
2483
 
2272
2484
  protected _getReasoningParams(
2273
2485
  options?: this['ParsedCallOptions']
2274
2486
  ): OpenAIClient.Reasoning | undefined {
2275
- return getGatedReasoningParams(this.model, this.reasoning, options);
2487
+ return getGatedReasoningParams(
2488
+ this.model,
2489
+ this.astraRulesApply,
2490
+ this.reasoning,
2491
+ options
2492
+ );
2276
2493
  }
2277
2494
 
2278
2495
  protected _convertCompletionsDeltaToBaseMessageChunk(
@@ -2386,12 +2603,20 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
2386
2603
  }
2387
2604
 
2388
2605
  class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2606
+ protected firstPartyEndpoint?: boolean;
2607
+
2608
+ /** @see {@link astraRulesApply} */
2609
+ protected get astraRulesApply(): boolean {
2610
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
2611
+ }
2612
+
2389
2613
  private promptCacheExplicit?: boolean;
2390
2614
  private safetyIdentifier?: string;
2391
2615
 
2392
2616
  constructor(fields?: LibreChatAzureOpenAIFields) {
2393
2617
  super(fields);
2394
2618
  this.promptCacheExplicit = fields?.promptCacheExplicit;
2619
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
2395
2620
  this.safetyIdentifier = fields?.safety_identifier;
2396
2621
  }
2397
2622
 
@@ -2402,7 +2627,7 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2402
2627
  promptCacheExplicit: this.promptCacheExplicit,
2403
2628
  safetyIdentifier: this.safetyIdentifier,
2404
2629
  });
2405
- if (shouldIncludeEncryptedReasoning(this.model, params)) {
2630
+ if (shouldIncludeEncryptedReasoning(this.model, params, this.astraRulesApply)) {
2406
2631
  params.include = [
2407
2632
  ...new Set([
2408
2633
  ...(params.include ?? []),
@@ -2410,7 +2635,9 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2410
2635
  ]),
2411
2636
  ];
2412
2637
  }
2413
- return stripIntentFromStrictTools(params);
2638
+ return stripIntentFromStrictTools(
2639
+ stripUnsupportedAstraParams(this.astraRulesApply, params, 'responses')
2640
+ );
2414
2641
  }
2415
2642
 
2416
2643
  async completionWithRetry(
@@ -2493,7 +2720,12 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2493
2720
  protected _getReasoningParams(
2494
2721
  options?: this['ParsedCallOptions']
2495
2722
  ): OpenAIClient.Reasoning | undefined {
2496
- return getGatedReasoningParams(this.model, this.reasoning, options);
2723
+ return getGatedReasoningParams(
2724
+ this.model,
2725
+ this.astraRulesApply,
2726
+ this.reasoning,
2727
+ options
2728
+ );
2497
2729
  }
2498
2730
 
2499
2731
  _getClientOptions(
@@ -2573,6 +2805,13 @@ function withLibreChatOpenAIFields(
2573
2805
  }
2574
2806
 
2575
2807
  export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
2808
+ protected firstPartyEndpoint?: boolean;
2809
+
2810
+ /** @see {@link astraRulesApply} */
2811
+ protected get astraRulesApply(): boolean {
2812
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
2813
+ }
2814
+
2576
2815
  _lc_stream_delay: number;
2577
2816
 
2578
2817
  constructor(
@@ -2580,6 +2819,7 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
2580
2819
  ) {
2581
2820
  super(withLibreChatOpenAIFields(fields));
2582
2821
  this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
2822
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
2583
2823
  }
2584
2824
 
2585
2825
  public get exposedClient(): CustomOpenAIClient {
@@ -2627,7 +2867,7 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
2627
2867
  getReasoningParams(
2628
2868
  options?: this['ParsedCallOptions']
2629
2869
  ): OpenAIClient.Reasoning | undefined {
2630
- return getReasoningParams(this.reasoning, options);
2870
+ return getReasoningParams(this.astraRulesApply, this.reasoning, options);
2631
2871
  }
2632
2872
 
2633
2873
  protected _getReasoningParams(
@@ -2670,6 +2910,13 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
2670
2910
  }
2671
2911
 
2672
2912
  export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
2913
+ protected firstPartyEndpoint?: boolean;
2914
+
2915
+ /** @see {@link astraRulesApply} */
2916
+ protected get astraRulesApply(): boolean {
2917
+ return astraRulesApply(this.model, this.firstPartyEndpoint);
2918
+ }
2919
+
2673
2920
  _lc_stream_delay: number;
2674
2921
 
2675
2922
  constructor(fields?: LibreChatAzureOpenAIFields) {
@@ -2677,6 +2924,7 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
2677
2924
  this.completions = new LibreChatAzureOpenAICompletions(fields);
2678
2925
  this.responses = new LibreChatAzureOpenAIResponses(fields);
2679
2926
  this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
2927
+ this.firstPartyEndpoint = fields?.firstPartyEndpoint;
2680
2928
  }
2681
2929
 
2682
2930
  public get exposedClient(): CustomOpenAIClient {
@@ -2686,6 +2934,7 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
2686
2934
  this._useResponsesApi(undefined)
2687
2935
  ) as CustomOpenAIClient;
2688
2936
  }
2937
+
2689
2938
  static lc_name(): 'LibreChatAzureOpenAI' {
2690
2939
  return 'LibreChatAzureOpenAI';
2691
2940
  }
@@ -2696,7 +2945,12 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
2696
2945
  getReasoningParams(
2697
2946
  options?: this['ParsedCallOptions']
2698
2947
  ): OpenAIClient.Reasoning | undefined {
2699
- return getGatedReasoningParams(this.model, this.reasoning, options);
2948
+ return getGatedReasoningParams(
2949
+ this.model,
2950
+ this.astraRulesApply,
2951
+ this.reasoning,
2952
+ options
2953
+ );
2700
2954
  }
2701
2955
 
2702
2956
  protected _getReasoningParams(
@@ -11,6 +11,9 @@ const DEFAULT_MAX_LLM_OUTPUT_CHARS = 50000;
11
11
  * this we drop it whole rather than emit a useless sliver. */
12
12
  const MIN_PARTIAL_HIGHLIGHT_CHARS = 200;
13
13
 
14
+ export const MISSING_SEARCH_RESULT_DATA_ERROR =
15
+ 'Search provider returned no result data';
16
+
14
17
  /** Resolves the per-search highlight budget from config, the
15
18
  * `SEARCH_MAX_LLM_OUTPUT_CHARS` env var, or the default (50,000 chars). */
16
19
  export function resolveMaxLLMOutputChars(maxOutputChars?: number): number {
@@ -219,9 +222,16 @@ function formatSource(
219
222
 
220
223
  export function formatResultsForLLM(
221
224
  turn: number,
222
- results: t.SearchResultData,
225
+ results?: t.SearchResultData | null,
223
226
  maxOutputChars?: number
224
227
  ): { output: string; references: t.ResultReference[] } {
228
+ if (results == null) {
229
+ return {
230
+ output: `Search failed: ${MISSING_SEARCH_RESULT_DATA_ERROR}`,
231
+ references: [],
232
+ };
233
+ }
234
+
225
235
  /** Bound highlight content to the per-search budget before formatting */
226
236
  const trimmedHighlights = trimHighlightsToBudget(
227
237
  results,
@@ -239,6 +249,10 @@ export function formatResultsForLLM(
239
249
 
240
250
  const references: t.ResultReference[] = [];
241
251
 
252
+ if (results.error != null && results.error !== '') {
253
+ outputLines.push(`Search failed: ${results.error}`);
254
+ }
255
+
242
256
  // Organic (web) results
243
257
  if (results.organic?.length != null && results.organic.length > 0) {
244
258
  addSection(`Web Results, Turn ${turn}`);
@@ -21,7 +21,10 @@ import { INTENT_PROPERTY } from '@/tools/intentArg';
21
21
  import { createCrwScraper } from './crw-scraper';
22
22
  import { expandHighlights } from './highlights';
23
23
  import { createSearchMetrics } from './metrics';
24
- import { formatResultsForLLM } from './format';
24
+ import {
25
+ formatResultsForLLM,
26
+ MISSING_SEARCH_RESULT_DATA_ERROR,
27
+ } from './format';
25
28
  import { createDefaultLogger } from './utils';
26
29
  import { createReranker } from './rerankers';
27
30
  import { Constants } from '@/common';
@@ -66,6 +69,12 @@ export function resolveSearchOutcome(
66
69
  return `Found ${count} result${count === 1 ? '' : 's'} for "${query}"`;
67
70
  }
68
71
 
72
+ export function normalizeSearchResultData(
73
+ result: t.SearchResultData | null | undefined
74
+ ): t.SearchResultData {
75
+ return result ?? { error: MISSING_SEARCH_RESULT_DATA_ERROR };
76
+ }
77
+
69
78
  /** Distinct rows across the main search's two collections. SearXNG derives
70
79
  * both from one result array — a row matching its news heuristic lands in
71
80
  * `organic` and `topStories` alike — so summing the lengths would report
@@ -431,12 +440,13 @@ function createTool({
431
440
  }),
432
441
  });
433
442
  const turn = runnableConfig.toolCall?.turn ?? 0;
443
+ const resultData = normalizeSearchResultData(searchResult);
434
444
  const { output, references } = formatResultsForLLM(
435
445
  turn,
436
- searchResult,
446
+ resultData,
437
447
  maxOutputChars
438
448
  );
439
- const data: t.SearchResultData = { turn, ...searchResult, references };
449
+ const data: t.SearchResultData = { turn, ...resultData, references };
440
450
  const outcome = resolveSearchOutcome(data, query);
441
451
  return [
442
452
  output,
package/src/types/llm.ts CHANGED
@@ -81,6 +81,29 @@ export type GoogleThinkingConfig = {
81
81
  export type ManagedRequestOptions = {
82
82
  promptCacheExplicit?: boolean;
83
83
  safety_identifier?: string;
84
+ /**
85
+ * Declares that this client talks to the first-party OpenAI or Azure surface.
86
+ * Gates the model-specific request *shaping* documented only for it —
87
+ * currently GPT-6 Astra's rejected sampling and logprob parameters, its
88
+ * unsupported reasoning efforts, and the encrypted reasoning it supports —
89
+ * and defaults to off.
90
+ *
91
+ * Shaping only. Which API serves the turn is not decided here: GPT-6 Astra
92
+ * serves tool calls only from the Responses API, and a caller wanting them
93
+ * must select it with `useResponsesApi`, alongside the rest of the request
94
+ * shaping that depends on which API is in use.
95
+ *
96
+ * The shaping runs inside this SDK's own request delegates. A caller that
97
+ * supplies its own `completions` or `responses` delegate replaces that
98
+ * construction and owns the request shaping for it — this flag cannot reach
99
+ * inside a delegate it did not build.
100
+ *
101
+ * Declared rather than inferred from a base URL: only the caller knows
102
+ * whether a URL is a faithful first-party route, a gateway, or a proxy with
103
+ * its own semantics, and every gate it controls removes capability, so
104
+ * guessing wrong silently degrades an endpoint the SDK cannot see.
105
+ */
106
+ firstPartyEndpoint?: boolean;
84
107
  };
85
108
  /**
86
109
  * Adaptive stream-smoothing configuration shared by every provider client.