Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,30 @@ struct ModelOptionsSnapshot: Encodable, Equatable {
case invalidValue
}

/// Option id of the Native MTP row, the composer's
/// `FloatingInputCard.nativeMTPOptionID`.
static let nativeMTPOptionId = "nativeMTPDepth"

/// What the model's bundle offers for Native MTP, as composer selection
/// reads it: nil without an MTP head, `blocked` when its tuning artifact
/// refuses MTP.
struct NativeMTP: Equatable, Sendable {
let blocked: Bool

/// File-only bundle inspection; run it off the main thread.
static func inspect(model: String) -> NativeMTP? {
guard let status = ModelRuntime.inspectLoadingModelMTP(name: model),
status.bundleHasMTP, status.isTargetMTPFamily
else { return nil }
return NativeMTP(blocked: status.isBlocked)
}
}

/// Built after `LocalReasoningCapability.resolveForDispatch` so local
/// bundles report their real thinking / effort contract, not the cold
/// main-thread miss.
@MainActor
static func make(for model: String) -> ModelOptionsSnapshot {
static func make(for model: String, nativeMTP: NativeMTP? = nil) -> ModelOptionsSnapshot {
let values = ModelProfileRegistry.normalizedOptions(
for: model,
persisted: ModelOptionsStore.shared.loadOptions(for: model)
Expand Down Expand Up @@ -135,7 +154,51 @@ struct ModelOptionsSnapshot: Encodable, Equatable {
)
}
}
return ModelOptionsSnapshot(model: model, thinking: thinking, options: options)
return ModelOptionsSnapshot(
model: model,
thinking: thinking,
options: options + [nativeMTP.map(nativeMTPOption)].compactMap { $0 }
)
}

/// The composer's Native MTP row. MTP lives in the global server
/// settings, not the per-model store, so the selection is read there;
/// a blocked bundle only offers Off.
@MainActor
private static func nativeMTPOption(_ nativeMTP: NativeMTP) -> Option {
let off = Segment(id: "off", label: L("Off (AR)"), description: nil)
let adaptive = Segment(id: "auto", label: L("On (Adaptive)"), description: nil)
let mode = ServerController.runtimeSettingsForConfigureTool().settings.mtp.mode
let selected = nativeMTP.blocked || mode == .off ? "off" : "auto"
return Option(
id: nativeMTPOptionId,
label: L("Native MTP"),
icon: "hare",
// The phone shows the choice alone, without the composer's copy.
help: nil,
kind: "segmented",
segments: nativeMTP.blocked ? [off] : [off, adaptive],
selected: selected,
on: nil,
// Off is the default, as the composer shows it.
explicit: selected != "off"
)
}

/// Stores a Native MTP choice through the composer's and Settings' one
/// route. A nil value resets to the default, Off.
@MainActor
static func applyNativeMTP(_ value: ModelOptionValue?, nativeMTP: NativeMTP?) async throws {
guard let nativeMTP else { throw ApplyError.unknownOption }
let segment = value?.stringValue ?? "off"
guard segment == "off" || (segment == "auto" && !nativeMTP.blocked) else {
throw ApplyError.invalidValue
}
var settings = ServerController.runtimeSettingsForConfigureTool().settings
settings.mtp.mode = segment == "off" ? .off : .auto
settings.mtp.draftTokenLimit = nil
settings.mtp.explicitDepth = nil
_ = await ServerController.applyRuntimeSettingsFromConfigureTool(settings)
}

