@orchagent/cli 0.2.24 → 0.2.26

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.
@@ -234,6 +234,58 @@ async function downloadDependenciesRecursively(config, depStatuses, visited = ne
234
234
  }
235
235
  }
236
236
  }
237
+ /**
238
+ * Detect all available LLM providers from environment and server.
239
+ * Returns array of provider configs for fallback support.
240
+ */
241
+ async function detectAllLlmKeys(supportedProviders, config) {
242
+ const providers = [];
243
+ const seen = new Set();
244
+ // Check environment variables for all providers
245
+ for (const provider of supportedProviders) {
246
+ if (provider === 'any') {
247
+ // Check all known providers
248
+ for (const [p, envVar] of Object.entries(llm_1.PROVIDER_ENV_VARS)) {
249
+ const key = process.env[envVar];
250
+ if (key && !seen.has(p)) {
251
+ seen.add(p);
252
+ providers.push({ provider: p, apiKey: key, model: (0, llm_1.getDefaultModel)(p) });
253
+ }
254
+ }
255
+ }
256
+ else {
257
+ const envVar = llm_1.PROVIDER_ENV_VARS[provider];
258
+ if (envVar) {
259
+ const key = process.env[envVar];
260
+ if (key && !seen.has(provider)) {
261
+ seen.add(provider);
262
+ providers.push({ provider, apiKey: key, model: (0, llm_1.getDefaultModel)(provider) });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ // Also check server keys if available
268
+ if (config?.apiKey) {
269
+ try {
270
+ const { fetchLlmKeys } = await Promise.resolve().then(() => __importStar(require('../lib/api')));
271
+ const serverKeys = await fetchLlmKeys(config);
272
+ for (const serverKey of serverKeys) {
273
+ if (!seen.has(serverKey.provider)) {
274
+ seen.add(serverKey.provider);
275
+ providers.push({
276
+ provider: serverKey.provider,
277
+ apiKey: serverKey.api_key,
278
+ model: serverKey.model || (0, llm_1.getDefaultModel)(serverKey.provider),
279
+ });
280
+ }
281
+ }
282
+ }
283
+ catch {
284
+ // Server fetch failed, continue with what we have
285
+ }
286
+ }
287
+ return providers;
288
+ }
237
289
  async function executePromptLocally(agentData, inputData, skillPrompts = [], config, providerOverride) {
238
290
  // If provider override specified, validate and use only that provider
239
291
  if (providerOverride) {
@@ -243,6 +295,44 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
243
295
  const providersToCheck = providerOverride
244
296
  ? [providerOverride]
245
297
  : agentData.supported_providers;
298
+ // Combine skill prompts with agent prompt (skills first, then agent)
299
+ let basePrompt = agentData.prompt || '';
300
+ if (skillPrompts.length > 0) {
301
+ basePrompt = [...skillPrompts, basePrompt].join('\n\n---\n\n');
302
+ }
303
+ // Build the prompt with input data (matches server behavior)
304
+ const prompt = (0, llm_1.buildPrompt)(basePrompt, inputData);
305
+ // When no provider override, detect all available providers for fallback support
306
+ if (!providerOverride) {
307
+ const allProviders = await detectAllLlmKeys(providersToCheck, config);
308
+ if (allProviders.length === 0) {
309
+ const providers = providersToCheck.join(', ');
310
+ throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
311
+ `Set an environment variable (e.g., OPENAI_API_KEY) or configure in web dashboard`);
312
+ }
313
+ // Apply agent default models to each provider config
314
+ const providersWithModels = allProviders.map((p) => ({
315
+ ...p,
316
+ model: agentData.default_models?.[p.provider] || p.model,
317
+ }));
318
+ // Show which provider is being used (primary)
319
+ const primary = providersWithModels[0];
320
+ if (providersWithModels.length > 1) {
321
+ process.stderr.write(`Running with ${primary.provider} (${primary.model}), ` +
322
+ `${providersWithModels.length - 1} fallback(s) available...\n`);
323
+ }
324
+ else {
325
+ process.stderr.write(`Running with ${primary.provider} (${primary.model})...\n`);
326
+ }
327
+ // Use fallback if multiple providers, otherwise single call
328
+ if (providersWithModels.length > 1) {
329
+ return await (0, llm_1.callLlmWithFallback)(providersWithModels, prompt, agentData.output_schema);
330
+ }
331
+ else {
332
+ return await (0, llm_1.callLlm)(primary.provider, primary.apiKey, primary.model, prompt, agentData.output_schema);
333
+ }
334
+ }
335
+ // Provider override: use single provider (existing behavior)
246
336
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, config);
247
337
  if (!detected) {
248
338
  const providers = providersToCheck.join(', ');
@@ -252,13 +342,8 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
252
342
  const { provider, key, model: serverModel } = detected;
253
343
  // Priority: server config model > agent default model > hardcoded default
254
344
  const model = serverModel || agentData.default_models?.[provider] || (0, llm_1.getDefaultModel)(provider);
255
- // Combine skill prompts with agent prompt (skills first, then agent)
256
- let basePrompt = agentData.prompt || '';
257
- if (skillPrompts.length > 0) {
258
- basePrompt = [...skillPrompts, basePrompt].join('\n\n---\n\n');
259
- }
260
- // Build the prompt with input data (matches server behavior)
261
- const prompt = (0, llm_1.buildPrompt)(basePrompt, inputData);
345
+ // Show which provider is being used (helpful for debugging rate limits)
346
+ process.stderr.write(`Running with ${provider} (${model})...\n`);
262
347
  // Call the LLM directly
263
348
  const response = await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
264
349
  return response;
@@ -61,5 +61,6 @@ function registerSearchCommand(program) {
61
61
  if (agents.length === limit) {
62
62
  process.stdout.write(`\nShowing top ${limit} results. Use --limit <n> for more.\n`);
63
63
  }
64
+ process.stdout.write('\nTip: Run "orchagent info <agent>" to see input schema and details.\n');
64
65
  });
65
66
  }
package/dist/lib/api.js CHANGED
@@ -59,6 +59,8 @@ exports.previewAgentVersion = previewAgentVersion;
59
59
  exports.reportInstall = reportInstall;
60
60
  const errors_1 = require("./errors");
61
61
  const DEFAULT_TIMEOUT_MS = 15000;
62
+ const MAX_RETRIES = 3;
63
+ const BASE_DELAY_MS = 1000;
62
64
  async function safeFetch(url, options) {
63
65
  try {
64
66
  return await fetch(url, {
@@ -74,6 +76,39 @@ async function safeFetch(url, options) {
74
76
  throw new errors_1.NetworkError(url, err instanceof Error ? err : undefined);
75
77
  }
76
78
  }
79
+ async function safeFetchWithRetry(url, options) {
80
+ let lastError;
81
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
82
+ try {
83
+ const response = await safeFetch(url, options);
84
+ // Don't retry client errors (except 429)
85
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
86
+ return response;
87
+ }
88
+ // Retry on 5xx or 429
89
+ if (response.status >= 500 || response.status === 429) {
90
+ if (attempt < MAX_RETRIES) {
91
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
92
+ const jitter = Math.random() * 500;
93
+ process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
94
+ await new Promise(r => setTimeout(r, delay + jitter));
95
+ continue;
96
+ }
97
+ }
98
+ return response;
99
+ }
100
+ catch (error) {
101
+ lastError = error;
102
+ if (attempt < MAX_RETRIES) {
103
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
104
+ const jitter = Math.random() * 500;
105
+ process.stderr.write(`Network error, retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
106
+ await new Promise(r => setTimeout(r, delay + jitter));
107
+ }
108
+ }
109
+ }
110
+ throw lastError ?? new errors_1.NetworkError(url);
111
+ }
77
112
  class ApiError extends Error {
78
113
  status;
79
114
  payload;
@@ -108,7 +143,7 @@ async function request(config, method, path, options = {}) {
108
143
  if (!config.apiKey) {
109
144
  throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
110
145
  }
111
- const response = await safeFetch(buildUrl(config.apiUrl, path), {
146
+ const response = await safeFetchWithRetry(buildUrl(config.apiUrl, path), {
112
147
  method,
113
148
  headers: {
114
149
  Authorization: `Bearer ${config.apiKey}`,
@@ -122,7 +157,7 @@ async function request(config, method, path, options = {}) {
122
157
  return (await response.json());
123
158
  }
124
159
  async function publicRequest(config, path) {
125
- const response = await safeFetch(buildUrl(config.apiUrl, path));
160
+ const response = await safeFetchWithRetry(buildUrl(config.apiUrl, path));
126
161
  if (!response.ok) {
127
162
  throw await parseError(response);
128
163
  }
package/dist/lib/llm.js CHANGED
@@ -39,15 +39,28 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = void 0;
42
+ exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = exports.LlmError = void 0;
43
+ exports.isRateLimitError = isRateLimitError;
43
44
  exports.detectLlmKeyFromEnv = detectLlmKeyFromEnv;
44
45
  exports.detectLlmKey = detectLlmKey;
45
46
  exports.getDefaultModel = getDefaultModel;
46
47
  exports.buildPrompt = buildPrompt;
47
48
  exports.callLlm = callLlm;
49
+ exports.callLlmWithFallback = callLlmWithFallback;
48
50
  exports.validateProvider = validateProvider;
49
51
  const errors_1 = require("./errors");
50
52
  const llm_errors_1 = require("./llm-errors");
53
+ class LlmError extends errors_1.CliError {
54
+ statusCode;
55
+ constructor(message, statusCode) {
56
+ super(message);
57
+ this.statusCode = statusCode;
58
+ }
59
+ }
60
+ exports.LlmError = LlmError;
61
+ function isRateLimitError(error) {
62
+ return error instanceof LlmError && error.statusCode === 429;
63
+ }
51
64
  // Environment variable names for each provider
52
65
  exports.PROVIDER_ENV_VARS = {
53
66
  openai: 'OPENAI_API_KEY',
@@ -169,6 +182,27 @@ async function callLlm(provider, apiKey, model, prompt, outputSchema) {
169
182
  }
170
183
  throw new errors_1.CliError(`Unsupported provider: ${provider}`);
171
184
  }
185
+ /**
186
+ * Call LLM with automatic fallback to next provider on rate limit.
187
+ * Tries each provider in order until one succeeds.
188
+ */
189
+ async function callLlmWithFallback(providers, prompt, outputSchema) {
190
+ let lastError;
191
+ for (const { provider, apiKey, model } of providers) {
192
+ try {
193
+ return await callLlm(provider, apiKey, model, prompt, outputSchema);
194
+ }
195
+ catch (error) {
196
+ lastError = error;
197
+ if (isRateLimitError(error)) {
198
+ process.stderr.write(`${provider} rate-limited, trying next provider...\n`);
199
+ continue;
200
+ }
201
+ throw error; // Don't retry non-rate-limit errors
202
+ }
203
+ }
204
+ throw lastError ?? new errors_1.CliError('All LLM providers failed');
205
+ }
172
206
  async function callOpenAI(apiKey, model, prompt, outputSchema) {
173
207
  const body = {
174
208
  model,
@@ -187,7 +221,8 @@ async function callOpenAI(apiKey, model, prompt, outputSchema) {
187
221
  });
188
222
  if (!response.ok) {
189
223
  const text = await response.text();
190
- throw (0, llm_errors_1.parseLlmError)('openai', text, response.status);
224
+ const parsed = (0, llm_errors_1.parseLlmError)('openai', text, response.status);
225
+ throw new LlmError(parsed.message, response.status);
191
226
  }
192
227
  const data = (await response.json());
193
228
  const content = data.choices?.[0]?.message?.content || '';
@@ -214,7 +249,8 @@ async function callAnthropic(apiKey, model, prompt, _outputSchema) {
214
249
  });
215
250
  if (!response.ok) {
216
251
  const text = await response.text();
217
- throw (0, llm_errors_1.parseLlmError)('anthropic', text, response.status);
252
+ const parsed = (0, llm_errors_1.parseLlmError)('anthropic', text, response.status);
253
+ throw new LlmError(parsed.message, response.status);
218
254
  }
219
255
  const data = (await response.json());
220
256
  const content = data.content?.[0]?.text || '';
@@ -236,7 +272,8 @@ async function callGemini(apiKey, model, prompt, _outputSchema) {
236
272
  });
237
273
  if (!response.ok) {
238
274
  const text = await response.text();
239
- throw (0, llm_errors_1.parseLlmError)('gemini', text, response.status);
275
+ const parsed = (0, llm_errors_1.parseLlmError)('gemini', text, response.status);
276
+ throw new LlmError(parsed.message, response.status);
240
277
  }
241
278
  const data = (await response.json());
242
279
  const content = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
@@ -264,10 +301,11 @@ async function callOllama(endpoint, model, prompt, _outputSchema) {
264
301
  });
265
302
  if (!response.ok) {
266
303
  if (response.status === 404) {
267
- throw new errors_1.CliError(`Model '${model}' not found. Run: ollama pull ${model}`);
304
+ throw new LlmError(`Model '${model}' not found. Run: ollama pull ${model}`, 404);
268
305
  }
269
306
  const text = await response.text();
270
- throw (0, llm_errors_1.parseLlmError)('ollama', text, response.status);
307
+ const parsed = (0, llm_errors_1.parseLlmError)('ollama', text, response.status);
308
+ throw new LlmError(parsed.message, response.status);
271
309
  }
272
310
  const data = await response.json();
273
311
  const content = data.choices?.[0]?.message?.content || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",