@actuate-media/cms-core 0.43.0 → 0.44.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resolve.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/ai/resolve.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveAllFeatureModels, resolveFeatureModel } from '../../ai/resolve.js';
3
+ const NO_CAPS = {
4
+ text: false,
5
+ vision: false,
6
+ imageGeneration: false,
7
+ embeddings: false,
8
+ toolCalling: false,
9
+ structuredOutput: false,
10
+ streaming: false,
11
+ reasoning: false,
12
+ longContext: false,
13
+ };
14
+ function model(id, caps, over = {}) {
15
+ const [provider = 'anthropic', providerModelId = id] = id.split(':');
16
+ return {
17
+ id,
18
+ provider: provider,
19
+ providerModelId,
20
+ displayName: id,
21
+ status: 'available',
22
+ capabilities: { ...NO_CAPS, ...caps },
23
+ inputModalities: ['text'],
24
+ outputModalities: ['text'],
25
+ lastSyncedAt: '2026-01-01T00:00:00.000Z',
26
+ source: 'provider',
27
+ metadata: {},
28
+ ...over,
29
+ };
30
+ }
31
+ function conn(over = {}) {
32
+ return {
33
+ id: 'p-anthropic',
34
+ provider: 'anthropic',
35
+ displayName: 'Anthropic',
36
+ enabled: true,
37
+ status: 'connected',
38
+ createdAt: '2026-01-01T00:00:00.000Z',
39
+ updatedAt: '2026-01-01T00:00:00.000Z',
40
+ updatedBy: 'u1',
41
+ ...over,
42
+ };
43
+ }
44
+ function config(over) {
45
+ return {
46
+ providers: over.providers ?? [conn()],
47
+ settings: {
48
+ defaultModelId: over.defaultModelId,
49
+ defaultVisionModelId: over.defaultVisionModelId,
50
+ features: over.features ?? [],
51
+ budget: { alertAtPercentages: [], disableWhenExceeded: false },
52
+ logsRetentionDays: 30,
53
+ },
54
+ };
55
+ }
56
+ const textFeature = {
57
+ key: 'ai.features.contentWriting',
58
+ label: 'Content writing',
59
+ description: '',
60
+ enabled: true,
61
+ category: 'content',
62
+ requiredCapabilities: ['text'],
63
+ approvalRequired: false,
64
+ allowBulkApply: false,
65
+ rolesAllowed: ['ADMIN'],
66
+ permissionsRequired: ['ai.use'],
67
+ };
68
+ describe('resolveFeatureModel', () => {
69
+ it('uses the global default when the feature has no override', () => {
70
+ const catalog = [model('anthropic:claude-x', { text: true })];
71
+ const cfg = config({ defaultModelId: 'anthropic:claude-x', features: [textFeature] });
72
+ const r = resolveFeatureModel(cfg, textFeature.key, catalog);
73
+ expect(r.modelId).toBe('anthropic:claude-x');
74
+ expect(r.source).toBe('global-default');
75
+ expect(r.providerId).toBe('p-anthropic');
76
+ expect(r.satisfiesCapabilities).toBe(true);
77
+ });
78
+ it('prefers the feature-level model override over the default', () => {
79
+ const catalog = [
80
+ model('anthropic:claude-x', { text: true }),
81
+ model('openai:gpt-y', { text: true }, { provider: 'openai' }),
82
+ ];
83
+ const cfg = config({
84
+ providers: [conn(), conn({ id: 'p-openai', provider: 'openai' })],
85
+ defaultModelId: 'anthropic:claude-x',
86
+ features: [{ ...textFeature, modelId: 'openai:gpt-y' }],
87
+ });
88
+ const r = resolveFeatureModel(cfg, textFeature.key, catalog);
89
+ expect(r.modelId).toBe('openai:gpt-y');
90
+ expect(r.source).toBe('feature-override');
91
+ expect(r.providerId).toBe('p-openai');
92
+ });
93
+ it('falls back to the capability-typed default for a vision feature', () => {
94
+ const visionFeature = {
95
+ ...textFeature,
96
+ key: 'ai.features.mediaAltText',
97
+ requiredCapabilities: ['vision'],
98
+ };
99
+ const catalog = [
100
+ model('anthropic:claude-text', { text: true }),
101
+ model('openai:gpt-vision', { text: true, vision: true }, { provider: 'openai' }),
102
+ ];
103
+ const cfg = config({
104
+ providers: [conn(), conn({ id: 'p-openai', provider: 'openai' })],
105
+ defaultModelId: 'anthropic:claude-text', // text-only, can't satisfy vision
106
+ defaultVisionModelId: 'openai:gpt-vision',
107
+ features: [visionFeature],
108
+ });
109
+ const r = resolveFeatureModel(cfg, visionFeature.key, catalog);
110
+ expect(r.modelId).toBe('openai:gpt-vision');
111
+ expect(r.source).toBe('capability-default');
112
+ expect(r.satisfiesCapabilities).toBe(true);
113
+ });
114
+ it('reports missing capabilities when no candidate satisfies them', () => {
115
+ const visionFeature = {
116
+ ...textFeature,
117
+ key: 'ai.features.mediaAltText',
118
+ requiredCapabilities: ['vision'],
119
+ };
120
+ const catalog = [model('anthropic:claude-text', { text: true })];
121
+ const cfg = config({ defaultModelId: 'anthropic:claude-text', features: [visionFeature] });
122
+ const r = resolveFeatureModel(cfg, visionFeature.key, catalog);
123
+ expect(r.modelId).toBe('anthropic:claude-text');
124
+ expect(r.satisfiesCapabilities).toBe(false);
125
+ expect(r.missingCapabilities).toContain('vision');
126
+ });
127
+ it('keeps an uncatalogued override and flags it as not satisfying capabilities', () => {
128
+ const cfg = config({
129
+ features: [{ ...textFeature, modelId: 'anthropic:ghost-model' }],
130
+ });
131
+ const r = resolveFeatureModel(cfg, textFeature.key, []);
132
+ expect(r.modelId).toBe('anthropic:ghost-model');
133
+ expect(r.source).toBe('feature-override');
134
+ expect(r.satisfiesCapabilities).toBe(false);
135
+ });
136
+ it('returns source "none" with no model configured', () => {
137
+ const cfg = config({ features: [textFeature] });
138
+ const r = resolveFeatureModel(cfg, textFeature.key, []);
139
+ expect(r.source).toBe('none');
140
+ expect(r.modelId).toBeUndefined();
141
+ expect(r.missingCapabilities).toContain('text');
142
+ });
143
+ it('does not select an unavailable model when an available one exists', () => {
144
+ const catalog = [
145
+ model('anthropic:dead', { text: true }, { status: 'unavailable' }),
146
+ model('anthropic:live', { text: true }),
147
+ ];
148
+ const cfg = config({
149
+ defaultModelId: 'anthropic:dead',
150
+ features: [{ ...textFeature, fallbackModelIds: ['anthropic:live'] }],
151
+ });
152
+ const r = resolveFeatureModel(cfg, textFeature.key, catalog);
153
+ expect(r.modelId).toBe('anthropic:live');
154
+ expect(r.source).toBe('fallback');
155
+ });
156
+ it('resolveAllFeatureModels keys by feature', () => {
157
+ const catalog = [model('anthropic:claude-x', { text: true })];
158
+ const cfg = config({ defaultModelId: 'anthropic:claude-x', features: [textFeature] });
159
+ const all = resolveAllFeatureModels(cfg, catalog);
160
+ expect(Object.keys(all)).toEqual([textFeature.key]);
161
+ expect(all[textFeature.key]?.modelId).toBe('anthropic:claude-x');
162
+ });
163
+ });
164
+ //# sourceMappingURL=resolve.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.test.js","sourceRoot":"","sources":["../../../src/__tests__/ai/resolve.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAQlF,MAAM,OAAO,GAAwB;IACnC,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,eAAe,EAAE,KAAK;IACtB,UAAU,EAAE,KAAK;IACjB,WAAW,EAAE,KAAK;IAClB,gBAAgB,EAAE,KAAK;IACvB,SAAS,EAAE,KAAK;IAChB,SAAS,EAAE,KAAK;IAChB,WAAW,EAAE,KAAK;CACnB,CAAA;AAED,SAAS,KAAK,CACZ,EAAU,EACV,IAAkC,EAClC,OAAyB,EAAE;IAE3B,MAAM,CAAC,QAAQ,GAAG,WAAW,EAAE,eAAe,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACpE,OAAO;QACL,EAAE;QACF,QAAQ,EAAE,QAA+B;QACzC,eAAe;QACf,WAAW,EAAE,EAAE;QACf,MAAM,EAAE,WAAW;QACnB,YAAY,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE;QACrC,eAAe,EAAE,CAAC,MAAM,CAAC;QACzB,gBAAgB,EAAE,CAAC,MAAM,CAAC;QAC1B,YAAY,EAAE,0BAA0B;QACxC,MAAM,EAAE,UAAU;QAClB,QAAQ,EAAE,EAAE;QACZ,GAAG,IAAI;KACR,CAAA;AACH,CAAC;AAED,SAAS,IAAI,CAAC,OAAsC,EAAE;IACpD,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,QAAQ,EAAE,WAAW;QACrB,WAAW,EAAE,WAAW;QACxB,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,WAAW;QACnB,SAAS,EAAE,0BAA0B;QACrC,SAAS,EAAE,0BAA0B;QACrC,SAAS,EAAE,IAAI;QACf,GAAG,IAAI;KACR,CAAA;AACH,CAAC;AAED,SAAS,MAAM,CAAC,IAKf;IACC,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,EAAE,CAAC;QACrC,QAAQ,EAAE;YACR,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,oBAAoB,EAAE,IAAI,CAAC,oBAAoB;YAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;YAC7B,MAAM,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE;YAC9D,iBAAiB,EAAE,EAAE;SACtB;KACF,CAAA;AACH,CAAC;AAED,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,4BAA4B;IACjC,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,EAAE;IACf,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,SAAkB;IAC5B,oBAAoB,EAAE,CAAC,MAAM,CAAC;IAC9B,gBAAgB,EAAE,KAAK;IACvB,cAAc,EAAE,KAAK;IACrB,YAAY,EAAE,CAAC,OAAO,CAAC;IACvB,mBAAmB,EAAE,CAAC,QAAQ,CAAC;CAChC,CAAA;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACrF,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5D,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;QAC5C,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACvC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACxC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,OAAO,GAAG;YACd,KAAK,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAC3C,KAAK,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;SAC9D,CAAA;QACD,MAAM,GAAG,GAAG,MAAM,CAAC;YACjB,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjE,cAAc,EAAE,oBAAoB;YACpC,QAAQ,EAAE,CAAC,EAAE,GAAG,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;SACxD,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5D,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QACtC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;QACzC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,aAAa,GAAG;YACpB,GAAG,WAAW;YACd,GAAG,EAAE,0BAA0B;YAC/B,oBAAoB,EAAE,CAAC,QAAQ,CAAC;SACjC,CAAA;QACD,MAAM,OAAO,GAAG;YACd,KAAK,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAC9C,KAAK,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;SACjF,CAAA;QACD,MAAM,GAAG,GAAG,MAAM,CAAC;YACjB,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjE,cAAc,EAAE,uBAAuB,EAAE,kCAAkC;YAC3E,oBAAoB,EAAE,mBAAmB;YACzC,QAAQ,EAAE,CAAC,aAAa,CAAC;SAC1B,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC9D,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QAC3C,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;QAC3C,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,aAAa,GAAG;YACpB,GAAG,WAAW;YACd,GAAG,EAAE,0BAA0B;YAC/B,oBAAoB,EAAE,CAAC,QAAQ,CAAC;SACjC,CAAA;QACD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAChE,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,cAAc,EAAE,uBAAuB,EAAE,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAC1F,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC9D,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,GAAG,GAAG,MAAM,CAAC;YACjB,QAAQ,EAAE,CAAC,EAAE,GAAG,WAAW,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC;SACjE,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACvD,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;QACzC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QAC/C,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACvD,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC7B,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAA;QACjC,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,OAAO,GAAG;YACd,KAAK,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;YAClE,KAAK,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SACxC,CAAA;QACD,MAAM,GAAG,GAAG,MAAM,CAAC;YACjB,cAAc,EAAE,gBAAgB;YAChC,QAAQ,EAAE,CAAC,EAAE,GAAG,WAAW,EAAE,gBAAgB,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;SACrE,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5D,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACxC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACrF,MAAM,GAAG,GAAG,uBAAuB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;QACnD,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -9,5 +9,6 @@ export { AI_FEATURE_DEFAULTS, cloneFeatureDefaults, mergeFeatures } from './feat
9
9
  export { curatedFallbackCatalog, normalizeModel } from './catalog.js';
