@poolzin/pool-bot 2026.2.4 → 2026.2.6

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.
Files changed (37) hide show
  1. package/dist/agents/auth-profiles/profiles.js +9 -0
  2. package/dist/agents/auth-profiles.js +1 -1
  3. package/dist/agents/huggingface-models.js +166 -0
  4. package/dist/agents/model-auth.js +6 -0
  5. package/dist/agents/model-forward-compat.js +187 -0
  6. package/dist/agents/pi-embedded-runner/model.js +10 -56
  7. package/dist/browser/constants.js +1 -1
  8. package/dist/browser/profiles.js +1 -1
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli/config-cli.js +17 -3
  11. package/dist/cli/program/register.onboard.js +38 -5
  12. package/dist/commands/auth-choice-options.js +71 -7
  13. package/dist/commands/auth-choice.apply.api-providers.js +202 -97
  14. package/dist/commands/auth-choice.apply.huggingface.js +130 -0
  15. package/dist/commands/auth-choice.apply.openrouter.js +77 -0
  16. package/dist/commands/auth-choice.apply.plugin-provider.js +1 -56
  17. package/dist/commands/auth-choice.apply.vllm.js +92 -0
  18. package/dist/commands/auth-choice.preferred-provider.js +10 -0
  19. package/dist/commands/models/auth.js +1 -58
  20. package/dist/commands/models/list.errors.js +14 -0
  21. package/dist/commands/models/list.list-command.js +32 -21
  22. package/dist/commands/models/list.registry.js +120 -28
  23. package/dist/commands/models/list.status-command.js +1 -0
  24. package/dist/commands/models/shared.js +14 -0
  25. package/dist/commands/onboard-auth.config-core.js +265 -8
  26. package/dist/commands/onboard-auth.credentials.js +47 -6
  27. package/dist/commands/onboard-auth.js +3 -3
  28. package/dist/commands/onboard-auth.models.js +67 -0
  29. package/dist/commands/onboard-custom.js +181 -70
  30. package/dist/commands/onboard-non-interactive/api-keys.js +10 -1
  31. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +15 -7
  32. package/dist/commands/onboard-non-interactive/local/auth-choice.js +322 -124
  33. package/dist/commands/provider-auth-helpers.js +61 -0
  34. package/dist/commands/zai-endpoint-detect.js +97 -0
  35. package/dist/config/legacy.migrations.part-3.js +57 -0
  36. package/dist/terminal/theme.js +1 -1
  37. package/package.json +1 -1
@@ -7,18 +7,24 @@ const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1";
7
7
  const DEFAULT_CONTEXT_WINDOW = 4096;
8
8
  const DEFAULT_MAX_TOKENS = 4096;
9
9
  const VERIFY_TIMEOUT_MS = 10000;
10
+ export class CustomApiError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.name = "CustomApiError";
15
+ this.code = code;
16
+ }
17
+ }
10
18
  const COMPATIBILITY_OPTIONS = [
11
19
  {
12
20
  value: "openai",
13
21
  label: "OpenAI-compatible",
14
22
  hint: "Uses /chat/completions",
15
- api: "openai-completions",
16
23
  },
17
24
  {
18
25
  value: "anthropic",
19
26
  label: "Anthropic-compatible",
20
27
  hint: "Uses /messages",
21
- api: "anthropic-messages",
22
28
  },
23
29
  {
24
30
  value: "unknown",
@@ -183,6 +189,155 @@ async function promptBaseUrlAndKey(params) {
183
189
  });
184
190
  return { baseUrl: baseUrlInput.trim(), apiKey: apiKeyInput.trim() };
185
191
  }
