Skip to content

Commit 78a3485

Browse files
committed
Expose adapter reasoning, public metadata getter for all active providers
1 parent 28ff63c commit 78a3485

10 files changed

Lines changed: 290 additions & 13 deletions

File tree

packages/apostrophe/modules/@apostrophecms/ai-adapter-anthropic/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,28 @@ module.exports = {
8585
// model's ceiling: this adapter posts and waits for a whole
8686
// answer, so the default stays where one response comfortably
8787
// completes inside the timeout. The published ceilings are
88-
// 128k for the Claude 5 models and 64k for Haiku 4.5
88+
// 128k for the Claude 5 models and 64k for Haiku 4.5.
89+
// `reasoning` declares what a call may pass, in this dialect's
90+
// own vocabulary: effort levels on the adaptive models, the
91+
// configured budget names on the budgeted ones.
8992
models: {
9093
'claude-haiku-4-5': {
94+
label: 'Haiku 4.5',
9195
contextWindow: 200000,
92-
maxOutputTokens: 32000
96+
maxOutputTokens: 32000,
97+
reasoning: reasoningValues('claude-haiku-4-5')
9398
},
9499
'claude-sonnet-5': {
100+
label: 'Sonnet 5',
95101
contextWindow: 1000000,
96-
maxOutputTokens: 64000
102+
maxOutputTokens: 64000,
103+
reasoning: reasoningValues('claude-sonnet-5')
97104
},
98105
'claude-opus-5': {
106+
label: 'Opus 5',
99107
contextWindow: 1000000,
100-
maxOutputTokens: 64000
108+
maxOutputTokens: 64000,
109+
reasoning: reasoningValues('claude-opus-5')
101110
}
102111
},
103112
validate() {
@@ -119,6 +128,11 @@ module.exports = {
119128
return self.normalizeError(error);
120129
}
121130
};
131+
function reasoningValues(model) {
132+
return self.options.adaptiveModels.includes(model)
133+
? [ ...EFFORT_LEVELS ]
134+
: Object.keys(self.options.thinkingBudgets);
135+
}
122136
},
123137
// Translate a normalized adapter request (see the engine's
124138
// buildRequest) to an Anthropic Messages API body: content parts

