feat: pullSystemAiModelStreaming for live model-download progress

Wraps HubAdmin/PullSystemAiModelStream: yields the backend's own
status and byte counts per layer, and hands the terminal response to
onFinished, whose errorKind stays the failure signal. Stubs
regenerated from the protos (additive only).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-09-08 17:51:28 +02:00
parent 1d3d33d55a
commit 4c7db58bce
4 changed files with 294 additions and 0 deletions

View file

@ -7230,6 +7230,186 @@ class PullSystemAiModelResponse extends $pb.GeneratedMessage {
void clearElapsedMs() => $_clearField(3); void clearElapsedMs() => $_clearField(3);
} }
enum PullProgressEvent_Event { update, finished, notSet }
/// One message in a PullSystemAiModelStream.
class PullProgressEvent extends $pb.GeneratedMessage {
factory PullProgressEvent({
PullProgressUpdate? update,
PullSystemAiModelResponse? finished,
}) {
final result = create();
if (update != null) result.update = update;
if (finished != null) result.finished = finished;
return result;
}
PullProgressEvent._();
factory PullProgressEvent.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory PullProgressEvent.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, PullProgressEvent_Event>
_PullProgressEvent_EventByTag = {
1: PullProgressEvent_Event.update,
2: PullProgressEvent_Event.finished,
0: PullProgressEvent_Event.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'PullProgressEvent',
package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.v1'),
createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<PullProgressUpdate>(1, _omitFieldNames ? '' : 'update',
subBuilder: PullProgressUpdate.create)
..aOM<PullSystemAiModelResponse>(2, _omitFieldNames ? '' : 'finished',
subBuilder: PullSystemAiModelResponse.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
PullProgressEvent clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
PullProgressEvent copyWith(void Function(PullProgressEvent) updates) =>
super.copyWith((message) => updates(message as PullProgressEvent))
as PullProgressEvent;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static PullProgressEvent create() => PullProgressEvent._();
@$core.override
PullProgressEvent createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static PullProgressEvent getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<PullProgressEvent>(create);
static PullProgressEvent? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
PullProgressEvent_Event whichEvent() =>
_PullProgressEvent_EventByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
void clearEvent() => $_clearField($_whichOneof(0));
/// The pull moved on.
@$pb.TagNumber(1)
PullProgressUpdate get update => $_getN(0);
@$pb.TagNumber(1)
set update(PullProgressUpdate value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasUpdate() => $_has(0);
@$pb.TagNumber(1)
void clearUpdate() => $_clearField(1);
@$pb.TagNumber(1)
PullProgressUpdate ensureUpdate() => $_ensure(0);
/// Terminal message, success or failure (see its error_kind).
@$pb.TagNumber(2)
PullSystemAiModelResponse get finished => $_getN(1);
@$pb.TagNumber(2)
set finished(PullSystemAiModelResponse value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasFinished() => $_has(1);
@$pb.TagNumber(2)
void clearFinished() => $_clearField(2);
@$pb.TagNumber(2)
PullSystemAiModelResponse ensureFinished() => $_ensure(1);
}
/// A single observation while a model downloads.
class PullProgressUpdate extends $pb.GeneratedMessage {
factory PullProgressUpdate({
$core.String? status,
$fixnum.Int64? completed,
$fixnum.Int64? total,
}) {
final result = create();
if (status != null) result.status = status;
if (completed != null) result.completed = completed;
if (total != null) result.total = total;
return result;
}
PullProgressUpdate._();
factory PullProgressUpdate.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory PullProgressUpdate.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'PullProgressUpdate',
package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.v1'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'status')
..a<$fixnum.Int64>(
2, _omitFieldNames ? '' : 'completed', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'total', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
PullProgressUpdate clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
PullProgressUpdate copyWith(void Function(PullProgressUpdate) updates) =>
super.copyWith((message) => updates(message as PullProgressUpdate))
as PullProgressUpdate;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static PullProgressUpdate create() => PullProgressUpdate._();
@$core.override
PullProgressUpdate createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static PullProgressUpdate getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<PullProgressUpdate>(create);
static PullProgressUpdate? _defaultInstance;
/// The backend's own status text, e.g. "pulling manifest". Passed
/// through verbatim rather than translated: it names layers and
/// digests a client cannot reconstruct.
@$pb.TagNumber(1)
$core.String get status => $_getSZ(0);
@$pb.TagNumber(1)
set status($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasStatus() => $_has(0);
@$pb.TagNumber(1)
void clearStatus() => $_clearField(1);
/// Bytes fetched so far for the layer this status refers to.
@$pb.TagNumber(2)
$fixnum.Int64 get completed => $_getI64(1);
@$pb.TagNumber(2)
set completed($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasCompleted() => $_has(1);
@$pb.TagNumber(2)
void clearCompleted() => $_clearField(2);
/// Size of that layer; zero for statuses that carry no size, which
/// clients show as indeterminate rather than as zero progress.
@$pb.TagNumber(3)
$fixnum.Int64 get total => $_getI64(2);
@$pb.TagNumber(3)
set total($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasTotal() => $_has(2);
@$pb.TagNumber(3)
void clearTotal() => $_clearField(3);
}
class HardwareInfoResponse extends $pb.GeneratedMessage { class HardwareInfoResponse extends $pb.GeneratedMessage {
factory HardwareInfoResponse({ factory HardwareInfoResponse({
$core.String? tier, $core.String? tier,

View file

@ -818,6 +818,19 @@ class HubAdminClient extends $grpc.Client {
return $createUnaryCall(_$pullSystemAiModel, request, options: options); return $createUnaryCall(_$pullSystemAiModel, request, options: options);
} }
/// Same pull, with live progress. A model pull moves gigabytes, so
/// the unary form above leaves a person waiting minutes with nothing
/// to look at. Emits Ollama's own status plus byte counts per layer,
/// ending in the response.
$grpc.ResponseStream<$0.PullProgressEvent> pullSystemAiModelStream(
$0.PullSystemAiModelRequest request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(
_$pullSystemAiModelStream, $async.Stream.fromIterable([request]),
options: options);
}
/// Drop every cached System-AI explanation. Surfaced in /// Drop every cached System-AI explanation. Surfaced in
/// Studio's Settings so operators can force a clean slate /// Studio's Settings so operators can force a clean slate
/// without changing model/privacy mode (which also flushes). /// without changing model/privacy mode (which also flushes).
@ -1139,6 +1152,11 @@ class HubAdminClient extends $grpc.Client {
'/chain.v1.HubAdmin/PullSystemAiModel', '/chain.v1.HubAdmin/PullSystemAiModel',
($0.PullSystemAiModelRequest value) => value.writeToBuffer(), ($0.PullSystemAiModelRequest value) => value.writeToBuffer(),
$0.PullSystemAiModelResponse.fromBuffer); $0.PullSystemAiModelResponse.fromBuffer);
static final _$pullSystemAiModelStream =
$grpc.ClientMethod<$0.PullSystemAiModelRequest, $0.PullProgressEvent>(
'/chain.v1.HubAdmin/PullSystemAiModelStream',
($0.PullSystemAiModelRequest value) => value.writeToBuffer(),
$0.PullProgressEvent.fromBuffer);
static final _$clearSystemLlmCache = static final _$clearSystemLlmCache =
$grpc.ClientMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>( $grpc.ClientMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>(
'/chain.v1.HubAdmin/ClearSystemLlmCache', '/chain.v1.HubAdmin/ClearSystemLlmCache',
@ -1579,6 +1597,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
($core.List<$core.int> value) => ($core.List<$core.int> value) =>
$0.PullSystemAiModelRequest.fromBuffer(value), $0.PullSystemAiModelRequest.fromBuffer(value),
($0.PullSystemAiModelResponse value) => value.writeToBuffer())); ($0.PullSystemAiModelResponse value) => value.writeToBuffer()));
$addMethod(
$grpc.ServiceMethod<$0.PullSystemAiModelRequest, $0.PullProgressEvent>(
'PullSystemAiModelStream',
pullSystemAiModelStream_Pre,
false,
true,
($core.List<$core.int> value) =>
$0.PullSystemAiModelRequest.fromBuffer(value),
($0.PullProgressEvent value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>( $addMethod($grpc.ServiceMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>(
'ClearSystemLlmCache', 'ClearSystemLlmCache',
clearSystemLlmCache_Pre, clearSystemLlmCache_Pre,
@ -2055,6 +2082,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
$async.Future<$0.PullSystemAiModelResponse> pullSystemAiModel( $async.Future<$0.PullSystemAiModelResponse> pullSystemAiModel(
$grpc.ServiceCall call, $0.PullSystemAiModelRequest request); $grpc.ServiceCall call, $0.PullSystemAiModelRequest request);
$async.Stream<$0.PullProgressEvent> pullSystemAiModelStream_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.PullSystemAiModelRequest> $request) async* {
yield* pullSystemAiModelStream($call, await $request);
}
$async.Stream<$0.PullProgressEvent> pullSystemAiModelStream(
$grpc.ServiceCall call, $0.PullSystemAiModelRequest request);
$async.Future<$0.ClearSystemLlmCacheResponse> clearSystemLlmCache_Pre( $async.Future<$0.ClearSystemLlmCacheResponse> clearSystemLlmCache_Pre(
$grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { $grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async {
return clearSystemLlmCache($call, await $request); return clearSystemLlmCache($call, await $request);

View file

@ -1768,6 +1768,55 @@ final $typed_data.Uint8List pullSystemAiModelResponseDescriptor = $convert.base6
'ChlQdWxsU3lzdGVtQWlNb2RlbFJlc3BvbnNlEh0KCmVycm9yX2tpbmQYASABKAlSCWVycm9yS2' 'ChlQdWxsU3lzdGVtQWlNb2RlbFJlc3BvbnNlEh0KCmVycm9yX2tpbmQYASABKAlSCWVycm9yS2'
'luZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh0KCmVsYXBzZWRfbXMYAyABKA1SCWVsYXBzZWRNcw=='); 'luZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh0KCmVsYXBzZWRfbXMYAyABKA1SCWVsYXBzZWRNcw==');
@$core.Deprecated('Use pullProgressEventDescriptor instead')
const PullProgressEvent$json = {
'1': 'PullProgressEvent',
'2': [
{
'1': 'update',
'3': 1,
'4': 1,
'5': 11,
'6': '.chain.v1.PullProgressUpdate',
'9': 0,
'10': 'update'
},
{
'1': 'finished',
'3': 2,
'4': 1,
'5': 11,
'6': '.chain.v1.PullSystemAiModelResponse',
'9': 0,
'10': 'finished'
},
],
'8': [
{'1': 'event'},
],
};
/// Descriptor for `PullProgressEvent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List pullProgressEventDescriptor = $convert.base64Decode(
'ChFQdWxsUHJvZ3Jlc3NFdmVudBI2CgZ1cGRhdGUYASABKAsyHC5jaGFpbi52MS5QdWxsUHJvZ3'
'Jlc3NVcGRhdGVIAFIGdXBkYXRlEkEKCGZpbmlzaGVkGAIgASgLMiMuY2hhaW4udjEuUHVsbFN5'
'c3RlbUFpTW9kZWxSZXNwb25zZUgAUghmaW5pc2hlZEIHCgVldmVudA==');
@$core.Deprecated('Use pullProgressUpdateDescriptor instead')
const PullProgressUpdate$json = {
'1': 'PullProgressUpdate',
'2': [
{'1': 'status', '3': 1, '4': 1, '5': 9, '10': 'status'},
{'1': 'completed', '3': 2, '4': 1, '5': 4, '10': 'completed'},
{'1': 'total', '3': 3, '4': 1, '5': 4, '10': 'total'},
],
};
/// Descriptor for `PullProgressUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List pullProgressUpdateDescriptor = $convert.base64Decode(
'ChJQdWxsUHJvZ3Jlc3NVcGRhdGUSFgoGc3RhdHVzGAEgASgJUgZzdGF0dXMSHAoJY29tcGxldG'
'VkGAIgASgEUgljb21wbGV0ZWQSFAoFdG90YWwYAyABKARSBXRvdGFs');
@$core.Deprecated('Use hardwareInfoResponseDescriptor instead') @$core.Deprecated('Use hardwareInfoResponseDescriptor instead')
const HardwareInfoResponse$json = { const HardwareInfoResponse$json = {
'1': 'HardwareInfoResponse', '1': 'HardwareInfoResponse',

View file

@ -111,6 +111,7 @@ typedef StoreEntry = pb.StoreEntry;
typedef StoreSearchResponse = pb.StoreSearchResponse; typedef StoreSearchResponse = pb.StoreSearchResponse;
typedef InstallModuleResponse = pb.InstallModuleResponse; typedef InstallModuleResponse = pb.InstallModuleResponse;
typedef InstallProgressUpdate = pb.InstallProgressUpdate; typedef InstallProgressUpdate = pb.InstallProgressUpdate;
typedef PullProgressUpdate = pb.PullProgressUpdate;
typedef Payload = pb_common.Payload; typedef Payload = pb_common.Payload;
typedef SubmitResponse = pb.SubmitResponse; typedef SubmitResponse = pb.SubmitResponse;
// Live-stream (T2) + detached-invocation (T3) wire types, re-exported // Live-stream (T2) + detached-invocation (T3) wire types, re-exported
@ -867,6 +868,34 @@ class HubClient {
} }
} }
/// Pull a model while watching it happen.
///
/// A model pull moves gigabytes; the unary [pullSystemAiModel]
/// leaves the caller waiting minutes with nothing to show. Emits the
/// backend's own status plus byte counts per layer, and calls
/// [onFinished] with the terminal response, whose `errorKind` is
/// non-empty when the pull failed.
Stream<PullProgressUpdate> pullSystemAiModelStreaming({
required String endpoint,
required String model,
String apiKeyEnv = '',
void Function(PullSystemAiModelResponse result)? onFinished,
}) async* {
final stream = _admin.pullSystemAiModelStream(pb.PullSystemAiModelRequest(
endpoint: endpoint,
model: model,
apiKeyEnv: apiKeyEnv,
));
await for (final event in stream) {
if (event.hasUpdate()) {
yield event.update;
} else if (event.hasFinished()) {
onFinished?.call(event.finished);
return;
}
}
}
/// All saved flows known to the hub. Each entry carries the /// All saved flows known to the hub. Each entry carries the
/// flow name, on-disk path and byte size. /// flow name, on-disk path and byte size.
Future<List<FlowSummary>> listFlows() async { Future<List<FlowSummary>> listFlows() async {