10
10
  export { getModels, resolveModelOrUnavailable, syncModels, type SyncModelsResult, } from './models.js';
11
11
  export { getUsageSummary } from './usage.js';
12
+ export { resolveFeatureModel, resolveAllFeatureModels, type FeatureModelSource, type ResolvedFeatureModel, } from './resolve.js';
12
13
  export { getAdapter, listModels as adapterListModels, testConnection as adapterTestConnection, type AdapterContext, } from './providers/index.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,UAAU,EACV,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EACL,UAAU,EACV,UAAU,IAAI,iBAAiB,EAC/B,cAAc,IAAI,qBAAqB,EACvC,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,UAAU,EACV,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,UAAU,EACV,UAAU,IAAI,iBAAiB,EAC/B,cAAc,IAAI,qBAAqB,EACvC,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA"}
package/dist/ai/index.js CHANGED
@@ -9,5 +9,6 @@ export { AI_FEATURE_DEFAULTS, cloneFeatureDefaults, mergeFeatures } from './feat
9
9
  export { curatedFallbackCatalog, normalizeModel } from './catalog.js';
10
10
  export { getModels, resolveModelOrUnavailable, syncModels, } from './models.js';
11
11
  export { getUsageSummary } from './usage.js';
12
+ export { resolveFeatureModel, resolveAllFeatureModels, } from './resolve.js';
12
13
  export { getAdapter, listModels as adapterListModels, testConnection as adapterTestConnection, } from './providers/index.js';
