Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Sources/Conduit/Core/Types/DeviceCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,21 @@ extension DeviceCapabilities {
private static func getChipType() -> String? {
#if os(macOS) || os(iOS)
var size = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
guard sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) == 0, size > 1 else {
return nil
}

if size > 0 {
var buffer = [CChar](repeating: 0, count: size)
sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0)
// Use failable String initializer with null-terminated C string
return String(cString: buffer)
var buffer = [UInt8](repeating: 0, count: size)
guard sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0) == 0 else {
return nil
}
return nil

let nulIndex = buffer.firstIndex(of: 0) ?? buffer.endIndex
guard nulIndex > buffer.startIndex else {
return nil
}

return String(decoding: buffer[..<nulIndex], as: UTF8.self)
#elseif os(Linux)
// Linux: Read from /proc/cpuinfo
// NOTE: Returns the "model name" field which contains CPU description
Expand Down
43 changes: 4 additions & 39 deletions Sources/Conduit/Core/Types/GenerationSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,6 @@ import class Foundation.JSONEncoder
import class Foundation.JSONDecoder
import struct Foundation.Decimal

// MARK: - EncodingError

/// Error that occurs during schema encoding.
private enum EncodingError: Error, LocalizedError {
case invalidValue(Any, Context)

var errorDescription: String? {
switch self {
case .invalidValue(let value, let context):
return "Invalid value during encoding: \(value). \(context.debugDescription)"
}
}

struct Context: Sendable {
let codingPath: [any CodingKey]
let debugDescription: String
}
}

/// A type that describes properties of an object and any guides
/// on their values.
///
Expand Down Expand Up @@ -57,15 +38,7 @@ public struct GenerationSchema: Sendable, Codable, CustomDebugStringConvertible
}
var propsContainer = container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .properties)
for (name, node) in obj.properties {
guard let key = DynamicCodingKey(stringValue: name) else {
throw EncodingError.invalidValue(
name,
EncodingError.Context(
codingPath: container.codingPath,
debugDescription: "Unable to create coding key for property '\(name)'"
)
)
}
let key = DynamicCodingKey(stringValue: name)
try propsContainer.encode(node, forKey: key)
}
try container.encode(Array(obj.required), forKey: .required)
Expand Down Expand Up @@ -220,12 +193,12 @@ public struct GenerationSchema: Sendable, Codable, CustomDebugStringConvertible
var stringValue: String
var intValue: Int?

init?(stringValue: String) {
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}