192
+ function resolveProviderApi(compatibility) {
193
+ return compatibility === "anthropic" ? "anthropic-messages" : "openai-completions";
194
+ }
195
+ function parseCustomApiCompatibility(raw) {
196
+ const compatibilityRaw = raw?.trim().toLowerCase();
197
+ if (!compatibilityRaw) {
198
+ return "openai";
199
+ }
200
+ if (compatibilityRaw !== "openai" && compatibilityRaw !== "anthropic") {
201
+ throw new CustomApiError("invalid_compatibility", 'Invalid --custom-compatibility (use "openai" or "anthropic").');
202
+ }
203
+ return compatibilityRaw;
204
+ }
205
+ export function resolveCustomProviderId(params) {
206
+ const providers = params.config.models?.providers ?? {};
207
+ const baseUrl = params.baseUrl.trim();
208
+ const explicitProviderId = params.providerId?.trim();
209
+ if (explicitProviderId && !normalizeEndpointId(explicitProviderId)) {
210
+ throw new CustomApiError("invalid_provider_id", "Custom provider ID must include letters, numbers, or hyphens.");
211
+ }
212
+ const requestedProviderId = explicitProviderId || buildEndpointIdFromUrl(baseUrl);
213
+ const providerIdResult = resolveUniqueEndpointId({
214
+ requestedId: requestedProviderId,
215
+ baseUrl,
216
+ providers,
217
+ });
218
+ return {
219
+ providerId: providerIdResult.providerId,
220
+ ...(providerIdResult.renamed
221
+ ? {
222
+ providerIdRenamedFrom: normalizeEndpointId(requestedProviderId) || "custom",
223
+ }
224
+ : {}),
225
+ };
226
+ }
227
+ export function parseNonInteractiveCustomApiFlags(params) {
228
+ const baseUrl = params.baseUrl?.trim() ?? "";
229
+ const modelId = params.modelId?.trim() ?? "";
230
+ if (!baseUrl || !modelId) {
231
+ throw new CustomApiError("missing_required", [
232
+ 'Auth choice "custom-api-key" requires a base URL and model ID.',
233
+ "Use --custom-base-url and --custom-model-id.",
234
+ ].join("\n"));
235
+ }
236
+ const apiKey = params.apiKey?.trim();
237
+ const providerId = params.providerId?.trim();
238
+ if (providerId && !normalizeEndpointId(providerId)) {
239
+ throw new CustomApiError("invalid_provider_id", "Custom provider ID must include letters, numbers, or hyphens.");
240
+ }
241
+ return {
242
+ baseUrl,
243
+ modelId,
244
+ compatibility: parseCustomApiCompatibility(params.compatibility),
245
+ ...(apiKey ? { apiKey } : {}),
246
+ ...(providerId ? { providerId } : {}),
247
+ };
248
+ }
249
+ export function applyCustomApiConfig(params) {
250
+ const baseUrl = params.baseUrl.trim();
251
+ try {
252
+ new URL(baseUrl);
253
+ }
254
+ catch {
255
+ throw new CustomApiError("invalid_base_url", "Custom provider base URL must be a valid URL.");
256
+ }
257
+ if (params.compatibility !== "openai" && params.compatibility !== "anthropic") {
258
+ throw new CustomApiError("invalid_compatibility", 'Custom provider compatibility must be "openai" or "anthropic".');
259
+ }
260
+ const modelId = params.modelId.trim();
261
+ if (!modelId) {
262
+ throw new CustomApiError("invalid_model_id", "Custom provider model ID is required.");
263
+ }
264
+ const providerIdResult = resolveCustomProviderId({
265
+ config: params.config,
266
+ baseUrl,
267
+ providerId: params.providerId,
268
+ });
269
+ const providerId = providerIdResult.providerId;
270
+ const providers = params.config.models?.providers ?? {};
271
+ const modelRef = modelKey(providerId, modelId);
272
+ const alias = params.alias?.trim() ?? "";
273
+ const aliasError = resolveAliasError({
274
+ raw: alias,
275
+ cfg: params.config,
276
+ modelRef,
277
+ });
278
+ if (aliasError) {
279
+ throw new CustomApiError("invalid_alias", aliasError);
280
+ }
281
+ const existingProvider = providers[providerId];
282
+ const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
283
+ const hasModel = existingModels.some((model) => model.id === modelId);
284
+ const nextModel = {
285
+ id: modelId,
286
+ name: `${modelId} (Custom Provider)`,
287
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
288
+ maxTokens: DEFAULT_MAX_TOKENS,
289
+ input: ["text"],
290
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
291
+ reasoning: false,
292
+ };
293
+ const mergedModels = hasModel ? existingModels : [...existingModels, nextModel];
294
+ const { apiKey: existingApiKey, ...existingProviderRest } = existingProvider ?? {};
295
+ const normalizedApiKey = params.apiKey?.trim() || (existingApiKey ? existingApiKey.trim() : undefined);
296
+ let config = {
297
+ ...params.config,
298
+ models: {
299
+ ...params.config.models,
300
+ mode: params.config.models?.mode ?? "merge",
301
+ providers: {
302
+ ...providers,
303
+ [providerId]: {
304
+ ...existingProviderRest,
305
+ baseUrl,
306
+ api: resolveProviderApi(params.compatibility),
307
+ ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
308
+ models: mergedModels.length > 0 ? mergedModels : [nextModel],
309
+ },
310
+ },
311
+ },
312
+ };
313
+ config = applyPrimaryModel(config, modelRef);
314
+ if (alias) {
315
+ config = {
316
+ ...config,
317
+ agents: {
318
+ ...config.agents,
319
+ defaults: {
320
+ ...config.agents?.defaults,
321
+ models: {
322
+ ...config.agents?.defaults?.models,
323
+ [modelRef]: {
324
+ ...config.agents?.defaults?.models?.[modelRef],
325
+ alias,
326
+ },
327
+ },
328
+ },
329
+ },
330
+ };
331
+ }
332
+ return {
333
+ config,
334
+ providerId,
335
+ modelId,
336
+ ...(providerIdResult.providerIdRenamedFrom
337
+ ? { providerIdRenamedFrom: providerIdResult.providerIdRenamedFrom }
338
+ : {}),
339
+ };
340
+ }
186
341
  export async function promptCustomApiConfig(params) {
187
342
  const { prompter, runtime, config } = params;
188
343
  const baseInput = await promptBaseUrlAndKey({ prompter });
@@ -202,8 +357,6 @@ export async function promptCustomApiConfig(params) {
202
357
  validate: (val) => (val.trim() ? undefined : "Model ID is required"),
203
358
  })).trim();
