* 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>
144 lines
No EOL
4.3 KiB
Swift
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()
|
|
} |