nexus-ai-pro 2.7.0 → 2.8.0

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 (165) hide show
  1. package/API_STABILITY.md +85 -3
  2. package/CHANGELOG.md +133 -1
  3. package/README.md +9 -2
  4. package/SECURITY.md +41 -1
  5. package/dist/batch/manager.js +16 -33
  6. package/dist/batch/openai-client.d.ts +83 -0
  7. package/dist/batch/openai-client.js +120 -0
  8. package/dist/batch/openai.d.ts +12 -21
  9. package/dist/batch/openai.js +33 -125
  10. package/dist/batch/poll.d.ts +18 -0
  11. package/dist/batch/poll.js +29 -0
  12. package/dist/cli/models.d.ts +36 -0
  13. package/dist/cli/models.js +170 -0
  14. package/dist/cli.js +11 -1
  15. package/dist/doctor/index.d.ts +16 -1
  16. package/dist/doctor/index.js +10 -5
  17. package/dist/embeddings/adapters.d.ts +2 -2
  18. package/dist/embeddings/adapters.js +24 -13
  19. package/dist/embeddings/batch-google.d.ts +40 -0
  20. package/dist/embeddings/batch-google.js +128 -0
  21. package/dist/embeddings/batch-mock.d.ts +45 -0
  22. package/dist/embeddings/batch-mock.js +71 -0
  23. package/dist/embeddings/batch-openai.d.ts +23 -0
  24. package/dist/embeddings/batch-openai.js +55 -0
  25. package/dist/embeddings/batch.d.ts +230 -0
  26. package/dist/embeddings/batch.js +282 -0
  27. package/dist/embeddings/cache.d.ts +81 -0
  28. package/dist/embeddings/cache.js +116 -0
  29. package/dist/embeddings/engine.d.ts +7 -0
  30. package/dist/embeddings/engine.js +110 -68
  31. package/dist/embeddings/google-wire.d.ts +32 -0
  32. package/dist/embeddings/google-wire.js +33 -0
  33. package/dist/embeddings/index.d.ts +1 -1
  34. package/dist/embeddings/inputs.d.ts +30 -0
  35. package/dist/embeddings/inputs.js +103 -0
  36. package/dist/embeddings/mock.d.ts +7 -1
  37. package/dist/embeddings/mock.js +8 -2
  38. package/dist/embeddings/models.js +21 -0
  39. package/dist/images/asset-stores.d.ts +47 -1
  40. package/dist/images/asset-stores.js +135 -2
  41. package/dist/images/asset-support.d.ts +94 -1
  42. package/dist/images/asset-support.js +159 -3
  43. package/dist/images/assets.d.ts +10 -3
  44. package/dist/images/assets.js +15 -2
  45. package/dist/media/index.d.ts +244 -0
  46. package/dist/media/index.js +262 -0
  47. package/dist/media/mistral.d.ts +34 -0
  48. package/dist/media/mistral.js +90 -0
  49. package/dist/media/mock.d.ts +36 -0
  50. package/dist/media/mock.js +45 -0
  51. package/dist/media/rag.d.ts +141 -0
  52. package/dist/media/rag.js +198 -0
  53. package/dist/models/data.d.ts +87 -0
  54. package/dist/models/data.js +235 -0
  55. package/dist/models/registry.d.ts +2 -1
  56. package/dist/models/registry.js +3 -2
  57. package/dist/postgres/embedding-cache.d.ts +29 -0
  58. package/dist/postgres/embedding-cache.js +58 -0
  59. package/dist/rag/chroma.d.ts +13 -1
  60. package/dist/rag/chroma.js +36 -13
  61. package/dist/rag/pinecone.js +2 -2
  62. package/dist/rag/pipeline.d.ts +17 -2
  63. package/dist/rag/pipeline.js +95 -14
  64. package/dist/rag/qdrant.js +2 -2
  65. package/dist/rag/redis.d.ts +2 -1
  66. package/dist/rag/redis.js +56 -18
  67. package/dist/rag/vector-helpers.d.ts +5 -0
  68. package/dist/rag/vector-helpers.js +7 -0
  69. package/dist/rag/weaviate.d.ts +15 -1
  70. package/dist/rag/weaviate.js +75 -12
  71. package/dist/sqlite/embedding-cache.d.ts +34 -0
  72. package/dist/sqlite/embedding-cache.js +61 -0
  73. package/dist/types/config.d.ts +5 -0
  74. package/dist/types/embeddings.d.ts +87 -5
  75. package/dist/types/images.d.ts +2 -2
  76. package/dist/types/lifecycle.d.ts +1 -1
  77. package/dist/utils/redis-command.d.ts +15 -0
  78. package/dist/utils/redis-command.js +5 -0
  79. package/dist/utils/sha256.d.ts +9 -0
  80. package/dist/utils/sha256.js +6 -0
  81. package/dist/utils/vector-bytes.d.ts +11 -0
  82. package/dist/utils/vector-bytes.js +18 -0
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/dist-cjs/batch/manager.js +16 -33
  86. package/dist-cjs/batch/openai-client.d.ts +83 -0
  87. package/dist-cjs/batch/openai-client.js +125 -0
  88. package/dist-cjs/batch/openai.d.ts +12 -21
  89. package/dist-cjs/batch/openai.js +32 -124
  90. package/dist-cjs/batch/poll.d.ts +18 -0
  91. package/dist-cjs/batch/poll.js +32 -0
  92. package/dist-cjs/cli/models.d.ts +36 -0
  93. package/dist-cjs/cli/models.js +175 -0
  94. package/dist-cjs/doctor/index.d.ts +16 -1
  95. package/dist-cjs/doctor/index.js +10 -5
  96. package/dist-cjs/embeddings/adapters.d.ts +2 -2
  97. package/dist-cjs/embeddings/adapters.js +23 -12
  98. package/dist-cjs/embeddings/batch-google.d.ts +40 -0
  99. package/dist-cjs/embeddings/batch-google.js +132 -0
  100. package/dist-cjs/embeddings/batch-mock.d.ts +45 -0
  101. package/dist-cjs/embeddings/batch-mock.js +75 -0
  102. package/dist-cjs/embeddings/batch-openai.d.ts +23 -0
  103. package/dist-cjs/embeddings/batch-openai.js +59 -0
  104. package/dist-cjs/embeddings/batch.d.ts +230 -0
  105. package/dist-cjs/embeddings/batch.js +320 -0
  106. package/dist-cjs/embeddings/cache.d.ts +81 -0
  107. package/dist-cjs/embeddings/cache.js +125 -0
  108. package/dist-cjs/embeddings/engine.d.ts +7 -0
  109. package/dist-cjs/embeddings/engine.js +143 -68
  110. package/dist-cjs/embeddings/google-wire.d.ts +32 -0
  111. package/dist-cjs/embeddings/google-wire.js +38 -0
  112. package/dist-cjs/embeddings/index.d.ts +1 -1
  113. package/dist-cjs/embeddings/inputs.d.ts +30 -0
  114. package/dist-cjs/embeddings/inputs.js +141 -0
  115. package/dist-cjs/embeddings/mock.d.ts +7 -1
  116. package/dist-cjs/embeddings/mock.js +8 -2
  117. package/dist-cjs/embeddings/models.js +21 -0
  118. package/dist-cjs/images/asset-stores.d.ts +47 -1
  119. package/dist-cjs/images/asset-stores.js +133 -0
  120. package/dist-cjs/images/asset-support.d.ts +94 -1
  121. package/dist-cjs/images/asset-support.js +169 -3
  122. package/dist-cjs/images/assets.d.ts +10 -3
  123. package/dist-cjs/images/assets.js +17 -1
  124. package/dist-cjs/media/index.d.ts +244 -0
  125. package/dist-cjs/media/index.js +269 -0
  126. package/dist-cjs/media/mistral.d.ts +34 -0
  127. package/dist-cjs/media/mistral.js +94 -0
  128. package/dist-cjs/media/mock.d.ts +36 -0
  129. package/dist-cjs/media/mock.js +49 -0
  130. package/dist-cjs/media/rag.d.ts +141 -0
  131. package/dist-cjs/media/rag.js +202 -0
  132. package/dist-cjs/models/data.d.ts +87 -0
  133. package/dist-cjs/models/data.js +245 -0
  134. package/dist-cjs/models/registry.d.ts +2 -1
  135. package/dist-cjs/models/registry.js +3 -2
  136. package/dist-cjs/postgres/embedding-cache.d.ts +29 -0
  137. package/dist-cjs/postgres/embedding-cache.js +96 -0
  138. package/dist-cjs/rag/chroma.d.ts +13 -1
  139. package/dist-cjs/rag/chroma.js +35 -12
  140. package/dist-cjs/rag/pinecone.js +1 -1
  141. package/dist-cjs/rag/pipeline.d.ts +17 -2
  142. package/dist-cjs/rag/pipeline.js +95 -14
  143. package/dist-cjs/rag/qdrant.js +1 -1
  144. package/dist-cjs/rag/redis.d.ts +2 -1
  145. package/dist-cjs/rag/redis.js +55 -17
  146. package/dist-cjs/rag/vector-helpers.d.ts +5 -0
  147. package/dist-cjs/rag/vector-helpers.js +8 -0
  148. package/dist-cjs/rag/weaviate.d.ts +15 -1
  149. package/dist-cjs/rag/weaviate.js +74 -11
  150. package/dist-cjs/sqlite/embedding-cache.d.ts +34 -0
  151. package/dist-cjs/sqlite/embedding-cache.js +99 -0
  152. package/dist-cjs/types/config.d.ts +5 -0
  153. package/dist-cjs/types/embeddings.d.ts +87 -5
  154. package/dist-cjs/types/images.d.ts +2 -2
  155. package/dist-cjs/types/lifecycle.d.ts +1 -1
  156. package/dist-cjs/utils/redis-command.d.ts +15 -0
  157. package/dist-cjs/utils/redis-command.js +8 -0
  158. package/dist-cjs/utils/sha256.d.ts +9 -0
  159. package/dist-cjs/utils/sha256.js +7 -0
  160. package/dist-cjs/utils/vector-bytes.d.ts +11 -0
  161. package/dist-cjs/utils/vector-bytes.js +22 -0
  162. package/dist-cjs/version.d.ts +1 -1
  163. package/dist-cjs/version.js +1 -1
  164. package/llms.txt +2 -1
  165. package/package.json +162 -4
