From 4c7db58bce440d0a0ba64982eb4d680a1db3f584 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Tue, 8 Sep 2026 17:51:28 +0200 Subject: [PATCH] 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 --- lib/src/generated/chain/v1/hub.pb.dart | 180 +++++++++++++++++++++ lib/src/generated/chain/v1/hub.pbgrpc.dart | 36 +++++ lib/src/generated/chain/v1/hub.pbjson.dart | 49 ++++++ lib/src/hub_client.dart | 29 ++++ 4 files changed, 294 insertions(+) diff --git a/lib/src/generated/chain/v1/hub.pb.dart b/lib/src/generated/chain/v1/hub.pb.dart index 0ff9c0c..dad3c65 100644 --- a/lib/src/generated/chain/v1/hub.pb.dart +++ b/lib/src/generated/chain/v1/hub.pb.dart @@ -7230,6 +7230,186 @@ class PullSystemAiModelResponse extends $pb.GeneratedMessage { 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(1, _omitFieldNames ? '' : 'update', + subBuilder: PullProgressUpdate.create) + ..aOM(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(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(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 { factory HardwareInfoResponse({ $core.String? tier, diff --git a/lib/src/generated/chain/v1/hub.pbgrpc.dart b/lib/src/generated/chain/v1/hub.pbgrpc.dart index 67faea6..50b3928 100644 --- a/lib/src/generated/chain/v1/hub.pbgrpc.dart +++ b/lib/src/generated/chain/v1/hub.pbgrpc.dart @@ -818,6 +818,19 @@ class HubAdminClient extends $grpc.Client { 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 /// Studio's Settings so operators can force a clean slate /// without changing model/privacy mode (which also flushes). @@ -1139,6 +1152,11 @@ class HubAdminClient extends $grpc.Client { '/chain.v1.HubAdmin/PullSystemAiModel', ($0.PullSystemAiModelRequest value) => value.writeToBuffer(), $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 = $grpc.ClientMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>( '/chain.v1.HubAdmin/ClearSystemLlmCache', @@ -1579,6 +1597,15 @@ abstract class HubAdminServiceBase extends $grpc.Service { ($core.List<$core.int> value) => $0.PullSystemAiModelRequest.fromBuffer(value), ($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>( 'ClearSystemLlmCache', clearSystemLlmCache_Pre, @@ -2055,6 +2082,15 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.PullSystemAiModelResponse> pullSystemAiModel( $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( $grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { return clearSystemLlmCache($call, await $request); diff --git a/lib/src/generated/chain/v1/hub.pbjson.dart b/lib/src/generated/chain/v1/hub.pbjson.dart index 1e40f80..8374ea2 100644 --- a/lib/src/generated/chain/v1/hub.pbjson.dart +++ b/lib/src/generated/chain/v1/hub.pbjson.dart @@ -1768,6 +1768,55 @@ final $typed_data.Uint8List pullSystemAiModelResponseDescriptor = $convert.base6 'ChlQdWxsU3lzdGVtQWlNb2RlbFJlc3BvbnNlEh0KCmVycm9yX2tpbmQYASABKAlSCWVycm9yS2' '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') const HardwareInfoResponse$json = { '1': 'HardwareInfoResponse', diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 63af57b..3a712b9 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -111,6 +111,7 @@ typedef StoreEntry = pb.StoreEntry; typedef StoreSearchResponse = pb.StoreSearchResponse; typedef InstallModuleResponse = pb.InstallModuleResponse; typedef InstallProgressUpdate = pb.InstallProgressUpdate; +typedef PullProgressUpdate = pb.PullProgressUpdate; typedef Payload = pb_common.Payload; typedef SubmitResponse = pb.SubmitResponse; // 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 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 /// flow name, on-disk path and byte size. Future> listFlows() async {