nexus-ai-pro 2.1.0 → 2.2.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.
- package/API_STABILITY.md +38 -0
- package/CHANGELOG.md +145 -1
- package/README.md +13 -8
- package/dist/cache/memory-cache.d.ts +8 -0
- package/dist/cache/memory-cache.js +9 -0
- package/dist/cache/semantic-cache.d.ts +6 -3
- package/dist/cache/semantic-cache.js +5 -2
- package/dist/cli/args.js +3 -0
- package/dist/cli/db.d.ts +16 -4
- package/dist/cli/db.js +169 -22
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +53 -0
- package/dist/cli.js +12 -1
- package/dist/core/lifecycle.js +11 -6
- package/dist/core/nexus.d.ts +6 -0
- package/dist/core/nexus.js +103 -22
- package/dist/doctor/index.d.ts +90 -0
- package/dist/doctor/index.js +237 -0
- package/dist/graph/functional.d.ts +80 -7
- package/dist/graph/functional.js +138 -35
- package/dist/graph/graph.d.ts +15 -5
- package/dist/graph/graph.js +72 -20
- package/dist/images/evals.js +4 -2
- package/dist/operations/adapters.d.ts +52 -4
- package/dist/operations/adapters.js +194 -7
- package/dist/operations/runner.js +1 -0
- package/dist/operations/stats.js +8 -1
- package/dist/operations/store.d.ts +5 -1
- package/dist/operations/store.js +7 -0
- package/dist/ops/circuit-breaker.d.ts +52 -0
- package/dist/ops/circuit-breaker.js +4 -0
- package/dist/ops/circuit-store.d.ts +13 -2
- package/dist/ops/circuit-store.js +68 -0
- package/dist/ops/circuit-sync.d.ts +17 -0
- package/dist/ops/circuit-sync.js +86 -0
- package/dist/ops/rate-limiter.d.ts +2 -0
- package/dist/ops/rate-limiter.js +2 -0
- package/dist/postgres/circuits.d.ts +21 -4
- package/dist/postgres/circuits.js +65 -4
- package/dist/postgres/client.d.ts +0 -2
- package/dist/postgres/client.js +0 -4
- package/dist/postgres/evaluate.d.ts +16 -4
- package/dist/postgres/evaluate.js +17 -5
- package/dist/postgres/index.d.ts +32 -11
- package/dist/postgres/index.js +65 -31
- package/dist/postgres/migrations.d.ts +60 -0
- package/dist/postgres/migrations.js +121 -0
- package/dist/postgres/operations.d.ts +14 -3
- package/dist/postgres/operations.js +45 -15
- package/dist/postgres/prompts.d.ts +11 -2
- package/dist/postgres/prompts.js +14 -3
- package/dist/postgres/rollups.d.ts +35 -0
- package/dist/postgres/rollups.js +129 -0
- package/dist/postgres/store.d.ts +11 -2
- package/dist/postgres/store.js +14 -3
- package/dist/postgres/traces.d.ts +13 -3
- package/dist/postgres/traces.js +25 -3
- package/dist/postgres/vectors.d.ts +11 -2
- package/dist/postgres/vectors.js +14 -3
- package/dist/router/auto-router.d.ts +34 -7
- package/dist/router/auto-router.js +201 -68
- package/dist/server/assistant.d.ts +4 -0
- package/dist/server/assistant.js +8 -0
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/runs.d.ts +24 -1
- package/dist/server/runs.js +90 -14
- package/dist/sqlite/index.d.ts +16 -3
- package/dist/sqlite/index.js +16 -3
- package/dist/sqlite/migrations.d.ts +26 -0
- package/dist/sqlite/migrations.js +63 -0
- package/dist/sqlite/operations.d.ts +14 -3
- package/dist/sqlite/operations.js +42 -15
- package/dist/sqlite/store.d.ts +9 -2
- package/dist/sqlite/store.js +13 -3
- package/dist/sqlite/vectors.d.ts +11 -2
- package/dist/sqlite/vectors.js +13 -2
- package/dist/store/tenant.d.ts +12 -0
- package/dist/store/tenant.js +29 -0
- package/dist/tenancy/index.d.ts +113 -0
- package/dist/tenancy/index.js +233 -0
- package/dist/tracing/instrument.js +2 -0
- package/dist/types/config.d.ts +24 -2
- package/dist/types/graph.d.ts +11 -1
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/operations.d.ts +6 -0
- package/dist/types/response.d.ts +20 -0
- package/dist/types/server.d.ts +10 -1
- package/dist/utils/schema-migrations.d.ts +97 -0
- package/dist/utils/schema-migrations.js +91 -0
- package/dist/utils/signals.d.ts +17 -0
- package/dist/utils/signals.js +26 -0
- package/dist/utils/tenant.d.ts +14 -0
- package/dist/utils/tenant.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/cache/memory-cache.d.ts +8 -0
- package/dist-cjs/cache/memory-cache.js +9 -0
- package/dist-cjs/cache/semantic-cache.d.ts +6 -3
- package/dist-cjs/cache/semantic-cache.js +5 -2
- package/dist-cjs/cli/args.js +3 -0
- package/dist-cjs/cli/db.d.ts +16 -4
- package/dist-cjs/cli/db.js +200 -20
- package/dist-cjs/cli/doctor.d.ts +17 -0
- package/dist-cjs/cli/doctor.js +56 -0
- package/dist-cjs/core/lifecycle.js +11 -6
- package/dist-cjs/core/nexus.d.ts +6 -0
- package/dist-cjs/core/nexus.js +103 -22
- package/dist-cjs/doctor/index.d.ts +90 -0
- package/dist-cjs/doctor/index.js +276 -0
- package/dist-cjs/graph/functional.d.ts +80 -7
- package/dist-cjs/graph/functional.js +137 -34
- package/dist-cjs/graph/graph.d.ts +15 -5
- package/dist-cjs/graph/graph.js +72 -20
- package/dist-cjs/images/evals.js +4 -2
- package/dist-cjs/operations/adapters.d.ts +52 -4
- package/dist-cjs/operations/adapters.js +193 -6
- package/dist-cjs/operations/runner.js +1 -0
- package/dist-cjs/operations/stats.js +8 -1
- package/dist-cjs/operations/store.d.ts +5 -1
- package/dist-cjs/operations/store.js +7 -0
- package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
- package/dist-cjs/ops/circuit-breaker.js +4 -0
- package/dist-cjs/ops/circuit-store.d.ts +13 -2
- package/dist-cjs/ops/circuit-store.js +68 -0
- package/dist-cjs/ops/circuit-sync.d.ts +17 -0
- package/dist-cjs/ops/circuit-sync.js +87 -0
- package/dist-cjs/ops/rate-limiter.d.ts +2 -0
- package/dist-cjs/ops/rate-limiter.js +2 -0
- package/dist-cjs/postgres/circuits.d.ts +21 -4
- package/dist-cjs/postgres/circuits.js +98 -3
- package/dist-cjs/postgres/client.d.ts +0 -2
- package/dist-cjs/postgres/client.js +0 -5
- package/dist-cjs/postgres/evaluate.d.ts +16 -4
- package/dist-cjs/postgres/evaluate.js +50 -4
- package/dist-cjs/postgres/index.d.ts +32 -11
- package/dist-cjs/postgres/index.js +69 -20
- package/dist-cjs/postgres/migrations.d.ts +60 -0
- package/dist-cjs/postgres/migrations.js +129 -0
- package/dist-cjs/postgres/operations.d.ts +14 -3
- package/dist-cjs/postgres/operations.js +78 -14
- package/dist-cjs/postgres/prompts.d.ts +11 -2
- package/dist-cjs/postgres/prompts.js +47 -2
- package/dist-cjs/postgres/rollups.d.ts +35 -0
- package/dist-cjs/postgres/rollups.js +168 -0
- package/dist-cjs/postgres/store.d.ts +11 -2
- package/dist-cjs/postgres/store.js +47 -2
- package/dist-cjs/postgres/traces.d.ts +13 -3
- package/dist-cjs/postgres/traces.js +58 -2
- package/dist-cjs/postgres/vectors.d.ts +11 -2
- package/dist-cjs/postgres/vectors.js +47 -2
- package/dist-cjs/router/auto-router.d.ts +34 -7
- package/dist-cjs/router/auto-router.js +201 -68
- package/dist-cjs/server/assistant.d.ts +4 -0
- package/dist-cjs/server/assistant.js +8 -0
- package/dist-cjs/server/errors.d.ts +12 -0
- package/dist-cjs/server/errors.js +9 -1
- package/dist-cjs/server/index.d.ts +1 -1
- package/dist-cjs/server/index.js +2 -1
- package/dist-cjs/server/runs.d.ts +24 -1
- package/dist-cjs/server/runs.js +89 -13
- package/dist-cjs/sqlite/index.d.ts +16 -3
- package/dist-cjs/sqlite/index.js +31 -10
- package/dist-cjs/sqlite/migrations.d.ts +26 -0
- package/dist-cjs/sqlite/migrations.js +71 -0
- package/dist-cjs/sqlite/operations.d.ts +14 -3
- package/dist-cjs/sqlite/operations.js +76 -15
- package/dist-cjs/sqlite/store.d.ts +9 -2
- package/dist-cjs/sqlite/store.js +47 -3
- package/dist-cjs/sqlite/vectors.d.ts +11 -2
- package/dist-cjs/sqlite/vectors.js +47 -2
- package/dist-cjs/store/tenant.d.ts +12 -0
- package/dist-cjs/store/tenant.js +33 -0
- package/dist-cjs/tenancy/index.d.ts +113 -0
- package/dist-cjs/tenancy/index.js +250 -0
- package/dist-cjs/tracing/instrument.js +2 -0
- package/dist-cjs/types/config.d.ts +24 -2
- package/dist-cjs/types/graph.d.ts +11 -1
- package/dist-cjs/types/messages.d.ts +5 -0
- package/dist-cjs/types/operations.d.ts +6 -0
- package/dist-cjs/types/response.d.ts +20 -0
- package/dist-cjs/types/server.d.ts +10 -1
- package/dist-cjs/utils/schema-migrations.d.ts +97 -0
- package/dist-cjs/utils/schema-migrations.js +102 -0
- package/dist-cjs/utils/signals.d.ts +17 -0
- package/dist-cjs/utils/signals.js +29 -0
- package/dist-cjs/utils/tenant.d.ts +14 -0
- package/dist-cjs/utils/tenant.js +22 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +4 -3
- package/package.json +71 -2
package/dist/postgres/store.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Store, StoreIndexOptions, StoreItem, StoreNamespace, StorePutOptions, StoreSearchOptions } from '../types/store.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
2
3
|
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
3
5
|
/** Options for the Postgres store. */
|
|
4
6
|
export interface PostgresStoreOptions {
|
|
5
7
|
/** Table name, optionally schema-qualified. Defaults to `nexus_store`. */
|
|
@@ -15,6 +17,10 @@ export interface PostgresStoreOptions {
|
|
|
15
17
|
/** Replaces the system clock, for tests. */
|
|
16
18
|
now?: () => Date;
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 schema.
|
|
22
|
+
*/
|
|
23
|
+
export declare function storeMigrations(options?: Pick<PostgresStoreOptions, 'table' | 'vectorDimensions'>): SchemaMigration[];
|
|
18
24
|
/** The schema, as statements. With `vectorDimensions`, pgvector's extension is created too. */
|
|
19
25
|
export declare function storeMigration(options?: Pick<PostgresStoreOptions, 'table' | 'vectorDimensions'>): string[];
|
|
20
26
|
/**
|
|
@@ -31,8 +37,11 @@ export declare class PostgresStore implements Store {
|
|
|
31
37
|
private readonly table;
|
|
32
38
|
private readonly now;
|
|
33
39
|
constructor(client: PostgresLikeClient, options?: PostgresStoreOptions);
|
|
34
|
-
/**
|
|
35
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
42
|
+
* Never runs implicitly. Safe on a database an older release created: what exists is kept.
|
|
43
|
+
*/
|
|
44
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
36
45
|
/** Stores an item, keeping its original `createdAt` when it replaces one. */
|
|
37
46
|
put<V>(namespace: StoreNamespace, key: string, value: V, options?: StorePutOptions): Promise<void>;
|
|
38
47
|
/** Reads an item, or `undefined` when it does not exist or has expired. */
|
package/dist/postgres/store.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { cosine, textOf } from '../store/helpers.js';
|
|
2
|
-
import { fromJson, quoteDerived, quoteTable
|
|
2
|
+
import { fromJson, quoteDerived, quoteTable } from './client.js';
|
|
3
3
|
import { jsonFieldEquals, likePrefix, SqlParams } from './sql.js';
|
|
4
4
|
const DEFAULT_TABLE = 'nexus_store';
|
|
5
5
|
const SEPARATOR = '\u001f';
|
|
6
|
+
export function storeMigrations(options = {}) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
component: `store:${options.table ?? DEFAULT_TABLE}`,
|
|
10
|
+
version: 1,
|
|
11
|
+
name: 'create the store table',
|
|
12
|
+
statements: storeMigration(options),
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
}
|
|
6
16
|
export function storeMigration(options = {}) {
|
|
7
17
|
const name = options.table ?? DEFAULT_TABLE;
|
|
8
18
|
const table = quoteTable(name);
|
|
@@ -38,8 +48,9 @@ export class PostgresStore {
|
|
|
38
48
|
this.table = quoteTable(options.table ?? DEFAULT_TABLE);
|
|
39
49
|
this.now = options.now ?? (() => new Date());
|
|
40
50
|
}
|
|
41
|
-
async migrate() {
|
|
42
|
-
await
|
|
51
|
+
async migrate(options = {}) {
|
|
52
|
+
const { applyPostgresMigrations } = await import('./migrations.js');
|
|
53
|
+
return applyPostgresMigrations(this.client, storeMigrations(this.options), options);
|
|
43
54
|
}
|
|
44
55
|
async put(namespace, key, value, options = {}) {
|
|
45
56
|
if (!key.trim())
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Run, RunFeedback, RunQuery, RunTree, TraceStore } from '../types/tracing.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
2
3
|
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
3
5
|
/** Options for the Postgres trace store. */
|
|
4
6
|
export interface PostgresTraceStoreOptions {
|
|
5
7
|
/** Table name, optionally schema-qualified. Defaults to `nexus_runs`. */
|
|
6
8
|
table?: string;
|
|
7
9
|
}
|
|
8
|
-
/** The schema, as statements. */
|
|
10
|
+
/** The schema, as statements: every migration's, in order. */
|
|
9
11
|
export declare function traceStoreMigration(options?: PostgresTraceStoreOptions): string[];
|
|
12
|
+
/**
|
|
13
|
+
* The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 table;
|
|
14
|
+
* version 2 indexes runs by `metadata.tenantId`, which a tenant-scoped trace store filters on.
|
|
15
|
+
*/
|
|
16
|
+
export declare function traceStoreMigrations(options?: PostgresTraceStoreOptions): SchemaMigration[];
|
|
10
17
|
/**
|
|
11
18
|
* Traces in Postgres.
|
|
12
19
|
*
|
|
@@ -20,8 +27,11 @@ export declare class PostgresTraceStore implements TraceStore {
|
|
|
20
27
|
private readonly options;
|
|
21
28
|
private readonly table;
|
|
22
29
|
constructor(client: PostgresLikeClient, options?: PostgresTraceStoreOptions);
|
|
23
|
-
/**
|
|
24
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
32
|
+
* Never runs implicitly. Safe on a database an older release created: what exists is kept.
|
|
33
|
+
*/
|
|
34
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
25
35
|
/** Stores a run, replacing an earlier version with the same id. */
|
|
26
36
|
save(run: Run): Promise<void>;
|
|
27
37
|
/** Reads a run. */
|
package/dist/postgres/traces.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { assembleTree } from '../tracing/query.js';
|
|
2
|
-
import { fromJson, quoteDerived, quoteTable
|
|
2
|
+
import { fromJson, quoteDerived, quoteTable } from './client.js';
|
|
3
3
|
import { jsonFieldEquals, SqlParams } from './sql.js';
|
|
4
4
|
const DEFAULT_TABLE = 'nexus_runs';
|
|
5
5
|
export function traceStoreMigration(options = {}) {
|
|
6
|
+
return traceStoreMigrations(options).flatMap((migration) => [...migration.statements]);
|
|
7
|
+
}
|
|
8
|
+
export function traceStoreMigrations(options = {}) {
|
|
6
9
|
const name = options.table ?? DEFAULT_TABLE;
|
|
7
10
|
const table = quoteTable(name);
|
|
11
|
+
const component = `traces:${name}`;
|
|
12
|
+
return [
|
|
13
|
+
{ component, version: 1, name: 'create the runs table', statements: createStatements(name, table) },
|
|
14
|
+
{
|
|
15
|
+
component,
|
|
16
|
+
version: 2,
|
|
17
|
+
name: 'index runs by tenant',
|
|
18
|
+
statements: [
|
|
19
|
+
`CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'tenant_started')} ON ${table} ((doc #>> '{metadata,tenantId}'), started_at DESC)`,
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
function createStatements(name, table) {
|
|
8
25
|
return [
|
|
9
26
|
`CREATE TABLE IF NOT EXISTS ${table} (
|
|
10
27
|
id text PRIMARY KEY,
|
|
@@ -34,8 +51,9 @@ export class PostgresTraceStore {
|
|
|
34
51
|
this.options = options;
|
|
35
52
|
this.table = quoteTable(options.table ?? DEFAULT_TABLE);
|
|
36
53
|
}
|
|
37
|
-
async migrate() {
|
|
38
|
-
await
|
|
54
|
+
async migrate(options = {}) {
|
|
55
|
+
const { applyPostgresMigrations } = await import('./migrations.js');
|
|
56
|
+
return applyPostgresMigrations(this.client, traceStoreMigrations(this.options), options);
|
|
39
57
|
}
|
|
40
58
|
async save(run) {
|
|
41
59
|
await this.client.query(`INSERT INTO ${this.table} (id, trace_id, parent_id, name, kind, status, started_at, latency_ms, cost, model, provider, doc)
|
|
@@ -93,6 +111,10 @@ export class PostgresTraceStore {
|
|
|
93
111
|
where.push(`EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(doc -> 'feedback', '[]'::jsonb)) AS item WHERE item ->> 'key' = ${params.add(query.feedbackKey)})`);
|
|
94
112
|
}
|
|
95
113
|
for (const [field, expected] of Object.entries(query.metadata ?? {})) {
|
|
114
|
+
if (field === 'tenantId' && typeof expected === 'string') {
|
|
115
|
+
where.push(`doc #>> '{metadata,tenantId}' = ${params.add(expected)} AND jsonb_typeof(doc #> '{metadata,tenantId}') = 'string'`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
96
118
|
where.push(jsonFieldEquals(`(doc -> 'metadata')`, field, expected, params));
|
|
97
119
|
}
|
|
98
120
|
const { rows } = await this.client.query(`SELECT doc::text AS doc FROM ${this.table}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EmbeddingProvider, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore } from '../hallucination/retrieval.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
2
3
|
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
3
5
|
/** Options for the pgvector store. */
|
|
4
6
|
export interface PostgresVectorStoreOptions {
|
|
5
7
|
/** Width of every vector. Must match the embedding function's output. */
|
|
@@ -15,6 +17,10 @@ export interface PostgresVectorStoreOptions {
|
|
|
15
17
|
*/
|
|
16
18
|
index?: 'hnsw' | 'none';
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 schema.
|
|
22
|
+
*/
|
|
23
|
+
export declare function vectorStoreMigrations(options: Omit<PostgresVectorStoreOptions, 'embed'>): SchemaMigration[];
|
|
18
24
|
/** The schema, as statements: the pgvector extension, the table, and its index. */
|
|
19
25
|
export declare function vectorStoreMigration(options: Omit<PostgresVectorStoreOptions, 'embed'>): string[];
|
|
20
26
|
/**
|
|
@@ -30,8 +36,11 @@ export declare class PostgresVectorStore implements VectorStore {
|
|
|
30
36
|
private readonly table;
|
|
31
37
|
private readonly embed;
|
|
32
38
|
constructor(client: PostgresLikeClient, options: PostgresVectorStoreOptions);
|
|
33
|
-
/**
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
41
|
+
* Never runs implicitly. Safe on a database an older release created: what exists is kept.
|
|
42
|
+
*/
|
|
43
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
35
44
|
/** Adds chunks, or replaces those whose id exists, embedding those without a vector in one batch. */
|
|
36
45
|
add(documents: VectorDocument[]): Promise<void>;
|
|
37
46
|
/** The chunks most similar to a query, best first. */
|
package/dist/postgres/vectors.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { createHashEmbeddings, } from '../hallucination/retrieval.js';
|
|
2
|
-
import { fromJson, quoteDerived, quoteTable
|
|
2
|
+
import { fromJson, quoteDerived, quoteTable } from './client.js';
|
|
3
3
|
import { SqlParams, textArray } from './sql.js';
|
|
4
4
|
const DEFAULT_TABLE = 'nexus_vectors';
|
|
5
|
+
export function vectorStoreMigrations(options) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
component: `vectors:${options.table ?? DEFAULT_TABLE}`,
|
|
9
|
+
version: 1,
|
|
10
|
+
name: 'create the vectors table',
|
|
11
|
+
statements: vectorStoreMigration(options),
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
5
15
|
export function vectorStoreMigration(options) {
|
|
6
16
|
const dimensions = options.dimensions;
|
|
7
17
|
if (!(Number.isInteger(dimensions) && dimensions > 0))
|
|
@@ -38,8 +48,9 @@ export class PostgresVectorStore {
|
|
|
38
48
|
this.table = quoteTable(options.table ?? DEFAULT_TABLE);
|
|
39
49
|
this.embed = options.embed ?? ((texts) => createHashEmbeddings(texts, options.dimensions));
|
|
40
50
|
}
|
|
41
|
-
async migrate() {
|
|
42
|
-
await
|
|
51
|
+
async migrate(options = {}) {
|
|
52
|
+
const { applyPostgresMigrations } = await import('./migrations.js');
|
|
53
|
+
return applyPostgresMigrations(this.client, vectorStoreMigrations(this.options), options);
|
|
43
54
|
}
|
|
44
55
|
async add(documents) {
|
|
45
56
|
if (documents.length === 0)
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
import type { RouterContext, RouteDecision } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Ranks configured providers' models for a request routed as `auto`.
|
|
4
|
+
*
|
|
5
|
+
* Everything that depends only on configuration — which models are candidates, which pass the
|
|
6
|
+
* allow and deny lists and the capability requirements, and how each scores for the strategy — is
|
|
7
|
+
* computed once into a plan and kept until the configuration changes. A request pays only for what
|
|
8
|
+
* can differ between requests: which circuits are open, and provider health.
|
|
9
|
+
*/
|
|
2
10
|
export declare class AutoRouter {
|
|
11
|
+
private readonly plans;
|
|
12
|
+
private readonly patterns;
|
|
13
|
+
private readonly registryIds;
|
|
14
|
+
/** Routes one request: the best candidate first, every other as a fallback in order. */
|
|
3
15
|
route(ctx: RouterContext): RouteDecision;
|
|
16
|
+
/** Plans cached by this router, for tests and diagnostics. */
|
|
17
|
+
get planCount(): number;
|
|
18
|
+
/** The plan for a request's configuration, built on first use and kept until the configuration changes. */
|
|
19
|
+
private plan;
|
|
4
20
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* open usually means a shared dependency is down rather than every provider individually, and
|
|
9
|
-
* attempting one call is strictly better than failing the request without trying anything.
|
|
21
|
+
* What a plan depends on, normalized into a string: the routing settings, the model settings, and
|
|
22
|
+
* the configured providers. A registry is identified by its object, so replacing
|
|
23
|
+
* `models.registry` builds a new plan; changing one in place does not.
|
|
10
24
|
*/
|
|
11
|
-
private
|
|
25
|
+
private planKey;
|
|
26
|
+
private registryId;
|
|
27
|
+
private buildPlan;
|
|
28
|
+
/**
|
|
29
|
+
* The plan's candidates in order for this request. Providers whose circuit is open are left out,
|
|
30
|
+
* unless that would leave nothing: every circuit open usually means a shared dependency is down,
|
|
31
|
+
* and one attempt is strictly better than failing without trying. Health moves a whole provider
|
|
32
|
+
* up or down, so the per-provider lists are merged by their adjusted scores rather than re-sorted.
|
|
33
|
+
*/
|
|
34
|
+
private rank;
|
|
12
35
|
private getCandidates;
|
|
13
36
|
/**
|
|
14
37
|
* Scores a model for a strategy. For privacy, a model counts as local when its name says so or when
|
|
@@ -16,15 +39,19 @@ export declare class AutoRouter {
|
|
|
16
39
|
* is preferred without renaming its models.
|
|
17
40
|
*/
|
|
18
41
|
private score;
|
|
42
|
+
private capabilitiesOf;
|
|
19
43
|
private localFallbackCapabilities;
|
|
20
44
|
private costScore;
|
|
21
45
|
private speedScore;
|
|
22
46
|
private qualityScore;
|
|
23
47
|
private preferencesForProvider;
|
|
24
48
|
private isAllowed;
|
|
49
|
+
/** Whether a model meets the routing requirements: the boolean ones as two bitmask tests, the rest by value. */
|
|
25
50
|
private matchesRequirements;
|
|
51
|
+
/** Whether a model name matches an allow or deny entry, with `*` as a wildcard. Patterns are compiled once. */
|
|
26
52
|
private matchesModelPattern;
|
|
27
53
|
private isProviderRunnable;
|
|
28
|
-
|
|
54
|
+
/** Each provider's health adjustment for this request, or nothing when health does not apply. */
|
|
55
|
+
private healthOffsets;
|
|
29
56
|
}
|
|
30
57
|
export declare function routeDirectModel(model: string, ctx: RouterContext): RouteDecision;
|
|
@@ -1,36 +1,138 @@
|
|
|
1
1
|
import { getModelRegistry, listModelsForProvider, resolveModel } from '../models/registry.js';
|
|
2
|
+
const PLAN_LIMIT = 16;
|
|
3
|
+
const BOOLEAN_CAPABILITIES = ['streaming', 'toolCalling', 'structuredOutputs', 'jsonMode', 'reasoning'];
|
|
2
4
|
export class AutoRouter {
|
|
5
|
+
plans = new Map();
|
|
6
|
+
patterns = new Map();
|
|
7
|
+
registryIds = new WeakMap();
|
|
3
8
|
route(ctx) {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
score: this.score(candidate.model, strategy, ctx.config, ctx.providers.get(candidate.providerName)?.info?.isLocal === true) +
|
|
8
|
-
candidate.weight +
|
|
9
|
-
this.healthScore(candidate.providerName, ctx),
|
|
10
|
-
reason: `auto route by ${strategy} score`,
|
|
11
|
-
}));
|
|
12
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
13
|
-
const winner = candidates[0];
|
|
9
|
+
const plan = this.plan(ctx);
|
|
10
|
+
const ranked = this.rank(plan, ctx);
|
|
11
|
+
const winner = ranked[0];
|
|
14
12
|
if (!winner) {
|
|
15
13
|
throw new Error('No configured providers are available for routing');
|
|
16
14
|
}
|
|
17
15
|
return {
|
|
18
16
|
providerName: winner.providerName,
|
|
19
17
|
model: winner.model,
|
|
20
|
-
reason:
|
|
21
|
-
fallbacks:
|
|
18
|
+
reason: `auto route by ${plan.strategy} score: ${winner.model}`,
|
|
19
|
+
fallbacks: ranked.slice(1).map((candidate) => candidate.attempt),
|
|
22
20
|
};
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
22
|
+
get planCount() {
|
|
23
|
+
return this.plans.size;
|
|
24
|
+
}
|
|
25
|
+
plan(ctx) {
|
|
26
|
+
const key = this.planKey(ctx);
|
|
27
|
+
const cached = this.plans.get(key);
|
|
28
|
+
if (cached) {
|
|
29
|
+
this.plans.delete(key);
|
|
30
|
+
this.plans.set(key, cached);
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
const plan = this.buildPlan(ctx);
|
|
34
|
+
this.plans.set(key, plan);
|
|
35
|
+
if (this.plans.size > PLAN_LIMIT)
|
|
36
|
+
this.plans.delete(this.plans.keys().next().value);
|
|
37
|
+
return plan;
|
|
38
|
+
}
|
|
39
|
+
planKey(ctx) {
|
|
40
|
+
const routing = ctx.config.routing;
|
|
41
|
+
const models = ctx.config.models;
|
|
42
|
+
const providers = [];
|
|
43
|
+
for (const [name, provider] of ctx.providers)
|
|
44
|
+
providers.push(`${name}${provider.info?.isLocal === true ? '+local' : ''}`);
|
|
45
|
+
return JSON.stringify([
|
|
46
|
+
routing?.strategy ?? 'quality',
|
|
47
|
+
routing?.candidateModels ?? null,
|
|
48
|
+
routing?.modelPreferences ?? null,
|
|
49
|
+
routing?.allowModels ?? null,
|
|
50
|
+
routing?.denyModels ?? null,
|
|
51
|
+
normalizedRequirements(routing?.requiredCapabilities),
|
|
52
|
+
models?.includeDefaults ?? null,
|
|
53
|
+
models?.aliases ?? null,
|
|
54
|
+
models?.registry ? this.registryId(models.registry) : 0,
|
|
55
|
+
providers,
|
|
56
|
+
]);
|
|
30
57
|
}
|
|
31
|
-
|
|
58
|
+
registryId(registry) {
|
|
59
|
+
let id = this.registryIds.get(registry);
|
|
60
|
+
if (id === undefined) {
|
|
61
|
+
id = this.registryIds.get(this.registryIds) ?? 1;
|
|
62
|
+
this.registryIds.set(registry, id);
|
|
63
|
+
this.registryIds.set(this.registryIds, id + 1);
|
|
64
|
+
}
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
buildPlan(ctx) {
|
|
68
|
+
const strategy = ctx.config.routing?.strategy || 'quality';
|
|
69
|
+
const registry = getModelRegistry(ctx.config);
|
|
70
|
+
const requirements = compileRequirements(ctx.config.routing?.requiredCapabilities);
|
|
71
|
+
const candidates = this.getCandidates(ctx, registry, requirements);
|
|
72
|
+
const planned = candidates.map((candidate, index) => ({
|
|
73
|
+
providerName: candidate.providerName,
|
|
74
|
+
model: candidate.model,
|
|
75
|
+
base: this.score(candidate.model, strategy, registry, ctx.providers.get(candidate.providerName)?.info?.isLocal === true) + candidate.weight,
|
|
76
|
+
index,
|
|
77
|
+
attempt: Object.freeze({ providerName: candidate.providerName, model: candidate.model }),
|
|
78
|
+
}));
|
|
79
|
+
const ranked = [...planned].sort(byScore);
|
|
80
|
+
const byProvider = new Map();
|
|
81
|
+
for (const candidate of ranked) {
|
|
82
|
+
const list = byProvider.get(candidate.providerName);
|
|
83
|
+
if (list)
|
|
84
|
+
list.push(candidate);
|
|
85
|
+
else
|
|
86
|
+
byProvider.set(candidate.providerName, [candidate]);
|
|
87
|
+
}
|
|
88
|
+
return { strategy, ranked, byProvider };
|
|
89
|
+
}
|
|
90
|
+
rank(plan, ctx) {
|
|
91
|
+
let excluded;
|
|
92
|
+
if (ctx.openCircuits?.length) {
|
|
93
|
+
const open = new Set(ctx.openCircuits);
|
|
94
|
+
if (plan.ranked.some((candidate) => !open.has(candidate.providerName)))
|
|
95
|
+
excluded = open;
|
|
96
|
+
}
|
|
97
|
+
const offsets = this.healthOffsets(ctx);
|
|
98
|
+
if (!offsets) {
|
|
99
|
+
return excluded ? plan.ranked.filter((candidate) => !excluded.has(candidate.providerName)) : plan.ranked;
|
|
100
|
+
}
|
|
101
|
+
const lists = [];
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const [providerName, list] of plan.byProvider) {
|
|
104
|
+
if (excluded?.has(providerName))
|
|
105
|
+
continue;
|
|
106
|
+
lists.push({ list, offset: offsets.get(providerName) ?? 0, at: 0 });
|
|
107
|
+
total += list.length;
|
|
108
|
+
}
|
|
109
|
+
const merged = new Array(total);
|
|
110
|
+
for (let position = 0; position < total; position += 1) {
|
|
111
|
+
let best = -1;
|
|
112
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
113
|
+
let bestIndex = Number.POSITIVE_INFINITY;
|
|
114
|
+
for (let which = 0; which < lists.length; which += 1) {
|
|
115
|
+
const head = lists[which];
|
|
116
|
+
const candidate = head.list[head.at];
|
|
117
|
+
if (!candidate)
|
|
118
|
+
continue;
|
|
119
|
+
const score = candidate.base + head.offset;
|
|
120
|
+
if (score > bestScore || (score === bestScore && candidate.index < bestIndex)) {
|
|
121
|
+
best = which;
|
|
122
|
+
bestScore = score;
|
|
123
|
+
bestIndex = candidate.index;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const chosen = lists[best];
|
|
127
|
+
merged[position] = chosen.list[chosen.at];
|
|
128
|
+
chosen.at += 1;
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
getCandidates(ctx, registry, requirements) {
|
|
32
133
|
const candidates = [];
|
|
33
134
|
const configuredModels = ctx.config.routing?.candidateModels;
|
|
135
|
+
const passes = (model) => !requirements || this.matchesRequirements(model, registry, requirements);
|
|
34
136
|
if (configuredModels?.length) {
|
|
35
137
|
for (const model of configuredModels) {
|
|
36
138
|
const resolved = resolveModel(model, ctx.config);
|
|
@@ -39,23 +141,19 @@ export class AutoRouter {
|
|
|
39
141
|
candidates.push({ providerName, model: resolved.model, weight: 0 });
|
|
40
142
|
}
|
|
41
143
|
}
|
|
42
|
-
return candidates.filter((candidate) =>
|
|
144
|
+
return candidates.filter((candidate) => passes(candidate.model));
|
|
43
145
|
}
|
|
44
146
|
const preferences = ctx.config.routing?.modelPreferences?.[ctx.config.routing?.strategy || 'quality'];
|
|
45
147
|
for (const providerName of ctx.providers.keys()) {
|
|
46
|
-
const preferred = this.preferencesForProvider(providerName, preferences, ctx.config);
|
|
148
|
+
const preferred = this.preferencesForProvider(providerName, preferences, ctx.config, registry);
|
|
47
149
|
if (preferred.length) {
|
|
48
150
|
candidates.push(...preferred);
|
|
49
151
|
}
|
|
50
152
|
}
|
|
51
|
-
return candidates.filter((candidate) =>
|
|
52
|
-
return this.isAllowed(candidate.model, ctx.config) && this.matchesRequirements(candidate.model, ctx.config);
|
|
53
|
-
});
|
|
153
|
+
return candidates.filter((candidate) => this.isAllowed(candidate.model, ctx.config) && passes(candidate.model));
|
|
54
154
|
}
|
|
55
|
-
score(model, strategy,
|
|
56
|
-
const
|
|
57
|
-
const normalized = model.startsWith('ollama/') ? model.slice(7) : model;
|
|
58
|
-
const caps = registry[model] || registry[normalized] || this.localFallbackCapabilities(model);
|
|
155
|
+
score(model, strategy, registry, providerIsLocal = false) {
|
|
156
|
+
const caps = this.capabilitiesOf(model, registry);
|
|
59
157
|
if (strategy === 'privacy')
|
|
60
158
|
return providerIsLocal || isLocalModel(model) ? 100 : 30;
|
|
61
159
|
if (strategy === 'speed')
|
|
@@ -64,6 +162,10 @@ export class AutoRouter {
|
|
|
64
162
|
return this.costScore(model, caps);
|
|
65
163
|
return this.qualityScore(model, caps);
|
|
66
164
|
}
|
|
165
|
+
capabilitiesOf(model, registry) {
|
|
166
|
+
const normalized = model.startsWith('ollama/') ? model.slice(7) : model;
|
|
167
|
+
return registry[model] || registry[normalized] || this.localFallbackCapabilities(model);
|
|
168
|
+
}
|
|
67
169
|
localFallbackCapabilities(model) {
|
|
68
170
|
return {
|
|
69
171
|
inputModalities: ['text'],
|
|
@@ -109,7 +211,7 @@ export class AutoRouter {
|
|
|
109
211
|
return 65;
|
|
110
212
|
return Math.min(85, caps.maxContextTokens / 2000);
|
|
111
213
|
}
|
|
112
|
-
preferencesForProvider(providerName, preferences, config) {
|
|
214
|
+
preferencesForProvider(providerName, preferences, config, registry) {
|
|
113
215
|
if (preferences?.length) {
|
|
114
216
|
return preferences
|
|
115
217
|
.map((preference) => typeof preference === 'string'
|
|
@@ -128,7 +230,7 @@ export class AutoRouter {
|
|
|
128
230
|
const registryModels = listModelsForProvider(providerName, config);
|
|
129
231
|
if (registryModels.length) {
|
|
130
232
|
return registryModels
|
|
131
|
-
.filter((model) => this.isProviderRunnable(providerName, model,
|
|
233
|
+
.filter((model) => this.isProviderRunnable(providerName, model, registry))
|
|
132
234
|
.map((model) => ({ providerName, model, weight: 0 }));
|
|
133
235
|
}
|
|
134
236
|
if (providerName === 'ollama') {
|
|
@@ -169,52 +271,44 @@ export class AutoRouter {
|
|
|
169
271
|
return false;
|
|
170
272
|
return !deny.some((entry) => this.matchesModelPattern(model, entry));
|
|
171
273
|
}
|
|
172
|
-
matchesRequirements(model,
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const registry = getModelRegistry(config);
|
|
177
|
-
const normalized = model.startsWith('ollama/') ? model.slice(7) : model;
|
|
178
|
-
const caps = registry[model] || registry[normalized] || this.localFallbackCapabilities(model);
|
|
179
|
-
if (requirements.streaming !== undefined && caps.streaming !== requirements.streaming)
|
|
180
|
-
return false;
|
|
181
|
-
if (requirements.toolCalling !== undefined && caps.toolCalling !== requirements.toolCalling)
|
|
182
|
-
return false;
|
|
183
|
-
if (requirements.structuredOutputs !== undefined && caps.structuredOutputs !== requirements.structuredOutputs)
|
|
274
|
+
matchesRequirements(model, registry, requirements) {
|
|
275
|
+
const caps = this.capabilitiesOf(model, registry);
|
|
276
|
+
const { mustBe, mustNotBe } = capabilityMasks(caps);
|
|
277
|
+
if ((mustBe & requirements.mustBe) !== requirements.mustBe)
|
|
184
278
|
return false;
|
|
185
|
-
if (
|
|
279
|
+
if ((mustNotBe & requirements.mustNotBe) !== requirements.mustNotBe)
|
|
186
280
|
return false;
|
|
187
|
-
|
|
281
|
+
const source = requirements.source;
|
|
282
|
+
if (source.minContextTokens && caps.maxContextTokens < source.minContextTokens)
|
|
188
283
|
return false;
|
|
189
|
-
if (
|
|
284
|
+
if (source.maxInputCostPer1k !== undefined && caps.costPer1kInput > source.maxInputCostPer1k)
|
|
190
285
|
return false;
|
|
191
|
-
if (
|
|
286
|
+
if (source.maxOutputCostPer1k !== undefined && caps.costPer1kOutput > source.maxOutputCostPer1k)
|
|
192
287
|
return false;
|
|
193
|
-
if (
|
|
288
|
+
if (source.statuses?.length && caps.status && !source.statuses.includes(caps.status))
|
|
194
289
|
return false;
|
|
195
|
-
if (
|
|
290
|
+
if (source.inputModalities?.some((modality) => !caps.inputModalities.includes(modality)))
|
|
196
291
|
return false;
|
|
197
|
-
if (
|
|
198
|
-
return false;
|
|
199
|
-
if (requirements.outputModalities?.some((modality) => !caps.outputModalities.includes(modality)))
|
|
292
|
+
if (source.outputModalities?.some((modality) => !caps.outputModalities.includes(modality)))
|
|
200
293
|
return false;
|
|
201
294
|
return true;
|
|
202
295
|
}
|
|
203
296
|
matchesModelPattern(model, pattern) {
|
|
204
297
|
if (pattern === model)
|
|
205
298
|
return true;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
299
|
+
let regex = this.patterns.get(pattern);
|
|
300
|
+
if (regex === undefined) {
|
|
301
|
+
regex = pattern.includes('*')
|
|
302
|
+
? new RegExp(`^${pattern
|
|
303
|
+
.split('*')
|
|
304
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
305
|
+
.join('.*')}$`)
|
|
306
|
+
: null;
|
|
307
|
+
this.patterns.set(pattern, regex);
|
|
308
|
+
}
|
|
309
|
+
return regex ? regex.test(model) : false;
|
|
215
310
|
}
|
|
216
|
-
isProviderRunnable(providerName, model,
|
|
217
|
-
const registry = getModelRegistry(config);
|
|
311
|
+
isProviderRunnable(providerName, model, registry) {
|
|
218
312
|
const caps = registry[model];
|
|
219
313
|
if (!caps?.endpoints)
|
|
220
314
|
return true;
|
|
@@ -226,15 +320,54 @@ export class AutoRouter {
|
|
|
226
320
|
return caps.endpoints.includes('generateContent');
|
|
227
321
|
return true;
|
|
228
322
|
}
|
|
229
|
-
|
|
323
|
+
healthOffsets(ctx) {
|
|
230
324
|
if (!ctx.config.health?.enabled || !ctx.health?.length)
|
|
231
|
-
return
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
325
|
+
return undefined;
|
|
326
|
+
const offsets = new Map();
|
|
327
|
+
for (const snapshot of ctx.health) {
|
|
328
|
+
if (offsets.has(snapshot.providerName))
|
|
329
|
+
continue;
|
|
330
|
+
offsets.set(snapshot.providerName, snapshot.healthy ? snapshot.score / 10 : -100);
|
|
331
|
+
}
|
|
332
|
+
return offsets;
|
|
236
333
|
}
|
|
237
334
|
}
|
|
335
|
+
function compileRequirements(requirements) {
|
|
336
|
+
if (!requirements)
|
|
337
|
+
return undefined;
|
|
338
|
+
let mustBe = 0;
|
|
339
|
+
let mustNotBe = 0;
|
|
340
|
+
BOOLEAN_CAPABILITIES.forEach((name, bit) => {
|
|
341
|
+
const wanted = requirements[name];
|
|
342
|
+
if (wanted === true)
|
|
343
|
+
mustBe |= 1 << bit;
|
|
344
|
+
else if (wanted === false)
|
|
345
|
+
mustNotBe |= 1 << bit;
|
|
346
|
+
});
|
|
347
|
+
return { mustBe, mustNotBe, source: requirements };
|
|
348
|
+
}
|
|
349
|
+
function capabilityMasks(caps) {
|
|
350
|
+
let mustBe = 0;
|
|
351
|
+
let mustNotBe = 0;
|
|
352
|
+
BOOLEAN_CAPABILITIES.forEach((name, bit) => {
|
|
353
|
+
const value = name === 'reasoning' ? Boolean(caps.reasoning) : caps[name];
|
|
354
|
+
if (value === true)
|
|
355
|
+
mustBe |= 1 << bit;
|
|
356
|
+
else if (value === false)
|
|
357
|
+
mustNotBe |= 1 << bit;
|
|
358
|
+
});
|
|
359
|
+
return { mustBe, mustNotBe };
|
|
360
|
+
}
|
|
361
|
+
function normalizedRequirements(requirements) {
|
|
362
|
+
if (!requirements)
|
|
363
|
+
return null;
|
|
364
|
+
return Object.keys(requirements)
|
|
365
|
+
.sort()
|
|
366
|
+
.map((key) => [key, requirements[key]]);
|
|
367
|
+
}
|
|
368
|
+
function byScore(a, b) {
|
|
369
|
+
return b.base - a.base || a.index - b.index;
|
|
370
|
+
}
|
|
238
371
|
function isLocalModel(model) {
|
|
239
372
|
return (model.startsWith('ollama/') ||
|
|
240
373
|
model.startsWith('lmstudio/') ||
|