/// Stores one choice the way the Mac composer's picker rows do. A nil
Expand Down
21 changes: 18 additions & 3 deletions Packages/OsaurusCore/Networking/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5385,8 +5385,23 @@ final class HTTPHandler: ChannelInboundHandler, Sendable {
// Off-main bundle read, so local models report their real
// thinking / effort contract instead of the cold-cache miss.
_ = await LocalReasoningCapability.resolveForDispatch(modelId: request.model)
let outcome: (HTTPResponseStatus, String) = await MainActor.run {
if isWrite, let option = request.option {
// Native MTP is a server setting, written through its own async
// route rather than the per-model option store.
let nativeMTP = ModelOptionsSnapshot.NativeMTP.inspect(model: request.model)
let isNativeMTPWrite = isWrite && request.option == ModelOptionsSnapshot.nativeMTPOptionId
var nativeMTPFailure: (HTTPResponseStatus, String)?
if isNativeMTPWrite {
do {
try await ModelOptionsSnapshot.applyNativeMTP(request.value, nativeMTP: nativeMTP)
} catch ModelOptionsSnapshot.ApplyError.unknownOption {
nativeMTPFailure = (.notFound, #"{"error":"unknown_option"}"#)
} catch {
nativeMTPFailure = (.badRequest, #"{"error":"invalid_value"}"#)
}
}
let outcome: (HTTPResponseStatus, String) = await MainActor.run { [nativeMTPFailure] in
if let nativeMTPFailure { return nativeMTPFailure }
if isWrite, !isNativeMTPWrite, let option = request.option {
do {
try ModelOptionsSnapshot.apply(model: request.model, optionId: option, value: request.value)
} catch ModelOptionsSnapshot.ApplyError.unknownOption {
Expand All @@ -5395,7 +5410,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable {
return (.badRequest, #"{"error":"invalid_value"}"#)
}
}
let snapshot = ModelOptionsSnapshot.make(for: request.model)
let snapshot = ModelOptionsSnapshot.make(for: request.model, nativeMTP: nativeMTP)
guard let json = try? JSONEncoder.osaurusCanonical().encode(snapshot) else {
return (.internalServerError, #"{"error":"encoding_failed"}"#)
}
Expand Down
2 changes: 1 addition & 1 deletion Packages/OsaurusCore/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -167002,7 +167002,7 @@
}
}
},
"Open Osaurus on your iPhone, then generate a code here and type it in. Your iPhone must be on the same network as this Mac while pairing." : {
"Open [Osaurus](https://testflight.apple.com/join/yTF86bVb) (in TestFlight currently) on your iPhone, then generate a code here and type it in. Your iPhone must be on the same network as this Mac while pairing." : {

},
"Open osaurus.ai/themes" : {
Expand Down
20 changes: 16 additions & 4 deletions Packages/OsaurusCore/Views/Settings/OsaurusConnectView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,25 @@ struct OsaurusConnectView: View {

// MARK: Pairing

/// The pairing intro, "Osaurus" linking to the iPhone app's TestFlight
/// for anyone who has yet to install it, underlined like a web link.
private var pairingIntro: AttributedString {
var text = AttributedString(
localized:
"Open [Osaurus](https://testflight.apple.com/join/yTF86bVb) (in TestFlight currently) on your iPhone, then generate a code here and type it in. Your iPhone must be on the same network as this Mac while pairing.",
bundle: .module
)
for run in text.runs where run.link != nil {
text[run.range].underlineStyle = .single
text[run.range].foregroundColor = theme.accentColor
}
return text
}

@ViewBuilder private var pairingSection: some View {
SettingsSection(title: L("Pair an iPhone"), icon: "iphone.gen3") {
VStack(alignment: .leading, spacing: 14) {
Text(
"Open Osaurus on your iPhone, then generate a code here and type it in. Your iPhone must be on the same network as this Mac while pairing.",
bundle: .module
)
Text(pairingIntro)
.font(.system(size: 12))
.foregroundColor(theme.secondaryText)

Expand Down
5 changes: 5 additions & 0 deletions docs/MOBILE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,11 @@ applies and nothing is sent to the model. Fields without a value (`icon`,
stores one choice (`null` resets it) and answers with the same body as
`POST`. `404 unknown_option`, `400 invalid_value`.

A local bundle with a native MTP head also gets a last `nativeMTPDepth`
option (Native MTP: `off` / `auto`, only `off` when its tuning blocks MTP).
Unlike the others it is the Mac's global Speculative Decoding setting, not a
per-model choice; setting it writes that setting, `null` meaning `off`.

### 12.4 `PUT /models/favorites`

`{"key":"<favorite_key from 12.1>","favorite":true}` adds the model to the
Expand Down
Loading