packages/apostrophe/modules/@apostrophecms/ai-adapter-google/index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const FINISH_REASONS = {
4141
const ASPECTS = [
4242
'1:1', '3:2', '2:3', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'
4343
];
44+
// The `thinkingLevel` values every current text model accepts
45+
const THINKING_LEVELS = Object.freeze([
46+
'minimal', 'low', 'medium', 'high'
47+
]);
4448
// The normalized quality tiers → the dialect's output resolution
4549
// (the uppercase K is required)
4650
const IMAGE_SIZES = {
@@ -93,22 +97,31 @@ module.exports = {
9397
reasoning: 'high'
9498
}
9599
},
100+
// `reasoning` is the dialect's `thinkingLevel` vocabulary,
101+
// shared by both current text models
96102
models: {
97103
'gemini-3.1-flash-lite': {
104+
label: 'Gemini 3.1 Flash-Lite',
98105
contextWindow: 1048576,
99-
maxOutputTokens: 65536
106+
maxOutputTokens: 65536,
107+
reasoning: THINKING_LEVELS
100108
},
101109
'gemini-3.5-flash': {
110+
label: 'Gemini 3.5 Flash',
102111
contextWindow: 1048576,
103-
maxOutputTokens: 65536
112+
maxOutputTokens: 65536,
113+
reasoning: THINKING_LEVELS
104114
},
105115
'gemini-3.1-flash-image': {
116+
label: 'Gemini 3.1 Flash Image',
106117
aspects: ASPECTS
107118
},
108119
'gemini-3-pro-image': {
120+
label: 'Gemini 3 Pro Image',
109121
aspects: ASPECTS
110122
},
111123
'gemini-3.1-flash-lite-image': {
124+
label: 'Gemini 3.1 Flash-Lite Image',
112125
aspects: ASPECTS
113126
}
114127
},

packages/apostrophe/modules/@apostrophecms/ai-adapter-openai-compatible/index.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323
const image = require('../ai-adapter-openai/lib/image');
2424

25+
// The `reasoning_effort` values the native service accepts on every
26+
// current text model. The native tools-request degrade drops reasoning
27+
// as behavior; the vocabulary itself is unchanged by it. Aliased
28+
// entries describe other services and declare their own.
29+
const REASONING_EFFORTS = Object.freeze([
30+
'none', 'low', 'medium', 'high', 'xhigh', 'max'
31+
]);
32+
2533
module.exports = {
2634
options: {
2735
// Per-request timeout in milliseconds; a timed-out call is a
@@ -66,16 +74,22 @@ module.exports = {
6674
},
6775
models: {
6876
'gpt-5.6-luna': {
77+
label: 'GPT-5.6 Luna',
6978
contextWindow: 1050000,
70-
maxOutputTokens: 128000
79+
maxOutputTokens: 128000,
80+
reasoning: REASONING_EFFORTS
7181
},
7282
'gpt-5.6-terra': {
83+
label: 'GPT-5.6 Terra',
7384
contextWindow: 1050000,
74-
maxOutputTokens: 128000
85+
maxOutputTokens: 128000,
86+
reasoning: REASONING_EFFORTS
7587
},
7688
'gpt-5.6-sol': {
89+
label: 'GPT-5.6 Sol',
7790
contextWindow: 1050000,
78-
maxOutputTokens: 128000
91+
maxOutputTokens: 128000,
92+
reasoning: REASONING_EFFORTS
7993
},
8094
...image.models
8195
},

packages/apostrophe/modules/@apostrophecms/ai-adapter-openai/index.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919

2020
const image = require('./lib/image');
2121

22+
// The `reasoning.effort` values every current text model accepts —
23+
// the whole GPT-5.6 tier shares one vocabulary
24+
const REASONING_EFFORTS = Object.freeze([
25+
'none', 'low', 'medium', 'high', 'xhigh', 'max'
26+
]);
27+
2228
module.exports = {
2329
options: {
2430
// Per-request timeout in milliseconds; a timed-out call is a
@@ -60,16 +66,22 @@ module.exports = {
6066
},
6167
models: {
6268
'gpt-5.6-luna': {
69+
label: 'GPT-5.6 Luna',
6370
contextWindow: 1050000,
64-
maxOutputTokens: 128000
71+
maxOutputTokens: 128000,
72+
reasoning: REASONING_EFFORTS
6573
},
6674
'gpt-5.6-terra': {
75+
label: 'GPT-5.6 Terra',
6776
contextWindow: 1050000,
68-
maxOutputTokens: 128000
77+
maxOutputTokens: 128000,
78+
reasoning: REASONING_EFFORTS
6979
},
7080
'gpt-5.6-sol': {
81+
label: 'GPT-5.6 Sol',
7182
contextWindow: 1050000,
72-
maxOutputTokens: 128000
83+
maxOutputTokens: 128000,
84+
reasoning: REASONING_EFFORTS
7385
},
7486
...image.models
7587
},

packages/apostrophe/modules/@apostrophecms/ai-adapter-openai/lib/image.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ module.exports = async function image(deps, request) {
5050
// fixed to its three.
5151
module.exports.models = {
5252
'gpt-image-2': {
53+
label: 'GPT Image 2',
5354
aspects: Object.keys(SIZES)
5455
},
5556
'gpt-image-1': {
57+
label: 'GPT Image 1',
5658
aspects: [ '1:1', '3:2', '2:3' ]
5759
}
5860
};

packages/apostrophe/modules/@apostrophecms/ai/index.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,53 @@ module.exports = {
311311
return info;
312312
},
313313

314+
/**
315+
* Synchronous introspection of the whole routing configuration, shaped
316+
* for building pickers: the resolved effort table with its default
317+
* level, and every configured provider with its adapter's label,
318+
* capabilities and merged per-model metadata. A model's optional
319+
* `reasoning` array lists the values a call may pass as `reasoning`
320+
* for it, in the provider's own vocabulary — declared by the adapter,
321+
* extendable per model on the provider entry, and never enforced: the
322+
* adapter keeps its own rejections, and a model without a declaration
323+
* still answers. Everything returned is a copy, safe to serialize or
324+
* amend, and nothing here reaches the browser unless the caller sends
325+
* it there. Under mock mode with no providers the catalog is empty —
326+
* `self.active` answers "is AI usable", this method answers "what is
327+
* configured".
328+
*
329+
* @returns {AiModelCatalog}
330+
*/
331+
modelCatalog() {
332+
const providers = {};
333+
for (const [ name, record ] of Object.entries(self.providers)) {
334+
const models = {};
335+
for (const [ id, meta ] of Object.entries(record.models)) {
336+
models[id] = {
337+
...meta,
338+
...(meta.reasoning && { reasoning: [ ...meta.reasoning ] }),
339+
...(meta.aspects && { aspects: [ ...meta.aspects ] })
340+
};
341+
}
342+
providers[name] = {
343+
label: record.adapter.label,
344+
capabilities: { ...record.capabilities },
345+
models
346+
};
347+
}
348+
const levels = {};
349+
for (const [ level, row ] of Object.entries(self.effortTable)) {
350+
levels[level] = { ...row };
351+
}
352+
return {
353+
effort: {
354+
default: self.effortDefault,
355+
levels
356+
},
357+
providers
358+
};
359+
},
360+
314361
/**
315362
* The AI permission seam: whether this AI action is permitted for `req`.
316363
* Same signature and semantics as

packages/apostrophe/modules/@apostrophecms/ai/lib/types.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,14 @@
246246
* provider entry's, merged, the entry winning.
247247
*
248248
* @typedef {object} AiModelMeta
249+
* @property {string} [label] The model's human name, for pickers and
250+
* receipts ("Opus 5"); the id stays the wire truth.
249251
* @property {number} [contextWindow]
250252
* @property {number} [maxOutputTokens]
253+
* @property {string[]} [reasoning] The values a call may pass as its
254+
* `reasoning` for this model, in the provider's own vocabulary.
255+
* Informational: read back by modelCatalog, never enforced by the engine —
256+
* the adapter keeps its own rejections.
251257
* @property {string[]} [aspects] The image ratios the model supports, as 'W:H'.
252258
*/
253259

@@ -265,6 +271,20 @@
265271
* @property {string[]} [aspects] Present for an image resolution.
266272
*/
267273

274+
/**
275+
* What modelCatalog reports: the whole routing configuration, shaped for
276+
* building pickers. Every object is a copy, safe to serialize or amend.
277+
*
278+
* @typedef {object} AiModelCatalog
279+
* @property {{ default: string, levels: Object<string, { provider: string,
280+
* model: string, reasoning?: string }> }} effort The resolved routing
281+
* table and the level an effortless call lands on.
282+
* @property {Object<string, { label: string,
283+
* capabilities: Object<string, boolean>,
284+
* models: Object<string, AiModelMeta> }>} providers Configured providers
285+
* by name, each with its adapter's label and merged model metadata.
286+
*/
287+
268288
/**
269289
* A provider adapter: the translation between the normalized protocol above and
270290
* one service's dialect. Registered with addAdapter and instantiated per
@@ -274,6 +294,7 @@
274294
* @typedef {object} AiAdapter
275295
* @property {string} name The registry name. A provider entry names it with
276296
* `adapter`, or shares its own key with it.
297+
* @property {string} label The service's human name ("Anthropic (Claude)").
277298
* @property {string} [envKey] The environment variable the key is read from
278299
* unless the entry names its own.
279300
* @property {string} [baseUrl]

packages/apostrophe/test/ai-adapter-anthropic.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,49 @@ describe('AI adapter: anthropic', function() {
10411041
});
10421042
});
10431043

1044+
describe('model metadata', function() {
1045+
it('labels every model', function() {
1046+
const { models } = adapter.adapter();
1047+
const labels = Object.fromEntries(
1048+
Object.entries(models).map(([ id, meta ]) => [ id, meta.label ])
1049+
);
1050+
assert.deepEqual(labels, {
1051+
'claude-haiku-4-5': 'Haiku 4.5',
1052+
'claude-sonnet-5': 'Sonnet 5',
1053+
'claude-opus-5': 'Opus 5'
1054+
});
1055+
});
1056+
1057+
it('declares effort levels as the reasoning of an adaptive model', function() {
1058+
const { models } = adapter.adapter();
1059+
const levels = [ 'low', 'medium', 'high', 'xhigh', 'max' ];
1060+
assert.deepEqual(models['claude-sonnet-5'].reasoning, levels);
1061+
assert.deepEqual(models['claude-opus-5'].reasoning, levels);
1062+
});
1063+
1064+
it('declares the configured budget names as the reasoning of a budgeted model', function() {
1065+
assert.deepEqual(
1066+
adapter.adapter().models['claude-haiku-4-5'].reasoning,
1067+
[ 'low', 'medium', 'high' ]
1068+
);
1069+
// The definition is built from the options, so an extended budget
1070+
// table reaches a declaration built afterwards
1071+
const saved = adapter.options.thinkingBudgets;
1072+
try {
1073+
adapter.options.thinkingBudgets = {
1074+
...saved,
1075+
xhigh: 32768
1076+
};
1077+
assert.deepEqual(
1078+
adapter.adapter().models['claude-haiku-4-5'].reasoning,
1079+
[ 'low', 'medium', 'high', 'xhigh' ]
1080+
);
1081+
} finally {
1082+
adapter.options.thinkingBudgets = saved;
1083+
}
1084+
});
1085+
});
1086+
10441087
describe('environment key', function() {
10451088
before(async function() {
10461089
process.env.APOS_ANTHROPIC_KEY = 'sk-env';

0 commit comments

Comments
 (0)