@f5-sales-demo/pi-ai 23.9.5 → 23.10.1

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": "23.9.5",
4
+ "version": "23.10.1",
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.124.0",
47
47
  "@aws-sdk/client-bedrock-runtime": "3.1127.0",
48
48
  "@bufbuild/protobuf": "2.14.1",
49
- "@f5-sales-demo/pi-utils": "23.9.5",
49
+ "@f5-sales-demo/pi-utils": "23.10.1",
50
50
  "@google/genai": "2.21.0",
51
51
  "@sinclair/typebox": "0.34.52",
52
52
  "@smithy/node-http-handler": "4.12.1",
@@ -12,7 +12,7 @@ import * as fs from "node:fs/promises";
12
12
  import * as path from "node:path";
13
13
  import { $envExact, getAgentDbPath, logger } from "@f5-sales-demo/pi-utils";
14
14
  import { getEnvApiKey } from "./stream";
15
- import type { Provider } from "./types";
15
+ import type { Model, Provider } from "./types";
16
16
  import type {
17
17
  CredentialRankingStrategy,
18
18
  UsageCredential,
@@ -44,6 +44,7 @@ import { loginHuggingface } from "./utils/oauth/huggingface";
44
44
  import { loginKagi } from "./utils/oauth/kagi";
45
45
  import { loginKilo } from "./utils/oauth/kilo";
46
46
  import { loginKimi } from "./utils/oauth/kimi";
47
+ import type { KiroCredentials } from "./utils/oauth/kiro-credentials";
47
48
  import { loginLiteLLM } from "./utils/oauth/litellm";
48
49
  import { loginLmStudio } from "./utils/oauth/lm-studio";
49
50
  import { loginMiniMaxCode, loginMiniMaxCodeCn } from "./utils/oauth/minimax-code";
@@ -267,6 +268,7 @@ type UsageRequestDescriptor = {
267
268
  };
268
269
 
269
270
  type AuthApiKeyOptions = {
271
+ kiroConnection?: Model["kiroConnection"];
270
272
  baseUrl?: string;
271
273
  modelId?: string;
272
274
  };
@@ -1284,7 +1286,19 @@ export class AuthStorage {
1284
1286
  break;
1285
1287
  }
1286
1288
  }
1287
- const newCredential: OAuthCredential = { type: "oauth", ...credentials };
1289
+ const newCredential: OAuthCredential = {
1290
+ type: "oauth",
1291
+ ...credentials,
1292
+ ...(provider === "kiro"
1293
+ ? {
1294
+ selectedAt: Math.max(
1295
+ Date.now(),
1296
+ ((this.#store.getLatestKiroSelection()?.credential as Partial<KiroCredentials> | undefined)
1297
+ ?.selectedAt ?? 0) + 1,
1298
+ ),
1299
+ }
1300
+ : {}),
1301
+ };
1288
1302
  await this.#upsertOAuthCredential(credentialProvider, newCredential);
1289
1303
  }
1290
1304
 
@@ -1971,13 +1985,29 @@ export class AuthStorage {
1971
1985
  sessionId?: string,
1972
1986
  options?: AuthApiKeyOptions,
1973
1987
  ): Promise<string | undefined> {
1974
- const credentials = this.#getStoredCredentials(provider)
1988
+ let credentials = this.#getStoredCredentials(provider)
1975
1989
  .map((entry, index) => ({ credential: entry.credential, index, credentialId: entry.id }))
1976
1990
  .filter(
1977
1991
  (entry): entry is { credential: OAuthCredential; index: number; credentialId: number } =>
1978
1992
  entry.credential.type === "oauth",
1979
1993
  );
1980
1994
 
1995
+ if (provider === "kiro") {
1996
+ const binding = options?.kiroConnection;
1997
+ const latest = binding ? undefined : this.#store.getLatestKiroSelection();
1998
+ credentials = binding
1999
+ ? credentials.filter(entry => {
2000
+ const c = entry.credential as OAuthCredential & Partial<KiroCredentials>;
2001
+ return (
2002
+ c.accountId === binding.accountId &&
2003
+ c.profileArn === binding.profileArn &&
2004
+ c.serviceRegion === binding.serviceRegion
2005
+ );
2006
+ })
2007
+ : credentials.filter(entry => {
2008
+ return latest && !latest.disabledCause && entry.credentialId === latest.id;
2009
+ });
2010
+ }
1981
2011
  if (credentials.length === 0) return undefined;
1982
2012
 
1983
2013
  const providerKey = this.#getProviderTypeKey(provider, "oauth");
@@ -2172,6 +2202,8 @@ export class AuthStorage {
2172
2202
  }
2173
2203
  if (!result) return undefined;
2174
2204
  const updated: OAuthCredential = {
2205
+ ...selection.credential,
2206
+ ...result.newCredentials,
2175
2207
  type: "oauth",
2176
2208
  access: result.newCredentials.access,
2177
2209
  refresh: result.newCredentials.refresh,
@@ -2425,6 +2457,12 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
2425
2457
 
2426
2458
  function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null {
2427
2459
  if (credential.type === "api_key") return null;
2460
+ if (provider === "kiro") {
2461
+ const c = credential as OAuthCredential & Partial<KiroCredentials>;
2462
+ return c.accountId && c.profileArn && c.serviceRegion
2463
+ ? JSON.stringify([c.accountId, c.profileArn, c.serviceRegion])
2464
+ : null;
2465
+ }
2428
2466
  return resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential));
2429
2467
  }