13
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,UAAU,GAEX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EACL,UAAU,EACV,UAAU,IAAI,iBAAiB,EAC/B,cAAc,IAAI,qBAAqB,GAExC,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,UAAU,GAEX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAGxB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,UAAU,EACV,UAAU,IAAI,iBAAiB,EAC/B,cAAc,IAAI,qBAAqB,GAExC,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Feature → model routing resolution.
3
+ *
4
+ * This is the authoritative runtime resolver: given the stored AI config and the
5
+ * current model catalog, it decides which provider connection + model a feature
6
+ * should use. Resolution order for each feature:
7
+ *
8
+ * 1. The feature's explicit `modelId` override (if it exists in the catalog).
9
+ * 2. A capability-typed default that matches the feature's dominant required
10
+ * capability (e.g. a vision feature falls back to `defaultVisionModelId`).
11
+ * 3. The global `defaultModelId`.
12
+ * 4. Any `fallbackModelIds` listed on the feature.
13
+ *
14
+ * The first candidate that is present in the catalog, available, AND satisfies
15
+ * every required capability wins. If none fully satisfy, we still return the
16
+ * best candidate we found and report the missing capabilities so callers (UI +
17
+ * enforcement) can warn rather than silently misroute. The admin UI mirrors a
18
+ * simplified version of this for live preview, but THIS function is the source
19
+ * of truth consumed by the AI runtime.
20
+ */
21
+ import type { AIConfigDocument, AIModel } from './types.js';
22
+ /** Where the resolved model came from in the precedence chain above. */
23
+ export type FeatureModelSource = 'feature-override' | 'capability-default' | 'global-default' | 'fallback' | 'none';
24
+ export interface ResolvedFeatureModel {
25
+ featureKey: string;
26
+ /** Catalog model id (`provider:modelId`), or the raw override when uncatalogued. */
27
+ modelId?: string;
28
+ /** Provider connection id that owns the resolved model. */
29
+ providerId?: string;
30
+ model?: AIModel;
31
+ source: FeatureModelSource;
32
+ /** True when a usable model was found that satisfies every required capability. */
33
+ satisfiesCapabilities: boolean;
34
+ /** Required capabilities the resolved model is missing (empty when satisfied). */
35
+ missingCapabilities: string[];
36
+ }
37
+ /** Resolve the provider + model a single feature should use. */
38
+ export declare function resolveFeatureModel(config: AIConfigDocument, featureKey: string, catalog: AIModel[]): ResolvedFeatureModel;
39
+ /** Resolve routing for every configured feature, keyed by feature key. */
40
+ export declare function resolveAllFeatureModels(config: AIConfigDocument, catalog: AIModel[]): Record<string, ResolvedFeatureModel>;
41
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/ai/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,OAAO,EAIR,MAAM,YAAY,CAAA;AAEnB,wEAAwE;AACxE,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,oBAAoB,GACpB,gBAAgB,GAChB,UAAU,GACV,MAAM,CAAA;AAEV,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,kBAAkB,CAAA;IAC1B,mFAAmF;IACnF,qBAAqB,EAAE,OAAO,CAAA;IAC9B,kFAAkF;IAClF,mBAAmB,EAAE,MAAM,EAAE,CAAA;CAC9B;AAiFD,gEAAgE;AAChE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAAE,GACjB,oBAAoB,CA2DtB;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,OAAO,EAAE,GACjB,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAMtC"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Feature → model routing resolution.
3
+ *
4
+ * This is the authoritative runtime resolver: given the stored AI config and the
5
+ * current model catalog, it decides which provider connection + model a feature
6
+ * should use. Resolution order for each feature:
7
+ *
8
+ * 1. The feature's explicit `modelId` override (if it exists in the catalog).
9
+ * 2. A capability-typed default that matches the feature's dominant required
10
+ * capability (e.g. a vision feature falls back to `defaultVisionModelId`).
11
+ * 3. The global `defaultModelId`.
12
+ * 4. Any `fallbackModelIds` listed on the feature.
13
+ *
14
+ * The first candidate that is present in the catalog, available, AND satisfies
15
+ * every required capability wins. If none fully satisfy, we still return the
16
+ * best candidate we found and report the missing capabilities so callers (UI +
17
+ * enforcement) can warn rather than silently misroute. The admin UI mirrors a
18
+ * simplified version of this for live preview, but THIS function is the source
19
+ * of truth consumed by the AI runtime.
20
+ */
21
+ /** Capability flags recognized on {@link AIModel.capabilities}. */
22
+ const CAPABILITY_KEYS = new Set([
23
+ 'text',
24
+ 'vision',
25
+ 'imageGeneration',
26
+ 'embeddings',
27
+ 'toolCalling',
28
+ 'structuredOutput',
29
+ 'streaming',
30
+ 'reasoning',
31
+ 'longContext',
32
+ ]);
33
+ function findModel(catalog, id) {
34
+ if (!id)
35
+ return undefined;
36
+ return catalog.find((m) => m.id === id || m.providerModelId === id);
37
+ }
38
+ /** Required capabilities a model is missing. Unknown capability strings are ignored. */
39
+ function missingCaps(model, required) {
40
+ const wanted = required.filter((c) => CAPABILITY_KEYS.has(c));
41
+ if (!model)
42
+ return wanted;
43
+ const caps = model.capabilities;
44
+ return wanted.filter((c) => !caps[c]);
45
+ }
46
+ /** Map a feature's dominant required capability to its typed default model id. */
47
+ function capabilityDefault(settings, required) {
48
+ if (required.includes('imageGeneration'))
49
+ return settings.defaultImageGenerationModelId;
50
+ if (required.includes('embeddings'))
51
+ return settings.defaultEmbeddingModelId;
52
+ if (required.includes('vision'))
53
+ return settings.defaultVisionModelId;
54
+ if (required.includes('reasoning'))
55
+ return settings.defaultReasoningModelId;
56
+ if (required.includes('text'))
57
+ return settings.defaultTextModelId;
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Resolve a provider connection id for a model's provider key. Prefers an
62
+ * explicit feature override, then the global default provider when it matches,
63
+ * then the first enabled connection, then any connection of that provider.
64
+ */
65
+ function providerConnId(providers, providerKey, preferredId) {
66
+ if (preferredId) {
67
+ const pref = providers.find((p) => p.id === preferredId && p.provider === providerKey);
68
+ if (pref)
69
+ return pref.id;
70
+ }
71
+ const enabled = providers.find((p) => p.provider === providerKey && p.enabled);
72
+ if (enabled)
73
+ return enabled.id;
74
+ return providers.find((p) => p.provider === providerKey)?.id;
75
+ }
76
+ function build(featureKey, model, source, config, required, preferredProviderId) {
77
+ const missing = missingCaps(model, required);
78
+ return {
79
+ featureKey,
80
+ modelId: model.id,
81
+ providerId: providerConnId(config.providers, model.provider, preferredProviderId ?? config.settings.defaultProviderId),
82
+ model,
83
+ source,
84
+ satisfiesCapabilities: missing.length === 0 && model.status !== 'unavailable',
85
+ missingCapabilities: missing,
86
+ };
87
+ }
88
+ /** Resolve the provider + model a single feature should use. */
89
+ export function resolveFeatureModel(config, featureKey, catalog) {
90
+ const { settings } = config;
91
+ const feature = settings.features.find((f) => f.key === featureKey);
92
+ const required = feature?.requiredCapabilities ?? [];
93
+ const candidates = [
94
+ { id: feature?.modelId, source: 'feature-override' },
95
+ { id: capabilityDefault(settings, required), source: 'capability-default' },
96
+ { id: settings.defaultModelId, source: 'global-default' },
97
+ ...(feature?.fallbackModelIds ?? []).map((id) => ({ id, source: 'fallback' })),
98
+ ];
99
+ // Best case: first candidate that is catalogued, available, and capable.
100
+ let firstFound;
101
+ for (const candidate of candidates) {
102
+ const model = findModel(catalog, candidate.id);
103
+ if (!model)
104
+ continue;
105
+ if (!firstFound)
106
+ firstFound = { model, source: candidate.source };
107
+ if (model.status !== 'unavailable' && missingCaps(model, required).length === 0) {
108
+ return build(featureKey, model, candidate.source, config, required, feature?.providerId);
109
+ }
110
+ }
111
+ // A model was found but it's unavailable or capability-short — return it with a warning.
112
+ if (firstFound) {
113
+ return build(featureKey, firstFound.model, firstFound.source, config, required, feature?.providerId);
114
+ }
115
+ // An explicit override that isn't in the catalog yet (provider not synced).
116
+ if (feature?.modelId) {
117
+ return {
118
+ featureKey,
119
+ modelId: feature.modelId,
120
+ providerId: feature.providerId ??
121
+ providerConnId(config.providers, feature.modelId.split(':')[0] ?? 'custom', config.settings.defaultProviderId),
122
+ source: 'feature-override',
123
+ satisfiesCapabilities: false,
124
+ missingCapabilities: required.filter((c) => CAPABILITY_KEYS.has(c)),
125
+ };
126
+ }
127
+ return {
128
+ featureKey,
129
+ source: 'none',
130
+ satisfiesCapabilities: required.filter((c) => CAPABILITY_KEYS.has(c)).length === 0,
131
+ missingCapabilities: missingCaps(undefined, required),
132
+ };
133
+ }
134
+ /** Resolve routing for every configured feature, keyed by feature key. */
135
+ export function resolveAllFeatureModels(config, catalog) {
136
+ const out = {};
137
+ for (const feature of config.settings.features) {
138
+ out[feature.key] = resolveFeatureModel(config, feature.key, catalog);
139
+ }
140
+ return out;
141
+ }
142
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/ai/resolve.ts"],"names":[],"mappings":"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"}
package/dist/ai/usage.js CHANGED
@@ -42,7 +42,7 @@ export async function getUsageSummary(db, period) {
42
42
  let rows = [];
43
43
  try {
44
44
  rows = await d.auditLog.findMany({
45
- where: { createdAt: { gte: start, lt: end } },
45
+ where: { timestamp: { gte: start, lt: end } },
46
46
  select: { details: true },
47
47
  });
48
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAwhC5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAiVD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqiYzD"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAwhC5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAiVD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqlYzD"}
@@ -10161,12 +10161,58 @@ export function registerCMSRoutes(router) {
10161
10161
  else
10162
10162
  return errorResponse('monthlyTokenLimit must be a non-negative number or null', 400);
10163
10163
  }
10164
- // Apply feature toggles by key (ignore unknown keys).
10165
- const toggles = new Map((body.features ?? []).map((f) => [f.key, Boolean(f.enabled)]));
10166
- const nextFeatures = config.settings.features.map((f) => toggles.has(f.key) ? { ...f, enabled: toggles.get(f.key) } : f);
10164
+ // Validate per-feature model/provider routing overrides. Empty string and
10165
+ // null both clear the override (revert to the default model).
10166
+ const featurePatches = body.features ?? [];
10167
+ for (const patch of featurePatches) {
10168
+ if (patch.modelId != null && patch.modelId !== '') {
10169
+ const m = catalog.find((x) => x.id === patch.modelId || x.providerModelId === patch.modelId);
10170
+ if (!m)
10171
+ return errorResponse(`Model "${patch.modelId}" is not in the catalog`, 400);
10172
+ if (m.status === 'unavailable') {
10173
+ return errorResponse(`Model "${patch.modelId}" is currently unavailable`, 400);
10174
+ }
10175
+ }
10176
+ if (patch.providerId != null &&
10177
+ patch.providerId !== '' &&
10178
+ !config.providers.some((p) => p.id === patch.providerId)) {
10179
+ return errorResponse(`Provider "${patch.providerId}" is not connected`, 400);
10180
+ }
10181
+ }
10182
+ // Apply feature patches by key (ignore unknown keys). Each patch may toggle
10183
+ // `enabled` and/or set the per-feature model/provider routing override.
10184
+ const patchByKey = new Map(featurePatches.map((f) => [f.key, f]));
10185
+ const clearOverride = (v) => v == null || v === '' ? undefined : v;
10186
+ const nextFeatures = config.settings.features.map((f) => {
10187
+ const patch = patchByKey.get(f.key);
10188
+ if (!patch)
10189
+ return f;
10190
+ const next = { ...f };
10191
+ if (typeof patch.enabled === 'boolean')
10192
+ next.enabled = patch.enabled;
10193
+ if ('modelId' in patch)
10194
+ next.modelId = clearOverride(patch.modelId);
10195
+ if ('providerId' in patch)
10196
+ next.providerId = clearOverride(patch.providerId);
10197
+ return next;
10198
+ });
10167
10199
  const changedFeatures = config.settings.features
10168
- .filter((f) => toggles.has(f.key) && toggles.get(f.key) !== f.enabled)
10169
- .map((f) => ({ key: f.key, from: f.enabled, to: toggles.get(f.key) }));
10200
+ .map((f) => {
10201
+ const patch = patchByKey.get(f.key);
10202
+ if (!patch)
10203
+ return null;
10204
+ const enabledChanged = typeof patch.enabled === 'boolean' && patch.enabled !== f.enabled;
10205
+ const nextModelId = 'modelId' in patch ? clearOverride(patch.modelId) : f.modelId;
10206
+ const modelChanged = 'modelId' in patch && (nextModelId ?? null) !== (f.modelId ?? null);
10207
+ if (!enabledChanged && !modelChanged)
10208
+ return null;
10209
+ return {
10210
+ key: f.key,
10211
+ ...(enabledChanged ? { enabledFrom: f.enabled, enabledTo: patch.enabled } : {}),
10212
+ ...(modelChanged ? { modelFrom: f.modelId ?? null, modelTo: nextModelId ?? null } : {}),
10213
+ };
10214
+ })
10215
+ .filter((x) => x !== null);
10170
10216
  const nextSettings = {
10171
10217
  ...config.settings,
10172
10218
  defaultProviderId: body.defaultProviderId === null