@f5-sales-demo/pi-ai 20.20.6 → 20.22.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## Unreleased
6
+
7
+ ### Fixed
8
+
9
+ - Omit an empty OpenAI-compatible `tools` field unless prior tool-call history requires the explicit empty array, and suppress the internal no-auth credential sentinel from keyless request headers ([#2942](https://github.com/f5-sales-demo/xcsh/issues/2942), [#3361](https://github.com/f5-sales-demo/xcsh/issues/3361)).
10
+
5
11
  ## [20.3.0] - 2026-08-04
6
12
 
7
13
  ### Changed
@@ -433,7 +439,7 @@
433
439
  - Removed `UsageCache` and `UsageCacheEntry` interfaces—caching is now handled internally by AuthStorage
434
440
  - Removed `google-gemini-cli-usage` export; use new `gemini` usage provider instead
435
441
  - Removed `resetInMs` computation from all usage providers
436
- - Removed cache TTL constants and cache management from usage fetchers (claude, github-copilot, google-antigravity, kimi, openai-codex, zai)
442
+ - Removed cache TTL constants and cache management from usage fetchers (claude, GitHub Copilot, google-antigravity, kimi, openai-codex, zai)
437
443
 
438
444
  ### Fixed
439
445
 
@@ -1294,7 +1300,7 @@
1294
1300
 
1295
1301
  ### Changed
1296
1302
 
1297
- - Updated @anthropic-ai/sdk to ^0.72.1
1303
+ - Updated `@anthropic-ai/sdk` to ^0.72.1
1298
1304
  - Updated @aws-sdk/client-bedrock-runtime to ^3.982.0
1299
1305
  - Updated @google/genai to ^1.39.0
1300
1306
  - Updated @smithy/node-http-handler to ^4.4.9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "20.20.6",
4
+ "version": "20.22.2",
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": "20.20.6",
49
+ "@f5-sales-demo/pi-utils": "20.22.2",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -18,6 +18,7 @@ import {
18
18
  type Message,
19
19
  type MessageAttribution,
20
20
  type Model,
21
+ NO_AUTH_API_KEY,
21
22
  type ServiceTier,
22
23
  type StopReason,
23
24
  type StreamFunction,
@@ -581,6 +582,7 @@ async function createClient(
581
582
  apiKey = $env.OPENAI_API_KEY;
582
583
  }
583
584
  const rawApiKey = apiKey;
585
+ const omitAuthorization = rawApiKey === NO_AUTH_API_KEY;
584
586
 
585
587
  let headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
586
588
  if (model.provider === "openrouter") {
@@ -609,7 +611,19 @@ async function createClient(
609
611
  let capturedErrorResponse: CapturedHttpErrorResponse | undefined;
610
612
  const wrappedFetch = Object.assign(
611
613
  async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
612
- const response = await fetch(input, init);
614
+ let fetchInput = input;
615
+ let fetchInit = init;
616
+ if (omitAuthorization) {
617
+ if (input instanceof Request) {
618
+ const headers = new Headers(input.headers);
619
+ headers.delete("Authorization");
620
+ fetchInput = new Request(input, { headers });
621
+ }
622
+ const headers = new Headers(init?.headers);
623
+ headers.delete("Authorization");
624
+ fetchInit = { ...init, headers };
625
+ }
626
+ const response = await fetch(fetchInput, fetchInit);
613
627
  if (response.ok) {
614
628
  capturedErrorResponse = undefined;
615
629
  return response;
@@ -715,7 +729,7 @@ function buildParams(
715
729
  params.service_tier = options.serviceTier;
716
730
  }
717
731
 
718
- if (context.tools) {
732
+ if (context.tools && context.tools.length > 0) {
719
733
  const builtTools = convertTools(context.tools, compat, toolStrictModeOverride);
720
734
  params.tools = builtTools.tools;
721
735
  toolStrictMode = builtTools.toolStrictMode;
package/src/types.ts CHANGED
@@ -30,6 +30,9 @@ import type { AssistantMessageEventStream } from "./utils/event-stream";
30
30
 
31
31
  export type { AssistantMessageEventStream } from "./utils/event-stream";
32
32
 
33
+ /** Internal credential sentinel that keeps keyless providers selectable without sending authentication. */
34
+ export const NO_AUTH_API_KEY = "N/A";
35
+
33
36
  export type KnownApi =
34
37
  | "openai-completions"
35
38
  | "openai-responses"
@@ -19,6 +19,11 @@ const CALLBACK_PATH = "/callback";
19
19
 
20
20
  export type CallbackResult = { code: string; state: string };
21
21
 
22
+ interface CallbackServer {
23
+ port: number;
24
+ stop(closeActiveConnections?: boolean): void;
25
+ }
26
+
22
27
  export interface OAuthCallbackFlowOptions {
23
28
  preferredPort: number;
24
29
  callbackPath?: string;
@@ -117,13 +122,13 @@ export abstract class OAuthCallbackFlow {
117
122
  /**
118
123
  * Start callback server, trying preferred port first, falling back to random.
119
124
  */
120
- async #startCallbackServer(expectedState: string): Promise<{ server: Bun.Server<unknown>; redirectUri: string }> {
125
+ async #startCallbackServer(expectedState: string): Promise<{ server: CallbackServer; redirectUri: string }> {
121
126
  try {
122
127
  const server = this.#createServer(this.preferredPort, expectedState);
123
128
  if (this.redirectUri) {
124
129
  return { server, redirectUri: this.redirectUri };
125
130
  }
126
- const redirectUri = `http://${this.callbackHostname}:${this.preferredPort}${this.callbackPath}`;
131
+ const redirectUri = `http://${this.callbackHostname}:${server.port}${this.callbackPath}`;
127
132
  return { server, redirectUri };
128
133
  } catch {
129
134
  if (this.redirectUri) {
@@ -142,13 +147,45 @@ export abstract class OAuthCallbackFlow {
142
147
  /**
143
148
  * Create HTTP server for OAuth callback.
144
149
  */
145
- #createServer(port: number, expectedState: string): Bun.Server<unknown> {
146
- return Bun.serve({
147
- hostname: this.callbackHostname,
150
+ #createServer(port: number, expectedState: string): CallbackServer {
151
+ if (this.callbackHostname !== DEFAULT_HOSTNAME) {
152
+ return this.#createSingleServer(this.callbackHostname, port, expectedState);
153
+ }
154
+
155
+ // Bun resolves a localhost listener to IPv6 on some Linux images while clients resolve
156
+ // localhost to IPv4. Bind both loopback addresses, but keep localhost in the redirect URI
157
+ // so OAuth providers receive the conventional, portable callback host.
158
+ const ipv4Server = this.#createSingleServer("127.0.0.1", port, expectedState);
159
+ try {
160
+ const ipv6Server = this.#createSingleServer("::1", ipv4Server.port, expectedState);
161
+ return {
162
+ port: ipv4Server.port,
163
+ stop: closeActiveConnections => {
164
+ ipv4Server.stop(closeActiveConnections);
165
+ ipv6Server.stop(closeActiveConnections);
166
+ },
167
+ };
168
+ } catch (error) {
169
+ ipv4Server.stop();
170
+ throw error;
171
+ }
172
+ }
173
+
174
+ #createSingleServer(hostname: string, port: number, expectedState: string): CallbackServer {
175
+ const server = Bun.serve({
176
+ hostname,
148
177
  port,
149
178
  reusePort: false,
150
179
  fetch: req => this.#handleCallback(req, expectedState),
151
180
  });
181
+ if (server.port === undefined) {
182
+ server.stop();
183
+ throw new Error(`OAuth callback server did not report a bound port for ${hostname}`);
184
+ }
185
+ return {
186
+ port: server.port,
187
+ stop: closeActiveConnections => server.stop(closeActiveConnections),
188
+ };
152
189
  }
153
190
 
154
191
  /**