init?(intValue: Int) {
init(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
Expand Down Expand Up @@ -578,15 +551,7 @@ public struct GenerationSchema: Sendable, Codable, CustomDebugStringConvertible
if !defs.isEmpty {
var defsContainer = container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .defs)
for (name, node) in defs {
guard let key = DynamicCodingKey(stringValue: name) else {
throw EncodingError.invalidValue(
name,
EncodingError.Context(
codingPath: encoder.codingPath,
debugDescription: "Unable to create coding key for definition '\(name)'"
)
)
}
let key = DynamicCodingKey(stringValue: name)
try defsContainer.encode(node, forKey: key)
}
}
Expand Down
48 changes: 25 additions & 23 deletions Tests/ConduitTests/Core/ModelIdentifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,17 +414,16 @@ final class ModelIdentifierTests: XCTestCase {
func testRegistryContainsAllExpectedModels() {
let allModels = ModelRegistry.allModels

// Should have 16 models total
XCTAssertEqual(allModels.count, 16)
XCTAssertFalse(allModels.isEmpty)

// Count by provider
let mlxModels = allModels.filter { $0.identifier.provider == .mlx }
let hfModels = allModels.filter { $0.identifier.provider == .huggingFace }
let appleModels = allModels.filter { $0.identifier.provider == .foundationModels }

XCTAssertEqual(mlxModels.count, 10) // 7 text gen + 3 embedding
XCTAssertEqual(hfModels.count, 5)
XCTAssertEqual(appleModels.count, 1)
XCTAssertFalse(mlxModels.isEmpty)
XCTAssertFalse(hfModels.isEmpty)
XCTAssertFalse(appleModels.isEmpty)
}

func testRegistryInfoLookup() {
Expand All @@ -448,13 +447,14 @@ final class ModelIdentifierTests: XCTestCase {
}

func testRegistryModelsByProvider() {
let allModels = ModelRegistry.allModels
let mlxModels = ModelRegistry.models(for: .mlx)
let hfModels = ModelRegistry.models(for: .huggingFace)
let appleModels = ModelRegistry.models(for: .foundationModels)

XCTAssertEqual(mlxModels.count, 10)
XCTAssertEqual(hfModels.count, 5)
XCTAssertEqual(appleModels.count, 1)
XCTAssertEqual(mlxModels.count, allModels.filter { $0.identifier.provider == .mlx }.count)
XCTAssertEqual(hfModels.count, allModels.filter { $0.identifier.provider == .huggingFace }.count)
XCTAssertEqual(appleModels.count, allModels.filter { $0.identifier.provider == .foundationModels }.count)

// Verify all MLX models are actually MLX
XCTAssertTrue(mlxModels.allSatisfy { $0.identifier.provider == .mlx })
Expand All @@ -469,14 +469,17 @@ final class ModelIdentifierTests: XCTestCase {
let reasoningModels = ModelRegistry.models(with: .reasoning)
let transcriptionModels = ModelRegistry.models(with: .transcription)

XCTAssertEqual(textGenModels.count, 12) // Most models support text generation
XCTAssertEqual(embeddingModels.count, 3) // BGE small, BGE large, Nomic
XCTAssertEqual(codeGenModels.count, 3) // Phi-3 Mini, Phi-4, Llama 3.1 70B
XCTAssertEqual(reasoningModels.count, 4) // Phi-3 Mini, Phi-4, Llama 3.1 70B, DeepSeek R1
XCTAssertEqual(transcriptionModels.count, 1) // Whisper Large V3
XCTAssertFalse(textGenModels.isEmpty)
XCTAssertFalse(embeddingModels.isEmpty)
XCTAssertFalse(codeGenModels.isEmpty)
XCTAssertFalse(reasoningModels.isEmpty)
XCTAssertFalse(transcriptionModels.isEmpty)

// Verify all embedding models actually have the capability
XCTAssertTrue(textGenModels.allSatisfy { $0.capabilities.contains(.textGeneration) })
XCTAssertTrue(embeddingModels.allSatisfy { $0.capabilities.contains(.embeddings) })
XCTAssertTrue(codeGenModels.allSatisfy { $0.capabilities.contains(.codeGeneration) })
XCTAssertTrue(reasoningModels.allSatisfy { $0.capabilities.contains(.reasoning) })
XCTAssertTrue(transcriptionModels.allSatisfy { $0.capabilities.contains(.transcription) })
}

func testRegistryRecommendedModels() {
Expand All @@ -500,32 +503,31 @@ final class ModelIdentifierTests: XCTestCase {
func testRegistryLocalModels() {
let localModels = ModelRegistry.localModels()

// Local models should be MLX + Apple
XCTAssertEqual(localModels.count, 11) // 10 MLX + 1 Apple

// All should not require network
XCTAssertTrue(localModels.allSatisfy { !$0.identifier.requiresNetwork })
XCTAssertTrue(localModels.allSatisfy { $0.identifier.isLocal })
XCTAssertEqual(localModels.count, ModelRegistry.allModels.filter { !$0.identifier.requiresNetwork }.count)

// Should include both MLX and Apple
let providers = Set(localModels.map { $0.identifier.provider })
XCTAssertTrue(providers.contains(.mlx))
XCTAssertTrue(providers.contains(.foundationModels))
XCTAssertFalse(providers.contains(.huggingFace))
}

func testRegistryCloudModels() {
let cloudModels = ModelRegistry.cloudModels()

// Cloud models should be HuggingFace only
XCTAssertEqual(cloudModels.count, 5)

// All should require network
XCTAssertTrue(cloudModels.allSatisfy { $0.identifier.requiresNetwork })
XCTAssertTrue(cloudModels.allSatisfy { !$0.identifier.isLocal })
XCTAssertEqual(cloudModels.count, ModelRegistry.allModels.filter { $0.identifier.requiresNetwork }.count)

// All should be HuggingFace
XCTAssertTrue(cloudModels.allSatisfy { $0.identifier.provider == .huggingFace })
// Cloud providers are expected to be remote provider families.
XCTAssertTrue(
cloudModels.allSatisfy {
[.huggingFace, .openAI, .openRouter, .anthropic, .kimi, .minimax, .azure].contains($0.identifier.provider)
}
)
}

// MARK: - ProviderType Tests
Expand Down
4 changes: 3 additions & 1 deletion Tests/ConduitTests/Core/ProtocolCompilationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ final class ProtocolCompilationTests: XCTestCase {

func testProviderTypeIsCaseIterable() {
let allCases = ProviderType.allCases
XCTAssertEqual(allCases.count, 10)
XCTAssertEqual(allCases.count, 12)
XCTAssertTrue(allCases.contains(.mlx))
XCTAssertTrue(allCases.contains(.coreml))
XCTAssertTrue(allCases.contains(.llama))
Expand All @@ -685,6 +685,8 @@ final class ProtocolCompilationTests: XCTestCase {
XCTAssertTrue(allCases.contains(.openRouter))
XCTAssertTrue(allCases.contains(.ollama))
XCTAssertTrue(allCases.contains(.anthropic))
XCTAssertTrue(allCases.contains(.kimi))
XCTAssertTrue(allCases.contains(.minimax))
XCTAssertTrue(allCases.contains(.azure))
}

Expand Down
18 changes: 18 additions & 0 deletions Tests/ConduitTests/Golden/GenerationSchemaGoldenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ final class GenerationSchemaGoldenTests: XCTestCase {
XCTAssertEqual(scoresItems["maximum"] as? Double, 100)
}

func testSchemaEncodingSupportsEmptyPropertyNames() throws {
let schema = GenerationSchema(
type: String.self,
properties: [
.init(name: "", type: String.self),
]
)

let data = try JSONEncoder().encode(schema)
let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any])
let defs = try XCTUnwrap(object["$defs"] as? [String: Any])
let root = try XCTUnwrap(defs[String(reflecting: String.self)] as? [String: Any])
let properties = try XCTUnwrap(root["properties"] as? [String: Any])
XCTAssertNotNil(properties[""])

_ = try JSONDecoder().decode(GenerationSchema.self, from: data)
}

private func canonicalJSONString(_ json: String) throws -> String {
let object = try JSONSerialization.jsonObject(with: Data(json.utf8), options: [.fragmentsAllowed])
let canonical = canonicalizeJSON(object)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// This file requires the MLX trait (Hub) to be enabled.

#if canImport(Hub)
#if CONDUIT_TRAIT_MLX && canImport(MLX) && canImport(Hub)

import Foundation
import Testing
Expand Down