routstrd 0.1.5 → 0.1.7

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,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@cashu/cashu-ts": "^3.1.1",
27
- "@routstr/sdk": "^0.2.8",
27
+ "@routstr/sdk": "^0.2.9",
28
28
  "applesauce-core": "^5.1.0",
29
29
  "applesauce-relay": "^5.1.0",
30
30
  "commander": "^14.0.2",
package/src/cli.ts CHANGED
@@ -365,9 +365,59 @@ program
365
365
  .command("models")
366
366
  .description("List available routstr21 models")
367
367
  .option("-r, --refresh", "Force refresh routstr21 models from Nostr", false)
368
- .action(async (options: { refresh: boolean }) => {
368
+ .option("-m, --model <id>", "Show providers for a specific model")
369
+ .action(async (options: { refresh: boolean; model?: string }) => {
369
370
  await ensureDaemonRunning();
370
371
 
372
+ if (options.model) {
373
+ // Show providers for specific model
374
+ const result = await callDaemon(`/models/${encodeURIComponent(options.model)}/providers`);
375
+ if (result.error) {
376
+ console.log(result.error);
377
+ process.exit(1);
378
+ }
379
+
380
+ const modelData = result.output as {
381
+ id: string;
382
+ name?: string;
383
+ description?: string;
384
+ context_length?: number;
385
+ providers: Array<{
386
+ baseUrl: string;
387
+ pricing: {
388
+ prompt: number;
389
+ completion: number;
390
+ request: number;
391
+ max_cost: number;
392
+ };
393
+ }>;
394
+ } | undefined;
395
+
396
+ if (!modelData) {
397
+ console.log("Model not found");
398
+ process.exit(1);
399
+ }
400
+
401
+ console.log(`\n${modelData.name || modelData.id}`);
402
+ if (modelData.description) {
403
+ console.log(` ${modelData.description}`);
404
+ }
405
+ if (modelData.context_length) {
406
+ console.log(` Context: ${modelData.context_length.toLocaleString()} tokens`);
407
+ }
408
+ console.log(`\n Providers (${modelData.providers.length}):`);
409
+ for (const provider of modelData.providers) {
410
+ console.log(`\n ${provider.baseUrl}`);
411
+ console.log(` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`);
412
+ console.log(` Completion: ${(provider.pricing.completion * 1000000).toFixed(2)} sats/M tokens`);
413
+ console.log(` Request: ${provider.pricing.request.toFixed(2)} sats`);
414
+ console.log(` Max cost: ${provider.pricing.max_cost.toFixed(2)} sats`);
415
+ }
416
+ console.log("");
417
+ return;
418
+ }
419
+
420
+ // List all models with interactive selection
371
421
  const result = await callDaemon(
372
422
  options.refresh ? "/models?refresh=true" : "/models",
373
423
  );
@@ -386,6 +436,7 @@ program
386
436
  console.log("No routstr21 models found");
387
437
  } else {
388
438
  console.log(`\nFound ${models.length} routstr21 models:`);
439
+ console.log("(Use 'routstrd models -m <model_id>' to see providers and pricing)\n");
389
440
  models.forEach((model, i) => {
390
441
  const details = [
391
442
  model.name && model.name !== model.id ? model.name : null,
@@ -393,8 +444,9 @@ program
393
444
  ]
394
445
  .filter(Boolean)
395
446
  .join(" - ");
396
- console.log(`${i + 1}. ${model.id}${details ? ` (${details})` : ""}`);
447
+ console.log(` ${String(i + 1).padStart(2)}. ${model.id}${details ? ` (${details})` : ""}`);
397
448
  });
449
+ console.log("");
398
450
  }
399
451
  }
400
452
  });
@@ -37,6 +37,7 @@ type DaemonDeps = {
37
37
  modelManager: any;
38
38
  ensureProvidersBootstrapped: () => Promise<void>;
39
39
  getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
40
+ getModelProviders: (modelId: string) => Promise<any>;
40
41
  mode?: ClientMode;
41
42
  providerManager: ProviderManager;
42
43
  };