2430
2468
 
@@ -2836,6 +2874,17 @@ export class AuthCredentialStore {
2836
2874
 
2837
2875
  // ─── AuthCredentialStore interface ──────────────────────────────────────
2838
2876
 
2877
+ /** Keep the most recent explicit native connection selected even after it is disabled. */
2878
+ getLatestKiroSelection(): StoredAuthCredential | undefined {
2879
+ const row = this.#db
2880
+ .prepare(
2881
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE provider = 'kiro' AND credential_type = 'oauth' ORDER BY CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.selectedAt'), 0) ELSE 0 END DESC, id DESC LIMIT 1",
2882
+ )
2883
+ .get() as AuthRow | undefined;
2884
+ if (!row) return undefined;
2885
+ const credential = deserializeCredential(row);
2886
+ return credential ? toStoredAuthCredential(row, credential) : undefined;
2887
+ }
2839
2888
  listAuthCredentials(provider?: string): StoredAuthCredential[] {
2840
2889
  const rows =
2841
2890
  (provider
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export * from "./providers/google";
20
20
  export * from "./providers/google-gemini-cli";
21
21
  export * from "./providers/google-vertex";
22
22
  export * from "./providers/kimi";
23
+ export * from "./providers/kiro";
24
+ export * from "./providers/kiro-service";
23
25
  export type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses";
24
26
  export * from "./providers/openai-completions";
25
27
  export * from "./providers/openai-responses";
@@ -28,6 +28,8 @@ export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
28
28
  export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload = unknown> {
29
29
  /** Provider id used for static lookup and cache namespacing. */
30
30
  providerId: Provider;
31
+ /** Credential/profile-specific namespace; model routing still uses providerId. */
32
+ cacheNamespace?: string;
31
33
  /** Optional static list override. When omitted, bundled models.json is used. */
32
34
  staticModels?: readonly Model<TApi>[];
33
35
  /** Optional override for the cache database path. Default: <agent-dir>/models.db. */
@@ -98,7 +100,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
98
100
  const staticModels = normalizeModelList<TApi>(
99
101
  options.staticModels ?? getBundledModels(options.providerId as GeneratedProvider),
100
102
  );
101
- const cache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath);
103
+ const cache = readModelCache<TApi>(options.cacheNamespace ?? options.providerId, ttlMs, now, dbPath);
102
104
  const dynamicFetcher = options.fetchDynamicModels;
103
105
  const hasDynamicFetcher = typeof dynamicFetcher === "function";
104
106
  const hasAuthoritativeCache = (cache?.authoritative ?? false) || !hasDynamicFetcher;
@@ -129,13 +131,13 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
129
131
  if (shouldFetchFromNetwork) {
130
132
  if (dynamicFetchSucceeded) {
131
133
  const snapshotModels = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), dynamicModels);
132
- writeModelCache(options.providerId, now(), snapshotModels, true, dbPath);
134
+ writeModelCache(options.cacheNamespace ?? options.providerId, now(), snapshotModels, true, dbPath);
133
135
  } else {
134
136
  // Dynamic fetch failed — update cache with a non-authoritative snapshot so
135
137
  // stale state remains visible while retry backoff still applies.
136
- const latestCache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath);
138
+ const latestCache = readModelCache<TApi>(options.cacheNamespace ?? options.providerId, ttlMs, now, dbPath);
137
139
  writeModelCache(
138
- options.providerId,
140
+ options.cacheNamespace ?? options.providerId,
139
141
  now(),
140
142
  mergeDynamicModels(
141
143
  mergeModelSources(staticModels, modelsDevModels),
@@ -284,4 +284,5 @@ export const DEFAULT_MODEL_PER_PROVIDER: Record<KnownProvider, string> = {
284
284
  "minimax-code-cn": "MiniMax-M2.5",
285
285
  "openai-codex": "gpt-6.1-sol",
286
286
  "gitlab-duo": "duo-chat-sonnet-4-5",
287
+ kiro: "auto",
287
288
  } as Record<KnownProvider, string>;
@@ -1,6 +1,8 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
+ import { discoverKiroModels, kiroCacheNamespace } from "../providers/kiro-service";
2
3
  import { fetchCodexModels } from "../utils/discovery/codex";
3
4
  import { fetchCursorUsableModels } from "../utils/discovery/cursor";
5
+ import type { KiroCredentials } from "../utils/oauth/kiro-credentials";
4
6
 
5
7
  // ---------------------------------------------------------------------------
6
8
  // OpenAI Codex
@@ -105,3 +107,14 @@ export interface ZaiModelManagerConfig {}
105
107
  export function zaiModelManagerOptions(_config: ZaiModelManagerConfig = {}): ModelManagerOptions<"anthropic-messages"> {
106
108
  return { providerId: "zai" };
107
109
  }
110
+
111
+ /** Kiro catalogs are private to the selected native account and profile. */
112
+ export function kiroModelManagerOptions(config: { apiKey: string }): ModelManagerOptions<"kiro-api"> {
113
+ const credentials = JSON.parse(config.apiKey) as KiroCredentials;
114
+ return {
115
+ providerId: "kiro",
116
+ cacheNamespace: kiroCacheNamespace(credentials),
117
+ staticModels: [],
118
+ fetchDynamicModels: async signal => (await discoverKiroModels(credentials, { signal })).models,
119
+ };
120
+ }
@@ -192,7 +192,7 @@ function getCacheControl(
192
192
  }
193
193
 
194
194
  // Stealth mode: Mimic Claude Code headers and tool prefixing.
195
- export const claudeCodeVersion = "2.1.304";
195
+ export const claudeCodeVersion = "2.1.311";
196
196
  export const claudeToolPrefix: string = "proxy_";
197
197
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
198
198
 
@@ -79,7 +79,7 @@ export function resolveAntigravityServingModelId(requestedModelId: string): stri
79
79
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
80
80
  */
81
81
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
82
- const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.64.0";
82
+ const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.65.0";
83
83
  const platform = process.platform === "win32" ? "win32" : process.platform;
84
84
  const arch = process.arch === "x64" ? "x64" : process.arch;
85
85
  return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hongyi Lyu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Model } from "../types";
3
+ import { type KiroCredentials, validKiroRegion } from "../utils/oauth/kiro-credentials";
4
+
5
+ export interface KiroServiceOptions {
6
+ fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
7
+ signal?: AbortSignal;
8
+ regions?: string[];
9
+ }
10
+ export interface KiroProfile {
11
+ arn: string;
12
+ name: string;
13
+ region: string;
14
+ }
15
+ export function kiroEndpoint(region: string): string {
16
+ if (!validKiroRegion(region)) throw new Error("Invalid Kiro service region");
17
+ return `https://q.${region}.amazonaws.com`;
18
+ }
19
+ export function kiroCacheNamespace(c: KiroCredentials): string {
20
+ if (!c.accountId || !c.profileArn || !c.serviceRegion)
21
+ throw new Error("Kiro account/profile selection required. Run /login kiro.");
22
+ return `kiro:${createHash("sha256")
23
+ .update(JSON.stringify([c.accountId, c.profileArn, c.serviceRegion]))
24
+ .digest("hex")}`;
25
+ }
26
+ export async function kiroRequest(url: string, init: RequestInit, options: KiroServiceOptions = {}): Promise<Response> {
27
+ const signal = options.signal
28
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(30000)])
29
+ : AbortSignal.timeout(30000);
30
+ signal.throwIfAborted();
31
+ return (options.fetch ?? globalThis.fetch)(url, { ...init, signal });
32
+ }
33
+ function headers(c: KiroCredentials): Record<string, string> {
34
+ return { Authorization: `Bearer ${c.access}`, "Content-Type": "application/json", "User-Agent": "xcsh-kiro" };
35
+ }
36
+ async function jsonResponse(response: Response): Promise<Record<string, any>> {
37
+ if (!response.ok)
38
+ throw new Error(
39
+ `Kiro service HTTP ${response.status}${response.status === 401 || response.status === 403 ? ". Reconnect with /login kiro." : ". Retry when the service is available."}`,
40
+ );
41
+ const body = await response.json();
42
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Malformed Kiro response");
43
+ return body;
44
+ }
45
+ async function pages<T>(
46
+ request: (token?: string) => Promise<Record<string, any>>,
47
+ field: string,
48
+ ): Promise<{ items: T[]; defaultModel?: unknown }> {
49
+ const items: T[] = [];
50
+ const seen = new Set<string>();
51
+ let token: string | undefined;
52
+ let defaultModel: unknown;
53
+ for (let page = 0; page < 100; page++) {
54
+ const body = await request(token);
55
+ if (!Array.isArray(body[field])) throw new Error(`Malformed Kiro ${field} response`);
56
+ items.push(...body[field]);
57
+ defaultModel = body.defaultModel ?? defaultModel;
58
+ if (!body.nextToken) return { items, defaultModel };
59
+ if (typeof body.nextToken !== "string" || seen.has(body.nextToken)) throw new Error("Invalid Kiro pagination");
60
+ token = body.nextToken;
61
+ seen.add(token);
62
+ }
63
+ throw new Error("Kiro pagination exceeded limit");
64
+ }
65
+ export async function discoverKiroProfiles(
66
+ c: KiroCredentials,
67
+ options: KiroServiceOptions = {},
68
+ ): Promise<KiroProfile[]> {
69
+ const profiles = new Map<string, KiroProfile>();
70
+ // Desktop 1.4.26 maps BuilderId to this service-owned profile; the API
71
+ // explicitly rejects ListAvailableProfiles for Builder ID.
72
+ if (c.authMethod === "builder-id")
73
+ return [
74
+ {
75
+ arn: "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX",
76
+ name: "AWS Builder ID",
77
+ region: "us-east-1",
78
+ },
79
+ ];
80
+ const regions = options.regions ?? ["us-east-1", "eu-central-1"];
81
+ for (const region of regions) {
82
+ const result = await pages<{ arn: string; name?: string }>(
83
+ async nextToken =>
84
+ jsonResponse(
85
+ await kiroRequest(
86
+ `${kiroEndpoint(region)}/ListAvailableProfiles`,
87
+ {
88
+ method: "POST",
89
+ headers: headers(c),
90
+ body: JSON.stringify({ ...(nextToken ? { nextToken } : {}) }),
91
+ },
92
+ options,
93
+ ),
94
+ ),
95
+ "profiles",
96
+ );
97
+ for (const p of result.items) {
98
+ if (typeof p.arn !== "string") throw new Error("Malformed Kiro profile");
99
+ const serviceRegion = p.arn.split(":")[3];
100
+ if (!serviceRegion || !validKiroRegion(serviceRegion)) throw new Error("Invalid Kiro profile region");
101
+ profiles.set(p.arn, { arn: p.arn, name: p.name ?? "Kiro profile", region: serviceRegion });
102
+ }
103
+ }
104
+ if (!profiles.size)
105
+ throw new Error("No Kiro profiles available for this account. Check entitlement or choose another account.");
106
+ return [...profiles.values()];
107
+ }
108
+ export async function discoverKiroModels(
109
+ c: KiroCredentials,
110
+ options: KiroServiceOptions = {},
111
+ ): Promise<{ models: Model<"kiro-api">[]; defaultModel?: string }> {
112
+ if (!c.profileArn || !c.serviceRegion) throw new Error("Select a Kiro profile with /login kiro");
113
+ const result = await pages<Record<string, any>>(async nextToken => {
114
+ const url = new URL(`${kiroEndpoint(c.serviceRegion!)}/ListAvailableModels`);
115
+ url.searchParams.set("origin", "KIRO_CLI");
116
+ url.searchParams.set("profileArn", c.profileArn!);
117
+ if (nextToken) url.searchParams.set("nextToken", nextToken);
118
+ return jsonResponse(await kiroRequest(url.toString(), { headers: headers(c) }, options));
119
+ }, "models");
120
+ const models = result.items.map(m => {
121
+ const input = m.tokenLimits?.maxInputTokens;
122
+ const output = m.tokenLimits?.maxOutputTokens;
123
+ if (
124
+ typeof m.modelId !== "string" ||
125
+ !m.modelId ||
126
+ !Number.isFinite(input) ||
127
+ input <= 0 ||
128
+ !Number.isFinite(output) ||
129
+ output <= 0
130
+ )
131
+ throw new Error("Kiro model is missing service limits or ID");
132
+ const model: Model<"kiro-api"> = {
133
+ id: m.modelId,
134
+ name: m.modelName ?? m.modelId,
135
+ provider: "kiro",
136
+ kiroConnection: { accountId: c.accountId!, profileArn: c.profileArn!, serviceRegion: c.serviceRegion! },
137
+ api: "kiro-api",
138
+ baseUrl: `${kiroEndpoint(c.serviceRegion!)}/generateAssistantResponse`,
139
+ reasoning: false,
140
+ input: m.supportedInputTypes?.includes("IMAGE") ? ["text", "image"] : ["text"],
141
+ contextWindow: input,
142
+ maxTokens: output,
143
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, pricingKnown: false },
144
+ };
145
+ return model;
146
+ });
147
+ if (!models.length) throw new Error("No Kiro models available for the selected profile");
148
+ const d = result.defaultModel;
149
+ const defaultModel =
150
+ typeof d === "string" ? d : d && typeof d === "object" ? (d as { modelId?: string }).modelId : undefined;
151
+ for (const model of models) model.providerDefault = model.id === defaultModel;
152
+ return { models, defaultModel: models.some(m => m.id === defaultModel) ? defaultModel : undefined };
153
+ }