Implement session cache upgrade phase 1 foundation
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -6,10 +6,20 @@ Native macOS SwiftUI app for local LLMs on Apple Silicon via MLX. Provides a cha
|
|||||||
|
|
||||||
**Always use `./build.sh` to build the project** — never call `xcodebuild` directly. The script runs xcodegen first (to pick up new/removed files) and uses the correct scheme, destination, and build directory.
|
**Always use `./build.sh` to build the project** — never call `xcodebuild` directly. The script runs xcodegen first (to pick up new/removed files) and uses the correct scheme, destination, and build directory.
|
||||||
|
|
||||||
|
**Always use `./test.sh` to run tests** — it regenerates the Xcode project first and runs the shared `MLXServer` test scheme so test runs are reproducible.
|
||||||
|
|
||||||
|
Tests are required for finished work when the change is reasonably testable.
|
||||||
|
Relevant tests must exist and must pass before work is considered complete.
|
||||||
|
|
||||||
|
Pre-existing errors don't exist: every error is your responsibility and you have to fix it before claiming you are done.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build (requires xcodegen: brew install xcodegen)
|
# Build (requires xcodegen: brew install xcodegen)
|
||||||
./build.sh
|
./build.sh
|
||||||
|
|
||||||
|
# Test
|
||||||
|
./test.sh
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
open "build/Debug/MLX Server.app"
|
open "build/Debug/MLX Server.app"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,12 +25,15 @@
|
|||||||
5946258F1DE88CE904584E0B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944C699FBB76C734C9DF2F2E /* ContentView.swift */; };
|
5946258F1DE88CE904584E0B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944C699FBB76C734C9DF2F2E /* ContentView.swift */; };
|
||||||
5C1E8FE1C521914CEF98D3AA /* ChatMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */; };
|
5C1E8FE1C521914CEF98D3AA /* ChatMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */; };
|
||||||
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0EAB35D7130D56B9E7484BA /* StatusBarView.swift */; };
|
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0EAB35D7130D56B9E7484BA /* StatusBarView.swift */; };
|
||||||
|
67262C5E24739F1FE0011439 /* StreamingSSEEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */; };
|
||||||
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733A0D1D4AC25DDDA6C8684 /* LocalModelResolver.swift */; };
|
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733A0D1D4AC25DDDA6C8684 /* LocalModelResolver.swift */; };
|
||||||
7CD765C1E2F9F4D7504C8D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B629DA084A9A40E54F8EA5FA /* Assets.xcassets */; };
|
7CD765C1E2F9F4D7504C8D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B629DA084A9A40E54F8EA5FA /* Assets.xcassets */; };
|
||||||
80646C5066BF79BC76E1D9D7 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */; };
|
80646C5066BF79BC76E1D9D7 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */; };
|
||||||
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */; };
|
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */; };
|
||||||
85FB1EB49D76A9F21E181346 /* ChatScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04EE8E6418EC6E9B66999B0 /* ChatScene.swift */; };
|
85FB1EB49D76A9F21E181346 /* ChatScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04EE8E6418EC6E9B66999B0 /* ChatScene.swift */; };
|
||||||
945474365D0B3E961811909A /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = D5E8E1C2DD8D8AABB4306193 /* MLXVLM */; };
|
945474365D0B3E961811909A /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = D5E8E1C2DD8D8AABB4306193 /* MLXVLM */; };
|
||||||
|
962083CCCC4AC848E0BBBC99 /* CancellationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */; };
|
||||||
|
A146BBA70CFBEC505BDCDF0D /* ImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1A89C076E717F87A60397D /* ImageDecoder.swift */; };
|
||||||
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */; };
|
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */; };
|
||||||
B1D9BC407DB7DB1489230C20 /* MonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4239CFF94B819C35A8D4D617 /* MonitorView.swift */; };
|
B1D9BC407DB7DB1489230C20 /* MonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4239CFF94B819C35A8D4D617 /* MonitorView.swift */; };
|
||||||
B5AA6E3B4BE21676226B342B /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD93859F0291F1A3E09DA5 /* ChatViewModel.swift */; };
|
B5AA6E3B4BE21676226B342B /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD93859F0291F1A3E09DA5 /* ChatViewModel.swift */; };
|
||||||
@@ -42,12 +45,25 @@
|
|||||||
D666A311788375E8A061C832 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4147321383E94E9F17A0154E /* SettingsView.swift */; };
|
D666A311788375E8A061C832 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4147321383E94E9F17A0154E /* SettingsView.swift */; };
|
||||||
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A52E2C9964ADA9D841A89B /* APIModels.swift */; };
|
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A52E2C9964ADA9D841A89B /* APIModels.swift */; };
|
||||||
DF5C525DBD2E3153256951C1 /* SceneManagementWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1592FD260014C4FBDB6995 /* SceneManagementWindow.swift */; };
|
DF5C525DBD2E3153256951C1 /* SceneManagementWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1592FD260014C4FBDB6995 /* SceneManagementWindow.swift */; };
|
||||||
|
E199D0BB09B61AC128AB093A /* CancellationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3489501F2F8E1BA382347CFA /* CancellationToken.swift */; };
|
||||||
|
E92B6656C251EDA246B8F582 /* ImageDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */; };
|
||||||
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */; };
|
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */; };
|
||||||
F546CE5955ED253D8A793D5E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A98257123539E9E738213BFA /* MarkdownUI */; };
|
F546CE5955ED253D8A793D5E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A98257123539E9E738213BFA /* MarkdownUI */; };
|
||||||
FAF7D4714AC6D02674920208 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B359324B5FD8D106C74338 /* ChatMessage.swift */; };
|
FAF7D4714AC6D02674920208 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B359324B5FD8D106C74338 /* ChatMessage.swift */; };
|
||||||
FCD48F8C132A2B830A15EEB4 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 3F5A4AC6DBAF7CA686ECA74E /* MLXLLM */; };
|
FCD48F8C132A2B830A15EEB4 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 3F5A4AC6DBAF7CA686ECA74E /* MLXLLM */; };
|
||||||
|
FE4405F66873C75CD6FA19A5 /* StreamingSSEEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
9F9E4F692B655CD8CE88479C /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 938BC479816FCA8527B731F9 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = BCD7107EE884C9B2F4C2C40E;
|
||||||
|
remoteInfo = MLXServer;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0F03A123A8908714A89315FE /* SceneCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCommands.swift; sourceTree = "<group>"; };
|
0F03A123A8908714A89315FE /* SceneCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCommands.swift; sourceTree = "<group>"; };
|
||||||
145B888FBDD4F931512C5473 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
145B888FBDD4F931512C5473 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
@@ -56,6 +72,7 @@
|
|||||||
24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentMigration.swift; sourceTree = "<group>"; };
|
24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentMigration.swift; sourceTree = "<group>"; };
|
||||||
2DC8C86D397B1FCA08E07CBD /* DownloadModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModalView.swift; sourceTree = "<group>"; };
|
2DC8C86D397B1FCA08E07CBD /* DownloadModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModalView.swift; sourceTree = "<group>"; };
|
||||||
2E2FCA55CEBEBCED78D9479A /* SaveChatCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChatCommands.swift; sourceTree = "<group>"; };
|
2E2FCA55CEBEBCED78D9479A /* SaveChatCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChatCommands.swift; sourceTree = "<group>"; };
|
||||||
|
3489501F2F8E1BA382347CFA /* CancellationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationToken.swift; sourceTree = "<group>"; };
|
||||||
37FEB592E5E717F817B03151 /* SceneManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneManagementView.swift; sourceTree = "<group>"; };
|
37FEB592E5E717F817B03151 /* SceneManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneManagementView.swift; sourceTree = "<group>"; };
|
||||||
386CD08DC6338F42460DFBE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
386CD08DC6338F42460DFBE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
38DFC212AF4359A45FBE22BA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = "<group>"; };
|
38DFC212AF4359A45FBE22BA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = "<group>"; };
|
||||||
@@ -63,8 +80,11 @@
|
|||||||
3D08828E16B17EF02C14243E /* APIServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServer.swift; sourceTree = "<group>"; };
|
3D08828E16B17EF02C14243E /* APIServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServer.swift; sourceTree = "<group>"; };
|
||||||
4147321383E94E9F17A0154E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
4147321383E94E9F17A0154E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
4239CFF94B819C35A8D4D617 /* MonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitorView.swift; sourceTree = "<group>"; };
|
4239CFF94B819C35A8D4D617 /* MonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitorView.swift; sourceTree = "<group>"; };
|
||||||
|
49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingSSEEncoderTests.swift; sourceTree = "<group>"; };
|
||||||
|
615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingSSEEncoder.swift; sourceTree = "<group>"; };
|
||||||
6B3AA91D2C7842D7366F9A41 /* ChatDocumentPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentPackage.swift; sourceTree = "<group>"; };
|
6B3AA91D2C7842D7366F9A41 /* ChatDocumentPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentPackage.swift; sourceTree = "<group>"; };
|
||||||
6EE59189918D06B8D2F588FC /* MLXServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLXServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
6EE59189918D06B8D2F588FC /* MLXServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLXServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
7C1A89C076E717F87A60397D /* ImageDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDecoder.swift; sourceTree = "<group>"; };
|
||||||
922CBDC9206737BD04AF2874 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = "<group>"; };
|
922CBDC9206737BD04AF2874 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = "<group>"; };
|
||||||
944C699FBB76C734C9DF2F2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
944C699FBB76C734C9DF2F2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
A4B359324B5FD8D106C74338 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
|
A4B359324B5FD8D106C74338 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
|
||||||
@@ -82,10 +102,13 @@
|
|||||||
D7C9BAD674E29688ACE53B0B /* ChatExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatExporter.swift; sourceTree = "<group>"; };
|
D7C9BAD674E29688ACE53B0B /* ChatExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatExporter.swift; sourceTree = "<group>"; };
|
||||||
DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesView.swift; sourceTree = "<group>"; };
|
DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesView.swift; sourceTree = "<group>"; };
|
||||||
E35452B166893B25E765FF70 /* InferenceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceStats.swift; sourceTree = "<group>"; };
|
E35452B166893B25E765FF70 /* InferenceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceStats.swift; sourceTree = "<group>"; };
|
||||||
|
E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDecoderTests.swift; sourceTree = "<group>"; };
|
||||||
E5E6AD02CDF23BDAB64700A7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = "<group>"; };
|
E5E6AD02CDF23BDAB64700A7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = "<group>"; };
|
||||||
E73B165A1822729C907791AE /* ToolCallParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCallParser.swift; sourceTree = "<group>"; };
|
E73B165A1822729C907791AE /* ToolCallParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCallParser.swift; sourceTree = "<group>"; };
|
||||||
EF518FEBF3A38E830E3CE1A5 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
|
EF518FEBF3A38E830E3CE1A5 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
|
||||||
F1A52E2C9964ADA9D841A89B /* APIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIModels.swift; sourceTree = "<group>"; };
|
F1A52E2C9964ADA9D841A89B /* APIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIModels.swift; sourceTree = "<group>"; };
|
||||||
|
F4CE2D594F7433C76169151A /* MLXServerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MLXServerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationTokenTests.swift; sourceTree = "<group>"; };
|
||||||
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSessionCache.swift; sourceTree = "<group>"; };
|
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSessionCache.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -104,6 +127,14 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
03BB61C0F16FAD47436AA178 /* MLXServerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
154AF0C071A7DC02EB5F6F49 /* Server */,
|
||||||
|
);
|
||||||
|
path = MLXServerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
05B1BAE308E64D2FB2E73823 /* Utilities */ = {
|
05B1BAE308E64D2FB2E73823 /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -126,10 +157,21 @@
|
|||||||
path = Documents;
|
path = Documents;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
154AF0C071A7DC02EB5F6F49 /* Server */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */,
|
||||||
|
E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */,
|
||||||
|
49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */,
|
||||||
|
);
|
||||||
|
path = Server;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
652987C2A419DBFC79E32CDE /* Products */ = {
|
652987C2A419DBFC79E32CDE /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6EE59189918D06B8D2F588FC /* MLXServer.app */,
|
6EE59189918D06B8D2F588FC /* MLXServer.app */,
|
||||||
|
F4CE2D594F7433C76169151A /* MLXServerTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -205,7 +247,10 @@
|
|||||||
children = (
|
children = (
|
||||||
F1A52E2C9964ADA9D841A89B /* APIModels.swift */,
|
F1A52E2C9964ADA9D841A89B /* APIModels.swift */,
|
||||||
3D08828E16B17EF02C14243E /* APIServer.swift */,
|
3D08828E16B17EF02C14243E /* APIServer.swift */,
|
||||||
|
3489501F2F8E1BA382347CFA /* CancellationToken.swift */,
|
||||||
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */,
|
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */,
|
||||||
|
7C1A89C076E717F87A60397D /* ImageDecoder.swift */,
|
||||||
|
615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */,
|
||||||
E73B165A1822729C907791AE /* ToolCallParser.swift */,
|
E73B165A1822729C907791AE /* ToolCallParser.swift */,
|
||||||
16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */,
|
16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */,
|
||||||
);
|
);
|
||||||
@@ -216,6 +261,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6816BF8EF7C92384DD7C9177 /* MLXServer */,
|
6816BF8EF7C92384DD7C9177 /* MLXServer */,
|
||||||
|
03BB61C0F16FAD47436AA178 /* MLXServerTests */,
|
||||||
652987C2A419DBFC79E32CDE /* Products */,
|
652987C2A419DBFC79E32CDE /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -246,6 +292,24 @@
|
|||||||
productReference = 6EE59189918D06B8D2F588FC /* MLXServer.app */;
|
productReference = 6EE59189918D06B8D2F588FC /* MLXServer.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
CE11F8C258BB944F38A5840D /* MLXServerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = A2168D037766ED36A199C6F7 /* Build configuration list for PBXNativeTarget "MLXServerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
6DEBF8BBA4F6DB333E0C55B0 /* Sources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
8870DD8F1917C831FD4FD595 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = MLXServerTests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = MLXServerTests;
|
||||||
|
productReference = F4CE2D594F7433C76169151A /* MLXServerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -276,6 +340,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
BCD7107EE884C9B2F4C2C40E /* MLXServer */,
|
BCD7107EE884C9B2F4C2C40E /* MLXServer */,
|
||||||
|
CE11F8C258BB944F38A5840D /* MLXServerTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -292,12 +357,23 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
6DEBF8BBA4F6DB333E0C55B0 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
962083CCCC4AC848E0BBBC99 /* CancellationTokenTests.swift in Sources */,
|
||||||
|
E92B6656C251EDA246B8F582 /* ImageDecoderTests.swift in Sources */,
|
||||||
|
FE4405F66873C75CD6FA19A5 /* StreamingSSEEncoderTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BC03844286F51DFAEF96B823 /* Sources */ = {
|
BC03844286F51DFAEF96B823 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */,
|
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */,
|
||||||
50DD129CCF2843482DEC3B96 /* APIServer.swift in Sources */,
|
50DD129CCF2843482DEC3B96 /* APIServer.swift in Sources */,
|
||||||
|
E199D0BB09B61AC128AB093A /* CancellationToken.swift in Sources */,
|
||||||
2E3A02DF9C6A5109E532D5E2 /* ChatDocumentController.swift in Sources */,
|
2E3A02DF9C6A5109E532D5E2 /* ChatDocumentController.swift in Sources */,
|
||||||
1A8833E3CCD3289C95E282A2 /* ChatDocumentManifest.swift in Sources */,
|
1A8833E3CCD3289C95E282A2 /* ChatDocumentManifest.swift in Sources */,
|
||||||
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */,
|
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */,
|
||||||
@@ -312,6 +388,7 @@
|
|||||||
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */,
|
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */,
|
||||||
C07A377244DCD67F4FE709FE /* DownloadModalView.swift in Sources */,
|
C07A377244DCD67F4FE709FE /* DownloadModalView.swift in Sources */,
|
||||||
4DC033E45880B2948B47DEB1 /* FocusedValues.swift in Sources */,
|
4DC033E45880B2948B47DEB1 /* FocusedValues.swift in Sources */,
|
||||||
|
A146BBA70CFBEC505BDCDF0D /* ImageDecoder.swift in Sources */,
|
||||||
2D08769282BD71C170DB0943 /* InferenceStats.swift in Sources */,
|
2D08769282BD71C170DB0943 /* InferenceStats.swift in Sources */,
|
||||||
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */,
|
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */,
|
||||||
50B6861FF8610B3ED4FFAD9D /* MLXServerApp.swift in Sources */,
|
50B6861FF8610B3ED4FFAD9D /* MLXServerApp.swift in Sources */,
|
||||||
@@ -328,6 +405,7 @@
|
|||||||
CFEE79815DFB80E51FE3745A /* SceneStore.swift in Sources */,
|
CFEE79815DFB80E51FE3745A /* SceneStore.swift in Sources */,
|
||||||
D666A311788375E8A061C832 /* SettingsView.swift in Sources */,
|
D666A311788375E8A061C832 /* SettingsView.swift in Sources */,
|
||||||
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */,
|
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */,
|
||||||
|
67262C5E24739F1FE0011439 /* StreamingSSEEncoder.swift in Sources */,
|
||||||
189362AAE2CDE5D4B3428334 /* ToolCallParser.swift in Sources */,
|
189362AAE2CDE5D4B3428334 /* ToolCallParser.swift in Sources */,
|
||||||
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */,
|
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -335,7 +413,49 @@
|
|||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
8870DD8F1917C831FD4FD595 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = BCD7107EE884C9B2F4C2C40E /* MLXServer */;
|
||||||
|
targetProxy = 9F9E4F692B655CD8CE88479C /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
18921C5B777D8B7FEF662D6F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mlxserver.MLXServerTests;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
2B83417701A93BF554428C56 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mlxserver.MLXServerTests;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
6C0C08FC4653A138A768ECF0 /* Release */ = {
|
6C0C08FC4653A138A768ECF0 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -524,6 +644,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Debug;
|
||||||
};
|
};
|
||||||
|
A2168D037766ED36A199C6F7 /* Build configuration list for PBXNativeTarget "MLXServerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
2B83417701A93BF554428C56 /* Debug */,
|
||||||
|
18921C5B777D8B7FEF662D6F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
116
MLXServer.xcodeproj/xcshareddata/xcschemes/MLXServer.xcscheme
Normal file
116
MLXServer.xcodeproj/xcshareddata/xcschemes/MLXServer.xcscheme
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1640"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
runPostActionsOnFailure = "NO">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
|
||||||
|
BuildableName = "MLXServer.app"
|
||||||
|
BlueprintName = "MLXServer"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "NO"
|
||||||
|
buildForProfiling = "NO"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CE11F8C258BB944F38A5840D"
|
||||||
|
BuildableName = "MLXServerTests.xctest"
|
||||||
|
BlueprintName = "MLXServerTests"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
|
||||||
|
BuildableName = "MLXServer.app"
|
||||||
|
BlueprintName = "MLXServer"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CE11F8C258BB944F38A5840D"
|
||||||
|
BuildableName = "MLXServerTests.xctest"
|
||||||
|
BlueprintName = "MLXServerTests"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
|
||||||
|
BuildableName = "MLXServer.app"
|
||||||
|
BlueprintName = "MLXServer"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
|
||||||
|
BuildableName = "MLXServer.app"
|
||||||
|
BlueprintName = "MLXServer"
|
||||||
|
ReferencedContainer = "container:MLXServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -15,6 +15,10 @@ struct ContentView: View {
|
|||||||
@State private var documentErrorMessage: String?
|
@State private var documentErrorMessage: String?
|
||||||
@State private var exportErrorMessage: String?
|
@State private var exportErrorMessage: String?
|
||||||
|
|
||||||
|
private var isRunningTests: Bool {
|
||||||
|
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
exportedContent
|
exportedContent
|
||||||
}
|
}
|
||||||
@@ -30,11 +34,11 @@ struct ContentView: View {
|
|||||||
delegate.chatViewModel = vm
|
delegate.chatViewModel = vm
|
||||||
}
|
}
|
||||||
// Auto-start API server if configured
|
// Auto-start API server if configured
|
||||||
if Preferences.apiAutoStart {
|
if Preferences.apiAutoStart && !isRunningTests {
|
||||||
vm.startAPIServer()
|
vm.startAPIServer()
|
||||||
}
|
}
|
||||||
// Restore autosaved session if no document is being opened
|
// Restore autosaved session if no document is being opened
|
||||||
if !documentController.hasPendingOpenRequests {
|
if !documentController.hasPendingOpenRequests && !isRunningTests {
|
||||||
Task {
|
Task {
|
||||||
await vm.restoreFromAutosave()
|
await vm.restoreFromAutosave()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import AppKit
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MLXLMCommon
|
import MLXLMCommon
|
||||||
import Network
|
import Network
|
||||||
@@ -288,7 +287,7 @@ final class APIServer {
|
|||||||
var messageImages: [UserInput.Image] = []
|
var messageImages: [UserInput.Image] = []
|
||||||
var messageImageBytes = 0
|
var messageImageBytes = 0
|
||||||
for urlString in imageURLs {
|
for urlString in imageURLs {
|
||||||
if let decoded = decodeBase64Image(urlString) {
|
if let decoded = ImageDecoder.decode(urlString) {
|
||||||
messageImages.append(decoded.image)
|
messageImages.append(decoded.image)
|
||||||
messageImageBytes += decoded.estimatedBytes
|
messageImageBytes += decoded.estimatedBytes
|
||||||
}
|
}
|
||||||
@@ -448,28 +447,6 @@ final class APIServer {
|
|||||||
modelManager.touchActivity()
|
modelManager.touchActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a base64 data URI (data:image/png;base64,...) into a UserInput.Image.
|
|
||||||
private func decodeBase64Image(_ urlString: String) -> DecodedImage? {
|
|
||||||
// Handle data URIs: data:image/png;base64,<data>
|
|
||||||
let base64String: String
|
|
||||||
if urlString.hasPrefix("data:") {
|
|
||||||
guard let commaIndex = urlString.firstIndex(of: ",") else { return nil }
|
|
||||||
base64String = String(urlString[urlString.index(after: commaIndex)...])
|
|
||||||
} else {
|
|
||||||
// Could be a plain base64 string
|
|
||||||
base64String = urlString
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let data = Data(base64Encoded: base64String),
|
|
||||||
let nsImage = NSImage(data: data),
|
|
||||||
let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let estimatedBytes = max(data.count, cgImage.width * cgImage.height * 4)
|
|
||||||
return DecodedImage(image: .ciImage(CIImage(cgImage: cgImage)), estimatedBytes: estimatedBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Non-streaming response
|
// MARK: - Non-streaming response
|
||||||
|
|
||||||
private func handleNonStreamingResponse(
|
private func handleNonStreamingResponse(
|
||||||
@@ -887,11 +864,6 @@ final class APIServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct DecodedImage {
|
|
||||||
let image: UserInput.Image
|
|
||||||
let estimatedBytes: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - HTTP request parser
|
// MARK: - HTTP request parser
|
||||||
|
|
||||||
private struct HTTPRequest {
|
private struct HTTPRequest {
|
||||||
|
|||||||
14
MLXServer/Server/CancellationToken.swift
Normal file
14
MLXServer/Server/CancellationToken.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
/// Thread-safe cancellation flag for cooperative stream shutdown.
|
||||||
|
final class CancellationToken: @unchecked Sendable {
|
||||||
|
private let lock = OSAllocatedUnfairLock(initialState: false)
|
||||||
|
|
||||||
|
var isCancelled: Bool {
|
||||||
|
lock.withLock { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
lock.withLock { $0 = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
31
MLXServer/Server/ImageDecoder.swift
Normal file
31
MLXServer/Server/ImageDecoder.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import AppKit
|
||||||
|
import CoreImage
|
||||||
|
import Foundation
|
||||||
|
import MLXLMCommon
|
||||||
|
|
||||||
|
/// Extracted from APIServer — decodes data URIs to UserInput.Image.
|
||||||
|
enum ImageDecoder {
|
||||||
|
struct DecodedImage {
|
||||||
|
let image: UserInput.Image
|
||||||
|
let estimatedBytes: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(_ urlString: String) -> DecodedImage? {
|
||||||
|
let base64String: String
|
||||||
|
if urlString.hasPrefix("data:") {
|
||||||
|
guard let commaIndex = urlString.firstIndex(of: ",") else { return nil }
|
||||||
|
base64String = String(urlString[urlString.index(after: commaIndex)...])
|
||||||
|
} else {
|
||||||
|
base64String = urlString
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = Data(base64Encoded: base64String),
|
||||||
|
let nsImage = NSImage(data: data),
|
||||||
|
let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let estimatedBytes = max(data.count, cgImage.width * cgImage.height * 4)
|
||||||
|
return DecodedImage(image: .ciImage(CIImage(cgImage: cgImage)), estimatedBytes: estimatedBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
MLXServer/Server/StreamingSSEEncoder.swift
Normal file
72
MLXServer/Server/StreamingSSEEncoder.swift
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Pre-computes static JSON parts for SSE streaming.
|
||||||
|
/// Only the dynamic delta payload is serialized per token.
|
||||||
|
struct StreamingSSEEncoder: Sendable {
|
||||||
|
private let requestId: String
|
||||||
|
private let created: Int
|
||||||
|
private let modelName: String
|
||||||
|
|
||||||
|
init(requestId: String, created: Int, modelName: String) {
|
||||||
|
self.requestId = requestId
|
||||||
|
self.created = created
|
||||||
|
self.modelName = modelName
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeContentDelta(_ text: String) -> Data {
|
||||||
|
Self.encodeChunk(
|
||||||
|
APIChatCompletionChunk(
|
||||||
|
id: requestId,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: created,
|
||||||
|
model: modelName,
|
||||||
|
choices: [
|
||||||
|
APIStreamChoice(
|
||||||
|
index: 0,
|
||||||
|
delta: APIDeltaMessage(role: nil, content: text, tool_calls: nil),
|
||||||
|
finish_reason: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usage: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeRoleDelta(_ role: String) -> Data {
|
||||||
|
Self.encodeChunk(
|
||||||
|
APIChatCompletionChunk(
|
||||||
|
id: requestId,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: created,
|
||||||
|
model: modelName,
|
||||||
|
choices: [
|
||||||
|
APIStreamChoice(
|
||||||
|
index: 0,
|
||||||
|
delta: APIDeltaMessage(role: role, content: nil, tool_calls: nil),
|
||||||
|
finish_reason: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usage: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func encodeFinalChunk(_ chunk: APIChatCompletionChunk) -> Data {
|
||||||
|
encodeChunk(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func encodeChunk(_ chunk: APIChatCompletionChunk) -> Data {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.sortedKeys]
|
||||||
|
|
||||||
|
guard let json = try? encoder.encode(chunk) else {
|
||||||
|
return Data("data: {}\n\n".utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = Data(capacity: json.count + 8)
|
||||||
|
data.append(Data("data: ".utf8))
|
||||||
|
data.append(json)
|
||||||
|
data.append(Data("\n\n".utf8))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
18
MLXServerTests/Server/CancellationTokenTests.swift
Normal file
18
MLXServerTests/Server/CancellationTokenTests.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MLX_Server
|
||||||
|
|
||||||
|
final class CancellationTokenTests: XCTestCase {
|
||||||
|
func testStartsNotCancelled() {
|
||||||
|
let token = CancellationToken()
|
||||||
|
|
||||||
|
XCTAssertFalse(token.isCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCancelSetsFlag() {
|
||||||
|
let token = CancellationToken()
|
||||||
|
|
||||||
|
token.cancel()
|
||||||
|
|
||||||
|
XCTAssertTrue(token.isCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
MLXServerTests/Server/ImageDecoderTests.swift
Normal file
20
MLXServerTests/Server/ImageDecoderTests.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MLX_Server
|
||||||
|
|
||||||
|
final class ImageDecoderTests: XCTestCase {
|
||||||
|
private let onePixelPNGBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lRyXWQAAAABJRU5ErkJggg=="
|
||||||
|
|
||||||
|
func testDecodeDataURI() {
|
||||||
|
let image = ImageDecoder.decode("data:image/png;base64,\(onePixelPNGBase64)")
|
||||||
|
|
||||||
|
XCTAssertNotNil(image)
|
||||||
|
XCTAssertGreaterThanOrEqual(image?.estimatedBytes ?? 0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodePlainBase64() {
|
||||||
|
let image = ImageDecoder.decode(onePixelPNGBase64)
|
||||||
|
|
||||||
|
XCTAssertNotNil(image)
|
||||||
|
XCTAssertGreaterThanOrEqual(image?.estimatedBytes ?? 0, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
MLXServerTests/Server/StreamingSSEEncoderTests.swift
Normal file
82
MLXServerTests/Server/StreamingSSEEncoderTests.swift
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MLX_Server
|
||||||
|
|
||||||
|
final class StreamingSSEEncoderTests: XCTestCase {
|
||||||
|
func testEncodeContentDeltaMatchesJSONEncoderOutput() throws {
|
||||||
|
let encoder = StreamingSSEEncoder(requestId: "chatcmpl-test", created: 1_234_567, modelName: "qwen\"model")
|
||||||
|
let text = "line 1\nline 2\t\"quoted\"\\slash"
|
||||||
|
|
||||||
|
let actual = encoder.encodeContentDelta(text)
|
||||||
|
let expected = try baselineData(
|
||||||
|
for: APIChatCompletionChunk(
|
||||||
|
id: "chatcmpl-test",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: 1_234_567,
|
||||||
|
model: "qwen\"model",
|
||||||
|
choices: [
|
||||||
|
APIStreamChoice(
|
||||||
|
index: 0,
|
||||||
|
delta: APIDeltaMessage(role: nil, content: text, tool_calls: nil),
|
||||||
|
finish_reason: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usage: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeRoleDeltaMatchesJSONEncoderOutput() throws {
|
||||||
|
let encoder = StreamingSSEEncoder(requestId: "chatcmpl-role", created: 99, modelName: "gemma")
|
||||||
|
|
||||||
|
let actual = encoder.encodeRoleDelta("assistant")
|
||||||
|
let expected = try baselineData(
|
||||||
|
for: APIChatCompletionChunk(
|
||||||
|
id: "chatcmpl-role",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: 99,
|
||||||
|
model: "gemma",
|
||||||
|
choices: [
|
||||||
|
APIStreamChoice(
|
||||||
|
index: 0,
|
||||||
|
delta: APIDeltaMessage(role: "assistant", content: nil, tool_calls: nil),
|
||||||
|
finish_reason: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usage: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFinalChunkMatchesBaseline() throws {
|
||||||
|
let chunk = APIChatCompletionChunk(
|
||||||
|
id: "chatcmpl-final",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: 7,
|
||||||
|
model: "gemma",
|
||||||
|
choices: [
|
||||||
|
APIStreamChoice(
|
||||||
|
index: 0,
|
||||||
|
delta: APIDeltaMessage(role: nil, content: nil, tool_calls: nil),
|
||||||
|
finish_reason: "stop"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usage: APIUsageInfo(prompt_tokens: 10, completion_tokens: 3, total_tokens: 13)
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(StreamingSSEEncoder.encodeFinalChunk(chunk), try baselineData(for: chunk))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func baselineData(for chunk: APIChatCompletionChunk) throws -> Data {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.sortedKeys]
|
||||||
|
let json = try encoder.encode(chunk)
|
||||||
|
var data = Data("data: ".utf8)
|
||||||
|
data.append(json)
|
||||||
|
data.append(Data("\n\n".utf8))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2558,9 +2558,9 @@ Each step should be independently buildable and testable.
|
|||||||
|
|
||||||
### Phase 1: Foundation (no behavior change yet)
|
### Phase 1: Foundation (no behavior change yet)
|
||||||
|
|
||||||
1. **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test.
|
1. [x] **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test.
|
||||||
2. **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move.
|
2. [x] **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move.
|
||||||
3. **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output.
|
3. [x] **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output.
|
||||||
|
|
||||||
### Phase 2: Core Engine
|
### Phase 2: Core Engine
|
||||||
|
|
||||||
|
|||||||
22
project.yml
22
project.yml
@@ -42,3 +42,25 @@ targets:
|
|||||||
product: MLXLMCommon
|
product: MLXLMCommon
|
||||||
- package: MarkdownUI
|
- package: MarkdownUI
|
||||||
product: MarkdownUI
|
product: MarkdownUI
|
||||||
|
MLXServerTests:
|
||||||
|
type: bundle.unit-test
|
||||||
|
platform: macOS
|
||||||
|
sources:
|
||||||
|
- MLXServerTests
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
GENERATE_INFOPLIST_FILE: "YES"
|
||||||
|
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server"
|
||||||
|
BUNDLE_LOADER: "$(TEST_HOST)"
|
||||||
|
dependencies:
|
||||||
|
- target: MLXServer
|
||||||
|
|
||||||
|
schemes:
|
||||||
|
MLXServer:
|
||||||
|
build:
|
||||||
|
targets:
|
||||||
|
MLXServer: all
|
||||||
|
MLXServerTests: [test]
|
||||||
|
test:
|
||||||
|
targets:
|
||||||
|
- MLXServerTests
|
||||||
|
|||||||
28
test.sh
Executable file
28
test.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BUILD_DIR="$PROJECT_DIR/build"
|
||||||
|
CONFIG="${1:-Debug}"
|
||||||
|
APP_NAME="MLX Server"
|
||||||
|
DESTINATION="${TEST_DESTINATION:-platform=macOS,arch=arm64}"
|
||||||
|
|
||||||
|
echo "==> Testing $APP_NAME ($CONFIG)"
|
||||||
|
|
||||||
|
# Regenerate Xcode project from project.yml (picks up any new/removed files)
|
||||||
|
if command -v xcodegen &>/dev/null; then
|
||||||
|
xcodegen generate --spec "$PROJECT_DIR/project.yml" --project "$PROJECT_DIR" 2>&1 | grep -v '^$'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests — filter to test progress, app warnings, build failures, and final result
|
||||||
|
xcodebuild \
|
||||||
|
-project "$PROJECT_DIR/MLXServer.xcodeproj" \
|
||||||
|
-scheme MLXServer \
|
||||||
|
-destination "$DESTINATION" \
|
||||||
|
-configuration "$CONFIG" \
|
||||||
|
SYMROOT="$BUILD_DIR" \
|
||||||
|
test 2>&1 | \
|
||||||
|
grep -E "(Test Suite|Test Case|Executed [0-9]+ tests|Testing started|Testing failed|Testing passed|error:|warning:.*MLXServer/|\*\* TEST|BUILD )"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Tests passed"
|
||||||
Reference in New Issue
Block a user