diff --git a/packages/api/src/agents/__tests__/skills.test.ts b/packages/api/src/agents/__tests__/skills.test.ts index 87c849fc8f..6d48466921 100644 --- a/packages/api/src/agents/__tests__/skills.test.ts +++ b/packages/api/src/agents/__tests__/skills.test.ts @@ -897,6 +897,38 @@ describe('injectSkillCatalog', () => { expect(agent.additional_instructions).toContain('desc-my-skill'); }); + it('warns when a skill description exceeds the catalog entry cap', async () => { + const { logger } = await import('@librechat/data-schemas'); + const warnSpy = jest.spyOn(logger, 'warn'); + const longDesc = 'x'.repeat(400); + const longSkill: PageSkill = { + ...makeSkill('long-skill', userObjectId), + description: longDesc, + }; + const shortSkill = makeSkill('short-skill', userObjectId); + const listSkillsByAccess = buildPager([[longSkill, shortSkill]]); + const agent = makeAgent(); + await injectSkillCatalog(baseParams({ listSkillsByAccess, agent })); + + const truncWarns = warnSpy.mock.calls + .map((call) => String(call[0])) + .filter((msg) => msg.includes('truncated to')); + expect(truncWarns).toHaveLength(1); + expect(truncWarns[0]).toContain('"long-skill"'); + expect(truncWarns[0]).toContain('was 400'); + /* Short description is not flagged. */ + expect( + warnSpy.mock.calls + .map((call) => String(call[0])) + .filter((msg) => msg.includes('"short-skill"') && msg.includes('truncated')), + ).toHaveLength(0); + + /* The catalog still reaches the model — the warning is additive. */ + expect(agent.additional_instructions).toContain('long-skill'); + expect(agent.additional_instructions).toContain('short-skill'); + warnSpy.mockRestore(); + }); + it('honors a configured maxCatalogSkills below the default hard limit', async () => { const first = makeSkill('first-skill', userObjectId); const second = makeSkill('second-skill', userObjectId); diff --git a/packages/api/src/agents/skills.ts b/packages/api/src/agents/skills.ts index f12fcf75e0..44495f120d 100644 --- a/packages/api/src/agents/skills.ts +++ b/packages/api/src/agents/skills.ts @@ -89,6 +89,13 @@ const MIN_SKILL_CATALOG_LIMIT = 1; const MAX_CATALOG_PAGES = 10; /** Page size used when paginating to fill the active-skill quota. */ const CATALOG_PAGE_SIZE = 100; +/** + * Per-entry description cap applied by `formatSkillCatalog` before the + * catalog is injected into agent context. `@librechat/agents` truncates + * silently, so this mirrors the default so we can warn when authors' skill + * descriptions will not reach the model verbatim. + */ +const SKILL_CATALOG_MAX_ENTRY_CHARS = 250; /** Hard ceiling on skill names a model spec can request by config. */ const MAX_MODEL_SPEC_SKILLS = SKILL_CATALOG_LIMIT; /** @@ -620,9 +627,19 @@ export async function injectSkillCatalog( * and those reads would otherwise be impossible. */ if (catalogVisibleSkills.length > 0) { + for (const s of catalogVisibleSkills) { + if (s.description.length > SKILL_CATALOG_MAX_ENTRY_CHARS) { + logger.warn( + `[injectSkillCatalog] skill "${s.name}" description truncated to ${SKILL_CATALOG_MAX_ENTRY_CHARS} chars for the model catalog (was ${s.description.length})`, + ); + } + } const catalog = formatSkillCatalog( catalogVisibleSkills.map((s) => ({ name: s.name, description: s.description })), - { contextWindowTokens: contextWindowTokens || 200_000 }, + { + contextWindowTokens: contextWindowTokens || 200_000, + maxEntryChars: SKILL_CATALOG_MAX_ENTRY_CHARS, + }, ); if (catalog) { agent.additional_instructions = agent.additional_instructions