204
359
  let compatibility = compatibilityChoice === "unknown" ? null : compatibilityChoice;
205
- let providerApi = COMPATIBILITY_OPTIONS.find((entry) => entry.value === compatibility)?.api ??
206
- "openai-completions";
207
360
  while (true) {
208
361
  let verifiedFromProbe = false;
209
362
  if (!compatibility) {
@@ -212,7 +365,6 @@ export async function promptCustomApiConfig(params) {
212
365
  if (openaiProbe.ok) {
213
366
  probeSpinner.stop("Detected OpenAI-compatible endpoint.");
214
367
  compatibility = "openai";
215
- providerApi = "openai-completions";
216
368
  verifiedFromProbe = true;
217
369
  }
218
370
  else {
@@ -220,7 +372,6 @@ export async function promptCustomApiConfig(params) {
220
372
  if (anthropicProbe.ok) {
221
373
  probeSpinner.stop("Detected Anthropic-compatible endpoint.");
222
374
  compatibility = "anthropic";
223
- providerApi = "anthropic-messages";
224
375
  verifiedFromProbe = true;
225
376
  }
226
377
  else {
@@ -311,74 +462,34 @@ export async function promptCustomApiConfig(params) {
311
462
  return undefined;
312
463
  },
313
464
  });
314
- const providerIdResult = resolveUniqueEndpointId({
315
- requestedId: providerIdInput,
316
- baseUrl,
317
- providers,
318
- });
319
- if (providerIdResult.renamed) {
320
- await prompter.note(`Endpoint ID "${providerIdInput}" already exists for a different base URL. Using "${providerIdResult.providerId}".`, "Endpoint ID");
321
- }
322
- const providerId = providerIdResult.providerId;
323
- const modelRef = modelKey(providerId, modelId);
324
465
  const aliasInput = await prompter.text({
325
466
  message: "Model alias (optional)",
326
467
  placeholder: "e.g. local, ollama",
327
468
  initialValue: "",
328
- validate: (value) => resolveAliasError({ raw: value, cfg: config, modelRef }),
329
- });
330
- const alias = aliasInput.trim();
331
- const existingProvider = providers[providerId];
332
- const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
333
- const hasModel = existingModels.some((model) => model.id === modelId);
334
- const nextModel = {
335
- id: modelId,
336
- name: `${modelId} (Custom Provider)`,
337
- contextWindow: DEFAULT_CONTEXT_WINDOW,
338
- maxTokens: DEFAULT_MAX_TOKENS,
339
- input: ["text"],
340
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
341
- reasoning: false,
342
- };
343
- const mergedModels = hasModel ? existingModels : [...existingModels, nextModel];
344
- const { apiKey: existingApiKey, ...existingProviderRest } = existingProvider ?? {};
345
- const normalizedApiKey = apiKey.trim() || (existingApiKey ? existingApiKey.trim() : undefined);
346
- let newConfig = {
347
- ...config,
348
- models: {
349
- ...config.models,
350
- mode: config.models?.mode ?? "merge",
351
- providers: {
352
- ...providers,
353
- [providerId]: {
354
- ...existingProviderRest,
355
- baseUrl,
356
- api: providerApi,
357
- ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
358
- models: mergedModels.length > 0 ? mergedModels : [nextModel],
359
- },
360
- },
469
+ validate: (value) => {
470
+ const requestedId = normalizeEndpointId(providerIdInput) || "custom";
471
+ const providerIdResult = resolveUniqueEndpointId({
472
+ requestedId,
473
+ baseUrl,
474
+ providers,
475
+ });
476
+ const modelRef = modelKey(providerIdResult.providerId, modelId);
477
+ return resolveAliasError({ raw: value, cfg: config, modelRef });
361
478
  },
362
- };
363
- newConfig = applyPrimaryModel(newConfig, modelRef);
364
- if (alias) {
365
- newConfig = {
366
- ...newConfig,
367
- agents: {
368
- ...newConfig.agents,
369
- defaults: {
370
- ...newConfig.agents?.defaults,
371
- models: {
372
- ...newConfig.agents?.defaults?.models,
373
- [modelRef]: {
374
- ...newConfig.agents?.defaults?.models?.[modelRef],
375
- alias,
376
- },
377
- },
378
- },
379
- },
380
- };
479
+ });
480
+ const resolvedCompatibility = compatibility ?? "openai";
481
+ const result = applyCustomApiConfig({
482
+ config,
483
+ baseUrl,
484
+ modelId,
485
+ compatibility: resolvedCompatibility,
486
+ apiKey,
487
+ providerId: providerIdInput,
488
+ alias: aliasInput,
489
+ });
490
+ if (result.providerIdRenamedFrom && result.providerId) {
491
+ await prompter.note(`Endpoint ID "${result.providerIdRenamedFrom}" already exists for a different base URL. Using "${result.providerId}".`, "Endpoint ID");
381
492
  }
382
- runtime.log(`Configured custom provider: ${providerId}/${modelId}`);
383
- return { config: newConfig, providerId, modelId };
493
+ runtime.log(`Configured custom provider: ${result.providerId}/${result.modelId}`);
494
+ return result;
384
495
  }
@@ -1,5 +1,6 @@
1
1
  import { ensureAuthProfileStore, resolveApiKeyForProfile, resolveAuthProfileOrder, } from "../../agents/auth-profiles.js";
2
2
  import { resolveEnvApiKey } from "../../agents/model-auth.js";
3
+ import { normalizeOptionalSecretInput } from "../../utils/normalize-secret-input.js";
3
4
  async function resolveApiKeyFromProfiles(params) {
4
5
  const store = ensureAuthProfileStore(params.agentDir);
5
6
  const order = resolveAuthProfileOrder({
@@ -23,12 +24,18 @@ async function resolveApiKeyFromProfiles(params) {
23
24
  return null;
24
25
  }
25
26
  export async function resolveNonInteractiveApiKey(params) {
26
- const flagKey = params.flagValue?.trim();
27
+ const flagKey = normalizeOptionalSecretInput(params.flagValue);
27
28
  if (flagKey)
28
29
  return { key: flagKey, source: "flag" };
29
30
  const envResolved = resolveEnvApiKey(params.provider);
30
31
  if (envResolved?.apiKey)
31
32
  return { key: envResolved.apiKey, source: "env" };
33
+ const explicitEnvVar = params.envVarName?.trim();
34
+ if (explicitEnvVar) {
35
+ const explicitEnvKey = normalizeOptionalSecretInput(process.env[explicitEnvVar]);
36
+ if (explicitEnvKey)
37
+ return { key: explicitEnvKey, source: "env" };
38
+ }
32
39
  if (params.allowProfile ?? true) {
33
40
  const profileKey = await resolveApiKeyFromProfiles({
34
41
  provider: params.provider,
@@ -38,6 +45,8 @@ export async function resolveNonInteractiveApiKey(params) {
38
45
  if (profileKey)
39
46
  return { key: profileKey, source: "profile" };
40
47
  }
48
+ if (params.required === false)
49
+ return null;
41
50
  const profileHint = params.allowProfile === false ? "" : `, or existing ${params.provider} API-key profile`;
42
51
  params.runtime.error(`Missing ${params.flagName} (or ${params.envVar} in env${profileHint}).`);
43
52
  params.runtime.exit(1);
@@ -13,21 +13,29 @@ const AUTH_CHOICE_FLAG_MAP = [
13
13
  { flag: "kimiCodeApiKey", authChoice: "kimi-code-api-key", label: "--kimi-code-api-key" },
14
14
  { flag: "syntheticApiKey", authChoice: "synthetic-api-key", label: "--synthetic-api-key" },
15
15
  { flag: "veniceApiKey", authChoice: "venice-api-key", label: "--venice-api-key" },
16
+ { flag: "togetherApiKey", authChoice: "together-api-key", label: "--together-api-key" },
16
17
  { flag: "zaiApiKey", authChoice: "zai-api-key", label: "--zai-api-key" },
17
18
  { flag: "xiaomiApiKey", authChoice: "xiaomi-api-key", label: "--xiaomi-api-key" },
18
19
  { flag: "xaiApiKey", authChoice: "xai-api-key", label: "--xai-api-key" },
20
+ { flag: "nvidiaApiKey", authChoice: "nvidia-api-key", label: "--nvidia-api-key" },
21
+ { flag: "huggingfaceApiKey", authChoice: "huggingface-api-key", label: "--huggingface-api-key" },
22
+ { flag: "litellmApiKey", authChoice: "litellm-api-key", label: "--litellm-api-key" },
19
23
  { flag: "minimaxApiKey", authChoice: "minimax-api", label: "--minimax-api-key" },
20
24
  { flag: "opencodeZenApiKey", authChoice: "opencode-zen", label: "--opencode-zen-api-key" },
21
25
  ];
26
+ function hasStringValue(val) {
27
+ return typeof val === "string" && val.trim().length > 0;
28
+ }
22
29
  // Infer auth choice from explicit provider API key flags.
23
30
  export function inferAuthChoiceFromFlags(opts) {
24
- const matches = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) => {
25
- const value = opts[flag];
26
- if (typeof value === "string") {
27
- return value.trim().length > 0;
28
- }
29
- return Boolean(value);
30
- });
31
+ const matches = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) => hasStringValue(opts[flag]));
32
+ // Special case: custom-api-key is inferred from custom flags
33
+ if (!matches.length &&
34
+ (hasStringValue(opts.customBaseUrl) ||
35
+ hasStringValue(opts.customModelId) ||
36
+ hasStringValue(opts.customApiKey))) {
37
+ return { choice: "custom-api-key", matches: [] };
38
+ }
31
39
  return {
32
40
  choice: matches[0]?.authChoice,
33
41
  matches,