From d079df37916f646f0a5b34343f317a03aa3a2e07 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 01:53:41 -0400 Subject: [PATCH 01/11] feat(app): add DeepSeek V4 target-only mlx-serve backend Signed-off-by: Philip John Basile --- README.md | 24 ++ .../Models/MTPLXModelOption.swift | 180 +++++++- .../Onboarding/HuggingFaceProbe.swift | 76 ++++ .../Services/DaemonSupervisor.swift | 97 ++++- .../Services/ExternalMlxServeAdapter.swift | 123 ++++++ .../Services/MTPLXCommandBuilder.swift | 8 +- .../MTPLXAppCore/Services/PortPreflight.swift | 18 +- .../Stores/MTPLXBackendStore.swift | 180 +++++++- .../Inference/InferenceParamsOverlay.swift | 21 + .../Views/Models/ModelPickerOverlay.swift | 13 +- .../Onboarding/Steps/ModelPickStep.swift | 8 +- .../HuggingFaceProbeForgeTests.swift | 150 +++++++ .../MTPLXAppCoreTests/MTPLXAppCoreTests.swift | 400 ++++++++++++++++++ docs/model-compatibility.md | 11 + docs/quickstart.md | 18 + mtplx/artifacts.py | 122 +++++- mtplx/backends/deepseek_v4_mlxserve.py | 138 ++++++ mtplx/backends/descriptors.py | 67 +++ mtplx/backends/registry.py | 44 ++ mtplx/commands/public.py | 154 ++++++- mtplx/hf_loader.py | 237 +++++++++-- .../models/deepseek_v4_target_only_config.py | 206 +++++++++ tests/test_deepseek_v4_mlxserve_backend.py | 244 +++++++++++ 23 files changed, 2468 insertions(+), 71 deletions(-) create mode 100644 apps/MTPLXApp/Sources/MTPLXAppCore/Services/ExternalMlxServeAdapter.swift create mode 100644 mtplx/backends/deepseek_v4_mlxserve.py create mode 100644 mtplx/models/deepseek_v4_target_only_config.py create mode 100644 tests/test_deepseek_v4_mlxserve_backend.py diff --git a/README.md b/README.md index e66c88fdd..effcae8b9 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,30 @@ comfortable. MTPLX defaults Laguna to a 32,768-token context and response cap, and checks larger explicit server contexts against the active Metal memory cap. +[DeepSeek-V4-Flash-0731 MLX M5 Max Target-Only](https://huggingface.co/philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly) +uses a separate, experimental external runtime route. MTPLX pins the public +artifact to `ac33e4f3ca3546e6cec104558d42161e15814e33`, admits the exact 44 +weight shards and required sidecars, then delegates serving to a separately +installed `mlx-serve` executable. This is target-only AR — it has no MTP or +DSpark weights — and it is not a native MTPLX backend: + +```bash +mtplx pull philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly + +MTPLX_MLX_SERVE_BIN=/path/to/mlx-serve \ +mtplx serve \ + --model philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly \ + --no-mtp --host 127.0.0.1 --port 8000 --yes +``` + +The route requires a 128 GB Apple Silicon Mac, defaults to an 8,192-token +context, disables PLD, decode-attention quantization, and vision, and preserves +the external runtime's memory preflight. MTPLX clears ambient `MLX_SERVE_*` +settings and launches with `MLX_SERVE_WIRED=fit` plus a 256 MB cache limit; +set `MTPLX_DSV4_WIRED` only to make an explicit override. Representative +streaming performance is unapproved, so neither this integration nor its dry +run output makes a throughput claim. + ## What MTPLX is not - Not an external-drafter system. The drafter is the target model's own MTP heads. diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Models/MTPLXModelOption.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Models/MTPLXModelOption.swift index 1d7c282da..20031224d 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Models/MTPLXModelOption.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Models/MTPLXModelOption.swift @@ -1,6 +1,31 @@ import Foundation public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { + // Not included in the recommended catalog: this target-only model needs a + // separately installed mlx-serve runtime and 128 GB Apple Silicon. The + // command builder still recognizes it so a saved/custom selection cannot + // launch as MTP. + static let externalAROnlyRepoID = + "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + static let externalAROnlyRevision = + "ac33e4f3ca3546e6cec104558d42161e15814e33" + /// SHA-256 of `config.json` at `externalAROnlyRevision`. Remote probing + /// checks this immutable byte identity; the local bridge independently + /// verifies every sidecar and shard hash before it can launch. + static let externalAROnlyConfigSHA256 = + "ab61e3230f196c6eba04bfa81158dd527a7f356b6d926cc4794907a19f35b75d" + private static let externalAROnlyDownloadBytes: Int64 = 103_855_774_263 + static let externalAROnlyWeightShards = Set( + (0...42).map { "model-layer-\($0).safetensors" } + + ["model-top.safetensors"] + ) + static let externalAROnlySidecars: Set = [ + "config.json", + "generation_config.json", + "model.safetensors.index.json", + "tokenizer.json", + "tokenizer_config.json", + ] public var id: String public var displayName: String public var shortName: String @@ -92,9 +117,32 @@ public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { /// alias, HF id, or a local path) resolves to a target-only AR model. /// Used by the command builder so every app launch path carries the /// correct `--no-mtp` shape without each caller re-deriving it. + public static func isExternalAROnlyReference(_ reference: String) -> Bool { + let trimmed = reference.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return false } + if isCanonicalExternalAROnlyRepoID(trimmed) { return true } + + // A saved local path is permitted only after it proves the same + // pinned source/revision/config/closed-shard contract. In + // particular, a remote repo that merely reuses this artifact's + // basename never switches the app into the external backend. + let localURL = URL(fileURLWithPath: Self.expand(trimmed)) + return FileManager.default.fileExists(atPath: localURL.path) + && hasCompleteExternalAROnlyInstall(at: localURL) + } + + /// Remote admission is intentionally stricter than a local-path hint: + /// only this spelling identifies the public immutable HF artifact. The + /// loose basename recognition above is retained solely so a previously + /// downloaded local folder can be launched through the safe Python gate. + public static func isCanonicalExternalAROnlyRepoID(_ repo: String) -> Bool { + repo == externalAROnlyRepoID + } + public static func isAROnlyReference(_ reference: String) -> Bool { let trimmed = reference.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } + if isExternalAROnlyReference(trimmed) { return true } let lower = trimmed.lowercased() for option in MTPLXModelOption.officialCatalog where option.arOnly { if option.id.lowercased() == lower { return true } @@ -199,6 +247,17 @@ public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { && Self.hasCompleteModelDirectory(at: assistant) } + // This exact pinned artifact is a deliberately external target-only + // route. It has neither an MTPLX runtime contract nor an MTP sidecar, + // so accepting it through the generic MTP contract below would leave + // the app unable to finish its own download flow. This is only a + // structural app preflight: the Python bridge still verifies all + // pinned hashes and retains mlx-serve's memory preflight before it + // will launch the model. + if Self.hasCompleteExternalAROnlyInstall(at: url) { + return true + } + let coreFiles = ["config.json", "tokenizer.json", "mtplx_runtime.json"] for name in coreFiles { if !fm.fileExists(atPath: url.appendingPathComponent(name).path) { @@ -213,6 +272,115 @@ public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { return Self.hasCompleteWeightSet(at: url) } + private static func hasCompleteExternalAROnlyInstall(at url: URL) -> Bool { + let fm = FileManager.default + let sourceURL = url.appendingPathComponent(".mtplx-source.json") + guard + let sourceData = fm.contents(atPath: sourceURL.path), + let source = try? JSONSerialization.jsonObject(with: sourceData) as? [String: Any], + source["repo_id"] as? String == externalAROnlyRepoID, + source["revision"] as? String == externalAROnlyRevision, + let configData = fm.contents(atPath: url.appendingPathComponent("config.json").path), + let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any], + isExternalAROnlyTargetConfig(config) + else { + return false + } + + for name in externalAROnlySidecars.union(externalAROnlyWeightShards) { + guard fm.fileExists(atPath: url.appendingPathComponent(name).path) else { + return false + } + } + + let indexURL = url.appendingPathComponent("model.safetensors.index.json") + guard + let indexData = fm.contents(atPath: indexURL.path), + let index = try? JSONSerialization.jsonObject(with: indexData) as? [String: Any], + let weightMap = index["weight_map"] as? [String: String], + Set(weightMap.values) == externalAROnlyWeightShards + else { + return false + } + // Closed 44-shard shape: an extra root safetensors file is as much a + // different artifact as a missing one. Do not accept a mixed folder + // merely because the index happens to point at the expected weights. + let rootShardNames = (try? fm.contentsOfDirectory(atPath: url.path)) ?? [] + let actualShards = Set(rootShardNames.filter { $0.hasSuffix(".safetensors") }) + guard actualShards == externalAROnlyWeightShards else { + return false + } + return true + } + + static func isExternalAROnlyTargetConfig(_ config: [String: Any]) -> Bool { + guard + config["architectures"] as? [String] == ["DeepseekV4ForCausalLM"], + config["model_type"] as? String == "deepseek_v4", + config["model_file"] == nil + else { + return false + } + for (key, expected) in [ + ("num_hidden_layers", 43), + ("hidden_size", 4096), + ("num_attention_heads", 64), + ("num_key_value_heads", 1), + ("head_dim", 512), + ("vocab_size", 129_280), + ("num_nextn_predict_layers", 0), + ("dspark_block_size", 0), + ("num_experts_per_tok", 6), + ("n_routed_experts", 256), + ] { + guard (config[key] as? NSNumber)?.intValue == expected else { + return false + } + } + guard let quantization = config["quantization"] as? [String: Any], + Self.isAffineQuantization(quantization, key: nil, bits: 8, groupSize: 64), + Self.isAffineQuantization(quantization, key: "embed", bits: 8, groupSize: 64), + Self.isAffineQuantization(quantization, key: "head", bits: 8, groupSize: 64) + else { + return false + } + for layer in 0..<43 { + let recipe: (Int, Int, Int, Int) = layer < 39 + ? (2, 3, 2, 128) + : (4, 4, 4, 64) + for (projection, bits) in zip(["w1", "w2", "w3"], [recipe.0, recipe.1, recipe.2]) { + let key = "layers.\(layer).ffn.experts.\(projection)" + guard Self.isAffineQuantization( + quantization, + key: key, + bits: bits, + groupSize: recipe.3 + ) else { + return false + } + } + } + return true + } + + private static func isAffineQuantization( + _ quantization: [String: Any], + key: String?, + bits: Int, + groupSize: Int + ) -> Bool { + let value: [String: Any] + if let key { + guard let nested = quantization[key] as? [String: Any] else { return false } + value = nested + } else { + value = quantization + } + return (value["bits"] as? NSNumber)?.intValue == bits + && (value["group_size"] as? NSNumber)?.intValue == groupSize + && value["mode"] as? String == "affine" + } + private static func hasMTPSidecar(at url: URL) -> Bool { let fm = FileManager.default for rel in Self.mtpSidecarCandidates(at: url) { @@ -777,11 +945,17 @@ public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { .lowercased() .replacingOccurrences(of: "/", with: "--") .replacingOccurrences(of: "_", with: "-") + // A remote custom model gets the external route only for the exact + // canonical owner/repo. Local path admission is handled separately + // by `isExternalAROnlyReference` after structural verification. + let externalAROnly = isCanonicalExternalAROnlyRepoID(repoID) return MTPLXModelOption( id: "custom-\(safeID)", displayName: repoName, shortName: repoName, - detail: "Custom Hugging Face model. MTPLX will use MTP when the repo includes a sidecar.", + detail: externalAROnly + ? "Experimental target-only route. Requires mlx-serve and a 128 GB Apple Silicon Mac; no MTP or DSpark." + : "Custom Hugging Face model. MTPLX will use MTP when the repo includes a sidecar.", hfModelID: repoID, localCandidates: [ "~/.mtplx/models/\(repoID.replacingOccurrences(of: "/", with: "--"))", @@ -789,7 +963,9 @@ public struct MTPLXModelOption: Codable, Equatable, Identifiable, Sendable { "~/Documents/MTPLX/hf-staging/\(repoName)", "~/Documents/MTPLX/models/hf-release/\(repoName)", ], - aliases: [repoID] + aliases: [repoID], + sizeBytes: externalAROnly ? externalAROnlyDownloadBytes : 0, + arOnly: externalAROnly ) } diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Onboarding/HuggingFaceProbe.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Onboarding/HuggingFaceProbe.swift index 13753c2df..ac075a3aa 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Onboarding/HuggingFaceProbe.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Onboarding/HuggingFaceProbe.swift @@ -1,3 +1,4 @@ +import CryptoKit import Foundation // MARK: - HuggingFaceProbe @@ -26,6 +27,7 @@ public struct HuggingFaceProbe: Sendable { private let runner: HTTPRunner private let endpointBase: String + private let externalAROnlyConfigSHA256: String /// `endpoint` is the user's HF download mirror from Settings /// (nil/empty = huggingface.co). The probe must follow it: on @@ -34,6 +36,19 @@ public struct HuggingFaceProbe: Sendable { public init(endpoint: String? = nil, runner: @escaping HTTPRunner = Self.defaultRunner) { self.endpointBase = Self.normalizedEndpoint(endpoint) self.runner = runner + self.externalAROnlyConfigSHA256 = MTPLXModelOption.externalAROnlyConfigSHA256 + } + + /// Test seam for the remote layout protocol. Production construction + /// always uses the immutable published config fingerprint above. + init( + endpoint: String? = nil, + runner: @escaping HTTPRunner, + externalAROnlyConfigSHA256: String + ) { + self.endpointBase = Self.normalizedEndpoint(endpoint) + self.runner = runner + self.externalAROnlyConfigSHA256 = externalAROnlyConfigSHA256 } static func normalizedEndpoint(_ raw: String?) -> String { @@ -54,6 +69,14 @@ public struct HuggingFaceProbe: Sendable { ) } + // The external route is intentionally not a name-only exception. A + // canonical owner/repo must still prove the immutable revision's + // exact config bytes and closed 44-shard tree before the UI permits + // the special mlx-serve route. + if MTPLXModelOption.isCanonicalExternalAROnlyRepoID(repo) { + return await probeExactExternalAROnlyRoute(repo: repo) + } + let configOutcome = await fetchConfig(repo: repo) let config: [String: Any] switch configOutcome { @@ -224,6 +247,59 @@ public struct HuggingFaceProbe: Sendable { } } + private func probeExactExternalAROnlyRoute(repo: String) async -> OtherModelProbe { + let revision = MTPLXModelOption.externalAROnlyRevision + guard let configURL = URL(string: "\(endpointBase)/\(repo)/resolve/\(revision)/config.json"), + let treeURL = URL(string: "\(endpointBase)/api/models/\(repo)/tree/\(revision)") + else { + return externalAROnlyProbeFailure(repo: repo, diagnostic: "url_build_failed") + } + + guard let (configStatus, configData) = try? await runner(configURL, "GET"), + configStatus == 200, + SHA256.hash(data: configData) + .map({ String(format: "%02x", $0) }) + .joined() == externalAROnlyConfigSHA256, + let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any], + MTPLXModelOption.isExternalAROnlyTargetConfig(config) + else { + return externalAROnlyProbeFailure(repo: repo, diagnostic: "pinned_config_mismatch") + } + + guard let (treeStatus, treeData) = try? await runner(treeURL, "GET"), + treeStatus == 200, + let entries = try? JSONSerialization.jsonObject(with: treeData) as? [[String: Any]] + else { + return externalAROnlyProbeFailure(repo: repo, diagnostic: "pinned_tree_unavailable") + } + + let paths = Set(entries.compactMap { $0["path"] as? String }) + let expectedFiles = MTPLXModelOption.externalAROnlyWeightShards + .union(MTPLXModelOption.externalAROnlySidecars) + let publishedShards = Set(paths.filter { $0.hasSuffix(".safetensors") }) + guard expectedFiles.isSubset(of: paths), + publishedShards == MTPLXModelOption.externalAROnlyWeightShards + else { + return externalAROnlyProbeFailure(repo: repo, diagnostic: "closed_44_shard_shape_mismatch") + } + + return OtherModelProbe( + verdict: .noMTP, + hfRepo: repo, + message: "Verified pinned target-only AR route: requires a separately installed mlx-serve binary and a 128 GB Apple Silicon Mac. MTP and DSpark are unavailable.", + diagnostic: "external_target_only_verified" + ) + } + + private func externalAROnlyProbeFailure(repo: String, diagnostic: String) -> OtherModelProbe { + OtherModelProbe( + verdict: .probeFailed, + hfRepo: repo, + message: "The DeepSeek V4 external route no longer matches its pinned immutable artifact. It was not added.", + diagnostic: diagnostic + ) + } + /// Classify the source artifact's quantization layout from the /// `config.json` we already fetched. Detection priority: /// diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/DaemonSupervisor.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/DaemonSupervisor.swift index c59a8df8f..e46b63da2 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/DaemonSupervisor.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/DaemonSupervisor.swift @@ -78,6 +78,7 @@ public final class DaemonSupervisor: @unchecked Sendable { command: DaemonCommand, healthBaseURL: URL, apiKey: String? = nil, + backendKind: DaemonBackendKind = .mtplx, probeHealth: Bool = true, timeoutSeconds: TimeInterval = 300, expectedLaunchID: String? = nil, @@ -93,18 +94,34 @@ public final class DaemonSupervisor: @unchecked Sendable { } onPhase?(.launching) - let healthClient = MTPLXAPIClient(baseURL: healthBaseURL, apiKey: apiKey) - if probeHealth, let existing = try? await healthClient.health(), existing.ok { - if adoptExistingAppOwnedDaemon, - canAdopt(existing, for: command, requireActualFanRamp: requireActualFanRamp) { - await adopt(existing) - onPhase?(.ready) - return existing + if probeHealth { + switch backendKind { + case .mtplx: + let healthClient = MTPLXAPIClient(baseURL: healthBaseURL, apiKey: apiKey) + if let existing = try? await healthClient.health(), existing.ok { + if adoptExistingAppOwnedDaemon, + canAdopt(existing, for: command, requireActualFanRamp: requireActualFanRamp) { + await adopt(existing) + onPhase?(.ready) + return existing + } + throw DaemonSupervisorError.portOccupied( + pid: existing.startup?.pid, + launchID: existing.startup?.launchId + ) + } + case .externalMlxServe: + // Raw mlx-serve health intentionally carries no MTPLX launch + // ID. It is never adoptable; a ready listener before we spawn + // is foreign to this app session and must not be replaced. + let external = ExternalMlxServeAdapter( + baseURL: healthBaseURL, + apiKey: apiKey + ) + if let existing = try? await external.health(), existing.ok { + throw DaemonSupervisorError.portOccupied(pid: nil, launchID: nil) + } } - throw DaemonSupervisorError.portOccupied( - pid: existing.startup?.pid, - launchID: existing.startup?.launchId - ) } let next = Process() @@ -157,6 +174,7 @@ public final class DaemonSupervisor: @unchecked Sendable { readyHealth = try await waitForHealth( baseURL: healthBaseURL, apiKey: apiKey, + backendKind: backendKind, timeoutSeconds: timeoutSeconds, expectedLaunchID: expectedLaunchID, requireActualFanRamp: requireActualFanRamp, @@ -283,6 +301,7 @@ public final class DaemonSupervisor: @unchecked Sendable { command: DaemonCommand, healthBaseURL: URL, apiKey: String? = nil, + backendKind: DaemonBackendKind = .mtplx, probeHealth: Bool = true, timeoutSeconds: TimeInterval = 300, expectedLaunchID: String? = nil, @@ -295,6 +314,7 @@ public final class DaemonSupervisor: @unchecked Sendable { command: command, healthBaseURL: healthBaseURL, apiKey: apiKey, + backendKind: backendKind, probeHealth: probeHealth, timeoutSeconds: timeoutSeconds, expectedLaunchID: expectedLaunchID, @@ -312,14 +332,17 @@ public final class DaemonSupervisor: @unchecked Sendable { requireActualFanRamp: Bool = false, onPhase: (@Sendable (DaemonStartupPhase) -> Void)? = nil ) async throws -> HealthPayload { - let health = try await waitForHealth( + guard let health = try await waitForHealth( baseURL: healthBaseURL, apiKey: apiKey, + backendKind: .mtplx, timeoutSeconds: timeoutSeconds, expectedLaunchID: expectedLaunchID, requireActualFanRamp: requireActualFanRamp, onPhase: onPhase - ) + ) else { + throw DaemonSupervisorError.healthTimeout + } lock.withLock { state = .running } onPhase?(.ready) return health @@ -442,11 +465,24 @@ public final class DaemonSupervisor: @unchecked Sendable { private func waitForHealth( baseURL: URL, apiKey: String?, + backendKind: DaemonBackendKind, timeoutSeconds: TimeInterval, expectedLaunchID: String?, requireActualFanRamp: Bool, onPhase: (@Sendable (DaemonStartupPhase) -> Void)? - ) async throws -> HealthPayload { + ) async throws -> HealthPayload? { + if backendKind == .externalMlxServe { + try await waitForExternalMlxServeHealth( + baseURL: baseURL, + apiKey: apiKey, + timeoutSeconds: timeoutSeconds, + onPhase: onPhase + ) + // The caller uses its explicit external backend mode for the + // post-ready path. Returning a fabricated MTPLX payload here + // would invite unsupported sessions/settings/metrics calls. + return nil + } let client = MTPLXAPIClient(baseURL: baseURL, apiKey: apiKey) let deadline = Date().addingTimeInterval(timeoutSeconds) var sawHealthyWithUnverifiedFan = false @@ -486,6 +522,39 @@ public final class DaemonSupervisor: @unchecked Sendable { throw DaemonSupervisorError.healthTimeout } + private func waitForExternalMlxServeHealth( + baseURL: URL, + apiKey: String?, + timeoutSeconds: TimeInterval, + onPhase: (@Sendable (DaemonStartupPhase) -> Void)? + ) async throws { + // Do not let URLSession's shared-session timeout outlive our launch + // deadline. This is still the same raw `/health` contract, with an + // isolated bounded probe session that is discarded on every start. + let client = ExternalMlxServeAdapter.livenessProbe(baseURL: baseURL, apiKey: apiKey) + defer { client.session.finishTasksAndInvalidate() } + let deadline = Date().addingTimeInterval(timeoutSeconds) + onPhase?(.waitingForOwnedHealth) + while Date() < deadline { + // The external endpoint cannot report MTPLX's launch UUID. The + // identity guard is instead the Process we just spawned: we only + // accept raw health while that exact app-owned wrapper is alive. + guard isRunning() else { + let tail = await logStore.snapshot().suffix(8).map(\.message).joined(separator: " | ") + let detail = tail.isEmpty + ? "external mlx-serve exited before /health became ready" + : "external mlx-serve exited before /health became ready: \(tail)" + throw DaemonSupervisorError.launchFailed(detail) + } + if let health = try? await client.health(), health.ok { + onPhase?(.warming) + return + } + try? await Task.sleep(nanoseconds: 250_000_000) + } + throw DaemonSupervisorError.healthTimeout + } + private func attach(pipe: Pipe, stream: LogEntry.Stream) { pipe.fileHandleForReading.readabilityHandler = { [logStore] handle in let data = handle.availableData diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/ExternalMlxServeAdapter.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/ExternalMlxServeAdapter.swift new file mode 100644 index 000000000..cfa7f6723 --- /dev/null +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/ExternalMlxServeAdapter.swift @@ -0,0 +1,123 @@ +import Foundation + +/// The two deliberately different server contracts the app can supervise. +/// +/// MTPLX's `/health` contains startup ownership, fan, and model metadata. +/// The exact DeepSeek V4 target-only route is intentionally not an MTPLX +/// daemon: its native `mlx-serve` process answers only `{ "status": "ok" }`. +/// Keeping the distinction in the type system prevents a successful native +/// launch from being followed by calls to MTPLX-only admin endpoints. +public enum DaemonBackendKind: Equatable, Sendable { + case mtplx + case externalMlxServe +} + +public struct ExternalMlxServeHealth: Codable, Equatable, Sendable { + public let status: String + + public var ok: Bool { + status.trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() == "ok" + } +} + +public enum ExternalMlxServeLiveness: Sendable { + case healthy(ExternalMlxServeHealth) + case aliveUnauthorized + case unreachable +} + +/// Minimal adapter for the native target-only server. It is intentionally +/// limited to `/health`; OpenAI chat traffic continues through the existing +/// chat client, while MTPLX-specific capabilities, sessions, settings, and +/// metrics are never assumed to exist on this server. +public struct ExternalMlxServeAdapter: Sendable { + public var baseURL: URL + public var apiKey: String? + public var session: URLSession + + public init( + baseURL: URL, + apiKey: String? = nil, + session: URLSession = .shared + ) { + self.baseURL = baseURL + self.apiKey = apiKey + self.session = session + } + + public func health() async throws -> ExternalMlxServeHealth { + var request = URLRequest(url: makeURL("/health")) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Accept") + if let apiKey, !apiKey.isEmpty { + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + } + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { + throw MTPLXAPIClientError.invalidResponse + } + guard (200..<300).contains(http.statusCode) else { + throw MTPLXAPIClientError.httpStatus( + http.statusCode, + String(data: data, encoding: .utf8) ?? "" + ) + } + return try JSONDecoder().decode(ExternalMlxServeHealth.self, from: data) + } + + /// A raw `status: ok` health response is the external server's complete + /// readiness contract. Authentication failures still prove the listener + /// is alive and must not cause an app-owned process reap. + public func livenessWithinDeadline(seconds: TimeInterval) async -> ExternalMlxServeLiveness { + await withTaskGroup(of: ExternalMlxServeLiveness?.self) { group in + group.addTask { + do { + let health = try await self.health() + return health.ok ? .healthy(health) : .unreachable + } catch MTPLXAPIClientError.httpStatus(401, _), + MTPLXAPIClientError.httpStatus(403, _) { + return .aliveUnauthorized + } catch { + return .unreachable + } + } + group.addTask { + try? await Task.sleep( + nanoseconds: UInt64(max(0, seconds) * 1_000_000_000) + ) + return nil + } + let winner = await group.next() ?? nil + group.cancelAll() + return winner ?? .unreachable + } + } + + public static func livenessProbe(baseURL: URL, apiKey: String?) -> ExternalMlxServeAdapter { + let configuration = URLSessionConfiguration.ephemeral + configuration.timeoutIntervalForRequest = 5 + configuration.timeoutIntervalForResource = 10 + configuration.httpMaximumConnectionsPerHost = 1 + configuration.waitsForConnectivity = false + return ExternalMlxServeAdapter( + baseURL: baseURL, + apiKey: apiKey, + session: URLSession(configuration: configuration) + ) + } + + private func makeURL(_ path: String) -> URL { + var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! + let basePath = components.percentEncodedPath + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let endpointPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let joinedPath = [basePath, endpointPath] + .filter { !$0.isEmpty } + .joined(separator: "/") + components.percentEncodedPath = joinedPath.isEmpty ? "/" : "/\(joinedPath)" + components.query = nil + components.fragment = nil + return components.url! + } +} diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/MTPLXCommandBuilder.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/MTPLXCommandBuilder.swift index e6f531f5a..0fa9c3e33 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/MTPLXCommandBuilder.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/MTPLXCommandBuilder.swift @@ -207,6 +207,7 @@ public struct MTPLXCommandBuilder: Sendable { // and a persisted/tuned depth would fail the daemon launch. One seam // here covers every app launch path. let arOnlyModel = MTPLXModelOption.isAROnlyReference(configuration.model) + let externalAROnlyModel = MTPLXModelOption.isExternalAROnlyReference(configuration.model) if arOnlyModel { arguments.append("--no-mtp") } @@ -296,7 +297,12 @@ public struct MTPLXCommandBuilder: Sendable { if configuration.enableThermalPolling { arguments.append("--enable-thermal-poll") } - let fanMode = MTPLXFanMode.normalized(configuration.fanMode) + // The external mlx-serve bridge has no MTPLX fan-controller contract; + // preserve its admission requirement instead of forwarding an app + // setting that guarantees a launch refusal. + let fanMode: MTPLXFanMode = externalAROnlyModel + ? .default + : MTPLXFanMode.normalized(configuration.fanMode) arguments.append(contentsOf: ["--fan-mode", fanMode.rawValue]) if fanMode == .max { arguments.append("--require-max-fans") diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/PortPreflight.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/PortPreflight.swift index 1cb68e30c..85abee271 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Services/PortPreflight.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Services/PortPreflight.swift @@ -17,6 +17,10 @@ public enum PortOccupantKind: Equatable, Sendable { /// A healthy MTPLX daemon answered `/health`. The supervisor decides /// separately whether it is adoptable (app-owned, same model). case mtplxServer(HealthPayload) + /// A raw native mlx-serve listener for the exact external DeepSeek route. + /// It is deliberately distinct from an MTPLX daemon and is never + /// adoptable: its health response has no app launch identity. + case externalMlxServeServer(ExternalMlxServeHealth) /// A live listener rejected the probe with 401/403 — an auth-protected /// server (often an MTPLX daemon with a different API key). Provably /// alive, never adoptable with the current credentials. @@ -35,6 +39,7 @@ public enum PortPreflight { public static func classify( baseURL: URL, apiKey: String?, + backendKind: DaemonBackendKind = .mtplx, timeoutSeconds: TimeInterval = 2 ) async -> PortOccupantKind { let configuration = URLSessionConfiguration.ephemeral @@ -42,10 +47,17 @@ public enum PortPreflight { configuration.timeoutIntervalForResource = timeoutSeconds let session = URLSession(configuration: configuration) defer { session.finishTasksAndInvalidate() } - let client = MTPLXAPIClient(baseURL: baseURL, apiKey: apiKey, session: session) do { - let health = try await client.health() - return health.ok ? .mtplxServer(health) : .foreign + switch backendKind { + case .mtplx: + let client = MTPLXAPIClient(baseURL: baseURL, apiKey: apiKey, session: session) + let health = try await client.health() + return health.ok ? .mtplxServer(health) : .foreign + case .externalMlxServe: + let client = ExternalMlxServeAdapter(baseURL: baseURL, apiKey: apiKey, session: session) + let health = try await client.health() + return health.ok ? .externalMlxServeServer(health) : .foreign + } } catch let error as URLError { switch error.code { case .cannotConnectToHost, .cannotFindHost, .networkConnectionLost: diff --git a/apps/MTPLXApp/Sources/MTPLXAppCore/Stores/MTPLXBackendStore.swift b/apps/MTPLXApp/Sources/MTPLXAppCore/Stores/MTPLXBackendStore.swift index cd75e9a12..d186ab01a 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppCore/Stores/MTPLXBackendStore.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppCore/Stores/MTPLXBackendStore.swift @@ -76,6 +76,7 @@ public enum BenchmarkDaemonReadinessError: Error, Equatable, LocalizedError { case modelDownloadRequired(String) case startupFailed(String) case unreachable(URL) + case unsupportedExternalBackend public var errorDescription: String? { switch self { @@ -85,6 +86,8 @@ public enum BenchmarkDaemonReadinessError: Error, Equatable, LocalizedError { return "Couldn't start MTPLX for the benchmark: \(reason)" case .unreachable(let url): return "Can't reach MTPLX at \(url.absoluteString)." + case .unsupportedExternalBackend: + return "MTPLX's benchmark, metrics, and live-control endpoints are unavailable for the external mlx-serve route." } } } @@ -308,6 +311,25 @@ public final class MTPLXBackendStore: ObservableObject { private var cancelledLaunchIDs: Set = [] private let daemonStartupTimeoutSeconds: TimeInterval = 600 + /// The DeepSeek target-only route is not an MTPLX daemon. Its raw native + /// server exposes OpenAI generation plus `/health`, but not the app's + /// mutable-settings, sessions, metrics, thermal, or benchmark surfaces. + /// Views use this to hide those controls instead of displaying controls + /// that would inevitably fail after a healthy launch. + public var isExternalMlxServeBackend: Bool { + MTPLXModelOption.isExternalAROnlyReference(configuration.model) + } + + public var supportsMTPLXLiveControls: Bool { + !isExternalMlxServeBackend + } + + private func daemonBackendKind(for configuration: MTPLXAppConfiguration) -> DaemonBackendKind { + MTPLXModelOption.isExternalAROnlyReference(configuration.model) + ? .externalMlxServe + : .mtplx + } + public init( configuration: MTPLXAppConfiguration = MTPLXAppConfiguration(), settingsStore: MTPLXSettingsStore = MTPLXSettingsStore(), @@ -377,7 +399,9 @@ public final class MTPLXBackendStore: ObservableObject { let launchID = UUID().uuidString let recoveryTarget = target do { - try await prepareRuntimeForDaemonStart() + if daemonBackendKind(for: next) == .mtplx { + try await prepareRuntimeForDaemonStart() + } if target == .openCode { let result = try openCodeIntegration.sync(configuration: next) await supervisor.logs.append( @@ -417,6 +441,7 @@ public final class MTPLXBackendStore: ObservableObject { command: command, healthBaseURL: baseURL, apiKey: next.apiKey, + backendKind: daemonBackendKind(for: next), probeHealth: true, timeoutSeconds: daemonStartupTimeoutSeconds, expectedLaunchID: launchID, @@ -458,6 +483,10 @@ public final class MTPLXBackendStore: ObservableObject { public func attachExistingDaemonIfOwned() async { await awaitDaemonTeardown() guard !supervisor.isRunning() else { return } + // Native mlx-serve exposes no launch ID, so it is never safe to + // adopt a listener from a previous app session. The normal start + // path performs an ownership-preserving port preflight instead. + guard !isExternalMlxServeBackend else { return } do { let target = defaultLaunchTarget(for: configuration) let command = (try? commandBuilder.buildServeCommand( @@ -557,7 +586,9 @@ public final class MTPLXBackendStore: ObservableObject { healthWatchTask = nil let launchID = UUID().uuidString do { - try await prepareRuntimeForDaemonStart() + if daemonBackendKind(for: configuration) == .mtplx { + try await prepareRuntimeForDaemonStart() + } // Pre-flight the configured port before any integration writes // its config: adoptable app-owned daemons are left for the // supervisor, stale app-owned daemons are replaced in place, @@ -597,6 +628,7 @@ public final class MTPLXBackendStore: ObservableObject { command: command, healthBaseURL: baseURL, apiKey: configuration.apiKey, + backendKind: daemonBackendKind(for: configuration), probeHealth: true, timeoutSeconds: daemonStartupTimeoutSeconds, expectedLaunchID: launchID, @@ -701,7 +733,8 @@ public final class MTPLXBackendStore: ObservableObject { ) async { let occupant = await PortPreflight.classify( baseURL: baseURL, - apiKey: configuration.apiKey + apiKey: configuration.apiKey, + backendKind: daemonBackendKind(for: configuration) ) let occupantDescription: String switch occupant { @@ -731,6 +764,11 @@ public final class MTPLXBackendStore: ObservableObject { return } occupantDescription = "an MTPLX server started outside the app" + case .externalMlxServeServer: + // Raw mlx-serve health carries no app launch identity. Never + // adopt or replace it; choosing a free port avoids a double-load + // while preserving the other process. + occupantDescription = "an external mlx-serve server" case .unauthorized: occupantDescription = "a server requiring a different API key" case .foreign: @@ -782,10 +820,11 @@ public final class MTPLXBackendStore: ObservableObject { let occupiedPort = configuration.port let occupant = await PortPreflight.classify( baseURL: baseURL, - apiKey: configuration.apiKey + apiKey: configuration.apiKey, + backendKind: daemonBackendKind(for: configuration) ) switch occupant { - case .mtplxServer, .unauthorized, .foreign: + case .mtplxServer, .externalMlxServeServer, .unauthorized, .foreign: await preflightConfiguredPort(target: target, launchID: launchID) return true case .free: @@ -1029,6 +1068,9 @@ public final class MTPLXBackendStore: ObservableObject { @discardableResult public func ensureDaemonReadyForBenchmark() async throws -> HealthPayload { + guard supportsMTPLXLiveControls else { + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } if let existing = try? await apiClient.health(), existing.ok { health = existing currentFanMode = verifiedFanMode(from: existing) @@ -1231,6 +1273,7 @@ public final class MTPLXBackendStore: ObservableObject { } public func refreshStaticState() async throws { + guard supportsMTPLXLiveControls else { return } let client = apiClient do { async let health = client.health() @@ -1255,6 +1298,7 @@ public final class MTPLXBackendStore: ObservableObject { } public func refreshSnapshot() async throws { + guard supportsMTPLXLiveControls else { return } do { apply(snapshot: try await apiClient.snapshot()) } catch is DecodingError { @@ -1268,6 +1312,9 @@ public final class MTPLXBackendStore: ObservableObject { } public func updateLiveSettings(_ next: MutableSettings) async throws { + guard supportsMTPLXLiveControls else { + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } let merged = mergedLiveSettingsPatch(next) let livePatch = Self.liveMutableSettingsPatch(from: next) // Only the caller's own patch counts as a depth choice; the @@ -1301,6 +1348,7 @@ public final class MTPLXBackendStore: ObservableObject { } public func refreshLiveSettingsFromDaemon(persist: Bool = false) async throws { + guard supportsMTPLXLiveControls else { return } guard daemonState == .running || supervisor.isRunning() else { return } adoptDaemonSettings(try await apiClient.settings(), persist: persist) } @@ -1335,6 +1383,12 @@ public final class MTPLXBackendStore: ObservableObject { } private func flushPendingLiveSettingsIfNeeded(target: LaunchTarget? = nil) async throws { + guard supportsMTPLXLiveControls else { + pendingLiveSettings = nil + pendingLiveSettingsModel = nil + settings = nil + return + } guard let pending = pendingLiveSettings else { return } guard Self.targetCarriesSettingsSampler(target) else { pendingLiveSettings = nil @@ -1362,6 +1416,12 @@ public final class MTPLXBackendStore: ObservableObject { } private func flushFreshLaunchLiveOnlySettingsIfNeeded() async throws { + guard supportsMTPLXLiveControls else { + pendingLiveSettings = nil + pendingLiveSettingsModel = nil + settings = nil + return + } guard let pending = pendingLiveSettings else { return } guard pendingLiveSettingsModel == nil || pendingLiveSettingsModel == configuration.model else { pendingLiveSettings = nil @@ -1695,20 +1755,36 @@ public final class MTPLXBackendStore: ObservableObject { } public func cancel(requestId: String) async throws { + guard supportsMTPLXLiveControls else { + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } _ = try await apiClient.cancel(requestId: requestId) } public func clearCache() async throws { + guard supportsMTPLXLiveControls else { + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } _ = try await apiClient.clearCache() self.sessions = try await apiClient.sessions() } public func clearSession(sessionId: String) async throws { + guard supportsMTPLXLiveControls else { + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } _ = try await apiClient.clearSession(sessionId: sessionId) self.sessions = try await apiClient.sessions() } public func startMetricsStream() { + guard supportsMTPLXLiveControls else { + streamTask?.cancel() + streamTask = nil + connectionState = .idle + startExternalMlxServeHealthWatchdog() + return + } streamTask?.cancel() let client = MetricsStreamClient(apiClient: apiClient) let interval = configuration.performanceLock ? 1000 : configuration.streamSnapshotIntervalMs @@ -1816,6 +1892,55 @@ public final class MTPLXBackendStore: ObservableObject { } } + /// Watch only the native raw-health contract after an external target-only + /// launch. The process was spawned by this supervisor and its port was + /// checked before launch; MTPLX admin routes are never used as a liveness + /// proxy for this backend. + private func startExternalMlxServeHealthWatchdog() { + healthWatchTask?.cancel() + let probeClient = ExternalMlxServeAdapter.livenessProbe( + baseURL: baseURL, + apiKey: configuration.apiKey + ) + healthWatchTask = Task { @MainActor [weak self] in + defer { probeClient.session.finishTasksAndInvalidate() } + var consecutiveMisses = 0 + var loggedUnauthorized = false + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 3_000_000_000) + guard let self, !Task.isCancelled else { return } + guard self.shouldProbeDaemonHealth else { + consecutiveMisses = 0 + continue + } + switch await probeClient.livenessWithinDeadline( + seconds: Self.watchdogProbeDeadlineSeconds + ) { + case .healthy: + consecutiveMisses = 0 + continue + case .aliveUnauthorized: + consecutiveMisses = 0 + if !loggedUnauthorized { + loggedUnauthorized = true + await self.supervisor.logs.append( + "external mlx-serve rejected raw health authentication; listener is alive, check the API key", + stream: .system + ) + } + continue + case .unreachable: + consecutiveMisses += 1 + } + guard consecutiveMisses >= 2 else { continue } + self.markDaemonUnreachableIfNeeded( + reason: "The external mlx-serve backend stopped answering health checks. Start it again." + ) + return + } + } + } + private func markDaemonUnreachableIfNeeded(reason: String) { switch daemonState { case .running, .warming, .starting: @@ -2242,7 +2367,8 @@ public final class MTPLXBackendStore: ObservableObject { } private func requiresStartupFanRamp(_ configuration: MTPLXAppConfiguration) -> Bool { - fanMode(for: configuration) == .max + daemonBackendKind(for: configuration) == .mtplx + && fanMode(for: configuration) == .max } private func modeRequiresFanRestore(_ mode: String?) -> Bool { @@ -2266,6 +2392,12 @@ public final class MTPLXBackendStore: ObservableObject { /// pre-set the mode so the UI flips immediately; on failure /// `currentFanMode` is rolled back to the previous state. public func setFanMode(_ mode: String) async throws { + guard supportsMTPLXLiveControls else { + // The external mlx-serve launcher is fixed to its safe default + // fan contract. Persisting an MTPLX-only live request here would + // turn a healthy external server into a spurious 404. + throw BenchmarkDaemonReadinessError.unsupportedExternalBackend + } let previous = currentFanMode let previousConfiguration = configuration let fanMode = MTPLXFanMode.normalized(mode) @@ -2301,6 +2433,11 @@ public final class MTPLXBackendStore: ObservableObject { /// Pull thermal detection + current mode + fan summary. Used after /// daemon start so `FanModeToggle` can decide whether to render. public func refreshThermalStatus() async { + guard supportsMTPLXLiveControls else { + thermalStatus = nil + currentFanMode = nil + return + } thermalStatus = try? await apiClient.thermalStatus() if let mode = thermalStatus?.values["current_mode"]?.stringValue, !mode.isEmpty { currentFanMode = MTPLXFanMode.normalized(mode).rawValue @@ -2309,6 +2446,7 @@ public final class MTPLXBackendStore: ObservableObject { } private func shouldRestoreFanModeOnStop() -> Bool { + guard supportsMTPLXLiveControls else { return false } if fanRestoreRequiredOnStop { return true } @@ -2526,6 +2664,27 @@ public final class MTPLXBackendStore: ObservableObject { target: LaunchTarget?, configuration: MTPLXAppConfiguration ) async { + guard daemonBackendKind(for: configuration) == .mtplx else { + // A raw mlx-serve target-only server is ready at this point, but + // it does not implement any MTPLX app endpoint. Do not turn the + // successful `/health {status: ok}` into a failure by probing + // capabilities, sessions, settings, metrics, thermal, or model + // inventory routes that belong exclusively to MTPLX. + health = nil + capabilities = nil + sessions = nil + sessionBank = nil + settings = nil + pendingLiveSettings = nil + pendingLiveSettingsModel = nil + connectionState = .idle + await supervisor.logs.append( + "external mlx-serve ready; MTPLX live controls and metrics are unavailable", + stream: .system + ) + startExternalMlxServeHealthWatchdog() + return + } do { try await refreshStaticState() } catch { @@ -2623,10 +2782,18 @@ public final class MTPLXBackendStore: ObservableObject { } public func refreshPrefillHistory() async { + guard supportsMTPLXLiveControls else { + prefillHistory = nil + return + } prefillHistory = try? await apiClient.prefillHistory() } public func refreshModels() async { + guard supportsMTPLXLiveControls else { + models = nil + return + } models = try? await apiClient.models() } @@ -2652,6 +2819,7 @@ public final class MTPLXBackendStore: ObservableObject { } private func scheduleLateHealthRecovery(launchID: String, target: LaunchTarget?) { + guard supportsMTPLXLiveControls else { return } guard supervisor.isRunning() else { return } lateHealthRecoveryTask?.cancel() lateHealthRecoveryTask = Task { @MainActor [weak self] in diff --git a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Inference/InferenceParamsOverlay.swift b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Inference/InferenceParamsOverlay.swift index 310e128e3..3623a5346 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Inference/InferenceParamsOverlay.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Inference/InferenceParamsOverlay.swift @@ -162,6 +162,7 @@ struct InferenceParamsOverlay: View { @ViewBuilder private var popoverSurface: some View { VStack(alignment: .leading, spacing: 0) { + if backend.supportsMTPLXLiveControls { // Header pinned at the top so the popover always identifies // itself; never scrolls away. header @@ -200,6 +201,9 @@ struct InferenceParamsOverlay: View { if kvDirty || contextWindowDirty { applyBar } + } else { + externalBackendNotice + } } .background( ZStack { @@ -213,6 +217,23 @@ struct InferenceParamsOverlay: View { ) } + private var externalBackendNotice: some View { + VStack(alignment: .leading, spacing: 10) { + Text("External mlx-serve backend") + .font(.system(.callout, design: .rounded).weight(.semibold)) + .foregroundStyle(Brand.typeBody) + Text("This target-only DeepSeek route is running through mlx-serve. MTPLX live sampling, MTP, DSpark, sessions, metrics, cache, and fan controls are unavailable for this backend.") + .font(.caption) + .foregroundStyle(Brand.typeSecondary) + .fixedSize(horizontal: false, vertical: true) + Text("The model’s context window and API key are applied when you restart it from Settings.") + .font(.caption2) + .foregroundStyle(Brand.typeTertiary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(14) + } + /// Cap on the inner scroll area so the whole popover stays bounded /// — header + apply bar live outside it. Tuned to fit comfortably /// inside the default app window without ever clipping at the top diff --git a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Models/ModelPickerOverlay.swift b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Models/ModelPickerOverlay.swift index 0a91254fa..e8e8352fa 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Models/ModelPickerOverlay.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Models/ModelPickerOverlay.swift @@ -809,6 +809,11 @@ private struct CustomModelProbeRow: View { @Binding var acknowledgedNoMTP: Bool let onAddAnyway: () -> Void + private var isExternalTargetOnlyRoute: Bool { + probe.diagnostic == "external_target_only_verified" + && MTPLXModelOption.isCanonicalExternalAROnlyRepoID(probe.hfRepo) + } + var body: some View { let (symbol, color) = icon VStack(alignment: .leading, spacing: 8) { @@ -831,13 +836,17 @@ private struct CustomModelProbeRow: View { if probe.verdict == .noMTP { HStack(alignment: .center, spacing: 10) { Toggle(isOn: $acknowledgedNoMTP) { - Text("Add anyway without the speed boost") + Text( + isExternalTargetOnlyRoute + ? "I have mlx-serve and a 128 GB Apple Silicon Mac" + : "Add anyway without the speed boost" + ) .font(.caption) .foregroundStyle(Brand.typeSecondary) } .toggleStyle(.checkbox) Spacer(minLength: 8) - Button("Add") { + Button(isExternalTargetOnlyRoute ? "Add route" : "Add") { onAddAnyway() } .font(.system(size: 11, weight: .semibold)) diff --git a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Onboarding/Steps/ModelPickStep.swift b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Onboarding/Steps/ModelPickStep.swift index f7d534ea4..ef57aed85 100644 --- a/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Onboarding/Steps/ModelPickStep.swift +++ b/apps/MTPLXApp/Sources/MTPLXAppHost/Views/Onboarding/Steps/ModelPickStep.swift @@ -597,6 +597,8 @@ struct ModelPickStep: View { @ViewBuilder private func probeResultRow(_ probe: OtherModelProbe) -> some View { let (symbol, color) = probeIcon(for: probe.verdict) + let isExternalTargetOnlyRoute = probe.diagnostic == "external_target_only_verified" + && MTPLXModelOption.isCanonicalExternalAROnlyRepoID(probe.hfRepo) VStack(alignment: .leading, spacing: 8) { HStack(alignment: .top, spacing: 8) { Image(systemName: symbol) @@ -619,7 +621,11 @@ struct ModelPickStep: View { get: { orchestrator.state.hasAcknowledgedOtherWarning }, set: { newValue in if newValue { orchestrator.acknowledgeOtherWarning() } } )) { - Text("Continue anyway - I know it'll be slower") + Text( + isExternalTargetOnlyRoute + ? "Continue - I have mlx-serve and a 128 GB Apple Silicon Mac" + : "Continue anyway - I know it'll be slower" + ) .font(.caption) .foregroundStyle(Brand.typeSecondary) } diff --git a/apps/MTPLXApp/Tests/MTPLXAppCoreTests/HuggingFaceProbeForgeTests.swift b/apps/MTPLXApp/Tests/MTPLXAppCoreTests/HuggingFaceProbeForgeTests.swift index 1307c81ee..fd761fbb1 100644 --- a/apps/MTPLXApp/Tests/MTPLXAppCoreTests/HuggingFaceProbeForgeTests.swift +++ b/apps/MTPLXApp/Tests/MTPLXAppCoreTests/HuggingFaceProbeForgeTests.swift @@ -1,3 +1,4 @@ +import CryptoKit import Foundation import XCTest @testable import MTPLXAppCore @@ -596,4 +597,153 @@ final class HuggingFaceProbeForgeTests: XCTestCase { let result = await probe.probe(repo: "mirror-org/some-model") XCTAssertEqual(result.verdict, .noMTP) } + + func testProbeExplainsThePinnedDeepSeekExternalRoute() async { + let fake = FakeRunner() + let repo = "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + let revision = "ac33e4f3ca3546e6cec104558d42161e15814e33" + let configData = try! JSONSerialization.data(withJSONObject: Self.externalTargetOnlyConfigFixture()) + let configBody = String(decoding: configData, as: UTF8.self) + let configHash = SHA256.hash(data: configData) + .map({ String(format: "%02x", $0) }) + .joined() + fake.install( + url: "https://huggingface.co/\(repo)/resolve/\(revision)/config.json", + body: configBody + ) + let names = (0...42).map { "model-layer-\($0).safetensors" } + + ["model-top.safetensors", "config.json", "generation_config.json", "model.safetensors.index.json", "tokenizer.json", "tokenizer_config.json"] + let tree = names.map { ["path": $0] } + let treeData = try! JSONSerialization.data(withJSONObject: tree) + fake.install( + url: "https://huggingface.co/api/models/\(repo)/tree/\(revision)", + body: String(decoding: treeData, as: UTF8.self) + ) + let probe = HuggingFaceProbe( + runner: fake.runner(), + externalAROnlyConfigSHA256: configHash + ) + + let result = await probe.probe( + repo: "https://huggingface.co/philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly/tree/main" + ) + + XCTAssertEqual(result.verdict, .noMTP) + XCTAssertEqual( + result.hfRepo, + "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + ) + XCTAssertTrue(result.message.contains("mlx-serve")) + XCTAssertTrue(result.message.contains("128 GB")) + XCTAssertTrue(result.message.contains("DSpark")) + XCTAssertEqual(result.diagnostic, "external_target_only_verified") + } + + func testPinnedDeepSeekExternalRouteRejectsRemoteConfigAndClosedShardShapeDrift() async { + let repo = "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + let revision = "ac33e4f3ca3546e6cec104558d42161e15814e33" + let config = Self.externalTargetOnlyConfigFixture() + let configData = try! JSONSerialization.data(withJSONObject: config) + let configBody = String(decoding: configData, as: UTF8.self) + let configHash = SHA256.hash(data: configData) + .map({ String(format: "%02x", $0) }) + .joined() + let required = (0...42).map { "model-layer-\($0).safetensors" } + + ["model-top.safetensors", "config.json", "generation_config.json", "model.safetensors.index.json", "tokenizer.json", "tokenizer_config.json"] + + func makeProbe(configBody: String, names: [String], expectedHash: String) -> HuggingFaceProbe { + let fake = FakeRunner() + fake.install( + url: "https://huggingface.co/\(repo)/resolve/\(revision)/config.json", + body: configBody + ) + let treeData = try! JSONSerialization.data(withJSONObject: names.map { ["path": $0] }) + fake.install( + url: "https://huggingface.co/api/models/\(repo)/tree/\(revision)", + body: String(decoding: treeData, as: UTF8.self) + ) + return HuggingFaceProbe( + runner: fake.runner(), + externalAROnlyConfigSHA256: expectedHash + ) + } + + let wrongConfig = await makeProbe( + configBody: configBody.replacingOccurrences(of: "\"dspark_block_size\":0", with: "\"dspark_block_size\":1"), + names: required, + expectedHash: configHash + ).probe(repo: repo) + XCTAssertEqual(wrongConfig.verdict, .probeFailed) + XCTAssertEqual(wrongConfig.diagnostic, "pinned_config_mismatch") + + let missing = await makeProbe( + configBody: configBody, + names: required.filter { $0 != "model-layer-42.safetensors" }, + expectedHash: configHash + ).probe(repo: repo) + XCTAssertEqual(missing.verdict, .probeFailed) + XCTAssertEqual(missing.diagnostic, "closed_44_shard_shape_mismatch") + + let extra = await makeProbe( + configBody: configBody, + names: required + ["model-layer-43.safetensors"], + expectedHash: configHash + ).probe(repo: repo) + XCTAssertEqual(extra.verdict, .probeFailed) + XCTAssertEqual(extra.diagnostic, "closed_44_shard_shape_mismatch") + } + + func testPinnedDeepSeekExternalRouteDoesNotTrustASpoofedOwner() async { + let fake = FakeRunner() + let spoofedRepo = "someone-else/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + let configData = try! JSONSerialization.data(withJSONObject: Self.externalTargetOnlyConfigFixture()) + fake.install( + url: "https://huggingface.co/\(spoofedRepo)/resolve/main/config.json", + body: String(decoding: configData, as: UTF8.self) + ) + + let result = await HuggingFaceProbe(runner: fake.runner()).probe(repo: spoofedRepo) + + XCTAssertEqual(result.verdict, .noMTP) + XCTAssertNotEqual(result.diagnostic, "external_target_only_verified") + XCTAssertFalse(MTPLXModelOption.isCanonicalExternalAROnlyRepoID(spoofedRepo)) + XCTAssertFalse( + MTPLXModelOption.customHuggingFaceModel(repoID: spoofedRepo)?.arOnly ?? true + ) + } + + private static func externalTargetOnlyConfigFixture() -> [String: Any] { + var quantization: [String: Any] = [ + "bits": 8, + "group_size": 64, + "mode": "affine", + "embed": ["bits": 8, "group_size": 64, "mode": "affine"], + "head": ["bits": 8, "group_size": 64, "mode": "affine"], + ] + for layer in 0..<43 { + let recipe: ([Int], Int) = layer < 39 ? ([2, 3, 2], 128) : ([4, 4, 4], 64) + for (projection, bits) in zip(["w1", "w2", "w3"], recipe.0) { + quantization["layers.\(layer).ffn.experts.\(projection)"] = [ + "bits": bits, + "group_size": recipe.1, + "mode": "affine", + ] + } + } + return [ + "architectures": ["DeepseekV4ForCausalLM"], + "model_type": "deepseek_v4", + "num_hidden_layers": 43, + "hidden_size": 4096, + "num_attention_heads": 64, + "num_key_value_heads": 1, + "head_dim": 512, + "vocab_size": 129_280, + "num_nextn_predict_layers": 0, + "dspark_block_size": 0, + "num_experts_per_tok": 6, + "n_routed_experts": 256, + "quantization": quantization, + ] + } } diff --git a/apps/MTPLXApp/Tests/MTPLXAppCoreTests/MTPLXAppCoreTests.swift b/apps/MTPLXApp/Tests/MTPLXAppCoreTests/MTPLXAppCoreTests.swift index 243f9829c..f4988a9a5 100644 --- a/apps/MTPLXApp/Tests/MTPLXAppCoreTests/MTPLXAppCoreTests.swift +++ b/apps/MTPLXApp/Tests/MTPLXAppCoreTests/MTPLXAppCoreTests.swift @@ -1069,6 +1069,28 @@ final class MTPLXAppCoreTests: XCTestCase { XCTAssertFalse(command.arguments.contains("--open-dashboard")) } + func testCommandBuilderAdmitsExternalDeepSeekTargetOnlyRouteAsAR() throws { + let fake = try makeExecutable(named: "mtplx") + let builder = MTPLXCommandBuilder(environment: ["PATH": fake.deletingLastPathComponent().path]) + let model = "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + let command = try builder.buildServeCommand( + configuration: MTPLXAppConfiguration( + executablePath: fake.path, + model: model, + fanMode: "max", + lastTunedDepth: 3 + ) + ) + + XCTAssertTrue(MTPLXModelOption.isExternalAROnlyReference(model)) + XCTAssertTrue(MTPLXModelOption.isAROnlyReference(model)) + XCTAssertFalse(MTPLXModelOption.officialCatalog.contains { $0.hfModelID == model }) + XCTAssertTrue(command.arguments.contains("--no-mtp")) + XCTAssertFalse(command.arguments.contains("--depth")) + XCTAssertTrue(command.arguments.containsInOrder(["--fan-mode", "default"])) + XCTAssertFalse(command.arguments.contains("--require-max-fans")) + } + func testCommandBuilderEmitsRestartRequiredRuntimeSettings() throws { let fake = try makeExecutable(named: "mtplx") let builder = MTPLXCommandBuilder(environment: ["PATH": fake.deletingLastPathComponent().path]) @@ -3517,6 +3539,114 @@ final class MTPLXAppCoreTests: XCTestCase { XCTAssertTrue(MTPLXModelOption.hasCompleteInstall(at: model.path)) } + func testModelInstallDetectionAcceptsOnlyThePinnedDeepSeekTargetOnlyLayout() throws { + unsetenv("MTPLX_APP_DISABLE_LOCAL_MODEL_SCAN") + let previousHome = getenv("HOME").map { String(cString: $0) } + let root = temporaryDirectory() + setenv("HOME", root.path, 1) + defer { + if let previousHome { + setenv("HOME", previousHome, 1) + } else { + unsetenv("HOME") + } + } + + let repo = "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + let revision = "ac33e4f3ca3546e6cec104558d42161e15814e33" + let model = root.appendingPathComponent( + ".mtplx/models/philipjohnbasile--DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly", + isDirectory: true + ) + try FileManager.default.createDirectory(at: model, withIntermediateDirectories: true) + let config = Self.externalTargetOnlyConfigFixture() + try JSONSerialization.data(withJSONObject: config).write( + to: model.appendingPathComponent("config.json") + ) + try """ + {"repo_id":"\(repo)","revision":"\(revision)"} + """.write( + to: model.appendingPathComponent(".mtplx-source.json"), + atomically: true, + encoding: .utf8 + ) + + let shards = (0...42).map { "model-layer-\($0).safetensors" } + + ["model-top.safetensors"] + let sidecars = [ + "generation_config.json", + "tokenizer.json", + "tokenizer_config.json", + ] + for name in sidecars + shards { + try Data([0]).write(to: model.appendingPathComponent(name)) + } + let weightMap = Dictionary(uniqueKeysWithValues: shards.enumerated().map { + ("weight.\($0.offset)", $0.element) + }) + let index = try JSONSerialization.data(withJSONObject: ["weight_map": weightMap]) + try index.write(to: model.appendingPathComponent("model.safetensors.index.json")) + + let option = try XCTUnwrap(MTPLXModelOption.customHuggingFaceModel(repoID: repo)) + XCTAssertTrue(option.arOnly) + XCTAssertEqual(option.sizeBytes, 103_855_774_263) + XCTAssertTrue(option.detail.contains("mlx-serve")) + XCTAssertTrue(MTPLXModelOption.hasCompleteInstall(at: model.path)) + XCTAssertTrue(MTPLXModelOption.isExternalAROnlyReference(model.path)) + XCTAssertFalse( + MTPLXModelOption.isExternalAROnlyReference( + "someone-else/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" + ) + ) + XCTAssertEqual(option.installedLocalPath, model.path) + XCTAssertEqual(option.resolvedReference, model.path) + + // Every marker is binding: a renamed mirror, an unpinned revision, + // a config drift, or a closed-shape violation must not look installed + // to the app before the Python bridge performs its full hash gate. + try """ + {"repo_id":"someone-else/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly","revision":"\(revision)"} + """.write( + to: model.appendingPathComponent(".mtplx-source.json"), + atomically: true, + encoding: .utf8 + ) + XCTAssertFalse(MTPLXModelOption.hasCompleteInstall(at: model.path)) + XCTAssertFalse(MTPLXModelOption.isExternalAROnlyReference(model.path)) + try """ + {"repo_id":"\(repo)","revision":"not-the-pinned-revision"} + """.write( + to: model.appendingPathComponent(".mtplx-source.json"), + atomically: true, + encoding: .utf8 + ) + XCTAssertFalse(MTPLXModelOption.hasCompleteInstall(at: model.path)) + try """ + {"repo_id":"\(repo)","revision":"\(revision)"} + """.write( + to: model.appendingPathComponent(".mtplx-source.json"), + atomically: true, + encoding: .utf8 + ) + var wrongConfig = config + wrongConfig["dspark_block_size"] = 1 + try JSONSerialization.data(withJSONObject: wrongConfig).write( + to: model.appendingPathComponent("config.json") + ) + XCTAssertFalse(MTPLXModelOption.hasCompleteInstall(at: model.path)) + try JSONSerialization.data(withJSONObject: config).write( + to: model.appendingPathComponent("config.json") + ) + try Data([0]).write(to: model.appendingPathComponent("model-extra.safetensors")) + XCTAssertFalse(MTPLXModelOption.hasCompleteInstall(at: model.path)) + try FileManager.default.removeItem(at: model.appendingPathComponent("model-extra.safetensors")) + + try FileManager.default.removeItem( + at: model.appendingPathComponent("model-layer-42.safetensors") + ) + XCTAssertFalse(MTPLXModelOption.hasCompleteInstall(at: model.path)) + } + func testModelInstallDetectionCanBeDisabledForFreshUserQA() throws { unsetenv("MTPLX_APP_DISABLE_LOCAL_MODEL_SCAN") let root = temporaryDirectory() @@ -5132,6 +5262,85 @@ final class MTPLXAppCoreTests: XCTestCase { XCTAssertNil(pendingModelDownload) } + @MainActor + func testExternalMlxServeStoreReachesReadyWithoutMTPLXEndpointProbesAndStops() async throws { + let root = temporaryDirectory() + let model = try Self.makeExternalTargetOnlyModelInstall(in: root) + let port = try freeTCPPort() + let requestLog = root.appendingPathComponent("external-request-paths.log") + let server = try makeExecutable( + named: "fake-mtplx-external-mlxserve", + body: """ + #!/bin/sh + exec /usr/bin/python3 -u - <<'PY' + import json + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + PORT = \(port) + REQUEST_LOG = r'''\(requestLog.path)''' + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_args): + return + + def do_GET(self): + with open(REQUEST_LOG, "a", encoding="utf-8") as handle: + handle.write(self.path + "\\n") + if self.path == "/health": + body = json.dumps({"status": "ok"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever() + PY + """ + ) + let backend = MTPLXBackendStore( + configuration: MTPLXAppConfiguration( + executablePath: server.path, + model: model.path, + port: port, + fanMode: "max" + ), + settingsStore: MTPLXSettingsStore(settingsURL: root.appendingPathComponent("settings.json")), + localFanRestorer: { true } + ) + + await backend.startDaemon(target: .chat) + + XCTAssertEqual(backend.daemonState, .running) + XCTAssertEqual(backend.startupPhase, .ready) + XCTAssertTrue(backend.isExternalMlxServeBackend) + XCTAssertFalse(backend.supportsMTPLXLiveControls) + XCTAssertEqual(backend.connectionState, .idle) + XCTAssertNil(backend.health) + XCTAssertNil(backend.capabilities) + XCTAssertNil(backend.sessions) + XCTAssertNil(backend.settings) + + // Let the external-only watchdog complete one full liveness pass; + // it must keep the raw server ready without falling back to MTPLX + // capabilities, metrics, sessions, or settings probes. + try await Task.sleep(nanoseconds: 3_300_000_000) + XCTAssertEqual(backend.daemonState, .running) + + let paths = try String(contentsOf: requestLog, encoding: .utf8) + .split(whereSeparator: \.isNewline) + .map(String.init) + XCTAssertFalse(paths.isEmpty) + XCTAssertTrue(paths.allSatisfy { $0 == "/health" }, "unexpected MTPLX endpoint(s): \(paths)") + + await backend.stopDaemon() + await backend.awaitDaemonTeardown() + XCTAssertEqual(backend.daemonState, .stopped) + } + func testStopDaemonRestoresFansWhenFanModeCacheIsStale() async throws { let root = temporaryDirectory() try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) @@ -8347,6 +8556,129 @@ final class MTPLXAppCoreTests: XCTestCase { } } + func testDaemonSupervisorAcceptsOwnedRawMlxServeHealthAndStopsIt() async throws { + let port = try freeTCPPort() + let script = try makeExecutable( + named: "fake-raw-mlxserve", + body: """ + #!/bin/sh + exec /usr/bin/python3 -u - <<'PY' + import json + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_args): + return + def do_GET(self): + if self.path == "/health": + body = json.dumps({"status": "ok"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + ThreadingHTTPServer(("127.0.0.1", \(port)), Handler).serve_forever() + PY + """ + ) + let supervisor = DaemonSupervisor(logStore: BoundedLogStore(capacity: 8)) + + let health = try await supervisor.start( + command: DaemonCommand(executableURL: script, arguments: []), + healthBaseURL: URL(string: "http://127.0.0.1:\(port)")!, + backendKind: .externalMlxServe, + probeHealth: true, + timeoutSeconds: 5, + expectedLaunchID: "not-claimable-by-raw-health" + ) + + XCTAssertNil(health, "raw mlx-serve must not be fabricated as MTPLX health") + XCTAssertTrue(supervisor.isRunning()) + await supervisor.stop(graceSeconds: 0.1) + XCTAssertFalse(supervisor.isRunning()) + } + + func testExternalMlxServePortOccupantIsNeverAdoptedOrLaunchedOver() async throws { + let port = try freeTCPPort() + let existing = try makeExecutable( + named: "existing-raw-mlxserve", + body: """ + #!/bin/sh + exec /usr/bin/python3 -u - <<'PY' + import json + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_args): + return + def do_GET(self): + if self.path == "/health": + body = json.dumps({"status": "ok"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + ThreadingHTTPServer(("127.0.0.1", \(port)), Handler).serve_forever() + PY + """ + ) + let existingProcess = Process() + existingProcess.executableURL = existing + try existingProcess.run() + defer { existingProcess.terminate() } + + let baseURL = URL(string: "http://127.0.0.1:\(port)")! + let deadline = Date().addingTimeInterval(5) + var occupant: PortOccupantKind = .free + while Date() < deadline { + occupant = await PortPreflight.classify( + baseURL: baseURL, + apiKey: nil, + backendKind: .externalMlxServe + ) + if case .externalMlxServeServer = occupant { + break + } + try await Task.sleep(nanoseconds: 100_000_000) + } + guard case .externalMlxServeServer(let health) = occupant else { + XCTFail("expected raw external listener, got \(occupant)") + return + } + XCTAssertTrue(health.ok) + + let launchMarker = temporaryDirectory().appendingPathComponent("must-not-launch") + let launch = try makeExecutable( + named: "must-not-replace-raw-mlxserve", + body: "#!/bin/sh\necho launched > '\(launchMarker.path)'\nsleep 5\n" + ) + let supervisor = DaemonSupervisor(logStore: BoundedLogStore()) + do { + _ = try await supervisor.start( + command: DaemonCommand(executableURL: launch, arguments: []), + healthBaseURL: baseURL, + backendKind: .externalMlxServe, + probeHealth: true, + timeoutSeconds: 1 + ) + XCTFail("expected portOccupied") + } catch DaemonSupervisorError.portOccupied(let pid, let launchID) { + XCTAssertNil(pid) + XCTAssertNil(launchID) + } + XCTAssertFalse(FileManager.default.fileExists(atPath: launchMarker.path)) + XCTAssertFalse(supervisor.isRunning()) + } + func testFakeDaemonHealthProbeAndMetricsStreamSmoke() async throws { let port = try freeTCPPort() let script = try makeExecutable( @@ -9408,6 +9740,74 @@ final class MTPLXAppCoreTests: XCTestCase { } } + /// Structural companion to the immutable DeepSeek V4 target-only config. + /// The app uses this signature as a fast local preflight; the Python + /// launcher independently checks the exact published file hash. + private static func externalTargetOnlyConfigFixture() -> [String: Any] { + var quantization: [String: Any] = [ + "bits": 8, + "group_size": 64, + "mode": "affine", + "embed": ["bits": 8, "group_size": 64, "mode": "affine"], + "head": ["bits": 8, "group_size": 64, "mode": "affine"], + ] + for layer in 0..<43 { + let recipe: ([Int], Int) = layer < 39 ? ([2, 3, 2], 128) : ([4, 4, 4], 64) + for (projection, bits) in zip(["w1", "w2", "w3"], recipe.0) { + quantization["layers.\(layer).ffn.experts.\(projection)"] = [ + "bits": bits, + "group_size": recipe.1, + "mode": "affine", + ] + } + } + return [ + "architectures": ["DeepseekV4ForCausalLM"], + "model_type": "deepseek_v4", + "num_hidden_layers": 43, + "hidden_size": 4096, + "num_attention_heads": 64, + "num_key_value_heads": 1, + "head_dim": 512, + "vocab_size": 129_280, + "num_nextn_predict_layers": 0, + "dspark_block_size": 0, + "num_experts_per_tok": 6, + "n_routed_experts": 256, + "quantization": quantization, + ] + } + + private static func makeExternalTargetOnlyModelInstall(in root: URL) throws -> URL { + let model = root.appendingPathComponent( + "DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly", + isDirectory: true + ) + try FileManager.default.createDirectory(at: model, withIntermediateDirectories: true) + try JSONSerialization.data(withJSONObject: externalTargetOnlyConfigFixture()).write( + to: model.appendingPathComponent("config.json") + ) + try """ + {"repo_id":"philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly","revision":"ac33e4f3ca3546e6cec104558d42161e15814e33"} + """.write( + to: model.appendingPathComponent(".mtplx-source.json"), + atomically: true, + encoding: .utf8 + ) + let shards = (0...42).map { "model-layer-\($0).safetensors" } + + ["model-top.safetensors"] + for name in ["generation_config.json", "tokenizer.json", "tokenizer_config.json"] + shards { + try Data([0]).write(to: model.appendingPathComponent(name)) + } + let weightMap = Dictionary(uniqueKeysWithValues: shards.enumerated().map { + ("weight.\($0.offset)", $0.element) + }) + try JSONSerialization.data(withJSONObject: ["weight_map": weightMap]).write( + to: model.appendingPathComponent("model.safetensors.index.json") + ) + return model + } + private func makeHTTPFixtureScript( port: Int, healthJSON: String, diff --git a/docs/model-compatibility.md b/docs/model-compatibility.md index 431185f93..52dda92ff 100644 --- a/docs/model-compatibility.md +++ b/docs/model-compatibility.md @@ -18,3 +18,14 @@ index, tokenizer, generation config, special tokens map, and Poolside chat template. Other Laguna variants — including the earlier uniform-4bit build — remain blocked until they have their own construction-time validation and runtime evidence. + +The external AR route is reserved for +`philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly` at immutable +revision `ac33e4f3ca3546e6cec104558d42161e15814e33`. Admission requires its +DeepSeek V4 target-only configuration, all 44 exact weight shards, required +sidecars, and the closed safetensors index; cached content is hash-checked and +a same-size corrupt file is repaired through an atomic re-download. MTPLX then +executes the separately installed `mlx-serve` binary. The required zero +`num_nextn_predict_layers` / zero `dspark_block_size` contract means an MTP or +DSpark artifact is not silently routed here. The external runtime's memory +preflight remains enabled. Its streaming and throughput are unapproved. diff --git a/docs/quickstart.md b/docs/quickstart.md index 1053f24df..0c1cb184c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -39,6 +39,24 @@ MTP runtime stays loaded, so terminal chat can use `/mtp off`, `/mtp on`, and `mlx-community/Laguna-S-2.1-oQ4e` instead install an unloaded AR route at construction because there is no MTP head to retain. +For the 128 GB DeepSeek-V4 target-only artifact, install or build `mlx-serve` +and use the external AR route: + +```bash +mtplx pull philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly + +MTPLX_MLX_SERVE_BIN=/path/to/mlx-serve \ +mtplx serve \ + --model philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly \ + --no-mtp --yes +``` + +The pull pins revision `ac33e4f3ca3546e6cec104558d42161e15814e33` and admits +the exact 44-shard publication. MTPLX removes ambient `MLX_SERVE_*` settings, +uses `MLX_SERVE_WIRED=fit` and a 256 MB cache limit, and leaves the external +memory preflight on. This is not MTP or DSpark support; representative +streaming performance is unapproved. + For scheduler selection and backend-specific concurrent implementations, see [Concurrency modes](concurrency.md). diff --git a/mtplx/artifacts.py b/mtplx/artifacts.py index e3ccc3812..1f6abd8df 100644 --- a/mtplx/artifacts.py +++ b/mtplx/artifacts.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import os import urllib.parse @@ -29,6 +30,13 @@ is_laguna_s_2_1_mlx_4bit_config, laguna_s_2_1_artifact_integrity_errors, ) +from .models.deepseek_v4_target_only_config import ( + DEEPSEEK_V4_TARGET_ONLY_REPO_ID, + DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES, + DEEPSEEK_V4_TARGET_ONLY_REVISION, + deepseek_v4_target_only_artifact_integrity_errors, + is_deepseek_v4_target_only_config, +) from .profiles import ( DEFAULT_FP16_HF_MODEL_ID, DEFAULT_FP16_PUBLIC_MODEL_ID, @@ -454,6 +462,8 @@ class ModelInspection: num_experts_per_tok: int | None = None laguna_s_2_1_mlx_4bit_match: bool = False laguna_s_2_1_artifacts_complete: bool = False + deepseek_v4_target_only_match: bool = False + deepseek_v4_target_only_artifacts_complete: bool = False mtp_pattern: str | None = None source: str = "local" quantization: dict[str, Any] = field(default_factory=dict) @@ -504,6 +514,8 @@ def to_dict(self) -> dict[str, Any]: "num_experts_per_tok": self.num_experts_per_tok, "laguna_s_2_1_mlx_4bit_match": self.laguna_s_2_1_mlx_4bit_match, "laguna_s_2_1_artifacts_complete": self.laguna_s_2_1_artifacts_complete, + "deepseek_v4_target_only_match": self.deepseek_v4_target_only_match, + "deepseek_v4_target_only_artifacts_complete": self.deepseek_v4_target_only_artifacts_complete, "quantization": self.quantization, "sidecars": self.sidecars, "model_files": list(self.model_files), @@ -929,12 +941,98 @@ def _local_laguna_artifacts_complete(model_path: Path) -> bool: return set(weight_map.values()) == set(LAGUNA_S_2_1_WEIGHT_SHARDS) -def _inspect_hf_model(repo_id: str) -> ModelInspection: - revision = ( - LAGUNA_S_2_1_REVISION - if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold() - else None +def _remote_deepseek_v4_target_only_artifacts_complete( + repo_id: str, files: set[str] +) -> bool: + """Require the complete, immutable public target-only file layout. + + Remote inspection can prove names at the pinned revision. The pull path + subsequently verifies every pinned content hash before a local directory is + admitted to the external runtime. + """ + + return bool( + repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold() + and DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES.issubset(files) + ) + + +def _sha256_file(path: Path) -> str: + """Hash an artifact without materializing a 100+ GB file in memory.""" + + digest = hashlib.sha256() + with path.open("rb") as handle: + while chunk := handle.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _local_deepseek_v4_target_only_artifacts_complete(model_path: Path) -> bool: + """Admit only an exact pull or the sealed content-identical Gold view.""" + + try: + source = json.loads( + (model_path / ".mtplx-source.json").read_text(encoding="utf-8") + ) + except (OSError, UnicodeError, json.JSONDecodeError): + source = None + if source == { + "repo_id": DEEPSEEK_V4_TARGET_ONLY_REPO_ID, + "revision": DEEPSEEK_V4_TARGET_ONLY_REVISION, + }: + return not deepseek_v4_target_only_artifact_integrity_errors(model_path) + + # The original target-only clone view predates MTPLX's source marker. It + # is admissible only when the independently sealed content manifest still + # matches every expected file; inode/ctime data deliberately does not + # participate because APFS clone materialization changes those fields. + seal_path = model_path / ".dsv4-target-only-gold-view-SEAL.json" + try: + seal_bytes = seal_path.read_bytes() + if hashlib.sha256(seal_bytes).hexdigest() != ( + "50cd20ae84b6c7ebe79c27e08da89c3e419fcde5b529fbd6c47f6387bbf0e79f" + ): + return False + seal = json.loads(seal_bytes) + outputs = seal.get("outputs") if isinstance(seal, dict) else None + except (OSError, UnicodeError, json.JSONDecodeError): + return False + if not isinstance(outputs, list): + return False + expected_names = set(DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES) | {"README.md"} + observed_names: set[str] = set() + for output in outputs: + if not isinstance(output, dict): + return False + name = output.get("logical_path") + identity = output.get("path_identity") + if ( + not isinstance(name, str) + or name in observed_names + or name not in expected_names + or not isinstance(identity, dict) + or not isinstance(identity.get("sha256"), str) + ): + return False + try: + current_sha = _sha256_file(model_path / name) + except OSError: + return False + if current_sha != identity["sha256"]: + return False + observed_names.add(name) + return observed_names == expected_names and not deepseek_v4_target_only_artifact_integrity_errors( + model_path, verify_shard_hashes=False ) + + +def _inspect_hf_model(repo_id: str) -> ModelInspection: + if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): + revision = LAGUNA_S_2_1_REVISION + elif repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + revision = DEEPSEEK_V4_TARGET_ONLY_REVISION + else: + revision = None revision_kwargs = {"revision": revision} if revision is not None else {} files, files_error = _hf_list_repo_files(repo_id, **revision_kwargs) config, config_path, config_error = _hf_download_json( @@ -1062,6 +1160,10 @@ def _inspect_hf_model(repo_id: str) -> ModelInspection: repo_id, files, ), + deepseek_v4_target_only_match=is_deepseek_v4_target_only_config(config), + deepseek_v4_target_only_artifacts_complete=( + _remote_deepseek_v4_target_only_artifacts_complete(repo_id, files) + ), mtp_pattern=_mtp_pattern_from_config(config), quantization=quant, sidecars={name: name in files for name in MULTIMODAL_SIDECARS}, @@ -1091,6 +1193,8 @@ def _inspect_hf_model(repo_id: str) -> ModelInspection: num_experts_per_tok=inspection.num_experts_per_tok, laguna_s_2_1_mlx_4bit_match=inspection.laguna_s_2_1_mlx_4bit_match, laguna_s_2_1_artifacts_complete=inspection.laguna_s_2_1_artifacts_complete, + deepseek_v4_target_only_match=inspection.deepseek_v4_target_only_match, + deepseek_v4_target_only_artifacts_complete=inspection.deepseek_v4_target_only_artifacts_complete, mtp_pattern=inspection.mtp_pattern, quantization=inspection.quantization, sidecars=inspection.sidecars, @@ -1151,6 +1255,8 @@ def inspect_model(model_dir: Path | str) -> ModelInspection: laguna_s_2_1_artifacts_complete=_local_laguna_artifacts_complete( Path(pair["target_model"]) ), + deepseek_v4_target_only_match=False, + deepseek_v4_target_only_artifacts_complete=False, mtp_pattern="assistant-pair", quantization=target_quant, sidecars={name: False for name in MULTIMODAL_SIDECARS}, @@ -1224,6 +1330,10 @@ def inspect_model(model_dir: Path | str) -> ModelInspection: laguna_s_2_1_artifacts_complete=_local_laguna_artifacts_complete( model_path ), + deepseek_v4_target_only_match=is_deepseek_v4_target_only_config(config), + deepseek_v4_target_only_artifacts_complete=( + _local_deepseek_v4_target_only_artifacts_complete(model_path) + ), mtp_pattern=_mtp_pattern_from_config(config), quantization=quant, sidecars={name: (model_path / name).exists() for name in MULTIMODAL_SIDECARS}, @@ -1248,6 +1358,8 @@ def inspect_model(model_dir: Path | str) -> ModelInspection: num_experts_per_tok=inspection.num_experts_per_tok, laguna_s_2_1_mlx_4bit_match=inspection.laguna_s_2_1_mlx_4bit_match, laguna_s_2_1_artifacts_complete=inspection.laguna_s_2_1_artifacts_complete, + deepseek_v4_target_only_match=inspection.deepseek_v4_target_only_match, + deepseek_v4_target_only_artifacts_complete=inspection.deepseek_v4_target_only_artifacts_complete, mtp_pattern=inspection.mtp_pattern, quantization=inspection.quantization, sidecars=inspection.sidecars, diff --git a/mtplx/backends/deepseek_v4_mlxserve.py b/mtplx/backends/deepseek_v4_mlxserve.py new file mode 100644 index 000000000..95266a029 --- /dev/null +++ b/mtplx/backends/deepseek_v4_mlxserve.py @@ -0,0 +1,138 @@ +"""External ``mlx-serve`` launch contract for DeepSeek V4 target-only MLX. + +MTPLX does not implement the DeepSeek V4 target graph itself. This module +keeps that boundary explicit: artifact recognition remains in MTPLX while the +OpenAI-compatible server process is the native ``mlx-serve`` executable. +""" + +from __future__ import annotations + +import os +import shutil +import stat +from pathlib import Path +from typing import Mapping + + +BACKEND_ID = "deepseek_v4_mlxserve_ar" +BINARY_ENV = "MTPLX_MLX_SERVE_BIN" +CWD_ENV = "MTPLX_MLX_SERVE_CWD" +DEFAULT_CONTEXT_WINDOW = 8_192 +DEFAULT_TIMEOUT_SECONDS = 300 +DEFAULT_CACHE_LIMIT_BYTES = 256 * 1024 * 1024 + + +class DeepSeekV4MlxServeError(RuntimeError): + """The external runtime could not be admitted safely.""" + + +def _direct_executable(path: Path) -> Path: + try: + resolved = path.expanduser().resolve(strict=True) + info = resolved.stat() + except (OSError, RuntimeError) as exc: + raise DeepSeekV4MlxServeError( + f"mlx-serve executable is unavailable: {path}" + ) from exc + if not stat.S_ISREG(info.st_mode) or not os.access(resolved, os.X_OK): + raise DeepSeekV4MlxServeError( + f"mlx-serve path is not an executable regular file: {resolved}" + ) + return resolved + + +def resolve_binary(env: Mapping[str, str] | None = None) -> Path: + values = os.environ if env is None else env + configured = str(values.get(BINARY_ENV) or "").strip() + if configured: + return _direct_executable(Path(configured)) + discovered = shutil.which("mlx-serve", path=values.get("PATH")) + if not discovered: + raise DeepSeekV4MlxServeError( + "DeepSeek V4 requires mlx-serve on PATH or MTPLX_MLX_SERVE_BIN" + ) + return _direct_executable(Path(discovered)) + + +def resolve_working_directory( + binary: Path, + env: Mapping[str, str] | None = None, +) -> Path: + values = os.environ if env is None else env + configured = str(values.get(CWD_ENV) or "").strip() + if configured: + candidate = Path(configured).expanduser().resolve(strict=True) + if not candidate.is_dir(): + raise DeepSeekV4MlxServeError( + f"MTPLX_MLX_SERVE_CWD is not a directory: {candidate}" + ) + return candidate + + # Source builds currently use cwd-relative dylib search paths. Detect the + # standard /zig-out/bin/mlx-serve layout without baking in a user path. + parents = binary.parents + if len(parents) >= 3 and parents[0].name == "bin" and parents[1].name == "zig-out": + source_root = parents[2] + if (source_root / "lib" / "mlx" / "lib" / "libmlx.dylib").is_file(): + return source_root + return Path.cwd() + + +def child_environment(env: Mapping[str, str] | None = None) -> dict[str, str]: + source = os.environ if env is None else env + child = { + str(key): str(value) + for key, value in source.items() + if not str(key).startswith(("MLX_SERVE_", "MLXSERVE_")) + and key != "MTPLX_DSV4_WIRED" + } + # ``fit`` is the previously exercised residency setting for this roughly + # 100 GB target-only artifact. Keep it explicit instead of inheriting a + # caller's ambient MLX_SERVE_WIRED state. This is a launch-safety default, + # not a representative streaming-performance claim; callers may make an + # explicit override via the non-filtered MTPLX_DSV4_WIRED variable. + child["MLX_SERVE_WIRED"] = str( + source.get("MTPLX_DSV4_WIRED", "fit") + ).strip() or "fit" + child["MLX_SERVE_CACHE_LIMIT"] = str(DEFAULT_CACHE_LIMIT_BYTES) + return child + + +def build_command( + *, + binary: Path, + model: str, + host: str, + port: int, + context_window: int | None, + api_key: str | None, +) -> list[str]: + context = DEFAULT_CONTEXT_WINDOW if context_window is None else int(context_window) + if context <= 0 or context > 1_048_576: + raise DeepSeekV4MlxServeError( + "DeepSeek V4 context window must be between 1 and 1048576" + ) + command = [ + str(binary), + "--model", + str(model), + "--serve", + "--host", + str(host), + "--port", + str(int(port)), + "--no-pld", + "--no-decode-attn-quant", + "--no-vision", + "--ctx-size", + str(context), + "--timeout", + str(DEFAULT_TIMEOUT_SECONDS), + "--max-resident-models", + "1", + "--max-resident-mem", + "110GB", + ] + if api_key: + command.extend(("--api-key", str(api_key))) + return command diff --git a/mtplx/backends/descriptors.py b/mtplx/backends/descriptors.py index 90f16c4e5..76a85a008 100644 --- a/mtplx/backends/descriptors.py +++ b/mtplx/backends/descriptors.py @@ -429,6 +429,62 @@ def supports(self, capability: str) -> bool: ) +DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR = BackendDescriptor( + backend_id="deepseek_v4_mlxserve_ar", + architecture_id="deepseek-v4-mlxserve-ar", + model_family="deepseek", + display_name="DeepSeek V4 Flash 0731 target-only AR (mlx-serve)", + artifact_layout="single_mlx_folder_external_mlx_serve_target_only_ar", + runtime_capabilities=("target_logits", "target_only_ar", "external_mlx_serve"), + sampler_defaults=SamplerDefaults(temperature=0.6, top_p=0.95, top_k=20), + reasoning_codec=ReasoningCodec( + parser="none", + display_name="DeepSeek V4 native response channels", + default_mode="auto", + supported=True, + modes=("auto", "on", "off"), + history_policy="native_mlx_serve", + effort_levels=("low", "high", "max"), + ), + draft_semantics=DraftSemantics( + request_field="depth", + display_label="Draft depth", + default=1, + minimum=1, + maximum=1, + unit="depth", + ), + uses_external_assistant=False, + uses_draft_lm_head=False, + tune_policy=TunePolicy( + supported=False, + supported_families=(), + unsupported_reason="This artifact is target-only; it has no MTP or DSpark weights.", + ), + kv_quant_policy=KVQuantPolicy( + supported=False, + disabled_reason="The external launch keeps decode-attention quantization disabled.", + ), + context_window_policy=ContextWindowPolicy( + maximum=1_048_576, + default=8_192, + source="deepseek_v4_flash_0731_target_only_config", + ), + default_max_response_tokens=32_768, + default_tool_prompt_mode="native", + required_tool_prompt_mode="native", + allows_chat_template_path=False, + validation_status="external_runtime_experimental_performance_unapproved", + status="external_runtime_experimental_performance_unapproved", + profile_policy="external-runtime-owned", + notes=( + "The checkpoint has no MTP or DSpark weights.", + "MTPLX delegates serving to a separately installed mlx-serve binary.", + "Representative streaming performance is unapproved; dry runs do not establish a speed claim.", + ), +) + + MLX_LM_AR_DESCRIPTOR = BackendDescriptor( backend_id="mlx_lm_ar", architecture_id="mlx-lm-ar-family", @@ -806,6 +862,7 @@ def supports(self, capability: str) -> bool: DESCRIPTORS_BY_BACKEND_ID: dict[str, BackendDescriptor] = { QWEN3_NEXT_DESCRIPTOR.backend_id: QWEN3_NEXT_DESCRIPTOR, LAGUNA_AR_DESCRIPTOR.backend_id: LAGUNA_AR_DESCRIPTOR, + DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR.backend_id: DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR, MLX_LM_AR_DESCRIPTOR.backend_id: MLX_LM_AR_DESCRIPTOR, NATIVE_CONTRACT_DESCRIPTOR.backend_id: NATIVE_CONTRACT_DESCRIPTOR, GEMMA4_ASSISTANT_DESCRIPTOR.backend_id: GEMMA4_ASSISTANT_DESCRIPTOR, @@ -919,6 +976,8 @@ def tune_policy_for_model( descriptor: BackendDescriptor | None = None, ) -> TunePolicy: descriptor = descriptor or descriptor_from_inspection(inspection) + if descriptor.backend_id == DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR.backend_id: + return descriptor.tune_policy family = model_family_from_inspection( inspection, model_ref=model_ref, @@ -942,6 +1001,8 @@ def kv_quant_policy_for_model( descriptor: BackendDescriptor | None = None, ) -> KVQuantPolicy: descriptor = descriptor or descriptor_from_inspection(inspection) + if descriptor.backend_id == DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR.backend_id: + return descriptor.kv_quant_policy family = model_family_from_inspection( inspection, model_ref=model_ref, @@ -986,6 +1047,10 @@ def context_window_policy_for_model( descriptor: BackendDescriptor | None = None, ) -> ContextWindowPolicy: descriptor = descriptor or descriptor_from_inspection(inspection) + if descriptor.backend_id == DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR.backend_id: + return descriptor.context_window_policy.with_resolved_max( + _context_window_from_inspection(inspection) + ) family = model_family_from_inspection( inspection, model_ref=model_ref, @@ -1012,6 +1077,8 @@ def reasoning_policy_for_model( descriptor: BackendDescriptor | None = None, ) -> ReasoningCodec: descriptor = descriptor or descriptor_from_inspection(inspection) + if descriptor.backend_id == DEEPSEEK_V4_MLXSERVE_AR_DESCRIPTOR.backend_id: + return descriptor.reasoning_codec family = model_family_from_inspection( inspection, model_ref=model_ref, diff --git a/mtplx/backends/registry.py b/mtplx/backends/registry.py index e3200d1ee..9397e1385 100644 --- a/mtplx/backends/registry.py +++ b/mtplx/backends/registry.py @@ -13,6 +13,7 @@ RUNTIME_CONTRACT_FILE = "mtplx_runtime.json" SUPPORTED_ARCH_IDS = { "laguna-s-2.1-ar", + "deepseek-v4-mlxserve-ar", "deepseek-v4", "qwen3-next-mtp", "deepseek-v3-mtp", @@ -137,6 +138,23 @@ def to_dict(self) -> dict[str, Any]: "with mtp=False." ), ), + "deepseek-v4-mlxserve-ar": ArchitectureSupport( + arch_id="deepseek-v4-mlxserve-ar", + display_name="DeepSeek V4 Flash 0731 target-only (external mlx-serve)", + family="deepseek", + backend="deepseek_v4_mlxserve_ar", + support_level="external-runtime-experimental", + runtime_compatibility="external-mem-preflight-required", + can_run_verified=True, + aliases=(), + config_markers=(), + family_gate="exact-pinned-target-only-artifact", + notes=( + "Only the exact pinned 44-shard target-only artifact is admitted. " + "MTPLX delegates execution to an independently installed mlx-serve binary; " + "this is neither MTP nor DSpark support." + ), + ), "lfm2-moe-ar": ArchitectureSupport( arch_id="lfm2-moe-ar", display_name="LiquidAI LFM2.5 MoE (MLX)", @@ -1236,6 +1254,32 @@ def compatibility_for_inspection(inspection: Any) -> CompatibilityVerdict: contract_path = getattr(inspection, "runtime_contract_path", None) if not contract_path: contract_path = str(_contract_path(model_dir)) if _contract_path(model_dir).exists() else None + if ( + not has_mtp + and bool(getattr(inspection, "deepseek_v4_target_only_match", False)) + and bool( + getattr(inspection, "deepseek_v4_target_only_artifacts_complete", False) + ) + ): + support = ARCHITECTURE_CATALOG["deepseek-v4-mlxserve-ar"] + return CompatibilityVerdict( + tier=TIER_AR_ONLY, + arch_id=support.arch_id, + supported=True, + recognized=True, + can_run=True, + exit_code=EXIT_VERIFIED, + message=( + "Exact DeepSeek V4 Flash 0731 target-only artifact admitted for " + "the external mlx-serve AR route. It has no MTP or DSpark weights." + ), + recommended_backend=support.backend, + recommended_profile=DEFAULT_PROFILE_NAME, + mtp_supported="no", + runtime_compatibility=support.runtime_compatibility, + support_level=support.support_level, + support_notes=support.notes, + ) if _requires_remote_code(model_dir): support = architecture_support_for(detected_arch_id) return CompatibilityVerdict( diff --git a/mtplx/commands/public.py b/mtplx/commands/public.py index 5b983e32d..c620c41e7 100644 --- a/mtplx/commands/public.py +++ b/mtplx/commands/public.py @@ -67,6 +67,14 @@ TIER_ARCH_COMPATIBLE_UNVERIFIED, architecture_catalog, ) +from mtplx.backends.deepseek_v4_mlxserve import ( + BACKEND_ID as DEEPSEEK_V4_MLXSERVE_BACKEND_ID, + DeepSeekV4MlxServeError, + build_command as build_deepseek_v4_mlxserve_command, + child_environment as deepseek_v4_mlxserve_environment, + resolve_binary as resolve_deepseek_v4_mlxserve_binary, + resolve_working_directory as resolve_deepseek_v4_mlxserve_working_directory, +) from mtplx.backends.descriptors import ( descriptor_for_architecture_id, descriptor_for_backend_id, @@ -715,15 +723,29 @@ def _apply_runtime_compatibility_mode( setattr(args, "depth", 0) setattr(args, "load_mtp", False) return None - if runtime_compatibility != "native-ar-only": + if runtime_compatibility not in { + "native-ar-only", + "external-mem-preflight-required", + }: return None if _generation_mode_from_args(args) != GENERATION_MODE_AR: # Same founder directive as the missing-head case: target-only AR # architectures degrade loudly instead of blocking on --no-mtp. - printer( - "target-only AR architecture -> mtp_off: serving autoregressive " - "(this checkpoint family has no native MTP head)." - ) + if runtime_compatibility == "external-mem-preflight-required": + compatibility_note = ( + "external target-only AR artifact -> mtp_off: delegating to " + "mlx-serve after its memory preflight (no MTP or DSpark)." + ) + else: + compatibility_note = ( + "target-only AR architecture -> mtp_off: serving autoregressive " + "(this checkpoint family has no native MTP head)." + ) + # JSON dry runs must remain one parseable document, while still + # preserving the explicit target-only admission explanation that the + # terminal path prints before it changes generation mode. + setattr(args, "_runtime_compatibility_note", compatibility_note) + printer(compatibility_note) _set_generation_mode_on_args(args, GENERATION_MODE_AR) setattr(args, "depth", 0) setattr(args, "load_mtp", False) @@ -8055,7 +8077,7 @@ def _redact_command_tokens(cmd: list[str]) -> list[str]: def _serve_dry_run_env_delta(env: dict[str, str]) -> dict[str, str]: delta: dict[str, str] = {} for key, value in sorted(env.items()): - if not key.startswith("MTPLX_"): + if not key.startswith(("MTPLX_", "MLX_SERVE_")): continue if os.environ.get(key) == value: continue @@ -8066,6 +8088,109 @@ def _serve_dry_run_env_delta(env: dict[str, str]) -> dict[str, str]: return delta +def _deepseek_v4_mlxserve_admission_error( + *, + dry_run: bool, + quiet_json: bool, + detail: str, +) -> int: + """Render external-runtime admission failures without corrupting JSON UX.""" + + if dry_run and quiet_json: + _print( + { + "ok": False, + "dry_run": True, + "target": "server", + "error": "external_runtime_admission_failed", + "backend_id": DEEPSEEK_V4_MLXSERVE_BACKEND_ID, + "external_runtime": "mlx-serve", + "detail": _redact_secret_value(detail), + } + ) + else: + _print_serve_start_line(f"error: {detail}") + return 2 + + +def _serve_deepseek_v4_mlxserve( + args: Any, + *, + runtime_model: str, + profile_name: str, + model_id: str, + generation_mode: str, + fan_mode: str, + api_key: str | None, + dry_run: bool, + quiet_json: bool, +) -> int: + """Hand the exact target-only artifact to its external native runtime.""" + + if fan_mode != "default": + return _deepseek_v4_mlxserve_admission_error( + dry_run=dry_run, + quiet_json=quiet_json, + detail="the external DeepSeek V4 backend currently requires --fan-mode default", + ) + try: + binary = resolve_deepseek_v4_mlxserve_binary() + cwd = resolve_deepseek_v4_mlxserve_working_directory(binary) + child_env = deepseek_v4_mlxserve_environment() + command = build_deepseek_v4_mlxserve_command( + binary=binary, + model=runtime_model, + host=str(args.host), + port=int(args.port), + context_window=getattr(args, "context_window", None), + api_key=api_key, + ) + except (DeepSeekV4MlxServeError, OSError, ValueError) as exc: + return _deepseek_v4_mlxserve_admission_error( + dry_run=dry_run, + quiet_json=quiet_json, + detail=str(exc), + ) + + if not quiet_json: + _print_serve_handoff(args, runtime_model, profile_name) + _print_serve_start_line("[4/6] Backend selected: external mlx-serve target-only AR") + _print_serve_start_line(" MTP/DSpark: unavailable for this artifact") + + if dry_run: + payload = _serve_dry_run_payload( + args, + runtime_model=runtime_model, + profile_name=profile_name, + model_id=model_id, + generation_mode=generation_mode, + cmd=command, + env=child_env, + ) + payload.update( + { + "backend_id": DEEPSEEK_V4_MLXSERVE_BACKEND_ID, + "external_runtime": "mlx-serve", + "external_runtime_cwd": str(cwd), + "mtp_available": False, + "dspark_available": False, + "memory_preflight": "required", + } + ) + if bool(getattr(args, "json", False)): + _print(payload) + else: + _print_serve_dry_run_human(payload) + return 0 + + return _run_server_child_with_app_parent_watchdog( + command, + env=child_env, + cwd=cwd, + app_parent_pid=_app_parent_pid_from_env(child_env), + ) + + def _serve_dry_run_payload( args: Any, *, @@ -8100,6 +8225,9 @@ def _serve_dry_run_payload( } if bool(getattr(args, "download", False)): payload["download_requested"] = True + compatibility_note = getattr(args, "_runtime_compatibility_note", None) + if isinstance(compatibility_note, str) and compatibility_note: + payload["runtime_compatibility_note"] = compatibility_note return payload @@ -8576,7 +8704,7 @@ def cmd_serve_public(args: Any) -> int: mode_exit = _apply_runtime_compatibility_mode( args, inspection, - printer=_print_serve_start_line, + printer=(lambda _line: None) if quiet_json else _print_serve_start_line, ) if mode_exit is not None: return mode_exit @@ -8592,6 +8720,18 @@ def cmd_serve_public(args: Any) -> int: _apply_backend_serve_defaults(args, inspection) _apply_qwen36_35b_optimized_speed_defaults(args, model_id) backend_descriptor = descriptor_from_inspection(inspection) + if backend_descriptor.backend_id == DEEPSEEK_V4_MLXSERVE_BACKEND_ID: + return _serve_deepseek_v4_mlxserve( + args, + runtime_model=str(runtime_model), + profile_name=profile.name, + model_id=str(model_id), + generation_mode=generation_mode, + fan_mode=fan_mode, + api_key=api_key, + dry_run=dry_run, + quiet_json=quiet_json, + ) draft_lm_head = _model_draft_lm_head_spec(inspection, profile) or { "bits": 4, "group_size": 64, diff --git a/mtplx/hf_loader.py b/mtplx/hf_loader.py index f3c2913b3..c00350b62 100644 --- a/mtplx/hf_loader.py +++ b/mtplx/hf_loader.py @@ -4,6 +4,7 @@ import contextlib import errno +import hashlib import importlib import json import os @@ -22,6 +23,15 @@ LAGUNA_S_2_1_REVISION, laguna_s_2_1_artifact_integrity_errors, ) +from mtplx.models.deepseek_v4_target_only_config import ( + DEEPSEEK_V4_TARGET_ONLY_REPO_BYTES, + DEEPSEEK_V4_TARGET_ONLY_REPO_ID, + DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES, + DEEPSEEK_V4_TARGET_ONLY_REVISION, + DEEPSEEK_V4_TARGET_ONLY_SHARD_SHA256, + DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256, + deepseek_v4_target_only_artifact_integrity_errors, +) from mtplx.profiles import DEFAULT_PROFILE_NAME @@ -56,16 +66,32 @@ def _effective_model_revision(repo_id: str, revision: str | None) -> str | None: f"{LAGUNA_S_2_1_REVISION}" ) return LAGUNA_S_2_1_REVISION + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + if revision is not None and revision != DEEPSEEK_V4_TARGET_ONLY_REVISION: + raise ValueError( + "DeepSeek V4 target-only support is pinned to revision " + f"{DEEPSEEK_V4_TARGET_ONLY_REVISION}" + ) + return DEEPSEEK_V4_TARGET_ONLY_REVISION return revision +def _pinned_source_identity(repo_id: str) -> tuple[str, str] | None: + if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): + return LAGUNA_S_2_1_REPO_ID, LAGUNA_S_2_1_REVISION + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + return DEEPSEEK_V4_TARGET_ONLY_REPO_ID, DEEPSEEK_V4_TARGET_ONLY_REVISION + return None + + def _source_marker_matches( destination: Path, *, repo_id: str, revision: str | None, ) -> bool: - if repo_id.casefold() != LAGUNA_S_2_1_REPO_ID.casefold(): + pinned = _pinned_source_identity(repo_id) + if pinned is None: return True try: payload = json.loads( @@ -73,7 +99,11 @@ def _source_marker_matches( ) except (OSError, UnicodeError, json.JSONDecodeError): return False - return payload == {"repo_id": repo_id, "revision": revision} + canonical_repo_id, canonical_revision = pinned + return payload == { + "repo_id": canonical_repo_id, + "revision": canonical_revision, + } and revision == canonical_revision def _write_source_marker( @@ -82,6 +112,9 @@ def _write_source_marker( repo_id: str, revision: str | None, ) -> None: + pinned = _pinned_source_identity(repo_id) + if pinned is not None: + repo_id, revision = pinned (destination / SOURCE_MARKER_FILE).write_text( json.dumps( {"repo_id": repo_id, "revision": revision}, @@ -104,19 +137,68 @@ def _validate_pinned_laguna_files(destination: Path, repo_id: str) -> None: ) -def _pull_validation(path: Path, repo_id: str) -> dict[str, Any]: +def _validate_pinned_deepseek_v4_files(destination: Path, repo_id: str) -> None: + if repo_id.casefold() != DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + return + missing_or_wrong = deepseek_v4_target_only_artifact_integrity_errors(destination) + if missing_or_wrong: + raise RuntimeError( + "pinned DeepSeek V4 target-only snapshot is incomplete or differs " + f"from revision {DEEPSEEK_V4_TARGET_ONLY_REVISION}: " + + ", ".join(sorted(missing_or_wrong)) + ) + + +def _validate_pinned_model_files(destination: Path, repo_id: str) -> None: + _validate_pinned_laguna_files(destination, repo_id) + _validate_pinned_deepseek_v4_files(destination, repo_id) + + +def _pinned_artifact_integrity_errors( + destination: Path, repo_id: str +) -> tuple[str, ...]: + """Name same-size files which must be atomically re-fetched by a pull.""" + + if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): + return laguna_s_2_1_artifact_integrity_errors(destination) + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + return deepseek_v4_target_only_artifact_integrity_errors(destination) + return () + + +def _pinned_file_sha256(repo_id: str, path: str) -> str | None: + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + return ( + DEEPSEEK_V4_TARGET_ONLY_SHARD_SHA256.get(path) + or DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256.get(path) + ) + return None + + +def _pull_validation( + path: Path, repo_id: str, *, pinned_integrity_checked: bool = False +) -> dict[str, Any]: validation = validate_mtplx_model_files(path) - if repo_id.casefold() != LAGUNA_S_2_1_REPO_ID.casefold(): + if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): + required_files = LAGUNA_S_2_1_REQUIRED_FILES + elif repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + required_files = DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES + else: return validation - _validate_pinned_laguna_files(path, repo_id) + if not pinned_integrity_checked: + _validate_pinned_model_files(path, repo_id) return { **validation, "ok": True, "missing_files": [], "contract_error": None, - "required_files": sorted(LAGUNA_S_2_1_REQUIRED_FILES), + "required_files": sorted(required_files), "mtp_supported": False, - "runtime_compatibility": "native-ar-only", + "runtime_compatibility": ( + "external-mem-preflight-required" + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold() + else "native-ar-only" + ), } @@ -358,7 +440,12 @@ def _repo_requires_qwen_mtplx_payload(repo_id: str) -> bool: return lower.startswith("youssofal/qwen3.") and "mtplx" in lower -def _cached_model_ready_for_repo(path: Path, repo_id: str) -> bool: +def _cached_model_ready_for_repo( + path: Path, + repo_id: str, + *, + pinned_integrity_errors: tuple[str, ...] | None = None, +) -> bool: if not cached_model_is_complete(path): return False if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): @@ -368,9 +455,26 @@ def _cached_model_ready_for_repo(path: Path, repo_id: str) -> bool: revision=LAGUNA_S_2_1_REVISION, ): return False - try: - _validate_pinned_laguna_files(path, repo_id) - except RuntimeError: + errors = ( + laguna_s_2_1_artifact_integrity_errors(path) + if pinned_integrity_errors is None + else pinned_integrity_errors + ) + if errors: + return False + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold(): + if not _source_marker_matches( + path, + repo_id=repo_id, + revision=DEEPSEEK_V4_TARGET_ONLY_REVISION, + ): + return False + errors = ( + deepseek_v4_target_only_artifact_integrity_errors(path) + if pinned_integrity_errors is None + else pinned_integrity_errors + ) + if errors: return False if _repo_requires_qwen_mtplx_payload(repo_id): return bool(validate_mtplx_model_files(path).get("ok")) @@ -610,6 +714,29 @@ def _iter_response_bytes(response: Any) -> Iterator[bytes]: raise RuntimeError("Hugging Face response does not support byte streaming") +def _verify_downloaded_file( + path: Path, + *, + repo_file: RepoFile, + expected_sha256: str | None, +) -> None: + if repo_file.size_bytes is not None and path.stat().st_size != repo_file.size_bytes: + raise RuntimeError( + f"incomplete download for {repo_file.path}: " + f"expected {repo_file.size_bytes} bytes, got {path.stat().st_size}" + ) + if expected_sha256 is None: + return + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(DOWNLOAD_CHUNK_SIZE), b""): + digest.update(chunk) + if digest.hexdigest() != expected_sha256: + raise RuntimeError( + f"downloaded file failed pinned SHA-256 verification: {repo_file.path}" + ) + + def _download_repo_file( repo_file: RepoFile, *, @@ -626,21 +753,22 @@ def _download_repo_file( progress_interval_s: float, last_emit_at: float, last_emit_size: int, + force: bool = False, + expected_sha256: str | None = None, ) -> tuple[float, int]: target = _safe_destination_for_repo_file(destination, repo_file) target.parent.mkdir(parents=True, exist_ok=True) expected_size = repo_file.size_bytes - if expected_size is not None and target.exists() and target.stat().st_size == expected_size: + if not force and expected_size is not None and target.exists() and target.stat().st_size == expected_size: return last_emit_at, last_emit_size - if expected_size is None and target.exists() and target.stat().st_size > 0: + if not force and expected_size is None and target.exists() and target.stat().st_size > 0: return last_emit_at, last_emit_size partial = target.with_name(target.name + ".incomplete") - if target.exists(): - if not partial.exists(): - target.replace(partial) - else: - target.unlink() + # Retain a known-bad target until the replacement is complete and hashed; + # a failed repair must not discard the only local copy. + if force and partial.exists(): + partial.unlink() existing = partial.stat().st_size if partial.exists() else 0 if expected_size is not None and existing > expected_size: partial.unlink() @@ -657,6 +785,11 @@ def _download_repo_file( partial.unlink(missing_ok=True) existing = 0 elif existing > 0 and status_code == 416 and expected_size is not None and existing == expected_size: + _verify_downloaded_file( + partial, + repo_file=repo_file, + expected_sha256=expected_sha256, + ) partial.replace(target) return _emit_current_download_size( callback, @@ -687,11 +820,11 @@ def _download_repo_file( last_emit_size=last_emit_size, file_path=repo_file.path, ) - if expected_size is not None and partial.stat().st_size != expected_size: - raise RuntimeError( - f"incomplete download for {repo_file.path}: " - f"expected {expected_size} bytes, got {partial.stat().st_size}" - ) + _verify_downloaded_file( + partial, + repo_file=repo_file, + expected_sha256=expected_sha256, + ) partial.replace(target) return _emit_current_download_size( callback, @@ -712,6 +845,7 @@ def _download_snapshot_with_structured_progress( destination: Path, progress_callback: DownloadProgressCallback | None, progress_interval_s: float, + force_paths: frozenset[str] = frozenset(), ) -> tuple[Path, int | None]: HfApi, hf_hub_url, get_session, build_hf_headers, hf_raise_for_status = _hub_runtime() try: @@ -760,6 +894,12 @@ def _download_snapshot_with_structured_progress( progress_interval_s=max(0.1, progress_interval_s), last_emit_at=last_emit_at, last_emit_size=last_emit_size, + force=repo_file.path in force_paths, + expected_sha256=( + _pinned_file_sha256(repo_id, repo_file.path) + if repo_file.path in force_paths + else None + ), ) except Exception as exc: raise RuntimeError(_classify_pull_error(exc, repo_id)) from exc @@ -798,6 +938,14 @@ def list_cached_models(*, cache_dir: str | Path | None = None) -> list[CachedMod if not child.is_dir() or child.name.startswith("."): continue repo_id = child.name.replace("--", "/") + try: + validation = _pull_validation(child, repo_id) + except RuntimeError as exc: + validation = { + **validate_mtplx_model_files(child), + "ok": False, + "contract_error": str(exc), + } rows.append( CachedModel( repo_id=repo_id, @@ -805,7 +953,7 @@ def list_cached_models(*, cache_dir: str | Path | None = None) -> list[CachedMod size_bytes=directory_size_bytes(child), has_runtime_contract=(child / "mtplx_runtime.json").exists(), has_config=(child / "config.json").exists(), - validation=validate_mtplx_model_files(child), + validation=validation, ) ) return rows @@ -857,9 +1005,28 @@ def pull_model( destination = cached_model_path(repo_id, cache_dir=root) started_size = directory_size_bytes(destination) + pinned_integrity_errors = ( + _pinned_artifact_integrity_errors(destination, repo_id) + if destination.is_dir() and _pinned_source_identity(repo_id) is not None + else None + ) + repair_paths = ( + frozenset( + error.partition(":")[0] + for error in pinned_integrity_errors + if error.partition(":")[0] + ) + if pinned_integrity_errors + else frozenset() + ) + pinned_integrity_checked = pinned_integrity_errors is not None if ( destination.exists() - and _cached_model_ready_for_repo(destination, repo_id) + and _cached_model_ready_for_repo( + destination, + repo_id, + pinned_integrity_errors=pinned_integrity_errors, + ) and _source_marker_matches( destination, repo_id=repo_id, @@ -871,7 +1038,7 @@ def pull_model( reused_existing = True resumed_existing = False validation = validate_mtplx_model_files(resolved) - _validate_pinned_laguna_files(resolved, repo_id) + _validate_pinned_model_files(resolved, repo_id) if repo_id.lower().startswith("youssofal/qwen3.6-27b-mtplx") and not validation["ok"]: raise RuntimeError( "cached MTPLX model is incomplete: " @@ -895,6 +1062,8 @@ def pull_model( total_bytes = ( LAGUNA_S_2_1_REPO_BYTES if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold() + else DEEPSEEK_V4_TARGET_ONLY_REPO_BYTES + if repo_id.casefold() == DEEPSEEK_V4_TARGET_ONLY_REPO_ID.casefold() else _query_repo_total_bytes(repo_id, revision=revision) if progress_callback is not None else None @@ -921,13 +1090,17 @@ def pull_model( else contextlib.nullcontext() ) with progress_suppression: - if progress_callback is not None: + # A generic Hub snapshot treats a same-size local file as reusable. + # For a pinned identity known to be corrupt, force the streaming + # path so every bad target is hash-verified then atomically replaced. + if progress_callback is not None or repair_paths: resolved, total_bytes_from_download = _download_snapshot_with_structured_progress( repo_id=repo_id, revision=revision, destination=destination, progress_callback=progress_callback, progress_interval_s=progress_interval_s, + force_paths=repair_paths, ) if total_bytes_from_download: total_bytes = total_bytes_from_download @@ -966,8 +1139,8 @@ def pull_model( "downloaded MTPLX model is incomplete: " + ", ".join(validation["missing_files"] or [str(validation.get("contract_error"))]) ) - _validate_pinned_laguna_files(resolved, repo_id) - if repo_id.casefold() == LAGUNA_S_2_1_REPO_ID.casefold(): + _validate_pinned_model_files(resolved, repo_id) + if _pinned_source_identity(repo_id) is not None: _write_source_marker( resolved, repo_id=repo_id, @@ -996,7 +1169,11 @@ def pull_model( "size_bytes": directory_size_bytes(resolved), "has_runtime_contract": (resolved / "mtplx_runtime.json").exists(), "has_config": (resolved / "config.json").exists(), - "validation": _pull_validation(resolved, repo_id), + "validation": _pull_validation( + resolved, + repo_id, + pinned_integrity_checked=pinned_integrity_checked, + ), } diff --git a/mtplx/models/deepseek_v4_target_only_config.py b/mtplx/models/deepseek_v4_target_only_config.py new file mode 100644 index 000000000..250725006 --- /dev/null +++ b/mtplx/models/deepseek_v4_target_only_config.py @@ -0,0 +1,206 @@ +"""Pinned identity for the public DeepSeek V4 target-only MLX artifact.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + + +DEEPSEEK_V4_TARGET_ONLY_REPO_ID = ( + "philipjohnbasile/DeepSeek-V4-Flash-0731-MLX-M5Max-TargetOnly" +) +# Pin the immutable weight publication rather than a later model-card-only +# commit. Documentation can evolve without silently changing the runtime +# artifact that MTPLX admits. +DEEPSEEK_V4_TARGET_ONLY_REVISION = "ac33e4f3ca3546e6cec104558d42161e15814e33" +DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS = tuple( + [f"model-layer-{idx}.safetensors" for idx in range(43)] + ["model-top.safetensors"] +) + + +def _shard_sizes() -> dict[str, int]: + sizes = { + "model-layer-0.safetensors": 2_232_226_400, + "model-layer-1.safetensors": 2_232_226_400, + "model-layer-2.safetensors": 2_262_658_336, + "model-top.safetensors": 1_125_524_468, + } + for layer in range(3, 10): + sizes[f"model-layer-{layer}.safetensors"] = ( + 2_234_674_232 if layer % 2 else 2_256_453_912 + ) + for layer in range(10, 39): + sizes[f"model-layer-{layer}.safetensors"] = ( + 2_234_674_280 if layer % 2 else 2_256_453_968 + ) + for layer in range(39, 43): + sizes[f"model-layer-{layer}.safetensors"] = ( + 3_778_178_160 if layer % 2 else 3_799_957_848 + ) + return sizes + + +DEEPSEEK_V4_TARGET_ONLY_SHARD_SIZES = _shard_sizes() +DEEPSEEK_V4_TARGET_ONLY_SHARD_SHA256 = { + "model-layer-0.safetensors": "1c7a2069ad82137ed463a0632d6baee4eec8d719eebbf08df79e5d2c61877fd4", + "model-layer-1.safetensors": "96d7c122914cc6c72290852971a09112411e30b11c16c90a9fc056e9a808b4d4", + "model-layer-2.safetensors": "c8fd7bca1dacb40d325911110a0871910661dcb2487beaf43f9bb633368c9beb", + "model-layer-3.safetensors": "feeec5fb91e5c2e52dc7894f37f5f485317c91d4e6c00e3850c32ab2c658788c", + "model-layer-4.safetensors": "a047c055c20689b648018b64f5905568c116b4edf88a98ba672fc5de46a4f858", + "model-layer-5.safetensors": "0d32c40401ee99a160cef49069d58f34d09395359fd9b0d54adfdf37f7453b4d", + "model-layer-6.safetensors": "8974d3acdd6a389bf2f8035e848155d4395d3307556e8b588a298c2aa1bf046d", + "model-layer-7.safetensors": "732d08c85c6624e54a247da41d1d145928fe78e98eb24b76efd389d65a4893a4", + "model-layer-8.safetensors": "a9df491f2a54a21c1050a14a7f3f9a731e2231c0ef783de7029e211f46f48628", + "model-layer-9.safetensors": "1e073b390d0b3666aa3131fc0241275c0d7714b00a6517482703925465561d5a", + "model-layer-10.safetensors": "afb100ff10ba50581c4f06346439b1f59c97773e6679134e0c00240041382590", + "model-layer-11.safetensors": "de7c1c783c97ad9dc7589fbfe51823849ea63071ec81a7ca59dcb3d03b1273db", + "model-layer-12.safetensors": "827302c65f67142dd0c5b6d573d9cadba2a5df68aabbde285bdaf060249b8fc2", + "model-layer-13.safetensors": "987cba23a581b46954c8dfa2dbcd863dca76f66597cc1a83e14682293e79d487", + "model-layer-14.safetensors": "9204ed3ba22db5d2b1dc83e47e09cbe2e0afc0edf2ce1b16a003b0efb03c9fa4", + "model-layer-15.safetensors": "7d848857e65751f56b260b932d72aef99491229d501acdc141214767782809aa", + "model-layer-16.safetensors": "dd5a3766aaaa8acde59baa03dcbe585ed4a01edcfcaa9d245706207acef29b5b", + "model-layer-17.safetensors": "77c33de412a75d19d933fa2d9574d6b4f09a8f46d22673cda0377a61f02c375f", + "model-layer-18.safetensors": "3e3380b3f9d323a0f11d9f599812d9cf0032ddebded5ae06fe0c140cfe545728", + "model-layer-19.safetensors": "90c4e2a4a6b8a64a9f18b4757c84a0152d67c12510a7e8556e3ef22d0cf23b4d", + "model-layer-20.safetensors": "a0cc9a6e31332182a4d3f853d377fd6363ec28975071904177158a5bc6495f51", + "model-layer-21.safetensors": "4ed8c6545bf2cf2c1491c7d2473cbda918aec217eafed78005a2b81dc6a1e243", + "model-layer-22.safetensors": "c698e641ddcc60fd0759602c4643fb270d65450971c8c704bbfb33907f8ab0c5", + "model-layer-23.safetensors": "f477b9ae3af9e625403256b41cb0fd74bce2fd65361aea78942bb61c5e5329ed", + "model-layer-24.safetensors": "321985d1cb6c3ddcc43de2eacd87a1602c6850f3378b2850ae56013d99e09305", + "model-layer-25.safetensors": "9e8021f7d6e0c6bed50e1c10208454324cbef75c23fe6b4e577d0d1a738eafb3", + "model-layer-26.safetensors": "59ed379f35e732d78e11810043ca6b87362ac2c926be63abe7aa9f21d1b59f91", + "model-layer-27.safetensors": "d7e5f5edf015168d093fea227d7708f468079b8bcea532807175ca42951beab4", + "model-layer-28.safetensors": "00d98eaa48f811b58d09d6ebee6ea31e588a1e84df4ad3f8c216180345f6901a", + "model-layer-29.safetensors": "47a41736a48f2819c2f79f5316aa9bbe56f668ab61ac6b183d55be2ef0df8bb2", + "model-layer-30.safetensors": "06105af5adbabfefa412400676f2a7dac4773f360d04e257223fca0d5614a06f", + "model-layer-31.safetensors": "4051556446455e19585622f7ddfee8adc1dafde2e0c893386b1817f05d54ffe2", + "model-layer-32.safetensors": "bb55bf7c25105f10f6837badb375172742cf8c696db55a6465ba238cf0a4f20f", + "model-layer-33.safetensors": "6cf3427ac909a96132782a2e51209da804b60bf6a9bf596448cc521e5067f70f", + "model-layer-34.safetensors": "b19ce6bc09991cb7598aa17f2cc413e4d3c8ab06a38ab4574edcded7db0d4abb", + "model-layer-35.safetensors": "4f2632eda38cc96549641e1ec6428879a44c9749bd14eb3471b14734b3e4ee51", + "model-layer-36.safetensors": "fba407165aef0fd25322f8bec897e0c4bfacefbdce2976a0b194399c115f37a5", + "model-layer-37.safetensors": "f05c0f5294d910a7f078be9c8c9d424bce536b93c50af301a95d5bf2e80bd69e", + "model-layer-38.safetensors": "14cef91cb94947fd1e6f82bc7d8798264e336c757a0923bfa6699c8e91bf4aa2", + "model-layer-39.safetensors": "a0594fe95badf8540efe476ed870339f1449aeea774decedd4a39a5a280441b7", + "model-layer-40.safetensors": "9752520416dfc125b0ecee74e18bf8678c2ae8d11fc6fcd7ee8ded757f230440", + "model-layer-41.safetensors": "41216ecd544e1fb02167491cd84a44e2a975d2ad2b58fcf6e7f507801b8606dc", + "model-layer-42.safetensors": "0755a615f5d49837ab2f7afea89e0601a4f05e4d004664690b58e1957de0c488", + "model-top.safetensors": "11a33b3911d6723ee60a30822218068d96a4a23e6daef2e36b9bd2551b815e73", +} +DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256 = { + "config.json": "ab61e3230f196c6eba04bfa81158dd527a7f356b6d926cc4794907a19f35b75d", + "generation_config.json": "d14db17fca8dc5492af88fd7938475e3a9fdddddd7596a5062d2ecd58abb942b", + "model.safetensors.index.json": "589b2290d9a24081171d28424e4cd170be6e1820853a286e446cd580d456da8e", + "tokenizer.json": "8f9f37ca37fdc4f5fd36d5cf4d3b0e8392edb4e894fd10cc0d70b4957c8633cf", + "tokenizer_config.json": "58a9fb7a7e68144c0b6fe4bc8349ee77d818702f95c9c6aa41143d9e27d7c2e6", +} +DEEPSEEK_V4_TARGET_ONLY_REQUIRED_FILES = frozenset( + (*DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256, *DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS) +) +DEEPSEEK_V4_TARGET_ONLY_REPO_BYTES = 103_855_774_263 + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def is_deepseek_v4_target_only_config(config: dict[str, Any]) -> bool: + if not isinstance(config, dict) or "model_file" in config: + return False + quantization = config.get("quantization") + if not isinstance(quantization, dict): + return False + try: + if not ( + config.get("architectures") == ["DeepseekV4ForCausalLM"] + and config.get("model_type") == "deepseek_v4" + and int(config.get("num_hidden_layers") or 0) == 43 + and int(config.get("hidden_size") or 0) == 4096 + and int(config.get("num_attention_heads") or 0) == 64 + and int(config.get("num_key_value_heads") or 0) == 1 + and int(config.get("head_dim") or 0) == 512 + and int(config.get("vocab_size") or 0) == 129_280 + and int(config.get("num_nextn_predict_layers") or 0) == 0 + and int(config.get("dspark_block_size") or 0) == 0 + and int(config.get("num_experts_per_tok") or 0) == 6 + and int(config.get("n_routed_experts") or 0) == 256 + and quantization.get("bits") == 8 + and quantization.get("group_size") == 64 + and quantization.get("mode") == "affine" + ): + return False + except (TypeError, ValueError): + return False + + def affine(name: str, bits: int, group_size: int) -> bool: + value = quantization.get(name) + return bool( + isinstance(value, dict) + and value.get("bits") == bits + and value.get("group_size") == group_size + and value.get("mode") == "affine" + ) + + if not affine("embed", 8, 64) or not affine("head", 8, 64): + return False + # The public target-only view has a layer-specific expert recipe; accepting + # only the global metadata would let a different conversion claim the same + # family identity. Layers 0-38 use 2/3/2-bit g128 experts; 39-42 use + # 4/4/4-bit g64 experts. + for layer in range(43): + bits = (2, 3, 2) if layer < 39 else (4, 4, 4) + group_size = 128 if layer < 39 else 64 + for projection, projection_bits in zip( + ("w1", "w2", "w3"), bits, strict=True + ): + if not affine( + f"layers.{layer}.ffn.experts.{projection}", + projection_bits, + group_size, + ): + return False + return True + + +def deepseek_v4_target_only_artifact_integrity_errors( + model_path: Path | str, + *, + verify_shard_hashes: bool = True, +) -> tuple[str, ...]: + root = Path(model_path) + errors: list[str] = [] + for name, expected_size in DEEPSEEK_V4_TARGET_ONLY_SHARD_SIZES.items(): + path = root / name + try: + if not path.is_file() or path.stat().st_size != expected_size: + errors.append(name) + elif ( + verify_shard_hashes + and _sha256(path) != (DEEPSEEK_V4_TARGET_ONLY_SHARD_SHA256[name]) + ): + errors.append(name) + except OSError: + errors.append(name) + for name, expected_sha256 in DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256.items(): + path = root / name + try: + if not path.is_file() or _sha256(path) != expected_sha256: + errors.append(name) + except OSError: + errors.append(name) + try: + index = json.loads((root / "model.safetensors.index.json").read_text()) + weight_map = index.get("weight_map") if isinstance(index, dict) else None + if not isinstance(weight_map, dict) or set(weight_map.values()) != set( + DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS + ): + errors.append("model.safetensors.index.json:weight_map") + except (OSError, UnicodeError, json.JSONDecodeError): + errors.append("model.safetensors.index.json:parse") + return tuple(sorted(set(errors))) diff --git a/tests/test_deepseek_v4_mlxserve_backend.py b/tests/test_deepseek_v4_mlxserve_backend.py new file mode 100644 index 000000000..e4a35f438 --- /dev/null +++ b/tests/test_deepseek_v4_mlxserve_backend.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from mtplx.backends.deepseek_v4_mlxserve import ( + BACKEND_ID, + DeepSeekV4MlxServeError, + build_command, + child_environment, + resolve_binary, +) +from mtplx.backends.descriptors import descriptor_for_backend_id +from mtplx.backends.registry import compatibility_for_inspection +from mtplx.cli import build_parser +from mtplx.commands import public +from mtplx import hf_loader +from mtplx.models import deepseek_v4_target_only_config as contract + + +def _quantization() -> dict[str, object]: + quantization: dict[str, object] = { + "bits": 8, + "group_size": 64, + "mode": "affine", + "embed": {"bits": 8, "group_size": 64, "mode": "affine"}, + "head": {"bits": 8, "group_size": 64, "mode": "affine"}, + } + for layer in range(43): + bits = (2, 3, 2) if layer < 39 else (4, 4, 4) + group_size = 128 if layer < 39 else 64 + for projection, projection_bits in zip(("w1", "w2", "w3"), bits, strict=True): + quantization[f"layers.{layer}.ffn.experts.{projection}"] = { + "bits": projection_bits, + "group_size": group_size, + "mode": "affine", + } + return quantization + + +def _config() -> dict[str, object]: + return { + "architectures": ["DeepseekV4ForCausalLM"], + "model_type": "deepseek_v4", + "num_hidden_layers": 43, + "hidden_size": 4096, + "num_attention_heads": 64, + "num_key_value_heads": 1, + "head_dim": 512, + "vocab_size": 129_280, + "num_nextn_predict_layers": 0, + "dspark_block_size": 0, + "num_experts_per_tok": 6, + "n_routed_experts": 256, + "quantization": _quantization(), + } + + +def _inspection(**overrides: object) -> SimpleNamespace: + values: dict[str, object] = { + "model_dir": "/tmp/deepseek-v4-target-only", + "architecture": "DeepseekV4ForCausalLM", + "model_type": "deepseek_v4", + "mtp_num_hidden_layers": 0, + "deepseek_v4_target_only_match": True, + "deepseek_v4_target_only_artifacts_complete": True, + "mtp": None, + "runtime_contract_data": None, + "runtime_contract_error": None, + "runtime_contract_path": None, + } + values.update(overrides) + return SimpleNamespace(**values) + + +def test_public_identity_is_immutable_and_complete() -> None: + assert contract.DEEPSEEK_V4_TARGET_ONLY_REVISION == "ac33e4f3ca3546e6cec104558d42161e15814e33" + assert len(contract.DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS) == 44 + assert set(contract.DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS) == set( + contract.DEEPSEEK_V4_TARGET_ONLY_SHARD_SIZES + ) + assert sum(contract.DEEPSEEK_V4_TARGET_ONLY_SHARD_SIZES.values()) == 103_849_215_724 + assert contract.is_deepseek_v4_target_only_config(_config()) is True + + +def test_pull_identity_rejects_a_noncanonical_deepseek_revision() -> None: + assert hf_loader._effective_model_revision( + contract.DEEPSEEK_V4_TARGET_ONLY_REPO_ID, None + ) == contract.DEEPSEEK_V4_TARGET_ONLY_REVISION + with pytest.raises(ValueError, match="pinned to revision"): + hf_loader._effective_model_revision( + contract.DEEPSEEK_V4_TARGET_ONLY_REPO_ID, "not-the-public-revision" + ) + + +def test_target_only_config_rejects_expert_recipe_drift() -> None: + config = _config() + quantization = config["quantization"] + assert isinstance(quantization, dict) + quantization["layers.8.ffn.experts.w2"] = { + "bits": 4, + "group_size": 64, + "mode": "affine", + } + assert contract.is_deepseek_v4_target_only_config(config) is False + + +def test_integrity_rejects_same_size_shard_content_drift( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + shard = tmp_path / "model-layer-0.safetensors" + shard.write_bytes(b"good") + index = tmp_path / "model.safetensors.index.json" + index.write_text(json.dumps({"weight_map": {"weight": shard.name}}), encoding="utf-8") + monkeypatch.setattr(contract, "DEEPSEEK_V4_TARGET_ONLY_WEIGHT_SHARDS", (shard.name,)) + monkeypatch.setattr(contract, "DEEPSEEK_V4_TARGET_ONLY_SHARD_SIZES", {shard.name: 4}) + monkeypatch.setattr( + contract, + "DEEPSEEK_V4_TARGET_ONLY_SHARD_SHA256", + {shard.name: hashlib.sha256(b"good").hexdigest()}, + ) + monkeypatch.setattr( + contract, + "DEEPSEEK_V4_TARGET_ONLY_SIDECAR_SHA256", + {index.name: hashlib.sha256(index.read_bytes()).hexdigest()}, + ) + assert contract.deepseek_v4_target_only_artifact_integrity_errors(tmp_path) == () + shard.write_bytes(b"evil") + assert contract.deepseek_v4_target_only_artifact_integrity_errors(tmp_path) == (shard.name,) + + +def test_exact_admission_is_external_ar_not_mtp_or_dspark() -> None: + verdict = compatibility_for_inspection(_inspection()) + assert verdict.tier == "AR-only" + assert verdict.arch_id == "deepseek-v4-mlxserve-ar" + assert verdict.recommended_backend == BACKEND_ID + assert verdict.mtp_supported == "no" + assert verdict.runtime_compatibility == "external-mem-preflight-required" + assert descriptor_for_backend_id(BACKEND_ID).uses_draft_lm_head is False + + +def test_external_launch_is_closed_and_keeps_memory_preflight(tmp_path: Path) -> None: + binary = tmp_path / "mlx-serve" + binary.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + binary.chmod(0o755) + admitted = resolve_binary({"MTPLX_MLX_SERVE_BIN": str(binary)}) + command = build_command( + binary=admitted, + model="/models/dsv4", + host="127.0.0.1", + port=8123, + context_window=None, + api_key=None, + ) + environment = child_environment( + {"PATH": "/usr/bin", "MLX_SERVE_WIRED": "off", "MLXSERVE_DEVICE": "foreign"} + ) + assert environment == { + "PATH": "/usr/bin", + "MLX_SERVE_WIRED": "fit", + "MLX_SERVE_CACHE_LIMIT": "268435456", + } + assert "--skip-mem-preflight" not in command + assert {"--no-pld", "--no-decode-attn-quant", "--no-vision"}.issubset(command) + + +def test_external_binary_resolution_uses_the_admitted_path(tmp_path: Path) -> None: + binary = tmp_path / "mlx-serve" + binary.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + binary.chmod(0o755) + + assert resolve_binary({"PATH": str(tmp_path)}) == binary.resolve() + + +def _patch_external_route(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + binary = tmp_path / "mlx-serve" + binary.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + binary.chmod(0o755) + inspection = { + "model_dir": str(tmp_path), + "recommended_backend": BACKEND_ID, + "compatibility": { + "tier": "AR-only", + "can_run": True, + "exit_code": 0, + "runtime_compatibility": "external-mem-preflight-required", + "recommended_backend": BACKEND_ID, + }, + } + monkeypatch.setattr(public, "_serve_should_onboard", lambda _args: False) + monkeypatch.setattr(public, "_resolve_runtime_model_path", lambda *_args, **_kwargs: (str(tmp_path), None)) + monkeypatch.setattr(public, "_model_gate", lambda *_args, **_kwargs: (inspection, None)) + monkeypatch.setattr(public, "resolve_deepseek_v4_mlxserve_binary", lambda: binary.resolve()) + monkeypatch.setattr(public, "resolve_deepseek_v4_mlxserve_working_directory", lambda _binary: tmp_path) + monkeypatch.setattr(os, "environ", {"PATH": "/usr/bin"}) + return binary + + +def test_cli_dry_run_reports_external_admission_without_loading_model( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + _patch_external_route(monkeypatch, tmp_path) + args = build_parser().parse_args(["serve", "--model", str(tmp_path), "--yes"]) + args.dry_run = True + args.json = True + assert public.cmd_serve_public(args) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["backend_id"] == BACKEND_ID + assert payload["external_runtime"] == "mlx-serve" + assert payload["generation_mode"] == "ar" + assert payload["mtp_available"] is False + assert payload["dspark_available"] is False + assert payload["memory_preflight"] == "required" + assert "no MTP or DSpark" in payload["runtime_compatibility_note"] + assert "--skip-mem-preflight" not in payload["argv"] + + +def test_cli_dry_run_reports_missing_external_binary_as_json( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + _patch_external_route(monkeypatch, tmp_path) + monkeypatch.setattr( + public, + "resolve_deepseek_v4_mlxserve_binary", + lambda: (_ for _ in ()).throw(DeepSeekV4MlxServeError("mlx-serve is required")), + ) + args = build_parser().parse_args(["serve", "--model", str(tmp_path), "--yes"]) + args.dry_run = True + args.json = True + assert public.cmd_serve_public(args) == 2 + assert json.loads(capsys.readouterr().out) == { + "ok": False, + "dry_run": True, + "target": "server", + "error": "external_runtime_admission_failed", + "backend_id": BACKEND_ID, + "external_runtime": "mlx-serve", + "detail": "mlx-serve is required", + } From ca071ab4d4b7e331198d4bfc7586f5323208ee39 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 02:12:58 -0400 Subject: [PATCH 02/11] ci: materialize current-main PR 254 candidate --- .../workflows/temporary-materialize-pr254.yml | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/temporary-materialize-pr254.yml diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml new file mode 100644 index 000000000..d607a1e5e --- /dev/null +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -0,0 +1,78 @@ +name: Temporary materialize PR 254 current-main candidate + +on: + push: + branches: + - codex/deepseek-v4-mlxserve-v26 + +permissions: + contents: write + +concurrency: + group: temporary-materialize-pr254 + cancel-in-progress: true + +jobs: + materialize: + runs-on: macos-15-arm64 + steps: + - uses: actions/checkout@v4 + with: + ref: codex/deepseek-v4-mlxserve-v26 + fetch-depth: 0 + + - name: Extract and run the audited semantic rebase + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git fetch origin agent/pr254-rebase-inspect + git show origin/agent/pr254-rebase-inspect:.github/workflows/temporary-rebase-254.yml > /tmp/pr254-source.yml + python - <<'PY' + from pathlib import Path + + lines = Path("/tmp/pr254-source.yml").read_text(encoding="utf-8").splitlines() + blocks: list[str] = [] + index = 0 + while index < len(lines): + if lines[index] == " run: |": + index += 1 + block: list[str] = [] + while index < len(lines): + current = lines[index] + if current and len(current) - len(current.lstrip()) <= 8: + break + if current.startswith(" "): + block.append(current[10:]) + elif not current: + block.append("") + else: + raise SystemExit(f"unexpected workflow indentation: {current!r}") + index += 1 + blocks.append("\n".join(block)) + continue + index += 1 + if len(blocks) < 2: + raise SystemExit(f"expected at least two run blocks, found {len(blocks)}") + Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") + Path("/tmp/pr254-squash.sh").write_text(blocks[1] + "\n", encoding="utf-8") + PY + bash -n /tmp/pr254-rebase.sh + bash -n /tmp/pr254-squash.sh + bash /tmp/pr254-rebase.sh + rm -f .github/workflows/temporary-materialize-pr254.yml + bash /tmp/pr254-squash.sh + test "$(git rev-list --count upstream/main..HEAD)" = "1" + git diff --check upstream/main...HEAD + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/PhilipJohnBasile/MTPLX.git" \ + HEAD:sync/pr254-rebased-inspect \ + --force + { + echo "## PR 254 current-main candidate" + echo "" + echo "- upstream main: \`$(git rev-parse upstream/main)\`" + echo "- candidate: \`$(git rev-parse HEAD)\`" + echo "- commits ahead: 1" + } >> "$GITHUB_STEP_SUMMARY" From dae82f51edd7981d464f3fb4cfe809004077337e Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 02:17:51 -0400 Subject: [PATCH 03/11] ci: trigger current-main PR 254 materialization --- .github/workflows/temporary-materialize-pr254.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index d607a1e5e..298d1c3f4 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -27,6 +27,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail + echo "materializing clean PR 254 candidate" git fetch origin agent/pr254-rebase-inspect git show origin/agent/pr254-rebase-inspect:.github/workflows/temporary-rebase-254.yml > /tmp/pr254-source.yml python - <<'PY' From d96091e5de0f2c3ad8fec3ca8c155d05bd86d63d Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 10:05:58 -0400 Subject: [PATCH 04/11] ci: diagnose and materialize clean PR 254 rebase --- .../workflows/temporary-materialize-pr254.yml | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index 298d1c3f4..c21e7598c 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -14,22 +14,27 @@ concurrency: jobs: materialize: - runs-on: macos-15-arm64 + runs-on: macos-15 + timeout-minutes: 30 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: ref: codex/deepseek-v4-mlxserve-v26 fetch-depth: 0 - - name: Extract and run the audited semantic rebase + - name: Rebuild one clean feature commit on current upstream main shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - echo "materializing clean PR 254 candidate" + git config user.name "Philip John Basile" + git config user.email "PBasile@Basilecom.com" + git remote add upstream https://github.com/youssofal/MTPLX.git || true + git fetch upstream main git fetch origin agent/pr254-rebase-inspect git show origin/agent/pr254-rebase-inspect:.github/workflows/temporary-rebase-254.yml > /tmp/pr254-source.yml + python - <<'PY' from pathlib import Path @@ -55,25 +60,61 @@ jobs: continue index += 1 if len(blocks) < 2: - raise SystemExit(f"expected at least two run blocks, found {len(blocks)}") + raise SystemExit(f"expected at least two guarded run blocks, found {len(blocks)}") Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") Path("/tmp/pr254-squash.sh").write_text(blocks[1] + "\n", encoding="utf-8") PY + + cat > /tmp/pr254-run.sh <<'SH' + set -euo pipefail + # Start from the feature-only commit, not the accumulated workflow commits. + git reset --hard d079df37916f646f0a5b34343f317a03aa3a2e07 bash -n /tmp/pr254-rebase.sh bash -n /tmp/pr254-squash.sh bash /tmp/pr254-rebase.sh - rm -f .github/workflows/temporary-materialize-pr254.yml + rm -f \ + .github/workflows/temporary-materialize-pr254.yml \ + .github/workflows/temporary-rebase-254.yml \ + .github/workflows/temporary-execute-rebase-254-macos14.yml \ + .github/workflows/temporary-execute-rebase-254-quote-fix.yml \ + .github/pr254-wrapper-template.yml \ + .github/temporary-rebase-254-trigger.txt bash /tmp/pr254-squash.sh test "$(git rev-list --count upstream/main..HEAD)" = "1" + test "$(git rev-parse HEAD^)" = "$(git rev-parse upstream/main)" + test -z "$(git diff --name-only upstream/main...HEAD -- '.github/workflows/temporary-*' '.github/pr254-wrapper-template.yml' '.github/temporary-rebase-254-trigger.txt')" + git show -s --format=%B HEAD | grep -F "Signed-off-by: Philip John Basile " git diff --check upstream/main...HEAD + SH + + set +e + bash /tmp/pr254-run.sh > /tmp/pr254-run.log 2>&1 + status=$? + set -e + cat /tmp/pr254-run.log + + if [ "$status" -ne 0 ]; then + git rebase --abort >/dev/null 2>&1 || true + git reset --hard upstream/main + mkdir -p diagnostics + cp /tmp/pr254-run.log diagnostics/pr254-current-main-failure.log + git add diagnostics/pr254-current-main-failure.log + git commit -m "diagnostics: capture PR 254 current-main failure" + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/PhilipJohnBasile/MTPLX.git" \ + HEAD:agent/pr254-failure-log --force + exit "$status" + fi + + candidate="$(git rev-parse HEAD)" git push \ "https://x-access-token:${GH_TOKEN}@github.com/PhilipJohnBasile/MTPLX.git" \ - HEAD:sync/pr254-rebased-inspect \ - --force + HEAD:sync/pr254-rebased-inspect --force { echo "## PR 254 current-main candidate" echo "" echo "- upstream main: \`$(git rev-parse upstream/main)\`" - echo "- candidate: \`$(git rev-parse HEAD)\`" + echo "- candidate: \`$candidate\`" echo "- commits ahead: 1" + echo "- temporary repair files: absent" } >> "$GITHUB_STEP_SUMMARY" From 0988276a33d2c9190c101826bba35cc12da0bd3b Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 10:08:29 -0400 Subject: [PATCH 05/11] ci: resolve combined PR 254 rebase conflicts --- .../workflows/temporary-materialize-pr254.yml | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index c21e7598c..b9b41ad99 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -61,13 +61,42 @@ jobs: index += 1 if len(blocks) < 2: raise SystemExit(f"expected at least two guarded run blocks, found {len(blocks)}") + + # Current Git rebases the Python and both Swift overlaps in one stop. + # The older guarded script expected hf_loader first and Swift second; + # stage hf_loader, then let its existing Swift resolver finish the + # same rebase stop before calling rebase --continue once. + rebase = blocks[0] + old_expected = ' test "$conflicts" = "mtplx/hf_loader.py"' + new_expected = ( + " expected=$'apps/MTPLXApp/Sources/MTPLXAppCore/Services/" + "DaemonSupervisor.swift\\napps/MTPLXApp/Sources/MTPLXAppCore/Stores/" + "MTPLXBackendStore.swift\\nmtplx/hf_loader.py'\n" + " test \"$conflicts\" = \"$expected\"" + ) + if rebase.count(old_expected) != 1: + raise SystemExit("hf_loader conflict expectation anchor changed") + rebase = rebase.replace(old_expected, new_expected, 1) + + old_continue = ''' git add mtplx/hf_loader.py + set +e + GIT_EDITOR=true git rebase --continue + rc=$? + set -e''' + new_continue = ''' git add mtplx/hf_loader.py + # The two Swift paths remain unmerged in this same rebase stop. + rc=1''' + if rebase.count(old_continue) != 1: + raise SystemExit("hf_loader continuation anchor changed") + rebase = rebase.replace(old_continue, new_continue, 1) + blocks[0] = rebase + Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") Path("/tmp/pr254-squash.sh").write_text(blocks[1] + "\n", encoding="utf-8") PY cat > /tmp/pr254-run.sh <<'SH' set -euo pipefail - # Start from the feature-only commit, not the accumulated workflow commits. git reset --hard d079df37916f646f0a5b34343f317a03aa3a2e07 bash -n /tmp/pr254-rebase.sh bash -n /tmp/pr254-squash.sh From 0018412ade6b5dd9519325bc40d71cef8456c30a Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:01:27 -0400 Subject: [PATCH 06/11] ci: fix extracted PR 254 conflict anchors --- .../workflows/temporary-materialize-pr254.yml | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index b9b41ad99..1b33eae5f 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -63,29 +63,28 @@ jobs: raise SystemExit(f"expected at least two guarded run blocks, found {len(blocks)}") # Current Git rebases the Python and both Swift overlaps in one stop. - # The older guarded script expected hf_loader first and Swift second; - # stage hf_loader, then let its existing Swift resolver finish the - # same rebase stop before calling rebase --continue once. + # The extracted shell block has its YAML indentation removed, so the + # inner `if` body begins with two spaces here. rebase = blocks[0] - old_expected = ' test "$conflicts" = "mtplx/hf_loader.py"' + old_expected = ' test "$conflicts" = "mtplx/hf_loader.py"' new_expected = ( - " expected=$'apps/MTPLXApp/Sources/MTPLXAppCore/Services/" + " expected=$'apps/MTPLXApp/Sources/MTPLXAppCore/Services/" "DaemonSupervisor.swift\\napps/MTPLXApp/Sources/MTPLXAppCore/Stores/" "MTPLXBackendStore.swift\\nmtplx/hf_loader.py'\n" - " test \"$conflicts\" = \"$expected\"" + " test \"$conflicts\" = \"$expected\"" ) if rebase.count(old_expected) != 1: raise SystemExit("hf_loader conflict expectation anchor changed") rebase = rebase.replace(old_expected, new_expected, 1) - old_continue = ''' git add mtplx/hf_loader.py - set +e - GIT_EDITOR=true git rebase --continue - rc=$? - set -e''' - new_continue = ''' git add mtplx/hf_loader.py - # The two Swift paths remain unmerged in this same rebase stop. - rc=1''' + old_continue = ''' git add mtplx/hf_loader.py + set +e + GIT_EDITOR=true git rebase --continue + rc=$? + set -e''' + new_continue = ''' git add mtplx/hf_loader.py + # The two Swift paths remain unmerged in this same rebase stop. + rc=1''' if rebase.count(old_continue) != 1: raise SystemExit("hf_loader continuation anchor changed") rebase = rebase.replace(old_continue, new_continue, 1) From 1dccc0dd3ff2f797a8e1cdd2f0498743d8d10615 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:05:12 -0400 Subject: [PATCH 07/11] ci: restore valid PR 254 materializer YAML --- .../workflows/temporary-materialize-pr254.yml | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index 1b33eae5f..383ed7a3a 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -77,14 +77,18 @@ jobs: raise SystemExit("hf_loader conflict expectation anchor changed") rebase = rebase.replace(old_expected, new_expected, 1) - old_continue = ''' git add mtplx/hf_loader.py - set +e - GIT_EDITOR=true git rebase --continue - rc=$? - set -e''' - new_continue = ''' git add mtplx/hf_loader.py - # The two Swift paths remain unmerged in this same rebase stop. - rc=1''' + old_continue = ( + " git add mtplx/hf_loader.py\n" + " set +e\n" + " GIT_EDITOR=true git rebase --continue\n" + " rc=$?\n" + " set -e" + ) + new_continue = ( + " git add mtplx/hf_loader.py\n" + " # The two Swift paths remain unmerged in this same rebase stop.\n" + " rc=1" + ) if rebase.count(old_continue) != 1: raise SystemExit("hf_loader continuation anchor changed") rebase = rebase.replace(old_continue, new_continue, 1) From 2316c5163f43335abf8ee63e0c5f757359272708 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:09:14 -0400 Subject: [PATCH 08/11] ci: validate current PR 254 semantic resolution --- .../workflows/temporary-materialize-pr254.yml | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index 383ed7a3a..c1802ce27 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -15,7 +15,7 @@ concurrency: jobs: materialize: runs-on: macos-15 - timeout-minutes: 30 + timeout-minutes: 45 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -24,7 +24,7 @@ jobs: ref: codex/deepseek-v4-mlxserve-v26 fetch-depth: 0 - - name: Rebuild one clean feature commit on current upstream main + - name: Rebuild and validate one clean feature commit on current upstream main shell: bash run: | set -euo pipefail @@ -59,12 +59,12 @@ jobs: blocks.append("\n".join(block)) continue index += 1 - if len(blocks) < 2: - raise SystemExit(f"expected at least two guarded run blocks, found {len(blocks)}") + if len(blocks) < 3: + raise SystemExit(f"expected at least three guarded run blocks, found {len(blocks)}") # Current Git rebases the Python and both Swift overlaps in one stop. # The extracted shell block has its YAML indentation removed, so the - # inner `if` body begins with two spaces here. + # inner shell `if` body begins with two spaces here. rebase = blocks[0] old_expected = ' test "$conflicts" = "mtplx/hf_loader.py"' new_expected = ( @@ -92,10 +92,38 @@ jobs: if rebase.count(old_continue) != 1: raise SystemExit("hf_loader continuation anchor changed") rebase = rebase.replace(old_continue, new_continue, 1) + + # Current main and the feature now conflict only at the waitForHealth + # return/body seam. Preserve the feature's explicit external-health + # branch and current main's normal MTPLX client path from that hunk. + wait_start_marker = ( + ' if (\n' + ' "private func waitForHealth(" in ours\n' + ) + wait_end_marker = ' kinds.add("wait_for_health")\n' + wait_start = rebase.find(wait_start_marker) + if wait_start < 0: + raise SystemExit("waitForHealth resolver start anchor changed") + wait_end = rebase.find(wait_end_marker, wait_start) + if wait_end < 0: + raise SystemExit("waitForHealth resolver end anchor changed") + wait_end += len(wait_end_marker) + replacement_wait = ( + ' if (\n' + ' ours.strip() == ") async throws -> HealthPayload {"\n' + ' and ") async throws -> HealthPayload?" in theirs\n' + ' and "waitForExternalMlxServeHealth" in theirs\n' + ' and "let client = MTPLXAPIClient" in theirs\n' + ' ):\n' + ' text = theirs\n' + ' kinds.add("wait_for_health")\n' + ) + rebase = rebase[:wait_start] + replacement_wait + rebase[wait_end:] blocks[0] = rebase Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") Path("/tmp/pr254-squash.sh").write_text(blocks[1] + "\n", encoding="utf-8") + Path("/tmp/pr254-validate.sh").write_text(blocks[2] + "\n", encoding="utf-8") PY cat > /tmp/pr254-run.sh <<'SH' @@ -103,6 +131,7 @@ jobs: git reset --hard d079df37916f646f0a5b34343f317a03aa3a2e07 bash -n /tmp/pr254-rebase.sh bash -n /tmp/pr254-squash.sh + bash -n /tmp/pr254-validate.sh bash /tmp/pr254-rebase.sh rm -f \ .github/workflows/temporary-materialize-pr254.yml \ @@ -117,6 +146,7 @@ jobs: test -z "$(git diff --name-only upstream/main...HEAD -- '.github/workflows/temporary-*' '.github/pr254-wrapper-template.yml' '.github/temporary-rebase-254-trigger.txt')" git show -s --format=%B HEAD | grep -F "Signed-off-by: Philip John Basile " git diff --check upstream/main...HEAD + bash /tmp/pr254-validate.sh SH set +e @@ -146,7 +176,8 @@ jobs: echo "## PR 254 current-main candidate" echo "" echo "- upstream main: \`$(git rev-parse upstream/main)\`" - echo "- candidate: \`$candidate\`" + echo "- validated candidate: \`$candidate\`" echo "- commits ahead: 1" echo "- temporary repair files: absent" + echo "- focused Python, package, hygiene, full Swift test, and release build: passed" } >> "$GITHUB_STEP_SUMMARY" From c30ee4bc99de041f9a2c02e839964a7bc7b1ab04 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:13:50 -0400 Subject: [PATCH 09/11] ci: resolve current PR 254 store seam --- .../workflows/temporary-materialize-pr254.yml | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index c1802ce27..dccfead14 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -62,9 +62,6 @@ jobs: if len(blocks) < 3: raise SystemExit(f"expected at least three guarded run blocks, found {len(blocks)}") - # Current Git rebases the Python and both Swift overlaps in one stop. - # The extracted shell block has its YAML indentation removed, so the - # inner shell `if` body begins with two spaces here. rebase = blocks[0] old_expected = ' test "$conflicts" = "mtplx/hf_loader.py"' new_expected = ( @@ -93,9 +90,6 @@ jobs: raise SystemExit("hf_loader continuation anchor changed") rebase = rebase.replace(old_continue, new_continue, 1) - # Current main and the feature now conflict only at the waitForHealth - # return/body seam. Preserve the feature's explicit external-health - # branch and current main's normal MTPLX client path from that hunk. wait_start_marker = ( ' if (\n' ' "private func waitForHealth(" in ours\n' @@ -119,6 +113,42 @@ jobs: ' kinds.add("wait_for_health")\n' ) rebase = rebase[:wait_start] + replacement_wait + rebase[wait_end:] + + refresh_start_marker = ( + ' if "public func refreshStaticState(" in ours ' + 'and "supportsMTPLXLiveControls" in theirs:\n' + ) + refresh_end_marker = ' kinds.add("refresh_static")\n' + refresh_start = rebase.find(refresh_start_marker) + if refresh_start < 0: + raise SystemExit("refreshStaticState resolver start anchor changed") + refresh_end = rebase.find(refresh_end_marker, refresh_start) + if refresh_end < 0: + raise SystemExit("refreshStaticState resolver end anchor changed") + refresh_end += len(refresh_end_marker) + replacement_refresh = ( + ' if (\n' + ' "public func refreshStaticState(" in ours\n' + ' and "public func refreshStaticState()" in theirs\n' + ' and "guard supportsMTPLXLiveControls else { return }" in theirs\n' + ' and "let client = apiClient" not in ours\n' + ' ):\n' + ' text = ours\n' + ' if not text.endswith("\\n"):\n' + ' text += "\\n"\n' + ' text += (\n' + ' " guard supportsMTPLXLiveControls else {\\n"\n' + ' " health = nil\\n"\n' + ' " capabilities = nil\\n"\n' + ' " sessions = nil\\n"\n' + ' " prefillHistory = nil\\n"\n' + ' " models = nil\\n"\n' + ' " return\\n"\n' + ' " }\\n"\n' + ' )\n' + ' kinds.add("refresh_static")\n' + ) + rebase = rebase[:refresh_start] + replacement_refresh + rebase[refresh_end:] blocks[0] = rebase Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") From 31f19bdac05359ac5a516239afdf97318b0c8c35 Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:18:33 -0400 Subject: [PATCH 10/11] ci: resolve PR 254 fresh-launch settings seam --- .../workflows/temporary-materialize-pr254.yml | 150 +++++++++++------- 1 file changed, 92 insertions(+), 58 deletions(-) diff --git a/.github/workflows/temporary-materialize-pr254.yml b/.github/workflows/temporary-materialize-pr254.yml index dccfead14..f5acda9d3 100644 --- a/.github/workflows/temporary-materialize-pr254.yml +++ b/.github/workflows/temporary-materialize-pr254.yml @@ -90,67 +90,100 @@ jobs: raise SystemExit("hf_loader continuation anchor changed") rebase = rebase.replace(old_continue, new_continue, 1) - wait_start_marker = ( - ' if (\n' - ' "private func waitForHealth(" in ours\n' + def replace_resolver(start_marker: str, end_marker: str, replacement: str, label: str) -> None: + global rebase + start = rebase.find(start_marker) + if start < 0: + raise SystemExit(f"{label} resolver start anchor changed") + end = rebase.find(end_marker, start) + if end < 0: + raise SystemExit(f"{label} resolver end anchor changed") + end += len(end_marker) + rebase = rebase[:start] + replacement + rebase[end:] + + replace_resolver( + ' if (\n "private func waitForHealth(" in ours\n', + ' kinds.add("wait_for_health")\n', + ( + ' if (\n' + ' ours.strip() == ") async throws -> HealthPayload {"\n' + ' and ") async throws -> HealthPayload?" in theirs\n' + ' and "waitForExternalMlxServeHealth" in theirs\n' + ' and "let client = MTPLXAPIClient" in theirs\n' + ' ):\n' + ' text = theirs\n' + ' kinds.add("wait_for_health")\n' + ), + "waitForHealth", ) - wait_end_marker = ' kinds.add("wait_for_health")\n' - wait_start = rebase.find(wait_start_marker) - if wait_start < 0: - raise SystemExit("waitForHealth resolver start anchor changed") - wait_end = rebase.find(wait_end_marker, wait_start) - if wait_end < 0: - raise SystemExit("waitForHealth resolver end anchor changed") - wait_end += len(wait_end_marker) - replacement_wait = ( - ' if (\n' - ' ours.strip() == ") async throws -> HealthPayload {"\n' - ' and ") async throws -> HealthPayload?" in theirs\n' - ' and "waitForExternalMlxServeHealth" in theirs\n' - ' and "let client = MTPLXAPIClient" in theirs\n' - ' ):\n' - ' text = theirs\n' - ' kinds.add("wait_for_health")\n' - ) - rebase = rebase[:wait_start] + replacement_wait + rebase[wait_end:] - refresh_start_marker = ( - ' if "public func refreshStaticState(" in ours ' - 'and "supportsMTPLXLiveControls" in theirs:\n' + replace_resolver( + ( + ' if "public func refreshStaticState(" in ours ' + 'and "supportsMTPLXLiveControls" in theirs:\n' + ), + ' kinds.add("refresh_static")\n', + ( + ' if (\n' + ' "public func refreshStaticState(" in ours\n' + ' and "public func refreshStaticState()" in theirs\n' + ' and "guard supportsMTPLXLiveControls else { return }" in theirs\n' + ' and "let client = apiClient" not in ours\n' + ' ):\n' + ' text = ours\n' + ' if not text.endswith("\\n"):\n' + ' text += "\\n"\n' + ' text += (\n' + ' " guard supportsMTPLXLiveControls else {\\n"\n' + ' " health = nil\\n"\n' + ' " capabilities = nil\\n"\n' + ' " sessions = nil\\n"\n' + ' " prefillHistory = nil\\n"\n' + ' " models = nil\\n"\n' + ' " return\\n"\n' + ' " }\\n"\n' + ' )\n' + ' kinds.add("refresh_static")\n' + ), + "refreshStaticState", ) - refresh_end_marker = ' kinds.add("refresh_static")\n' - refresh_start = rebase.find(refresh_start_marker) - if refresh_start < 0: - raise SystemExit("refreshStaticState resolver start anchor changed") - refresh_end = rebase.find(refresh_end_marker, refresh_start) - if refresh_end < 0: - raise SystemExit("refreshStaticState resolver end anchor changed") - refresh_end += len(refresh_end_marker) - replacement_refresh = ( - ' if (\n' - ' "public func refreshStaticState(" in ours\n' - ' and "public func refreshStaticState()" in theirs\n' - ' and "guard supportsMTPLXLiveControls else { return }" in theirs\n' - ' and "let client = apiClient" not in ours\n' - ' ):\n' - ' text = ours\n' - ' if not text.endswith("\\n"):\n' - ' text += "\\n"\n' - ' text += (\n' - ' " guard supportsMTPLXLiveControls else {\\n"\n' - ' " health = nil\\n"\n' - ' " capabilities = nil\\n"\n' - ' " sessions = nil\\n"\n' - ' " prefillHistory = nil\\n"\n' - ' " models = nil\\n"\n' - ' " return\\n"\n' - ' " }\\n"\n' - ' )\n' - ' kinds.add("refresh_static")\n' + + replace_resolver( + ( + ' if (\n' + ' "private func flushFreshLaunchLiveOnlySettingsIfNeeded(" in ours\n' + ), + ' kinds.add("flush_fresh")\n', + ( + ' if (\n' + ' "private func flushFreshLaunchLiveOnlySettingsIfNeeded(" in ours\n' + ' and "private func flushFreshLaunchLiveOnlySettingsIfNeeded()" in theirs\n' + ' and "guard supportsMTPLXLiveControls else {" in theirs\n' + ' and "guard let pending = pendingLiveSettings" not in ours\n' + ' ):\n' + ' text = ours\n' + ' if not text.endswith("\\n"):\n' + ' text += "\\n"\n' + ' text += (\n' + ' " guard supportsMTPLXLiveControls else {\\n"\n' + ' " pendingLiveSettings = nil\\n"\n' + ' " pendingLiveSettingsModel = nil\\n"\n' + ' " settings = nil\\n"\n' + ' " return\\n"\n' + ' " }\\n"\n' + ' )\n' + ' kinds.add("flush_fresh")\n' + ), + "flushFreshLaunchLiveOnlySettingsIfNeeded", ) - rebase = rebase[:refresh_start] + replacement_refresh + rebase[refresh_end:] - blocks[0] = rebase + extra_patch = Path(".github/temporary-pr254-extra.py") + if extra_patch.exists(): + namespace = {"rebase": rebase} + exec(compile(extra_patch.read_text(encoding="utf-8"), str(extra_patch), "exec"), namespace) + rebase = namespace["rebase"] + + blocks[0] = rebase Path("/tmp/pr254-rebase.sh").write_text(blocks[0] + "\n", encoding="utf-8") Path("/tmp/pr254-squash.sh").write_text(blocks[1] + "\n", encoding="utf-8") Path("/tmp/pr254-validate.sh").write_text(blocks[2] + "\n", encoding="utf-8") @@ -169,11 +202,12 @@ jobs: .github/workflows/temporary-execute-rebase-254-macos14.yml \ .github/workflows/temporary-execute-rebase-254-quote-fix.yml \ .github/pr254-wrapper-template.yml \ - .github/temporary-rebase-254-trigger.txt + .github/temporary-rebase-254-trigger.txt \ + .github/temporary-pr254-extra.py bash /tmp/pr254-squash.sh test "$(git rev-list --count upstream/main..HEAD)" = "1" test "$(git rev-parse HEAD^)" = "$(git rev-parse upstream/main)" - test -z "$(git diff --name-only upstream/main...HEAD -- '.github/workflows/temporary-*' '.github/pr254-wrapper-template.yml' '.github/temporary-rebase-254-trigger.txt')" + test -z "$(git diff --name-only upstream/main...HEAD -- '.github/workflows/temporary-*' '.github/pr254-wrapper-template.yml' '.github/temporary-*')" git show -s --format=%B HEAD | grep -F "Signed-off-by: Philip John Basile " git diff --check upstream/main...HEAD bash /tmp/pr254-validate.sh From 382698a8aa5ec4501f2bf811785ed7630d8caa3b Mon Sep 17 00:00:00 2001 From: Philip John Basile Date: Wed, 19 Aug 2026 20:22:50 -0400 Subject: [PATCH 11/11] ci: resolve PR 254 post-start lifecycle seam --- .github/temporary-pr254-extra.py | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/temporary-pr254-extra.py diff --git a/.github/temporary-pr254-extra.py b/.github/temporary-pr254-extra.py new file mode 100644 index 000000000..eff99da9a --- /dev/null +++ b/.github/temporary-pr254-extra.py @@ -0,0 +1,55 @@ +post_start_begin = ( + ' if (\n' + ' "private func refreshPostStartState(" in ours\n' +) +post_start_end_marker = ' kinds.add("post_start")\n' +post_start_start = rebase.find(post_start_begin) +if post_start_start < 0: + raise SystemExit("post-start resolver start anchor changed") +post_start_end = rebase.find(post_start_end_marker, post_start_start) +if post_start_end < 0: + raise SystemExit("post-start resolver end anchor changed") +post_start_end += len(post_start_end_marker) +post_start_replacement = ''' if ( + "configuration: MTPLXAppConfiguration," in ours + and "lifecycleEpoch: Int" in ours + and "recoveryGeneration: Int?" in ours + and ") async -> Bool" in ours + and "configuration: MTPLXAppConfiguration" in theirs + and "daemonBackendKind(for: configuration) == .mtplx" in theirs + and "external mlx-serve ready" in theirs + and "startExternalMlxServeHealthWatchdog" in theirs + ): + text = ours + if not text.endswith("\\n"): + text += "\\n" + text += ( + " guard daemonBackendKind(for: configuration) == .mtplx else {\\n" + " health = nil\\n" + " capabilities = nil\\n" + " sessions = nil\\n" + " sessionBank = nil\\n" + " settings = nil\\n" + " pendingLiveSettings = nil\\n" + " pendingLiveSettingsModel = nil\\n" + " connectionState = .idle\\n" + " await supervisor.logs.append(\\n" + " \\\"external mlx-serve ready; MTPLX live controls and metrics are unavailable\\\",\\n" + " stream: .system\\n" + " )\\n" + " guard daemonSessionIsCurrent(\\n" + " lifecycleEpoch: lifecycleEpoch,\\n" + " launchID: nil,\\n" + " recoveryGeneration: recoveryGeneration\\n" + " ) else { return false }\\n" + " startExternalMlxServeHealthWatchdog()\\n" + " return daemonSessionIsCurrent(\\n" + " lifecycleEpoch: lifecycleEpoch,\\n" + " launchID: nil,\\n" + " recoveryGeneration: recoveryGeneration\\n" + " )\\n" + " }\\n" + ) + kinds.add("post_start") +''' +rebase = rebase[:post_start_start] + post_start_replacement + rebase[post_start_end:]