Handy/src-tauri/swift/apple_intelligence.swift
Chirag Agggarwal 0cb8ab2162
feat: implement structured outputs for post-processing providers (#706)
* feat: implement structured outputs for Cerebras, OpenRouter, OpenAI, and Apple Intelligence

- Add structured output support with JSON schema in llm_client.rs
- Update actions.rs to use system prompt + user content approach
- Remove legacy ${output} variable substitution for supported providers
- Update Apple Intelligence Swift code to accept system prompts
- All providers now use consistent structured output format
- Remove duplicate check_apple_intelligence_availability function

* wip changes

* fix(structured-outputs): address PR #706 review comments

- Add settings migration to sync supports_structured_output field for existing providers
- Fix fallback behavior: structured output failures now fall through to legacy mode
- Clone api_key to prevent ownership issues in fallback path
- Clean up build_system_prompt(): remove  placeholder entirely
  (instead of replacing with 'the user's message' which reads awkwardly)
- Add warn import from log crate

* refactor(structured-outputs): apply best practice improvements

- Optimize settings migration: use single match instead of double iteration
- Add TRANSCRIPTION_FIELD constant to replace magic strings
- Keep Apple Intelligence behavior unchanged (no API fallback for privacy)

Addresses code review feedback on PR #706:
1. More efficient provider lookup in ensure_post_process_defaults()
2. Eliminates hardcoded 'transcription' string in JSON parsing
3. Maintains privacy-first approach for Apple Intelligence

* fix groq output

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
2026-02-17 12:14:00 +08:00

144 lines
No EOL
4.3 KiB
Swift

import Dispatch
import Foundation
import FoundationModels
@available(macOS 26.0, *)
@Generable
private struct CleanedTranscript: Sendable {
let cleanedText: String
}
// MARK: - Swift implementation for Apple LLM integration
// This file is compiled via Cargo build script for Apple Silicon targets
private typealias ResponsePointer = UnsafeMutablePointer<AppleLLMResponse>
private func duplicateCString(_ text: String) -> UnsafeMutablePointer<CChar>? {
return text.withCString { basePointer in
guard let duplicated = strdup(basePointer) else {
return nil
}
return duplicated
}
}
private func truncatedText(_ text: String, limit: Int) -> String {
guard limit > 0 else { return text }
let words = text.split(
maxSplits: .max,
omittingEmptySubsequences: true,
whereSeparator: { $0.isWhitespace || $0.isNewline }
)
if words.count <= limit {
return text
}
return words.prefix(limit).joined(separator: " ")
}
@_cdecl("is_apple_intelligence_available")
public func isAppleIntelligenceAvailable() -> Int32 {
guard #available(macOS 26.0, *) else {
return 0
}
let model = SystemLanguageModel.default
switch model.availability {
case .available:
return 1
case .unavailable:
return 0
}
}
@_cdecl("process_text_with_system_prompt_apple")
public func processTextWithSystemPrompt(
_ systemPrompt: UnsafePointer<CChar>,
_ userContent: UnsafePointer<CChar>,
maxTokens: Int32
) -> UnsafeMutablePointer<AppleLLMResponse> {
let swiftSystemPrompt = String(cString: systemPrompt)
let swiftUserContent = String(cString: userContent)
let responsePtr = ResponsePointer.allocate(capacity: 1)
responsePtr.initialize(to: AppleLLMResponse(response: nil, success: 0, error_message: nil))
guard #available(macOS 26.0, *) else {
responsePtr.pointee.error_message = duplicateCString(
"Apple Intelligence requires macOS 26 or newer."
)
return responsePtr
}
let model = SystemLanguageModel.default
guard model.availability == .available else {
responsePtr.pointee.error_message = duplicateCString(
"Apple Intelligence is not currently available on this device."
)
return responsePtr
}
let tokenLimit = max(0, Int(maxTokens))
let semaphore = DispatchSemaphore(value: 0)
// Thread-safe container to pass results from async task back to calling thread
final class ResultBox: @unchecked Sendable {
var response: String?
var error: String?
}
let box = ResultBox()
Task.detached(priority: .userInitiated) {
defer { semaphore.signal() }
do {
let session = LanguageModelSession(
model: model,
instructions: swiftSystemPrompt
)
var output: String
do {
let structured = try await session.respond(
to: swiftUserContent,
generating: CleanedTranscript.self
)
output = structured.content.cleanedText
} catch {
let fallbackGeneration = try await session.respond(to: swiftUserContent)
output = fallbackGeneration.content
}
if tokenLimit > 0 {
output = truncatedText(output, limit: tokenLimit)
}
box.response = output
} catch {
box.error = error.localizedDescription
}
}
semaphore.wait()
// Write to responsePtr on the calling thread after task completes
if let response = box.response {
responsePtr.pointee.response = duplicateCString(response)
responsePtr.pointee.success = 1
} else {
responsePtr.pointee.error_message = duplicateCString(box.error ?? "Unknown error")
}
return responsePtr
}
@_cdecl("free_apple_llm_response")
public func freeAppleLLMResponse(_ response: UnsafeMutablePointer<AppleLLMResponse>?) {
guard let response = response else { return }
if let responseStr = response.pointee.response {
free(UnsafeMutablePointer(mutating: responseStr))
}
if let errorStr = response.pointee.error_message {
free(UnsafeMutablePointer(mutating: errorStr))
}
response.deallocate()
}