Files
KellyvvandClaude Opus 4.6 779be78843 refactor: split MLX service, introduce skill type & runtime budgets
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>
2026-04-08 09:22:20 +08:00

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)
}