@@ -277,6 +278,7 @@ export function createDaemonRequestHandler(deps: {
277
278
  modelManager: any;
278
279
  ensureProvidersBootstrapped: () => Promise<void>;
279
280
  getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
281
+ getModelProviders: (modelId: string) => Promise<any>;
280
282
  mode?: "xcashu" | "apikeys";
281
283
  usageTrackingDriver: UsageTrackingDriver;
282
284
  providerManager: ProviderManager;
@@ -423,6 +425,23 @@ export function createDaemonRequestHandler(deps: {
423
425
  return;
424
426
  }
425
427
 
428
+ // Get providers for a specific model
429
+ const modelProvidersMatch = url.pathname.match(/^\/models\/([^\/]+)\/providers$/);
430
+ if (req.method === "GET" && modelProvidersMatch && modelProvidersMatch[1]) {
431
+ try {
432
+ const modelId = decodeURIComponent(modelProvidersMatch[1]);
433
+ const modelWithProviders = await deps.getModelProviders(modelId);
434
+ if (!modelWithProviders) {
435
+ sendJson(res, 404, { error: `Model '${modelId}' not found` });
436
+ return;
437
+ }
438
+ sendJson(res, 200, { output: modelWithProviders });
439
+ } catch (error) {
440
+ sendJson(res, 500, { error: toErrorMessage(error) });
441
+ }
442
+ return;
443
+ }
444
+
426
445
  if (req.method === "GET" && url.pathname === "/v1/models") {
427
446
  try {
428
447
  const forceRefresh =
@@ -50,7 +50,7 @@ async function main(): Promise<void> {
50
50
  const modelManager = new ModelManager(discoveryAdapter);
51
51
  // Create shared ProviderManager for consistent failure tracking across all requests
52
52
  const providerManager = new ProviderManager(providerRegistry, store);
53
- const { ensureProvidersBootstrapped, getRoutstr21Models } =
53
+ const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } =
54
54
  createModelService(modelManager);
55
55
 
56
56
  const walletClient = createCocodClient({ cocodPath: config.cocodPath });
@@ -74,6 +74,7 @@ async function main(): Promise<void> {
74
74
  modelManager,
75
75
  ensureProvidersBootstrapped,
76
76
  getRoutstr21Models,
77
+ getModelProviders,
77
78
  mode: config.mode || "apikeys",
78
79
  usageTrackingDriver,
79
80
  providerManager,
@@ -2,6 +2,20 @@ import { ModelManager } from "@routstr/sdk";
2
2
  import type { ExposedModel } from "./types";
3
3
  import { logger } from "../utils/logger";
4
4
 
5
+ export type ModelProviderInfo = {
6
+ baseUrl: string;
7
+ pricing: {
8
+ prompt: number;
9
+ completion: number;
10
+ request: number;
11
+ max_cost: number;
12
+ };
13
+ };
14
+
15
+ export type ModelWithProviders = ExposedModel & {
16
+ providers: ModelProviderInfo[];
17
+ };
18
+
5
19
  export function createModelService(modelManager: ModelManager) {
6
20
  let providerBootstrapPromise: Promise<void> | null = null;
7
21
 
@@ -42,8 +56,57 @@ export function createModelService(modelManager: ModelManager) {
42
56
  });
43
57
  };
44
58
 
59
+ const getModelProviders = async (
60
+ modelId: string,
61
+ ): Promise<ModelWithProviders | null> => {
62
+ await ensureProvidersBootstrapped();
63
+
64
+ const allModels = modelManager.getAllCachedModels();
65
+ const providers: ModelProviderInfo[] = [];
66
+
67
+ for (const [baseUrl, models] of Object.entries(allModels)) {
68
+ const model = models.find((m) => m.id === modelId);
69
+ if (model && model.sats_pricing) {
70
+ providers.push({
71
+ baseUrl,
72
+ pricing: {
73
+ prompt: model.sats_pricing.prompt,
74
+ completion: model.sats_pricing.completion,
75
+ request: model.sats_pricing.request,
76
+ max_cost: model.sats_pricing.max_cost,
77
+ },
78
+ });
79
+ }
80
+ }
81
+
82
+ // Sort by max_cost (cheapest first)
83
+ providers.sort((a, b) => a.pricing.max_cost - b.pricing.max_cost);
84
+
85
+ if (providers.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ // Get model metadata from first provider that has it
90
+ const cheapest = providers[0]!;
91
+ const firstProvider = allModels[cheapest.baseUrl];
92
+ const modelInfo = firstProvider?.find((m: { id: string }) => m.id === modelId);
93
+
94
+ if (!modelInfo) {
95
+ return null;
96
+ }
97
+
98
+ return {
99
+ id: modelInfo.id,
100
+ name: modelInfo.name,
101
+ description: modelInfo.description,
102
+ context_length: modelInfo.context_length,
103
+ providers,
104
+ };
105
+ };
106
+
45
107
  return {
46
108
  ensureProvidersBootstrapped,
47
109
  getRoutstr21Models,
110
+ getModelProviders,
48
111
  };
49
112
  }