feat(llm): a request carries how much the model thinks, in each provider's field
LlmRequest.effort (LlmEffort: low, medium, high, xhigh, max, Anthropic's names) goes as Anthropic's output_config.effort, as reasoning.effort to the Responses API and OpenRouter, and as reasoning_effort to OpenAI's chat completions, Gemini and any other host. endpoint.efforts says which levels a host takes (Gemini low to high, Mistral none); send refuses another before anything goes, the fake client too. LlmEffort.outputCeiling gives the room for output each level wants, as thinking counts towards the ceiling (Halkyon Learn's ADR 0021, D63). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
33c9a7df1c
commit
0590a4b43d
5 changed files with 291 additions and 10 deletions
|
|
@ -139,6 +139,25 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test("carries the effort in Anthropic's output_config (D63)", () async {
|
||||
final sent = Sent();
|
||||
await _reply(
|
||||
_documented['text']!,
|
||||
sent: sent,
|
||||
request: LlmRequest(
|
||||
messages: _question.messages,
|
||||
maxOutputTokens: LlmEffort.xhigh.outputCeiling,
|
||||
effort: LlmEffort.xhigh,
|
||||
),
|
||||
);
|
||||
expect(sent.body['output_config'], {'effort': 'xhigh'});
|
||||
expect(sent.body['max_tokens'], 32768);
|
||||
});
|
||||
|
||||
test('takes every level', () {
|
||||
expect(_endpoint.efforts, LlmEffort.values.toSet());
|
||||
});
|
||||
|
||||
test('sends tools, images, tool calls, results first, and its own blocks '
|
||||
'back unchanged', () async {
|
||||
final sent = Sent();
|
||||
|
|
|
|||
|
|
@ -52,4 +52,31 @@ void main() {
|
|||
expect(_at(url).onThisMachine, isFalse, reason: url);
|
||||
}
|
||||
});
|
||||
|
||||
group('effort (D63)', () {
|
||||
test('runs from low to max, by name', () {
|
||||
expect(LlmEffort.values.map((effort) => effort.key), [
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'xhigh',
|
||||
'max',
|
||||
]);
|
||||
expect(LlmEffort.named('xhigh'), LlmEffort.xhigh);
|
||||
expect(LlmEffort.named('XHIGH'), isNull);
|
||||
expect(LlmEffort.named('minimal'), isNull);
|
||||
});
|
||||
|
||||
test('leaves more room for output the higher it is: what the model thinks '
|
||||
'counts towards the ceiling', () {
|
||||
expect(LlmEffort.values.map((effort) => effort.outputCeiling), [
|
||||
8192,
|
||||
12288,
|
||||
16384,
|
||||
32768,
|
||||
65536,
|
||||
]);
|
||||
expect(LlmEffort.defaultOutputCeiling, LlmEffort.high.outputCeiling);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,19 @@ void main() {
|
|||
expect(body['max_output_tokens'], 16);
|
||||
});
|
||||
|
||||
test('carries the effort as reasoning.effort, every level (D63)', () async {
|
||||
final body = await _bodyFor(
|
||||
_openAi,
|
||||
LlmRequest(
|
||||
messages: _question.messages,
|
||||
maxOutputTokens: 4096,
|
||||
effort: LlmEffort.max,
|
||||
),
|
||||
);
|
||||
expect(body['reasoning'], {'effort': 'max'});
|
||||
expect(_openAi.efforts, LlmEffort.values.toSet());
|
||||
});
|
||||
|
||||
final conversation = LlmRequest(
|
||||
messages: [
|
||||
LlmMessage.user([
|
||||
|
|
|
|||
|
|
@ -157,6 +157,71 @@ void main() {
|
|||
expect(body['user'], 'p_8Zq3');
|
||||
});
|
||||
|
||||
group('the effort (D63)', () {
|
||||
LlmRequest at(LlmEffort effort) => LlmRequest(
|
||||
messages: _question.messages,
|
||||
maxOutputTokens: 4096,
|
||||
effort: effort,
|
||||
);
|
||||
|
||||
test(
|
||||
'goes as reasoning_effort to OpenAI, Gemini and any other host',
|
||||
() async {
|
||||
for (final endpoint in [_openAi, _gemini, _ollama]) {
|
||||
final body = await _bodyFor(endpoint, at(LlmEffort.medium));
|
||||
expect(body['reasoning_effort'], 'medium', reason: endpoint.host);
|
||||
expect(body.keys, isNot(contains('reasoning')));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('goes as reasoning.effort to OpenRouter', () async {
|
||||
final body = await _bodyFor(_openRouter, at(LlmEffort.max));
|
||||
expect(body['reasoning'], {'effort': 'max'});
|
||||
expect(body.keys, isNot(contains('reasoning_effort')));
|
||||
});
|
||||
|
||||
test('is left out without one', () async {
|
||||
for (final endpoint in [_openAi, _gemini, _openRouter, _ollama]) {
|
||||
final body = await _bodyFor(endpoint);
|
||||
expect(
|
||||
body.keys,
|
||||
isNot(anyOf(contains('reasoning_effort'), contains('reasoning'))),
|
||||
reason: endpoint.host,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("is each host's levels: Gemini's low to high, Mistral's none, "
|
||||
'every level elsewhere', () {
|
||||
expect(_gemini.efforts, {
|
||||
LlmEffort.low,
|
||||
LlmEffort.medium,
|
||||
LlmEffort.high,
|
||||
});
|
||||
expect(_mistral.efforts, isEmpty);
|
||||
for (final endpoint in [_openAi, _openRouter, _ollama]) {
|
||||
expect(
|
||||
endpoint.efforts,
|
||||
LlmEffort.values.toSet(),
|
||||
reason: endpoint.host,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a level the host takes not is refused before anything goes', () {
|
||||
final client = LlmClient(httpClient: replying(_documented['text']!));
|
||||
expect(
|
||||
() => client.send(_mistral, at(LlmEffort.low)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
expect(
|
||||
() => client.send(_gemini, at(LlmEffort.xhigh)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'to any other host: what Ollama, vLLM and llama.cpp all take',
|
||||
() async {
|
||||
|
|
|
|||
|
|
@ -57,5 +57,26 @@ void main() {
|
|||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test("refuses an effort the endpoint's host takes not, as HTTP's does", () {
|
||||
final client = FakeLlmClient()..replyText('Hallo');
|
||||
expect(
|
||||
() => client.send(
|
||||
LlmEndpoint(
|
||||
protocol: LlmProtocol.openai,
|
||||
model: 'm',
|
||||
apiKey: 'k',
|
||||
baseUrl: 'https://api.mistral.ai/v1',
|
||||
),
|
||||
LlmRequest(
|
||||
messages: _request.messages,
|
||||
maxOutputTokens: 10,
|
||||
effort: LlmEffort.low,
|
||||
),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
expect(client.sent, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue