feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.
Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 17:38:53 +01:00
|
|
|
/// An [LlmClient] for the tests of what uses this package: it answers from a
|
|
|
|
|
/// script and keeps what it was sent.
|
|
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
import 'dart:collection';
|
|
|
|
|
|
|
|
|
|
import 'package:halkyon_llm/halkyon_llm.dart';
|
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>
2026-10-26 14:53:48 +01:00
|
|
|
import 'package:halkyon_llm/src/client.dart' show checkEffort;
|
feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.
Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 17:38:53 +01:00
|
|
|
|
|
|
|
|
/// An [LlmClient] answering each request with the next reply scripted.
|
|
|
|
|
final class FakeLlmClient() implements LlmClient {
|
|
|
|
|
final _replies = Queue<_Reply>();
|
|
|
|
|
|
|
|
|
|
/// What it was sent, in order.
|
|
|
|
|
final List<(LlmEndpoint, LlmRequest)> sent = [];
|
|
|
|
|
|
|
|
|
|
/// How many answers were cancelled before they ended.
|
|
|
|
|
int cancelled = 0;
|
|
|
|
|
|
|
|
|
|
/// Whether [close] was called.
|
|
|
|
|
bool closed = false;
|
|
|
|
|
|
|
|
|
|
/// Scripts the next reply: [events], then [failure] if given.
|
|
|
|
|
void reply(List<LlmEvent> events, {LlmFailure? failure}) =>
|
|
|
|
|
_replies.add(_Reply(events, failure));
|
|
|
|
|
|
|
|
|
|
/// Scripts an answer of [text], in two pieces, counted as [usage].
|
|
|
|
|
void replyText(
|
|
|
|
|
String text, {
|
|
|
|
|
LlmUsage usage = const LlmUsage(inputTokens: 100, outputTokens: 20),
|
|
|
|
|
}) {
|
|
|
|
|
final half = text.length ~/ 2;
|
|
|
|
|
reply([
|
|
|
|
|
LlmTextDelta(text.substring(0, half)),
|
|
|
|
|
LlmTextDelta(text.substring(half)),
|
|
|
|
|
LlmFinished(
|
|
|
|
|
message: LlmMessage.assistant([LlmText(text)]),
|
|
|
|
|
stop: LlmStop.end,
|
|
|
|
|
usage: usage,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Scripts a failure of [kind] before anything is written, with [usage]
|
|
|
|
|
/// counted.
|
|
|
|
|
void fail(LlmFailureKind kind, {LlmUsage usage = LlmUsage.none}) => reply(
|
|
|
|
|
const [],
|
|
|
|
|
failure: LlmFailure(kind: kind, detail: 'scripted', usage: usage),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// Whether every reply scripted was given.
|
|
|
|
|
bool get allReplied => _replies.isEmpty;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Stream<LlmEvent> send(LlmEndpoint endpoint, LlmRequest request) {
|
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>
2026-10-26 14:53:48 +01:00
|
|
|
checkEffort(endpoint, request);
|
feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.
Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 17:38:53 +01:00
|
|
|
sent.add((endpoint, request));
|
|
|
|
|
if (_replies.isEmpty) {
|
|
|
|
|
throw StateError('No reply scripted for request ${sent.length}');
|
|
|
|
|
}
|
|
|
|
|
final reply = _replies.removeFirst();
|
|
|
|
|
var ended = false;
|
|
|
|
|
late final StreamController<LlmEvent> events;
|
|
|
|
|
events = StreamController(
|
|
|
|
|
onListen: () async {
|
|
|
|
|
for (final event in reply.events) {
|
|
|
|
|
if (events.isClosed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
events.add(event);
|
|
|
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
|
}
|
|
|
|
|
if (reply.failure case final failure?) {
|
|
|
|
|
events.addError(failure);
|
|
|
|
|
}
|
|
|
|
|
ended = true;
|
|
|
|
|
await events.close();
|
|
|
|
|
},
|
|
|
|
|
onCancel: () {
|
|
|
|
|
if (!ended) {
|
|
|
|
|
cancelled++;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return events.stream;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void close() => closed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class _Reply(final List<LlmEvent> events, final LlmFailure? failure);
|