@@ -1,42 +1,33 @@
1
1
  import type { BatchJobRef, BatchJobState, BatchOutputItem, BatchProvider, BatchProviderCallContext, BatchProviderInfo, BatchSubmitRequest } from '../types/batch.js';
2
+ import { type OpenAIBatchClientOptions } from './openai-client.js';
2
3
  /** Options for the OpenAI batch provider. */
3
- export interface OpenAIBatchProviderOptions {
4
- /** OpenAI API key. */
5
- apiKey: string;
6
- /** API base URL. Defaults to `https://api.openai.com/v1`. */
7
- baseUrl?: string;
8
- /** Sent as the `OpenAI-Organization` header. */
9
- organization?: string;
10
- /** Headers added to every request. */
11
- headers?: Record<string, string>;
12
- /** Replaces the global `fetch`. */
13
- fetch?: typeof fetch;
14
- /** Endpoint every item targets. Defaults to chat completions. */
4
+ export interface OpenAIBatchProviderOptions extends OpenAIBatchClientOptions {
5
+ /**
6
+ * Endpoint every item targets. Defaults to chat completions. Embeddings are batched by
7
+ * `OpenAIEmbeddingBatchProvider`, on `nexus-ai-pro/embeddings/batch/openai`; this adapter refuses
8
+ * `/v1/embeddings`, since its items are completions.
9
+ */
15
10
  endpoint?: '/v1/chat/completions' | '/v1/embeddings' | (string & {});
16
11
  }
17
12
  /**
18
13
  * OpenAI's Batch API: half price, 24-hour window.
19
14
  *
20
- * The wire protocol is a JSONL upload, a batch pointing at it, then a JSONL download — three
21
- * different content types across four endpoints, which is why this adapter is larger than a
22
- * synchronous one.
15
+ * The wire protocol is a JSONL upload, a batch pointing at it, then JSONL downloads of what succeeded
16
+ * and what failed — three different content types across four endpoints, which is why this adapter
17
+ * is larger than a synchronous one.
23
18
  */
24
19
  export declare class OpenAIBatchProvider implements BatchProvider {
25
20
  private readonly options;
26
21
  /** Always `openai`, with its batch limits. */
27
22
  readonly info: BatchProviderInfo;
23
+ private readonly client;
28
24
  constructor(options: OpenAIBatchProviderOptions);
29
25
  /** Uploads the items as JSONL and creates a batch over them. */
30
26
  submit(request: BatchSubmitRequest, context: BatchProviderCallContext): Promise<BatchJobRef>;
31
27
  /** Reads a batch's status and counts. */
32
28
  poll(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchJobState>;
33
- /** Downloads a finished batch's results. Empty until the output file exists. */
29
+ /** Downloads a finished batch's results: what succeeded and what failed. Empty until they exist. */
34
30
  results(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchOutputItem[]>;
35
31
  /** Cancels a batch. Items already processed keep their results. */
36
32
  cancel(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchJobState>;
37
- private toState;
38
- private toOutputItem;
39
- private request;
40
- private requestText;
41
- private send;
42
33
  }
@@ -1,15 +1,6 @@
1
1
  import { buildMeta } from '../core/usage.js';
2
- import { BatchProviderResponseError } from './errors.js';
3
- const STATUS_MAP = {
4
- validating: 'validating',
5
- in_progress: 'in_progress',
6
- finalizing: 'finalizing',
7
- completed: 'completed',
8
- failed: 'failed',
9
- expired: 'expired',
10
- cancelling: 'cancelling',
11
- cancelled: 'cancelled',
12
- };
2
+ import { BatchCapabilityError } from './errors.js';
3
+ import { lineError, OpenAIBatchClient } from './openai-client.js';
13
4
  export class OpenAIBatchProvider {
14
5
  options;
15
6
  info = {
@@ -22,13 +13,17 @@ export class OpenAIBatchProvider {
22
13
  discount: 0.5,
23
14
  },
24
15
  };
16
+ client;
25
17
  constructor(options) {
26
18
  this.options = options;
19
+ this.client = new OpenAIBatchClient(options);
27
20
  }
28
21
  async submit(request, context) {
29
22
  const endpoint = this.options.endpoint ?? '/v1/chat/completions';
30
- const jsonl = request.items
31
- .map((item) => JSON.stringify({
23
+ if (endpoint === '/v1/embeddings') {
24
+ throw new BatchCapabilityError('openai', 'endpoint', 'OpenAIBatchProvider batches completions; batch embeddings with OpenAIEmbeddingBatchProvider from nexus-ai-pro/embeddings/batch/openai');
25
+ }
26
+ const { batch, fileId } = await this.client.create(request.items.map((item) => ({
32
27
  custom_id: item.customId,
33
28
  method: 'POST',
34
29
  url: endpoint,
@@ -38,124 +33,37 @@ export class OpenAIBatchProvider {
38
33
  max_tokens: item.request.maxTokens,
39
34
  temperature: item.request.temperature,
40
35
  },
41
- }))
42
- .join('\n');
43
- const form = new FormData();
44
- form.append('purpose', 'batch');
45
- form.append('file', new Blob([jsonl], { type: 'application/jsonl' }), 'batch.jsonl');
46
- const file = (await this.request('/files', { method: 'POST', body: form }, context));
47
- if (!file.id)
48
- throw new BatchProviderResponseError('openai', 'the file upload returned no id');
49
- const batch = (await this.request('/batches', {
50
- method: 'POST',
51
- headers: { 'content-type': 'application/json' },
52
- body: JSON.stringify({
53
- input_file_id: file.id,
54
- endpoint,
55
- completion_window: request.completionWindow ?? '24h',
56
- metadata: request.metadata,
57
- }),
58
- }, context));
59
- if (!batch.id)
60
- throw new BatchProviderResponseError('openai', 'the batch response returned no id');
61
- return { id: batch.id, provider: 'openai', metadata: { inputFileId: file.id, endpoint } };
36
+ })), { endpoint, completionWindow: request.completionWindow, metadata: request.metadata }, context);
37
+ return { id: batch.id, provider: 'openai', metadata: { inputFileId: fileId, endpoint } };
62
38
  }
63
39
  async poll(ref, context) {
64
- const batch = (await this.request(`/batches/${encodeURIComponent(ref.id)}`, {}, context));
65
- return this.toState(ref, batch);
40
+ return this.client.state(ref, await this.client.read(ref.id, context));
66
41
  }
67
42
  async results(ref, context) {
68
- const batch = (await this.request(`/batches/${encodeURIComponent(ref.id)}`, {}, context));
69
- if (!batch.output_file_id)
70
- return [];
71
- const body = await this.requestText(`/files/${encodeURIComponent(batch.output_file_id)}/content`, context);
72
- return body
73
- .split('\n')
74
- .filter((line) => line.trim().length > 0)
75
- .map((line) => this.toOutputItem(line));
43
+ return (await this.client.lines(ref.id, context)).map(toOutputItem);
76
44
  }
77
45
  async cancel(ref, context) {
78
- const batch = (await this.request(`/batches/${encodeURIComponent(ref.id)}/cancel`, { method: 'POST' }, context));
79
- return this.toState(ref, batch);
80
- }
81
- toState(ref, batch) {
82
- const counts = batch.request_counts;
83
- return {
84
- ref,
85
- status: STATUS_MAP[batch.status ?? ''] ?? 'in_progress',
86
- counts: counts
87
- ? { total: counts.total ?? 0, completed: counts.completed ?? 0, failed: counts.failed ?? 0 }
88
- : undefined,
89
- createdAt: batch.created_at ? new Date(batch.created_at * 1000).toISOString() : undefined,
90
- completedAt: batch.completed_at ? new Date(batch.completed_at * 1000).toISOString() : undefined,
91
- expiresAt: batch.expires_at ? new Date(batch.expires_at * 1000).toISOString() : undefined,
92
- error: batch.errors?.data?.[0]
93
- ? { message: batch.errors.data[0].message ?? 'batch failed', code: batch.errors.data[0].code }
94
- : undefined,
95
- raw: batch,
96
- };
97
- }
98
- toOutputItem(line) {
99
- let parsed;
100
- try {
101
- parsed = JSON.parse(line);
102
- }
103
- catch (error) {
104
- throw new BatchProviderResponseError('openai', 'an output line was not valid JSON', error);
105
- }
106
- const customId = parsed.custom_id ?? '';
107
- if (parsed.error) {
108
- return { customId, error: { message: parsed.error.message ?? 'batch item failed', code: parsed.error.code } };
109
- }
110
- const body = parsed.response?.body;
111
- const status = parsed.response?.status_code ?? 200;
112
- if (!body || status >= 400) {
113
- return { customId, error: { message: `batch item failed with status ${status}`, status } };
114
- }
115
- const choice = body.choices?.[0];
116
- const response = {
117
- content: choice?.message?.content ?? '',
118
- role: 'assistant',
119
- finishReason: choice?.finish_reason === 'length' ? 'length' : 'stop',
120
- meta: buildMeta({
121
- provider: 'openai',
122
- model: body.model ?? '',
123
- latencyMs: 0,
124
- inputTokens: body.usage?.prompt_tokens,
125
- outputTokens: body.usage?.completion_tokens,
126
- }),
127
- };
128
- return { customId, response };
129
- }
130
- async request(path, init, context) {
131
- const response = await this.send(path, init, context);
132
- return response.json();
133
- }
134
- async requestText(path, context) {
135
- const response = await this.send(path, {}, context);
136
- return response.text();
137
- }
138
- async send(path, init, context) {
139
- const fetchImpl = this.options.fetch ?? fetch;
140
- const baseUrl = (this.options.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
141
- const headers = {
142
- authorization: `Bearer ${this.options.apiKey}`,
143
- ...(this.options.organization ? { 'openai-organization': this.options.organization } : {}),
144
- ...this.options.headers,
145
- ...(init.headers ?? {}),
146
- };
147
- const response = await fetchImpl(`${baseUrl}${path}`, { ...init, headers, signal: context.signal });
148
- if (!response.ok) {
149
- throw new BatchProviderResponseError('openai', `${path} failed: ${response.status} ${await safeText(response)}`);
150
- }
151
- return response;
46
+ return this.client.state(ref, await this.client.cancel(ref.id, context));
152
47
  }
153
48
  }
154
- async function safeText(response) {
155
- try {
156
- return (await response.text()).slice(0, 500);
157
- }
158
- catch {
159
- return '';
160
- }
49
+ function toOutputItem(line) {
50
+ const customId = line.custom_id ?? '';
51
+ const error = lineError(line);
52
+ if (error)
53
+ return { customId, error };
54
+ const body = line.response?.body;
55
+ const choice = body.choices?.[0];
56
+ const response = {
57
+ content: choice?.message?.content ?? '',
58
+ role: 'assistant',
59
+ finishReason: choice?.finish_reason === 'length' ? 'length' : 'stop',
60
+ meta: buildMeta({
61
+ provider: 'openai',
62
+ model: body.model ?? '',
63
+ latencyMs: 0,
64
+ inputTokens: body.usage?.prompt_tokens,
65
+ outputTokens: body.usage?.completion_tokens,
66
+ }),
67
+ };
68
+ return { customId, response };
161
69
  }
@@ -0,0 +1,18 @@
1
+ import { type BatchJobState } from '../types/batch.js';
2
+ /** How a batch is waited on. */
3
+ export interface BatchPollOptions {
4
+ /** The first wait between polls, in milliseconds. */
5
+ intervalMs: number;
6
+ /** The longest wait, once backoff has grown it. */
7
+ maxIntervalMs: number;
8
+ /** Stops waiting. The provider-side batch keeps running. */
9
+ signal: AbortSignal;
10
+ /** Called with each state that is not yet final. */
11
+ onState?: (state: BatchJobState) => void;
12
+ }
13
+ /**
14
+ * Polls a batch until it reaches a status it never leaves. A batch measured in hours does not deserve
15
+ * a fixed short interval: the wait grows by half each time up to the maximum, which keeps the poll
16
+ * count sane without delaying a fast batch, which the first few short polls still catch.
17
+ */
18
+ export declare function pollUntilSettled(poll: () => Promise<BatchJobState>, options: BatchPollOptions): Promise<BatchJobState>;
@@ -0,0 +1,29 @@
1
+ import { isTerminalBatchStatus } from '../types/batch.js';
2
+ export async function pollUntilSettled(poll, options) {
3
+ let interval = options.intervalMs;
4
+ let state = await poll();
5
+ while (!isTerminalBatchStatus(state.status)) {
6
+ if (options.signal.aborted)
7
+ throw options.signal.reason ?? new Error('aborted');
8
+ options.onState?.(state);
9
+ await delay(interval, options.signal);
10
+ interval = Math.min(Math.round(interval * 1.5), options.maxIntervalMs);
11
+ state = await poll();
12
+ }
13
+ return state;
14
+ }
15
+ function delay(ms, signal) {
16
+ if (ms <= 0)
17
+ return Promise.resolve();
18
+ return new Promise((resolve) => {
19
+ const timer = setTimeout(() => {
20
+ signal?.removeEventListener('abort', onAbort);
21
+ resolve();
22
+ }, ms);
23
+ function onAbort() {
24
+ clearTimeout(timer);
25
+ resolve();
26
+ }
27
+ signal?.addEventListener('abort', onAbort, { once: true });
28
+ });
29
+ }
@@ -0,0 +1,36 @@
1
+ import { type RegistryData } from '../models/data.js';
2
+ import { type ParsedArgs } from './args.js';
3
+ /** How the command reaches the outside: replaced in tests. */
4
+ export interface ModelsCommandIo {
5
+ fetch: typeof fetch;
6
+ env: Record<string, string | undefined>;
7
+ }
8
+ /**
9
+ * `nexus models export | check | refresh`: registry data as a file the application keeps.
10
+ *
11
+ * `export` writes the bundled data, as a file to start from. `check` validates a file and says how it
12
+ * differs from the bundled data. `refresh` lists each provider's models with the keys in the
13
+ * environment and compares them with the data: what the data lacks, and what the provider no longer
14
+ * lists. Prices are not in any provider's model list, so they stay a check against its pricing page.
15
+ */
16
+ export declare function runModelsCommand(subcommand: string, { positionals, flags }: ParsedArgs, io?: ModelsCommandIo): Promise<void>;
17
+ /** How the data compares with what one provider lists. */
18
+ export interface RefreshRow {
19
+ /** The provider. */
20
+ provider: string;
21
+ /** Models the provider lists that the data does not have. */
22
+ missing: string[];
23
+ /** Models the data has for the provider that it no longer lists. */
24
+ unlisted: string[];
25
+ }
26
+ /** Compares registry data with each provider's model list, completion and embedding models alike. */
27
+ export declare function refreshReport(data: RegistryData, listings: Record<string, string[]>): RefreshRow[];
28
+ interface ProviderList {
29
+ /** The environment variable that holds its key. */
30
+ env: string;
31
+ /** Every model id it lists. */
32
+ list(key: string, fetch: typeof globalThis.fetch): Promise<string[]>;
33
+ }
34
+ /** Each provider's own model list, read with the application's key: nothing else is contacted. */
35
+ export declare const PROVIDER_LISTS: Record<string, ProviderList>;
36
+ export {};
@@ -0,0 +1,170 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { bundledRegistryData, checkRegistryData, diffRegistryData, } from '../models/data.js';
3
+ import { CliUsageError, flagBool, listFlag, printTable, stringFlag, writeJson } from './args.js';
4
+ export async function runModelsCommand(subcommand, { positionals, flags }, io = { fetch: globalThis.fetch, env: process.env }) {
5
+ if (subcommand === 'export') {
6
+ const text = `${JSON.stringify(bundledRegistryData(), null, 2)}\n`;
7
+ const out = stringFlag(flags, 'out');
8
+ if (out) {
9
+ await writeFile(out, text);
10
+ console.log(`Wrote the bundled registry data to ${out}.`);
11
+ }
12
+ else
13
+ process.stdout.write(text);
14
+ return;
15
+ }
16
+ if (subcommand === 'check') {
17
+ const file = positionals[0];
18
+ if (!file)
19
+ throw new CliUsageError('Usage: nexus models check <registry.json> [--json]');
20
+ const text = await readFile(file, 'utf8');
21
+ const problems = checkRegistryData(text);
22
+ const diff = problems.length
23
+ ? undefined
24
+ : diffRegistryData(bundledRegistryData(), JSON.parse(text));
25
+ if (flagBool(flags, 'json'))
26
+ writeJson({ valid: problems.length === 0, problems, diff });
27
+ else if (problems.length) {
28
+ console.error(`${file} is not valid registry data:`);
29
+ for (const problem of problems)
30
+ console.error(` - ${problem}`);
31
+ }
32
+ else
33
+ printDiff(file, diff);
34
+ if (problems.length)
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ if (subcommand === 'refresh') {
39
+ const file = stringFlag(flags, 'file');
40
+ const data = file ? JSON.parse(await readFile(file, 'utf8')) : bundledRegistryData();
41
+ const wanted = listFlag(flags, 'providers') ?? Object.keys(PROVIDER_LISTS);
42
+ const unknown = wanted.filter((provider) => !(provider in PROVIDER_LISTS));
43
+ if (unknown.length) {
44
+ throw new CliUsageError(`Unknown provider ${unknown.join(', ')}. Choose from ${Object.keys(PROVIDER_LISTS).join(', ')}.`);
45
+ }
46
+ const listings = {};
47
+ const skipped = [];
48
+ for (const provider of wanted) {
49
+ const source = PROVIDER_LISTS[provider];
50
+ const key = io.env[source.env];
51
+ if (!key) {
52
+ skipped.push(`${provider} (no ${source.env})`);
53
+ continue;
54
+ }
55
+ listings[provider] = await source.list(key, io.fetch);
56
+ }
57
+ const report = refreshReport(data, listings);
58
+ if (flagBool(flags, 'json'))
59
+ writeJson({ report, skipped });
60
+ else
61
+ printRefresh(report, skipped);
62
+ if (flagBool(flags, 'strict') && report.some((row) => row.missing.length || row.unlisted.length))
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ throw new CliUsageError(`Unknown models command "${subcommand}". Use export, check, or refresh, or no command to list.`);
67
+ }
68
+ export function refreshReport(data, listings) {
69
+ return Object.entries(listings).map(([provider, listed]) => {
70
+ const known = new Set([
71
+ ...Object.entries(data.models)
72
+ .filter(([, entry]) => entry.provider === provider)
73
+ .map(([name]) => name),
74
+ ...Object.entries(data.embeddingModels ?? {})
75
+ .filter(([, entry]) => entry.provider === provider)
76
+ .map(([name]) => name),
77
+ ]);
78
+ const offered = new Set(listed);
79
+ return {
80
+ provider,
81
+ missing: [...offered].filter((name) => !known.has(name)).sort(),
82
+ unlisted: [...known].filter((name) => !offered.has(name)).sort(),
83
+ };
84
+ });
85
+ }
86
+ export const PROVIDER_LISTS = {
87
+ openai: { env: 'OPENAI_API_KEY', list: openAiStyle('https://api.openai.com/v1/models') },
88
+ anthropic: {
89
+ env: 'ANTHROPIC_API_KEY',
90
+ async list(key, fetch) {
91
+ const ids = [];
92
+ let after;
93
+ for (;;) {
94
+ const page = (await getJson(fetch, `https://api.anthropic.com/v1/models?limit=1000${after ? `&after_id=${encodeURIComponent(after)}` : ''}`, { 'x-api-key': key, 'anthropic-version': '2023-06-01' }));
95
+ ids.push(...(page.data ?? []).map((model) => model.id));
96
+ if (!page.has_more || !page.last_id)
97
+ return ids;
98
+ after = page.last_id;
99
+ }
100
+ },
101
+ },
102
+ google: {
103
+ env: 'GOOGLE_API_KEY',
104
+ async list(key, fetch) {
105
+ const ids = [];
106
+ let token;
107
+ for (;;) {
108
+ const page = (await getJson(fetch, `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000${token ? `&pageToken=${encodeURIComponent(token)}` : ''}`, { 'x-goog-api-key': key }));
109
+ ids.push(...(page.models ?? []).map((model) => model.name.replace(/^models\//, '')));
110
+ if (!page.nextPageToken)
111
+ return ids;
112
+ token = page.nextPageToken;
113
+ }
114
+ },
115
+ },
116
+ mistral: { env: 'MISTRAL_API_KEY', list: openAiStyle('https://api.mistral.ai/v1/models') },
117
+ cohere: {
118
+ env: 'COHERE_API_KEY',
119
+ async list(key, fetch) {
120
+ const ids = [];
121
+ let token;
122
+ for (;;) {
123
+ const page = (await getJson(fetch, `https://api.cohere.com/v1/models?page_size=1000${token ? `&page_token=${encodeURIComponent(token)}` : ''}`, { authorization: `Bearer ${key}` }));
124
+ ids.push(...(page.models ?? []).map((model) => model.name));
125
+ if (!page.next_page_token)
126
+ return ids;
127
+ token = page.next_page_token;
128
+ }
129
+ },
130
+ },
131
+ groq: { env: 'GROQ_API_KEY', list: openAiStyle('https://api.groq.com/openai/v1/models') },
132
+ deepseek: { env: 'DEEPSEEK_API_KEY', list: openAiStyle('https://api.deepseek.com/models') },
133
+ };
134
+ function openAiStyle(url) {
135
+ return async (key, fetch) => {
136
+ const page = (await getJson(fetch, url, { authorization: `Bearer ${key}` }));
137
+ return (page.data ?? []).map((model) => model.id);
138
+ };
139
+ }
140
+ async function getJson(fetch, url, headers) {
141
+ const response = await fetch(url, { headers });
142
+ if (!response.ok)
143
+ throw new Error(`${new URL(url).host} answered ${response.status} listing its models`);
144
+ return response.json();
145
+ }
146
+ function printDiff(file, diff) {
147
+ console.log(`${file} is valid registry data. Against the bundled data:`);
148
+ console.log(` ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed, ${diff.aliases.length} aliases moved`);
149
+ for (const name of diff.added)
150
+ console.log(` + ${name}`);
151
+ for (const name of diff.removed)
152
+ console.log(` - ${name}`);
153
+ for (const { model, changes } of diff.changed) {
154
+ console.log(` ~ ${model}: ${changes.map(({ field, from, to }) => `${field} ${JSON.stringify(from)} -> ${JSON.stringify(to)}`).join(', ')}`);
155
+ }
156
+ for (const { alias, from, to } of diff.aliases)
157
+ console.log(` @ ${alias}: ${from ?? '(none)'} -> ${to ?? '(none)'}`);
158
+ }
159
+ function printRefresh(report, skipped) {
160
+ if (report.length) {
161
+ printTable(report.map((row) => ({
162
+ provider: row.provider,
163
+ 'listed, not in the data': row.missing.join(', ') || '-',
164
+ 'in the data, not listed': row.unlisted.join(', ') || '-',
165
+ })), ['provider', 'listed, not in the data', 'in the data, not listed']);
166
+ }
167
+ if (skipped.length)
168
+ console.log(`Skipped: ${skipped.join(', ')}.`);
169
+ console.log('Prices are not in any provider list: check new and changed entries against each pricing page.');
170
+ }
package/dist/cli.js CHANGED
@@ -24,9 +24,16 @@ async function main() {
24
24
  case 'scan':
25
25
  await runScan(parsed);
26
26
  return;
27
- case 'models':
27
+ case 'models': {
28
+ const [subcommand, ...args] = parsed.positionals;
29
+ if (subcommand) {
30
+ const { runModelsCommand } = await import('./cli/models.js');
31
+ await runModelsCommand(subcommand, { ...parsed, positionals: args });
32
+ return;
33
+ }
28
34
  runModels(parsed);
29
35
  return;
36
+ }
30
37
  case 'eval': {
31
38
  const [subcommand, ...args] = parsed.positionals;
32
39
  if (subcommand === 'run' || subcommand === 'compare' || subcommand === 'gate') {
@@ -401,6 +408,9 @@ function printHelp() {
401
408
  Usage:
402
409
  nexus scan [paths...] [--json] [--max-bytes 1000000] [--no-fail] [--reveal-values]
403
410
  nexus models [--provider openai] [--json]
411
+ nexus models export [--out registry.json]
412
+ nexus models check <registry.json> [--json]
413
+ nexus models refresh [--file registry.json] [--providers openai,anthropic] [--strict] [--json]
404
414
  nexus eval <eval.json|eval.mjs> [--json]
405
415
  nexus eval run <eval.mjs> [--out experiment.json] [--experiments dir] [--baseline baseline.json] [--fail-on-regression]
406
416
  [--name n] [--concurrency 4] [--repetitions 1] [--timeout-ms n] [--json]
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Checks a deployment before it serves traffic: the runtime, credentials, the database and its
3
+ * migrations, Redis, the operation queue, the model registry, provider health, and settings that
4
+ * only work in one process. `nexus doctor` runs the same checks from a shell.
5
+ *
6
+ * Every check is cheap and read-only, and anything heavy — the model registry, the migration lists —
7
+ * is loaded only when its check runs, so an application can call `diagnose()` at boot or from a
8
+ * health endpoint.
9
+ */
10
+ import type { NexusAIConfig } from '../types/config.js';
1
11
  import type { SchemaMigration } from '../utils/schema-migrations.js';
2
12
  /** How one check came out: `fail` breaks the deployment, `warn` needs a look, `skip` had nothing to check. */
3
13
  export type DoctorStatus = 'ok' | 'warn' | 'fail' | 'skip';
@@ -78,8 +88,13 @@ export interface DoctorOptions {
78
88
  stale?: boolean;
79
89
  }>;
80
90
  };
81
- /** How old the bundled model registry may be, in days. Defaults to the registry's own window. */
91
+ /** How old the model registry may be, in days. Defaults to the registry's own window. */
82
92
  registryMaxAgeDays?: number;
93
+ /**
94
+ * The application's model registry configuration, such as `withRegistryData()` produced, so the
95
+ * registry's age is measured from the data it supplies rather than the bundled data.
96
+ */
97
+ models?: NexusAIConfig['models'];
83
98
  /** How the application is deployed. */
84
99
  deployment?: DoctorDeployment;
85
100
  /** Replaces the clock, for tests. */
@@ -47,7 +47,7 @@ export async function diagnose(options = {}) {
47
47
  await run('node', 'Node.js version', () => nodeCheck(options.nodeVersion ?? process.versions.node));
48
48
  await run('peers', 'Optional packages', () => peersCheck(options.resolvePackage ?? defaultResolver()));
49
49
  await run('credentials', 'Provider credentials', () => credentialsCheck(options.env ?? process.env));
50
- await run('registry', 'Model registry age', () => registryCheck(now(), options.registryMaxAgeDays));
50
+ await run('registry', 'Model registry age', () => registryCheck(now(), options.registryMaxAgeDays, options.models));
51
51
  await run('database', 'Database connectivity', () => databaseCheck(options.database));
52
52
  await run('migrations', 'Schema migrations', () => migrationsCheck(options.database));
53
53
  await run('redis', 'Redis', () => redisCheck(options.redis));
@@ -97,14 +97,19 @@ function credentialsCheck(env) {
97
97
  hint: 'Set the API key of each hosted provider you use, such as OPENAI_API_KEY. Local providers need none.',
98
98
  };
99
99
  }
100
- async function registryCheck(now, maxAgeDays) {
100
+ async function registryCheck(now, maxAgeDays, models) {
101
101
  const { checkRegistryFreshness } = await import('../models/registry.js');
102
- const freshness = checkRegistryFreshness({ now, ...(maxAgeDays === undefined ? {} : { maxAgeDays }) });
102
+ const freshness = checkRegistryFreshness({
103
+ now,
104
+ ...(maxAgeDays === undefined ? {} : { maxAgeDays }),
105
+ ...(models ? { config: { models } } : {}),
106
+ });
107
+ const which = models?.verifiedAt ? 'supplied' : 'bundled';
103
108
  return freshness.stale
104
109
  ? {
105
110
  status: 'warn',
106
- detail: `The bundled registry was verified ${freshness.ageDays} days ago (${freshness.verifiedAt}), past its ${freshness.maxAgeDays}-day window.`,
107
- hint: 'Upgrade nexus-ai-pro, or override stale entries with models.registry.',
111
+ detail: `The ${which} registry was verified ${freshness.ageDays} days ago (${freshness.verifiedAt}), past its ${freshness.maxAgeDays}-day window.`,
112
+ hint: 'Supply newer registry data with withRegistryData(), checked by nexus models check, or upgrade nexus-ai-pro.',
108
113
  }
109
114
  : { status: 'ok', detail: `Verified ${freshness.ageDays} days ago (${freshness.verifiedAt}).` };
110
115
  }
@@ -52,7 +52,7 @@ export declare class GoogleEmbeddingProvider extends HttpEmbeddingProvider {
52
52
  readonly info: EmbeddingProviderInfo;
53
53
  constructor(options?: EmbeddingAdapterOptions);
54
54
  protected authHeaders(): Record<string, string>;
55
- /** Embeds a batch of texts. */
55
+ /** Embeds a batch of inputs: texts, or parts with images, audio, and documents. */
56
56
  embed(request: EmbeddingProviderRequest, context: EmbeddingProviderCallContext): Promise<EmbeddingProviderResult>;
57
57
  }
58
58
  /** Cohere v2 `/embed`. */
@@ -60,7 +60,7 @@ export declare class CohereEmbeddingProvider extends HttpEmbeddingProvider {
60
60
  /** Provider name, capabilities, and models. */
61
61
  readonly info: EmbeddingProviderInfo;
62
62
  constructor(options?: EmbeddingAdapterOptions);
63
- /** Embeds a batch of texts. */
63
+ /** Embeds a batch of inputs: texts, or parts with images. */
64
64
  embed(request: EmbeddingProviderRequest, context: EmbeddingProviderCallContext): Promise<EmbeddingProviderResult>;
65
65
  }
66
66
  /** Mistral `/v1/embeddings`, which follows the OpenAI request and response shape. */