Major restructuring of the agent stack to support content-type skills, data-driven runtime budgets, and the new translate skill. LLM/MLX/ - Split MLXLocalLLMService.swift (1500 → 700 lines) into Core/, Gemma4/, Installation/ subdirectories - New BundledModel: LinearBudgetFormula replaces tier tables, model parameters fully data-driven via ModelRuntimeProfile - New RuntimeBudgets: pure functions for text/thinking/multimodal budgets, zero hardcoded model IDs - Drop adjustedTextOutputTokens prepared-subtraction (chunked prefill made it obsolete and triggered false-positive truncation on long prompts) Skills/ - Add SkillType (.device / .content) metadata, parsed from SKILL.md frontmatter - New translate skill (type: content, no tools) with anaphora handling - Trim translate (4563→1804), device (2357→1680), calendar (2169→1790) - contacts SKILL.md adds multi-turn disambiguation + cancel guidance, semantics owned by body not framework - SkillRegistry validates allowed-tools against ToolRegistry at load time Agent/ - ToolCallParser becomes registry-aware: drops tool_call names not in ToolRegistry or protocol set (load_skill / list_skills); ToolRegistry is now the single white list, validation moves to parser boundary - Router sticky routing uses semantic user-turn boundary instead of fixed suffix(4); correctly handles long agent loops - AgentEngine system prompt template differentiates device vs content skills with explicit invocation rules - SYSPROMPT.md auto-migration: old single-placeholder template backed up to .bak when new dual-placeholder is detected - PromptBuilder.buildLoadedSkillPrompt receives availableTools list, explicitly tells model which tools (or none) are callable Tools/ - ToolRegistry split into Helpers/Stores/AppPermissions/Handlers/* for separation of concerns - Mark Tools/Handlers/* with explicit allowed argument signatures UI/ - Add long-press copy contextMenu on user and assistant bubbles - Enable .textSelection on user bubble Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
3.5 KiB
Swift
121 lines
3.5 KiB
Swift
import MLX
|
|
import MLXNN
|
|
import MLXLMCommon
|
|
|
|
// MARK: - ProportionalRoPE
|
|
|
|
/// Proportional RoPE for Gemma 4 full-attention layers.
|
|
///
|
|
/// Frequencies are computed relative to the full head dimension (not just the
|
|
/// rotated portion), and rotation is applied to the first rotated_dims//2
|
|
/// elements of each half of the head — matching HF's rotate_half convention.
|
|
public class ProportionalRoPE: Module {
|
|
let dims: Int
|
|
let traditional: Bool
|
|
let rotatedDims: Int
|
|
let _freqs: MLXArray?
|
|
|
|
public init(
|
|
dims: Int,
|
|
traditional: Bool = false,
|
|
base: Float = 10000.0,
|
|
partialRotaryFactor: Float = 1.0,
|
|
factor: Float = 1.0
|
|
) {
|
|
self.dims = dims
|
|
self.traditional = traditional
|
|
|
|
let ropeAngles = Int(partialRotaryFactor * Float(dims) / 2.0)
|
|
self.rotatedDims = 2 * ropeAngles
|
|
|
|
if rotatedDims > 0 {
|
|
let exponents = MLXArray(stride(from: 0, to: rotatedDims, by: 2))
|
|
.asType(.float32) / Float(dims)
|
|
self._freqs = factor * pow(MLXArray(base), exponents)
|
|
} else {
|
|
self._freqs = nil
|
|
}
|
|
}
|
|
|
|
public func callAsFunction(_ x: MLXArray, offset: Int = 0) -> MLXArray {
|
|
if rotatedDims <= 0 { return x }
|
|
|
|
let ellipsis = MLXEllipsisIndex.ellipsis
|
|
|
|
let head = x[.ellipsis, ..<dims]
|
|
let tail = x[.ellipsis, dims...]
|
|
|
|
let half = dims / 2
|
|
let left = head[.ellipsis, ..<half]
|
|
let right = head[.ellipsis, half...]
|
|
|
|
// Concatenate the parts to rotate from each half
|
|
let toRotate = concatenated(
|
|
[left[.ellipsis, ..<(rotatedDims / 2)], right[.ellipsis, ..<(rotatedDims / 2)]],
|
|
axis: -1
|
|
)
|
|
|
|
// Apply fast rope with custom frequencies
|
|
let rotated = MLXFast.RoPE(
|
|
toRotate,
|
|
dimensions: rotatedDims,
|
|
traditional: traditional,
|
|
base: nil,
|
|
scale: 1.0,
|
|
offset: offset,
|
|
freqs: _freqs
|
|
)
|
|
|
|
// Reassemble: rotated parts + unrotated remainders
|
|
let rotatedLeft = rotated[ellipsis, ..<(rotatedDims / 2)]
|
|
let rotatedRight = rotated[ellipsis, (rotatedDims / 2)...]
|
|
let leftRemainder = left[ellipsis, (rotatedDims / 2)...]
|
|
let rightRemainder = right[ellipsis, (rotatedDims / 2)...]
|
|
|
|
let newLeft = concatenated(
|
|
[rotatedLeft, leftRemainder],
|
|
axis: -1
|
|
)
|
|
let newRight = concatenated(
|
|
[rotatedRight, rightRemainder],
|
|
axis: -1
|
|
)
|
|
let newHead = concatenated([newLeft, newRight], axis: -1)
|
|
|
|
if tail.dim(-1) == 0 {
|
|
return newHead
|
|
}
|
|
return concatenated([newHead, tail], axis: -1)
|
|
}
|
|
}
|
|
|
|
// MARK: - RoPE Factory
|
|
|
|
/// Initialize the appropriate RoPE variant based on configuration.
|
|
public func initializeGemma4Rope(
|
|
dims: Int,
|
|
traditional: Bool = false,
|
|
base: Float = 10000.0,
|
|
ropeConfig: RoPELayerConfig?
|
|
) -> Module {
|
|
guard let config = ropeConfig else {
|
|
return RoPE(dimensions: dims, traditional: traditional, base: base)
|
|
}
|
|
|
|
let ropeType = config.ropeType ?? "default"
|
|
let theta = config.ropeTheta ?? base
|
|
|
|
if ropeType == "proportional" {
|
|
let partialFactor = config.partialRotaryFactor ?? 1.0
|
|
return ProportionalRoPE(
|
|
dims: dims,
|
|
traditional: traditional,
|
|
base: theta,
|
|
partialRotaryFactor: partialFactor
|
|
)
|
|
}
|
|
|
|
// Default: standard RoPE
|
|
return RoPE(dimensions: dims, traditional: traditional, base: theta)
|
|
}
|