mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
🌀 ci: Deterministic Circular Dependency Checks (#14579)
* 🌀 ci: Deterministic Circular Dependency Checks * 🌀 ci: Enforce Type-Level Edges in Circular Dependency Scan * 🌀 ci: Materialize Import-Type Expression Edges in Cycle Scan * 🌀 ci: Collect Inline Type-Only Specifier Edges in Cycle Scan
This commit is contained in:
parent
b253b623fe
commit
ad0f72dede
22 changed files with 413 additions and 194 deletions
35
.github/workflows/backend-review.yml
vendored
35
.github/workflows/backend-review.yml
vendored
|
|
@ -4,6 +4,8 @@ on:
|
|||
paths:
|
||||
- 'api/**'
|
||||
- 'packages/**'
|
||||
- 'config/circular-deps.mjs'
|
||||
- '.github/workflows/backend-review.yml'
|
||||
- '!**.md'
|
||||
|
||||
permissions:
|
||||
|
|
@ -157,7 +159,6 @@ jobs:
|
|||
|
||||
circular-deps:
|
||||
name: Circular dependency checks
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
|
|
@ -184,36 +185,8 @@ jobs:
|
|||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Download data-provider build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Rebuild @librechat/api and check for circular dependencies
|
||||
run: |
|
||||
output=$(npm run build:api 2>&1)
|
||||
echo "$output"
|
||||
if echo "$output" | grep -q "Circular depend"; then
|
||||
echo "Error: Circular dependency detected in @librechat/api!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Detect circular dependencies in rollup
|
||||
working-directory: ./packages/data-provider
|
||||
run: |
|
||||
output=$(npm run rollup:api)
|
||||
echo "$output"
|
||||
if echo "$output" | grep -q "Circular dependency"; then
|
||||
echo "Error: Circular dependency detected!"
|
||||
exit 1
|
||||
fi
|
||||
- name: Detect circular dependencies
|
||||
run: node config/circular-deps.mjs
|
||||
|
||||
test-api:
|
||||
name: 'Tests: api (shard ${{ matrix.shard }}/3)'
|
||||
|
|
|
|||
220
config/circular-deps.mjs
Normal file
220
config/circular-deps.mjs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const root = path.resolve(fileURLToPath(import.meta.url), '../..');
|
||||
|
||||
/**
|
||||
* Module graphs checked for cycles. `alias` mirrors each target's tsconfig
|
||||
* paths (or module-alias for the legacy server); `internal` lists the aliased
|
||||
* specifier prefixes that are first-party alongside relative/absolute imports.
|
||||
* `minModules` is a resolution-rot guard: if a graph shrinks below it, the
|
||||
* scan is no longer seeing the real codebase and must fail rather than
|
||||
* silently pass.
|
||||
*/
|
||||
const targets = [
|
||||
{
|
||||
name: '@librechat/api',
|
||||
dir: 'packages/api',
|
||||
entries: ['src/index.ts', 'src/telemetry.ts'],
|
||||
alias: { '~': 'src' },
|
||||
internal: ['~'],
|
||||
minModules: 200,
|
||||
},
|
||||
{
|
||||
name: 'librechat-data-provider',
|
||||
dir: 'packages/data-provider',
|
||||
entries: ['src/index.ts', 'src/react-query/index.ts'],
|
||||
alias: { 'librechat-data-provider/react-query': 'src/react-query/index.ts', src: 'src' },
|
||||
internal: ['src/', 'librechat-data-provider/react-query'],
|
||||
minModules: 20,
|
||||
/**
|
||||
* Grandfathered: the core type modules (schemas, config, api-endpoints,
|
||||
* types/{assistants,agents,runs,web}) hold six pre-existing type-only
|
||||
* knots that need their own untangling PR. Runtime edges are still
|
||||
* enforced; the exclusion is logged on every run so it cannot read as
|
||||
* full coverage.
|
||||
*/
|
||||
typeEdges: false,
|
||||
},
|
||||
{
|
||||
name: '@librechat/data-schemas',
|
||||
dir: 'packages/data-schemas',
|
||||
entries: ['src/index.ts', 'src/admin/capabilities.ts'],
|
||||
alias: { '~': 'src' },
|
||||
internal: ['~'],
|
||||
minModules: 75,
|
||||
},
|
||||
{
|
||||
name: '@librechat/client',
|
||||
dir: 'packages/client',
|
||||
entries: ['src/index.ts'],
|
||||
alias: { '~': 'src' },
|
||||
internal: ['~'],
|
||||
minModules: 100,
|
||||
},
|
||||
{
|
||||
name: 'api server',
|
||||
dir: 'api',
|
||||
entries: ['server/index.js'],
|
||||
alias: { '~': '.' },
|
||||
internal: ['~'],
|
||||
minModules: 150,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Recursively collects every type-only dependency specifier: `import type` /
|
||||
* `export type ... from` declarations, inline type specifiers (`import { type
|
||||
* Foo } from` — kind lives on the child specifier, source on the parent), and
|
||||
* `import('...')` type expressions (TSImportType), which nest arbitrarily deep
|
||||
* inside other declarations. All forms carry the specifier as `source.value`.
|
||||
*/
|
||||
const collectTypeSpecifiers = (node, specifiers) => {
|
||||
if (node === null || typeof node !== 'object') {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
collectTypeSpecifiers(item, specifiers);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const typeOnly =
|
||||
node.type === 'TSImportType' ||
|
||||
(node.importKind ?? node.exportKind) === 'type' ||
|
||||
(Array.isArray(node.specifiers) &&
|
||||
node.specifiers.some((s) => (s.importKind ?? s.exportKind) === 'type'));
|
||||
if (typeOnly && typeof node.source?.value === 'string') {
|
||||
specifiers.add(node.source.value);
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (value !== null && typeof value === 'object') {
|
||||
collectTypeSpecifiers(value, specifiers);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bundlers erase type-only edges before building the module graph, so purely
|
||||
* type-level cycles (the declaration-graph kind) would never be reported.
|
||||
* Re-materialize each type-only specifier as a bare side-effect import so the
|
||||
* scanned graph carries type edges too. Uses the real parser (not a regex) so
|
||||
* imports inside string templates never count.
|
||||
*/
|
||||
const typeEdgesPlugin = (parseAst) => ({
|
||||
name: 'type-edges',
|
||||
transform(code, id) {
|
||||
const extension = /\.([mc]?tsx?)(?:$|\?)/.exec(id)?.[1];
|
||||
if (!extension) {
|
||||
return null;
|
||||
}
|
||||
const { body } = parseAst(code, { lang: extension.endsWith('x') ? 'tsx' : 'ts' });
|
||||
const specifiers = new Set();
|
||||
collectTypeSpecifiers(body, specifiers);
|
||||
if (specifiers.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const edges = [...specifiers].map((s) => `\nimport ${JSON.stringify(s)};`).join('');
|
||||
return { code: code + edges, map: null };
|
||||
},
|
||||
});
|
||||
|
||||
/** Stub style/asset imports: rolldown no longer bundles CSS, and assets carry no module edges. */
|
||||
const assetsPlugin = {
|
||||
name: 'assets-as-empty',
|
||||
load(id) {
|
||||
if (/\.(css|scss|sass|less|svg|png|jpe?g|gif|webp)(?:$|\?)/.test(id)) {
|
||||
return { code: 'export {};', moduleType: 'js' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
/** Loads the rolldown instance the tsdown builds run on, keeping resolution semantics identical. */
|
||||
async function loadRolldown() {
|
||||
const apiRequire = createRequire(path.join(root, 'packages/api/package.json'));
|
||||
const tsdownRequire = createRequire(apiRequire.resolve('tsdown'));
|
||||
const { rolldown } = await import(pathToFileURL(tsdownRequire.resolve('rolldown')).href);
|
||||
const { parseAst } = await import(pathToFileURL(tsdownRequire.resolve('rolldown/parseAst')).href);
|
||||
return { rolldown, parseAst };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const stripAnsi = (message) => message.replace(/\u001B\[[0-9;]*m/g, '');
|
||||
const relativize = (message) => stripAnsi(message).replaceAll(root + path.sep, '');
|
||||
|
||||
async function scan({ rolldown, parseAst }, target) {
|
||||
const cycles = [];
|
||||
const unresolved = [];
|
||||
const isInternal = (id) =>
|
||||
id.startsWith('.') || path.isAbsolute(id) || target.internal.some((p) => id.startsWith(p));
|
||||
const alias = Object.fromEntries(
|
||||
Object.entries(target.alias).map(([key, dir]) => [key, path.join(root, target.dir, dir)]),
|
||||
);
|
||||
|
||||
try {
|
||||
const build = await rolldown({
|
||||
input: target.entries.map((entry) => path.join(root, target.dir, entry)),
|
||||
platform: 'node',
|
||||
resolve: { alias },
|
||||
external: (id) => !isInternal(id),
|
||||
plugins: [...(target.typeEdges === false ? [] : [typeEdgesPlugin(parseAst)]), assetsPlugin],
|
||||
checks: { circularDependency: true },
|
||||
onLog(_level, log) {
|
||||
if (log.code === 'CIRCULAR_DEPENDENCY') {
|
||||
cycles.push(relativize(log.message));
|
||||
} else if (log.code === 'UNRESOLVED_IMPORT') {
|
||||
unresolved.push(relativize(log.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
const { output } = await build.generate({ format: 'cjs' });
|
||||
const modules = output.reduce((sum, chunk) => sum + (chunk.moduleIds?.length ?? 0), 0);
|
||||
await build.close();
|
||||
return { target, cycles, unresolved, modules, error: null };
|
||||
} catch (error) {
|
||||
return { target, cycles, unresolved, modules: 0, error };
|
||||
}
|
||||
}
|
||||
|
||||
function report({ target, cycles, unresolved, modules, error }) {
|
||||
const problems = [];
|
||||
if (error) {
|
||||
problems.push(`build failed: ${relativize(error.message)}`);
|
||||
}
|
||||
if (cycles.length > 0) {
|
||||
problems.push(...cycles);
|
||||
}
|
||||
if (unresolved.length > 0) {
|
||||
problems.push(...unresolved.map((message) => `unresolved first-party import: ${message}`));
|
||||
}
|
||||
if (!error && modules < target.minModules) {
|
||||
problems.push(
|
||||
`graph has ${modules} modules, below the ${target.minModules} floor; the scan is no longer resolving the real codebase`,
|
||||
);
|
||||
}
|
||||
|
||||
if (problems.length === 0) {
|
||||
const scope =
|
||||
target.typeEdges === false
|
||||
? 'runtime edges only, type edges grandfathered'
|
||||
: 'runtime + type edges';
|
||||
console.log(`✓ ${target.name}: no circular dependencies (${modules} modules, ${scope})`);
|
||||
return true;
|
||||
}
|
||||
console.error(`✗ ${target.name}:`);
|
||||
for (const problem of problems) {
|
||||
console.error(` ${problem}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const engine = await loadRolldown();
|
||||
const results = await Promise.all(targets.map((target) => scan(engine, target)));
|
||||
const passed = results.map(report).every(Boolean);
|
||||
|
||||
if (!passed) {
|
||||
console.error('\nCircular dependency check failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -24,6 +24,12 @@ import type {
|
|||
import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents';
|
||||
import type { IMongoFile, FileOwnerScope } from '@librechat/data-schemas';
|
||||
import type { Response as ServerResponse } from 'express';
|
||||
import type {
|
||||
ResolvedManualSkill,
|
||||
ResolvedAlwaysApplySkill,
|
||||
TListSkillsByAccess,
|
||||
TGetSkillByName,
|
||||
} from './skills';
|
||||
import type {
|
||||
ServerRequest,
|
||||
EndpointDbMethods,
|
||||
|
|
@ -31,7 +37,6 @@ import type {
|
|||
InitializeResultBase,
|
||||
} from '~/types';
|
||||
import type { LCAvailableTools, RequestScopedMCPConnectionStore } from '../mcp/types';
|
||||
import type { ResolvedManualSkill, ResolvedAlwaysApplySkill } from './skills';
|
||||
import type { TFilterFilesByAgentAccess } from './resources';
|
||||
import {
|
||||
injectSkillCatalog,
|
||||
|
|
@ -529,77 +534,9 @@ export interface InitializeAgentDbMethods extends EndpointDbMethods {
|
|||
files?: Array<{ file_id: string }>;
|
||||
}> | null>;
|
||||
/** List skill summaries for catalog injection (paginated, omits body/frontmatter) */
|
||||
listSkillsByAccess?: (params: {
|
||||
accessibleIds: import('mongoose').Types.ObjectId[];
|
||||
limit: number;
|
||||
cursor?: string | null;
|
||||
}) => Promise<{
|
||||
skills: Array<{
|
||||
_id: import('mongoose').Types.ObjectId;
|
||||
name: string;
|
||||
description: string;
|
||||
author: import('mongoose').Types.ObjectId;
|
||||
/**
|
||||
* When `true`, the skill is excluded from the catalog injected into
|
||||
* the agent's additional_instructions and the model cannot invoke it
|
||||
* via the `skill` tool. Manual `$` invocation is unaffected.
|
||||
*/
|
||||
disableModelInvocation?: boolean;
|
||||
/**
|
||||
* When `false`, the skill is hidden from the `$` popover and rejected
|
||||
* by the manual-invocation resolver. Defaults to `true`.
|
||||
*/
|
||||
userInvocable?: boolean;
|
||||
/** True for deployment-directory skills that are loaded in memory. */
|
||||
deployment?: boolean;
|
||||
}>;
|
||||
has_more?: boolean;
|
||||
after?: string | null;
|
||||
}>;
|
||||
/**
|
||||
* Load a single skill by name, constrained to an ACL-accessible ID set.
|
||||
* Returns the full document (including `body`) so manual invocation can
|
||||
* prime SKILL.md without a second DB round-trip.
|
||||
*
|
||||
* `preferUserInvocable` (manual paths): on a same-name collision,
|
||||
* prefer the newest doc with `userInvocable !== false`.
|
||||
* `preferModelInvocable` (model paths — `skill` / `read_file`): on a
|
||||
* same-name collision, prefer the newest doc with
|
||||
* `disableModelInvocation !== true`. Both fall back to the newest match
|
||||
* so the explicit-rejection error paths still fire when only the
|
||||
* non-preferred variant exists.
|
||||
*/
|
||||
getSkillByName?: (
|
||||
name: string,
|
||||
accessibleIds: import('mongoose').Types.ObjectId[],
|
||||
options?: { preferUserInvocable?: boolean; preferModelInvocable?: boolean },
|
||||
) => Promise<{
|
||||
_id: import('mongoose').Types.ObjectId;
|
||||
name: string;
|
||||
body: string;
|
||||
author: import('mongoose').Types.ObjectId;
|
||||
/**
|
||||
* Skill-declared tool allowlist, forwarded verbatim from the skill doc.
|
||||
* Surfaced so the resolver can carry it onto `ResolvedManualSkill` for
|
||||
* future runtime enforcement without a second round-trip.
|
||||
*/
|
||||
allowedTools?: string[];
|
||||
/**
|
||||
* Set when the skill was authored with `disable-model-invocation: true`.
|
||||
* The skill tool handler short-circuits on this so a model that names
|
||||
* such a skill (e.g. via hallucination or stale catalog) gets a clear
|
||||
* rejection instead of silently executing.
|
||||
*/
|
||||
disableModelInvocation?: boolean;
|
||||
/**
|
||||
* Set when the skill was authored with `user-invocable: false`. The
|
||||
* manual-invocation resolver skips with a warn log so an API-direct
|
||||
* caller can't bypass the popover-side filter.
|
||||
*/
|
||||
userInvocable?: boolean;
|
||||
/** True for deployment-directory skills that are loaded in memory. */
|
||||
deployment?: boolean;
|
||||
} | null>;
|
||||
listSkillsByAccess?: TListSkillsByAccess;
|
||||
/** Load a single skill by name, constrained to an ACL-accessible ID set. */
|
||||
getSkillByName?: TGetSkillByName;
|
||||
/**
|
||||
* Load accessible skills with `alwaysApply: true`, eagerly including
|
||||
* `body` so the priming pipeline can splice at turn start without a
|
||||
|
|
|
|||
|
|
@ -6,10 +6,83 @@ import type { LCToolRegistry, LCTool, InjectedMessage } from '@librechat/agents'
|
|||
import type { BaseMessage } from '@librechat/agents/langchain/messages';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
import type { Types } from 'mongoose';
|
||||
import type { InitializeAgentDbMethods } from './initialize';
|
||||
import { registerCodeExecutionTools } from './tools';
|
||||
import { logAxiosError } from '~/utils';
|
||||
|
||||
/**
|
||||
* Load a single skill by name, constrained to an ACL-accessible ID set.
|
||||
* Returns the full document (including `body`) so manual invocation can
|
||||
* prime SKILL.md without a second DB round-trip.
|
||||
*
|
||||
* `preferUserInvocable` (manual paths): on a same-name collision,
|
||||
* prefer the newest doc with `userInvocable !== false`.
|
||||
* `preferModelInvocable` (model paths — `skill` / `read_file`): on a
|
||||
* same-name collision, prefer the newest doc with
|
||||
* `disableModelInvocation !== true`. Both fall back to the newest match
|
||||
* so the explicit-rejection error paths still fire when only the
|
||||
* non-preferred variant exists.
|
||||
*/
|
||||
export type TGetSkillByName = (
|
||||
name: string,
|
||||
accessibleIds: Types.ObjectId[],
|
||||
options?: { preferUserInvocable?: boolean; preferModelInvocable?: boolean },
|
||||
) => Promise<{
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
body: string;
|
||||
author: Types.ObjectId;
|
||||
/**
|
||||
* Skill-declared tool allowlist, forwarded verbatim from the skill doc.
|
||||
* Surfaced so the resolver can carry it onto `ResolvedManualSkill` for
|
||||
* future runtime enforcement without a second round-trip.
|
||||
*/
|
||||
allowedTools?: string[];
|
||||
/**
|
||||
* Set when the skill was authored with `disable-model-invocation: true`.
|
||||
* The skill tool handler short-circuits on this so a model that names
|
||||
* such a skill (e.g. via hallucination or stale catalog) gets a clear
|
||||
* rejection instead of silently executing.
|
||||
*/
|
||||
disableModelInvocation?: boolean;
|
||||
/**
|
||||
* Set when the skill was authored with `user-invocable: false`. The
|
||||
* manual-invocation resolver skips with a warn log so an API-direct
|
||||
* caller can't bypass the popover-side filter.
|
||||
*/
|
||||
userInvocable?: boolean;
|
||||
/** True for deployment-directory skills that are loaded in memory. */
|
||||
deployment?: boolean;
|
||||
} | null>;
|
||||
|
||||
/** List skill summaries for catalog injection (paginated, omits body/frontmatter). */
|
||||
export type TListSkillsByAccess = (params: {
|
||||
accessibleIds: Types.ObjectId[];
|
||||
limit: number;
|
||||
cursor?: string | null;
|
||||
}) => Promise<{
|
||||
skills: Array<{
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
description: string;
|
||||
author: Types.ObjectId;
|
||||
/**
|
||||
* When `true`, the skill is excluded from the catalog injected into
|
||||
* the agent's additional_instructions and the model cannot invoke it
|
||||
* via the `skill` tool. Manual `$` invocation is unaffected.
|
||||
*/
|
||||
disableModelInvocation?: boolean;
|
||||
/**
|
||||
* When `false`, the skill is hidden from the `$` popover and rejected
|
||||
* by the manual-invocation resolver. Defaults to `true`.
|
||||
*/
|
||||
userInvocable?: boolean;
|
||||
/** True for deployment-directory skills that are loaded in memory. */
|
||||
deployment?: boolean;
|
||||
}>;
|
||||
has_more?: boolean;
|
||||
after?: string | null;
|
||||
}>;
|
||||
|
||||
const SKILL_CATALOG_LIMIT = 100;
|
||||
const MIN_SKILL_CATALOG_LIMIT = 1;
|
||||
/** Max pages scanned per run when filtering out inactive skills. */
|
||||
|
|
@ -153,7 +226,7 @@ export interface ResolveModelSpecSkillIdsParams {
|
|||
/** Full VIEW-accessible skill IDs for this user before model-spec scoping. */
|
||||
accessibleSkillIds: Types.ObjectId[];
|
||||
/** DB lookup: name → skill doc constrained to the user's accessible IDs. */
|
||||
getSkillByName?: InitializeAgentDbMethods['getSkillByName'];
|
||||
getSkillByName?: TGetSkillByName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -331,7 +404,7 @@ export interface InjectSkillCatalogParams {
|
|||
toolRegistry: LCToolRegistry | undefined;
|
||||
accessibleSkillIds: Types.ObjectId[];
|
||||
contextWindowTokens: number;
|
||||
listSkillsByAccess: InitializeAgentDbMethods['listSkillsByAccess'];
|
||||
listSkillsByAccess: TListSkillsByAccess | undefined;
|
||||
/** When true, registers bash_tool alongside skill + read_file. */
|
||||
codeEnvAvailable?: boolean;
|
||||
/** When true, bash_tool registers with the hedged stateful-session description. */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { IMongoFile } from '@librechat/data-schemas';
|
||||
import type { SteerMediaClient, SteerFileFetcher } from '../media';
|
||||
import type { SteerFileFetcher } from '../request';
|
||||
import type { SteerMediaClient } from '../media';
|
||||
import { buildSteerMedia, stampSteerPartMedia } from '../media';
|
||||
|
||||
jest.spyOn(console, 'log').mockImplementation();
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ export {
|
|||
STEER_MAX_FILES,
|
||||
} from './request';
|
||||
export type {
|
||||
SteerRequestUser,
|
||||
SteerRequestBody,
|
||||
SteerRequestDeps,
|
||||
SteerRunContext,
|
||||
SteerCancelBody,
|
||||
SteerFileFetcher,
|
||||
SteerRequestResult,
|
||||
} from './request';
|
||||
export { buildSteerMedia, stampSteerPartMedia } from './media';
|
||||
export type { SteerMediaClient, SteerFileFetcher, StampedSteerMedia } from './media';
|
||||
export type { SteerMediaClient, StampedSteerMedia } from './media';
|
||||
export { createSteerIndexOffsetHandlers } from './offset';
|
||||
export type { SteerOffsetState } from './offset';
|
||||
export { toSteerFileRef } from './refs';
|
||||
export type { SteerRequestUser } from './refs';
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { ContentTypes } from 'librechat-data-provider';
|
|||
import type { IMongoFile } from '@librechat/data-schemas';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import type { SteerQueueItem } from '~/stream/interfaces/IJobStore';
|
||||
import type { SteerFileFetcher } from './request';
|
||||
import type { SteerMediaResult } from './runtime';
|
||||
import type { SteerRequestUser } from './request';
|
||||
import type { SteerRequestUser } from './refs';
|
||||
import { toSteerFileRef, collectFileIds, buildOwnerFilter } from './refs';
|
||||
import { prependFileContext } from '../client';
|
||||
|
||||
|
|
@ -18,13 +19,6 @@ export interface SteerMediaClient {
|
|||
): Promise<IMongoFile[] | undefined>;
|
||||
}
|
||||
|
||||
/** `db.getFiles`-shaped dependency (injected — this package has no DB access). */
|
||||
export type SteerFileFetcher = (
|
||||
filter: Record<string, unknown>,
|
||||
sortOptions: Record<string, unknown>,
|
||||
selectFields: Record<string, unknown>,
|
||||
) => Promise<IMongoFile[] | null | undefined>;
|
||||
|
||||
interface PseudoMessage {
|
||||
messageId: string;
|
||||
fileContext?: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import type { TFile } from 'librechat-data-provider';
|
||||
import type { SteerRequestUser } from './request';
|
||||
|
||||
export interface SteerRequestUser {
|
||||
id?: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the display-metadata fields a steer attachment ref may carry,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { createHash, randomUUID } from 'crypto';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { SteerEvents } from 'librechat-data-provider';
|
||||
import type { IMongoFile } from '@librechat/data-schemas';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import type {
|
||||
GenerationProtocolVersion,
|
||||
SteerQueueItem,
|
||||
SteerReceipt,
|
||||
} from '~/stream/interfaces/IJobStore';
|
||||
import type { SteerFileFetcher } from './media';
|
||||
import type { SteerRequestUser } from './refs';
|
||||
import {
|
||||
STEER_ENQUEUE_NOT_RUNNING,
|
||||
STEER_ENQUEUE_QUEUE_FULL,
|
||||
|
|
@ -34,10 +35,12 @@ export function getSteerMaxLength(): number {
|
|||
return parseInt(process.env.STEER_MAX_LENGTH ?? '', 10) || DEFAULT_STEER_MAX_LENGTH;
|
||||
}
|
||||
|
||||
export interface SteerRequestUser {
|
||||
id?: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
/** `db.getFiles`-shaped dependency (injected — this package has no DB access). */
|
||||
export type SteerFileFetcher = (
|
||||
filter: Record<string, unknown>,
|
||||
sortOptions: Record<string, unknown>,
|
||||
selectFields: Record<string, unknown>,
|
||||
) => Promise<IMongoFile[] | null | undefined>;
|
||||
|
||||
export interface SteerRequestBody {
|
||||
conversationId?: unknown;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { TFile } from 'librechat-data-provider';
|
||||
import type { ServerRequest } from '~/types';
|
||||
import type { ServerRequest } from '~/types/http';
|
||||
|
||||
export interface SaveBufferParams {
|
||||
userId: string;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,10 @@
|
|||
import { WebSearchToolDefinition, CalculatorToolDefinition } from '@librechat/agents';
|
||||
import type { ExtendedJsonSchema } from './schema';
|
||||
import { AskUserQuestionToolDefinition } from '~/agents/hitl/askUserQuestionTool';
|
||||
import { geminiToolkit } from '~/tools/toolkits/gemini';
|
||||
import { oaiToolkit } from '~/tools/toolkits/oai';
|
||||
|
||||
/** Extended JSON Schema type that includes standard validation keywords */
|
||||
export type ExtendedJsonSchema = {
|
||||
type?: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object' | 'null';
|
||||
enum?: (string | number | boolean | null)[];
|
||||
items?: ExtendedJsonSchema;
|
||||
properties?: Record<string, ExtendedJsonSchema>;
|
||||
required?: string[];
|
||||
description?: string;
|
||||
additionalProperties?: boolean | ExtendedJsonSchema;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
pattern?: string;
|
||||
format?: string;
|
||||
default?: unknown;
|
||||
const?: unknown;
|
||||
oneOf?: ExtendedJsonSchema[];
|
||||
anyOf?: ExtendedJsonSchema[];
|
||||
allOf?: ExtendedJsonSchema[];
|
||||
$ref?: string;
|
||||
$defs?: Record<string, ExtendedJsonSchema>;
|
||||
definitions?: Record<string, ExtendedJsonSchema>;
|
||||
};
|
||||
export type { ExtendedJsonSchema } from './schema';
|
||||
|
||||
export interface ToolRegistryDefinition {
|
||||
name: string;
|
||||
|
|
|
|||
26
packages/api/src/tools/registry/schema.ts
Normal file
26
packages/api/src/tools/registry/schema.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/** Extended JSON Schema type that includes standard validation keywords */
|
||||
export type ExtendedJsonSchema = {
|
||||
type?: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object' | 'null';
|
||||
enum?: (string | number | boolean | null)[];
|
||||
items?: ExtendedJsonSchema;
|
||||
properties?: Record<string, ExtendedJsonSchema>;
|
||||
required?: string[];
|
||||
description?: string;
|
||||
additionalProperties?: boolean | ExtendedJsonSchema;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
pattern?: string;
|
||||
format?: string;
|
||||
default?: unknown;
|
||||
const?: unknown;
|
||||
oneOf?: ExtendedJsonSchema[];
|
||||
anyOf?: ExtendedJsonSchema[];
|
||||
allOf?: ExtendedJsonSchema[];
|
||||
$ref?: string;
|
||||
$defs?: Record<string, ExtendedJsonSchema>;
|
||||
definitions?: Record<string, ExtendedJsonSchema>;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ExtendedJsonSchema } from '../registry/definitions';
|
||||
import type { ExtendedJsonSchema } from '../registry/schema';
|
||||
|
||||
/** Default description for Gemini image generation tool */
|
||||
const DEFAULT_GEMINI_IMAGE_GEN_DESCRIPTION =
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ExtendedJsonSchema } from '../registry/definitions';
|
||||
import type { ExtendedJsonSchema } from '../registry/schema';
|
||||
|
||||
/** Default descriptions for image generation tool */
|
||||
const DEFAULT_IMAGE_GEN_DESCRIPTION =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ClientOptions, OpenAIClientOptions } from '@librechat/agents';
|
||||
import type { TConfig } from 'librechat-data-provider';
|
||||
import type { EndpointTokenConfig, ServerRequest } from '~/types';
|
||||
import type { EndpointTokenConfig } from './tokens';
|
||||
import type { ServerRequest } from './http';
|
||||
|
||||
export type TCustomEndpointsConfig = Partial<{ [key: string]: Omit<TConfig, 'order'> }>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Agents } from 'librechat-data-provider';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type { ServerSentEvent } from '~/types';
|
||||
import type { ServerSentEvent } from './events';
|
||||
|
||||
export interface GenerationJobMetadata {
|
||||
userId: string;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export default defineConfig({
|
|||
dts: { oxc: true },
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
// Warn on module cycles at build time; CI enforces via config/circular-deps.mjs.
|
||||
checks: { circularDependency: true },
|
||||
// Externalize every third-party dependency (consumers provide the peers) and bundle
|
||||
// only first-party code: relative imports and the `~/*` tsconfig alias (-> src).
|
||||
// `neverBundle` is the 0.22 replacement for the deprecated `external` option.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export default defineConfig({
|
|||
dts: { oxc: true },
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
// Warn on module cycles at build time; CI enforces via config/circular-deps.mjs.
|
||||
checks: { circularDependency: true },
|
||||
// Force .mjs/.cjs (and .d.mts/.d.cts) regardless of package `type`, so the package can stay
|
||||
// CommonJS (jest.config.js / babel.config.js are CJS) while still shipping dual ESM/CJS.
|
||||
fixedExtension: true,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export default defineConfig({
|
|||
dts: false,
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
// Warn on module cycles at build time; CI enforces via config/circular-deps.mjs.
|
||||
checks: { circularDependency: true },
|
||||
deps: {
|
||||
// Match the prior Rollup build: bundle nothing third-party. Externalize every
|
||||
// bare import (deps, peers, and node built-ins like `crypto`); bundle only the
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { ResourceType } from 'librechat-data-provider';
|
||||
import type {
|
||||
BaseSystemCapability,
|
||||
SystemCapability,
|
||||
ConfigSection,
|
||||
CapabilityCategory,
|
||||
} from '~/types/admin';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System Capabilities
|
||||
|
|
@ -50,6 +45,39 @@ export const SystemCapabilities = {
|
|||
READ_AUDIT_LOG: 'read:audit_log',
|
||||
} as const;
|
||||
|
||||
/** Base capabilities derived from the SystemCapabilities constant. */
|
||||
export type BaseSystemCapability = (typeof SystemCapabilities)[keyof typeof SystemCapabilities];
|
||||
|
||||
/** Principal types that can receive config overrides. */
|
||||
export type ConfigAssignTarget = 'user' | 'group' | 'role';
|
||||
|
||||
/** Top-level keys of the configSchema from librechat.yaml. */
|
||||
export type ConfigSection = string & keyof TCustomConfig;
|
||||
|
||||
/** Section-level config capabilities derived from configSchema keys. */
|
||||
type ConfigSectionCapability = `manage:configs:${ConfigSection}` | `read:configs:${ConfigSection}`;
|
||||
|
||||
/** Principal-scoped config assignment capabilities. */
|
||||
type ConfigAssignCapability = `assign:configs:${ConfigAssignTarget}`;
|
||||
|
||||
/**
|
||||
* Union of all valid capability strings:
|
||||
* - Base capabilities from SystemCapabilities
|
||||
* - Section-level config capabilities (manage:configs:<section>, read:configs:<section>)
|
||||
* - Config assignment capabilities (assign:configs:<user|group|role>)
|
||||
*/
|
||||
export type SystemCapability =
|
||||
| BaseSystemCapability
|
||||
| ConfigSectionCapability
|
||||
| ConfigAssignCapability;
|
||||
|
||||
/** UI grouping of capabilities for the admin panel's capability editor. */
|
||||
export type CapabilityCategory = {
|
||||
key: string;
|
||||
labelKey: string;
|
||||
capabilities: BaseSystemCapability[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Capabilities that are implied by holding a broader capability.
|
||||
* e.g. `MANAGE_USERS` implies `READ_USERS`.
|
||||
|
|
|
|||
|
|
@ -1,40 +1,14 @@
|
|||
import type { PrincipalType, PrincipalModel, TCustomConfig } from 'librechat-data-provider';
|
||||
import type { SystemCapabilities } from '~/admin/capabilities';
|
||||
|
||||
/* ── Capability types ───────────────────────────────────────────────── */
|
||||
/* ── Capability types (defined alongside the SystemCapabilities constant) ── */
|
||||
|
||||
/** Base capabilities derived from the SystemCapabilities constant. */
|
||||
export type BaseSystemCapability = (typeof SystemCapabilities)[keyof typeof SystemCapabilities];
|
||||
|
||||
/** Principal types that can receive config overrides. */
|
||||
export type ConfigAssignTarget = 'user' | 'group' | 'role';
|
||||
|
||||
/** Top-level keys of the configSchema from librechat.yaml. */
|
||||
export type ConfigSection = string & keyof TCustomConfig;
|
||||
|
||||
/** Section-level config capabilities derived from configSchema keys. */
|
||||
type ConfigSectionCapability = `manage:configs:${ConfigSection}` | `read:configs:${ConfigSection}`;
|
||||
|
||||
/** Principal-scoped config assignment capabilities. */
|
||||
type ConfigAssignCapability = `assign:configs:${ConfigAssignTarget}`;
|
||||
|
||||
/**
|
||||
* Union of all valid capability strings:
|
||||
* - Base capabilities from SystemCapabilities
|
||||
* - Section-level config capabilities (manage:configs:<section>, read:configs:<section>)
|
||||
* - Config assignment capabilities (assign:configs:<user|group|role>)
|
||||
*/
|
||||
export type SystemCapability =
|
||||
| BaseSystemCapability
|
||||
| ConfigSectionCapability
|
||||
| ConfigAssignCapability;
|
||||
|
||||
/** UI grouping of capabilities for the admin panel's capability editor. */
|
||||
export type CapabilityCategory = {
|
||||
key: string;
|
||||
labelKey: string;
|
||||
capabilities: BaseSystemCapability[];
|
||||
};
|
||||
export type {
|
||||
BaseSystemCapability,
|
||||
ConfigAssignTarget,
|
||||
ConfigSection,
|
||||
SystemCapability,
|
||||
CapabilityCategory,
|
||||
} from '~/admin/capabilities';
|
||||
|
||||
/* ── Admin API response types ───────────────────────────────────────── */
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export default defineConfig({
|
|||
dts: { oxc: true },
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
// Warn on module cycles at build time; CI enforces via config/circular-deps.mjs.
|
||||
checks: { circularDependency: true },
|
||||
// Externalize all third-party deps (consumers provide the peers); bundle only `dotenv`
|
||||
// so the package stays self-contained for its env-loading side effect, matching the
|
||||
// prior Rollup build. `neverBundle` is the 0.22 replacement for the deprecated `external`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue