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 `./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
|
||||
# Build (requires xcodegen: brew install xcodegen)
|
||||
./build.sh
|
||||
|
||||
# Test
|
||||
./test.sh
|
||||
|
||||
# Run
|
||||
open "build/Debug/MLX Server.app"
|
||||
```
|
||||
|
||||
@@ -25,12 +25,15 @@
|
||||
5946258F1DE88CE904584E0B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944C699FBB76C734C9DF2F2E /* ContentView.swift */; };
|
||||
5C1E8FE1C521914CEF98D3AA /* ChatMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.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 */; };
|
||||
7CD765C1E2F9F4D7504C8D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B629DA084A9A40E54F8EA5FA /* Assets.xcassets */; };
|
||||
80646C5066BF79BC76E1D9D7 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */; };
|
||||
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */; };
|
||||
85FB1EB49D76A9F21E181346 /* ChatScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04EE8E6418EC6E9B66999B0 /* ChatScene.swift */; };
|
||||
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 */; };
|
||||
B1D9BC407DB7DB1489230C20 /* MonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4239CFF94B819C35A8D4D617 /* MonitorView.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 */; };
|
||||
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A52E2C9964ADA9D841A89B /* APIModels.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 */; };
|
||||
F546CE5955ED253D8A793D5E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A98257123539E9E738213BFA /* MarkdownUI */; };
|
||||
FAF7D4714AC6D02674920208 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B359324B5FD8D106C74338 /* ChatMessage.swift */; };
|
||||
FCD48F8C132A2B830A15EEB4 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 3F5A4AC6DBAF7CA686ECA74E /* MLXLLM */; };
|
||||
FE4405F66873C75CD6FA19A5 /* StreamingSSEEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */; };
|
||||
/* 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 */
|
||||
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>"; };
|
||||
@@ -56,6 +72,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -63,8 +80,11 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -82,10 +102,13 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -104,6 +127,14 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
03BB61C0F16FAD47436AA178 /* MLXServerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
154AF0C071A7DC02EB5F6F49 /* Server */,
|
||||
);
|
||||
path = MLXServerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
05B1BAE308E64D2FB2E73823 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -126,10 +157,21 @@
|
||||
path = Documents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
154AF0C071A7DC02EB5F6F49 /* Server */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */,
|
||||
E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */,
|
||||
49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */,
|
||||
);
|
||||
path = Server;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
652987C2A419DBFC79E32CDE /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6EE59189918D06B8D2F588FC /* MLXServer.app */,
|
||||
F4CE2D594F7433C76169151A /* MLXServerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -205,7 +247,10 @@
|
||||
children = (
|
||||
F1A52E2C9964ADA9D841A89B /* APIModels.swift */,
|
||||
3D08828E16B17EF02C14243E /* APIServer.swift */,
|
||||
3489501F2F8E1BA382347CFA /* CancellationToken.swift */,
|
||||
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */,
|
||||
7C1A89C076E717F87A60397D /* ImageDecoder.swift */,
|
||||
615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */,
|
||||
E73B165A1822729C907791AE /* ToolCallParser.swift */,
|
||||
16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */,
|
||||
);
|
||||
@@ -216,6 +261,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6816BF8EF7C92384DD7C9177 /* MLXServer */,
|
||||
03BB61C0F16FAD47436AA178 /* MLXServerTests */,
|
||||
652987C2A419DBFC79E32CDE /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -246,6 +292,24 @@
|
||||
productReference = 6EE59189918D06B8D2F588FC /* MLXServer.app */;
|
||||
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 */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -276,6 +340,7 @@
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
BCD7107EE884C9B2F4C2C40E /* MLXServer */,
|
||||
CE11F8C258BB944F38A5840D /* MLXServerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -292,12 +357,23 @@
|
||||
/* End PBXResourcesBuildPhase 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 */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */,
|
||||
50DD129CCF2843482DEC3B96 /* APIServer.swift in Sources */,
|
||||
E199D0BB09B61AC128AB093A /* CancellationToken.swift in Sources */,
|
||||
2E3A02DF9C6A5109E532D5E2 /* ChatDocumentController.swift in Sources */,
|
||||
1A8833E3CCD3289C95E282A2 /* ChatDocumentManifest.swift in Sources */,
|
||||
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */,
|
||||
@@ -312,6 +388,7 @@
|
||||
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */,
|
||||
C07A377244DCD67F4FE709FE /* DownloadModalView.swift in Sources */,
|
||||
4DC033E45880B2948B47DEB1 /* FocusedValues.swift in Sources */,
|
||||
A146BBA70CFBEC505BDCDF0D /* ImageDecoder.swift in Sources */,
|
||||
2D08769282BD71C170DB0943 /* InferenceStats.swift in Sources */,
|
||||
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */,
|
||||
50B6861FF8610B3ED4FFAD9D /* MLXServerApp.swift in Sources */,
|
||||
@@ -328,6 +405,7 @@
|
||||
CFEE79815DFB80E51FE3745A /* SceneStore.swift in Sources */,
|
||||
D666A311788375E8A061C832 /* SettingsView.swift in Sources */,
|
||||
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */,
|
||||
67262C5E24739F1FE0011439 /* StreamingSSEEncoder.swift in Sources */,
|
||||
189362AAE2CDE5D4B3428334 /* ToolCallParser.swift in Sources */,
|
||||
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */,
|
||||
);
|
||||
@@ -335,7 +413,49 @@
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
8870DD8F1917C831FD4FD595 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BCD7107EE884C9B2F4C2C40E /* MLXServer */;
|
||||
targetProxy = 9F9E4F692B655CD8CE88479C /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency 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 */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -524,6 +644,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
A2168D037766ED36A199C6F7 /* Build configuration list for PBXNativeTarget "MLXServerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2B83417701A93BF554428C56 /* Debug */,
|
||||
18921C5B777D8B7FEF662D6F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList 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 exportErrorMessage: String?
|
||||
|
||||
private var isRunningTests: Bool {
|
||||
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
exportedContent
|
||||
}
|
||||
@@ -30,11 +34,11 @@ struct ContentView: View {
|
||||
delegate.chatViewModel = vm
|
||||
}
|
||||
// Auto-start API server if configured
|
||||
if Preferences.apiAutoStart {
|
||||
if Preferences.apiAutoStart && !isRunningTests {
|
||||
vm.startAPIServer()
|
||||
}
|
||||
// Restore autosaved session if no document is being opened
|
||||
if !documentController.hasPendingOpenRequests {
|
||||
if !documentController.hasPendingOpenRequests && !isRunningTests {
|
||||
Task {
|
||||
await vm.restoreFromAutosave()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import MLXLMCommon
|
||||
import Network
|
||||
@@ -288,7 +287,7 @@ final class APIServer {
|
||||
var messageImages: [UserInput.Image] = []
|
||||
var messageImageBytes = 0
|
||||
for urlString in imageURLs {
|
||||
if let decoded = decodeBase64Image(urlString) {
|
||||
if let decoded = ImageDecoder.decode(urlString) {
|
||||
messageImages.append(decoded.image)
|
||||
messageImageBytes += decoded.estimatedBytes
|
||||
}
|
||||
@@ -448,28 +447,6 @@ final class APIServer {
|
||||
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
|
||||
|
||||
private func handleNonStreamingResponse(
|
||||
@@ -887,11 +864,6 @@ final class APIServer {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DecodedImage {
|
||||
let image: UserInput.Image
|
||||
let estimatedBytes: Int
|
||||
}
|
||||
|
||||
// MARK: - HTTP request parser
|
||||
|
||||
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)
|
||||
|
||||
1. **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test.
|
||||
2. **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move.
|
||||
3. **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output.
|
||||
1. [x] **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test.
|
||||
2. [x] **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move.
|
||||
3. [x] **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output.
|
||||
|
||||
### Phase 2: Core Engine
|
||||
|
||||
|
||||
22
project.yml
22
project.yml
@@ -42,3 +42,25 @@ targets:
|
||||
product: MLXLMCommon
|
||||
- package: 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