Apple intel integration (#391)

* feat: add Apple Intelligence post-processing provider

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat: guide apple intelligence output

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(build): add fallback stub for Apple Intelligence on older SDKs

- Checks if 'FoundationModels.framework' is present in the macOS SDK.
- If missing, compiles a stub Swift file that returns 'unavailable' errors instead of failing the build.
- Prevents build errors on older Xcode versions (or macOS versions < 26.0) where the macros are not supported.

* fix(ui): hide Apple Intelligence option when unavailable

- Checks for runtime availability of Apple Intelligence (via 'check_apple_intelligence_availability') in 'settings.rs'.
- Only adds the Apple Intelligence provider to the default settings if it is actually available on the device.
- Ensures the option does not appear in the UI for unsupported Macs (e.g., Intel or older macOS versions), even if the app was built with support enabled.

* move some files around as well as some ui text

* format

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
Chirag Agggarwal 2025-12-09 19:34:24 +05:30 committed by GitHub
parent 933c1b14c2
commit bfecade869
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 624 additions and 59 deletions

View file

@ -1,3 +1,128 @@
fn main() {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
build_apple_intelligence_bridge();
tauri_build::build()
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn build_apple_intelligence_bridge() {
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
const REAL_SWIFT_FILE: &str = "swift/apple_intelligence.swift";
const STUB_SWIFT_FILE: &str = "swift/apple_intelligence_stub.swift";
const BRIDGE_HEADER: &str = "swift/apple_intelligence_bridge.h";
println!("cargo:rerun-if-changed={REAL_SWIFT_FILE}");
println!("cargo:rerun-if-changed={STUB_SWIFT_FILE}");
println!("cargo:rerun-if-changed={BRIDGE_HEADER}");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let object_path = out_dir.join("apple_intelligence.o");
let static_lib_path = out_dir.join("libapple_intelligence.a");
let sdk_path = String::from_utf8(
Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.expect("Failed to locate macOS SDK")
.stdout,
)
.expect("SDK path is not valid UTF-8")
.trim()
.to_string();
// Check if the SDK supports FoundationModels (required for Apple Intelligence)
let framework_path =
Path::new(&sdk_path).join("System/Library/Frameworks/FoundationModels.framework");
let has_foundation_models = framework_path.exists();
let source_file = if has_foundation_models {
println!("cargo:warning=Building with Apple Intelligence support.");
REAL_SWIFT_FILE
} else {
println!("cargo:warning=Apple Intelligence SDK not found. Building with stubs.");
STUB_SWIFT_FILE
};
if !Path::new(source_file).exists() {
panic!("Source file {} is missing!", source_file);
}
let swiftc_path = String::from_utf8(
Command::new("xcrun")
.args(["--find", "swiftc"])
.output()
.expect("Failed to locate swiftc")
.stdout,
)
.expect("swiftc path is not valid UTF-8")
.trim()
.to_string();
let toolchain_swift_lib = Path::new(&swiftc_path)
.parent()
.and_then(|p| p.parent())
.map(|root| root.join("lib/swift/macosx"))
.expect("Unable to determine Swift toolchain lib directory");
let sdk_swift_lib = Path::new(&sdk_path).join("usr/lib/swift");
let status = Command::new("xcrun")
.args([
"swiftc",
"-target",
"arm64-apple-macosx26.0",
"-sdk",
&sdk_path,
"-O",
"-import-objc-header",
BRIDGE_HEADER,
"-c",
source_file,
"-o",
object_path
.to_str()
.expect("Failed to convert object path to string"),
])
.status()
.expect("Failed to invoke swiftc for Apple Intelligence bridge");
if !status.success() {
panic!("swiftc failed to compile {source_file}");
}
let status = Command::new("libtool")
.args([
"-static",
"-o",
static_lib_path
.to_str()
.expect("Failed to convert static lib path to string"),
object_path
.to_str()
.expect("Failed to convert object path to string"),
])
.status()
.expect("Failed to create static library for Apple Intelligence bridge");
if !status.success() {
panic!("libtool failed for Apple Intelligence bridge");
}
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static=apple_intelligence");
println!(
"cargo:rustc-link-search=native={}",
toolchain_swift_lib.display()
);
println!("cargo:rustc-link-search=native={}", sdk_swift_lib.display());
println!("cargo:rustc-link-lib=framework=Foundation");
if has_foundation_models {
println!("cargo:rustc-link-lib=framework=FoundationModels");
}
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
}

View file

@ -1,8 +1,10 @@
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::apple_intelligence;
use crate::audio_feedback::{play_feedback_sound, play_feedback_sound_blocking, SoundType};
use crate::managers::audio::AudioRecordingManager;
use crate::managers::history::HistoryManager;
use crate::managers::transcription::TranscriptionManager;
use crate::settings::{get_settings, AppSettings};
use crate::settings::{get_settings, AppSettings, APPLE_INTELLIGENCE_PROVIDER_ID};
use crate::shortcut;
use crate::tray::{change_tray_icon, TrayIconState};
use crate::utils::{self, show_recording_overlay, show_transcribing_overlay};
@ -86,12 +88,6 @@ async fn maybe_post_process_transcription(
return None;
}
let api_key = settings
.post_process_api_keys
.get(&provider.id)
.cloned()
.unwrap_or_default();
debug!(
"Starting LLM post-processing with provider '{}' (model: {})",
provider.id, model
@ -101,6 +97,48 @@ async fn maybe_post_process_transcription(
let processed_prompt = prompt.replace("${output}", transcription);
debug!("Processed prompt length: {} chars", processed_prompt.len());
if provider.id == APPLE_INTELLIGENCE_PROVIDER_ID {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
if !apple_intelligence::check_apple_intelligence_availability() {
debug!("Apple Intelligence selected but not currently available on this device");
return None;
}
let token_limit = model.trim().parse::<i32>().unwrap_or(0);
return match apple_intelligence::process_text(&processed_prompt, token_limit) {
Ok(result) => {
if result.trim().is_empty() {
debug!("Apple Intelligence returned an empty response");
None
} else {
debug!(
"Apple Intelligence post-processing succeeded. Output length: {} chars",
result.len()
);
Some(result)
}
}
Err(err) => {
error!("Apple Intelligence post-processing failed: {}", err);
None
}
};
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
debug!("Apple Intelligence provider selected on unsupported platform");
return None;
}
}
let api_key = settings
.post_process_api_keys
.get(&provider.id)
.cloned()
.unwrap_or_default();
// Create OpenAI-compatible client
let client = match crate::llm_client::create_client(&provider, api_key) {
Ok(client) => client,

View file

@ -0,0 +1,71 @@
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
// Define the response structure from Swift
#[repr(C)]
pub struct AppleLLMResponse {
pub response: *mut c_char,
pub success: c_int,
pub error_message: *mut c_char,
}
// Link to the Swift functions
extern "C" {
pub fn is_apple_intelligence_available() -> c_int;
pub fn process_text_with_apple_llm(
prompt: *const c_char,
max_tokens: i32,
) -> *mut AppleLLMResponse;
pub fn free_apple_llm_response(response: *mut AppleLLMResponse);
}
// Safe wrapper functions
pub fn check_apple_intelligence_availability() -> bool {
unsafe { is_apple_intelligence_available() == 1 }
}
pub fn process_text(prompt: &str, max_tokens: i32) -> Result<String, String> {
let prompt_cstr = CString::new(prompt).map_err(|e| e.to_string())?;
let response_ptr = unsafe { process_text_with_apple_llm(prompt_cstr.as_ptr(), max_tokens) };
if response_ptr.is_null() {
return Err("Null response from Apple LLM".to_string());
}
let response = unsafe { &*response_ptr };
let result = if response.success == 1 {
if response.response.is_null() {
Ok(String::new())
} else {
let c_str = unsafe { CStr::from_ptr(response.response) };
let rust_str = c_str.to_string_lossy().into_owned();
Ok(rust_str)
}
} else {
let error_c_str = if !response.error_message.is_null() {
unsafe { CStr::from_ptr(response.error_message) }
} else {
CStr::from_bytes_with_nul(b"Unknown error\0").unwrap()
};
let error_msg = error_c_str.to_string_lossy().into_owned();
Err(error_msg)
};
// Clean up the response
unsafe { free_apple_llm_response(response_ptr) };
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_availability() {
let available = check_apple_intelligence_availability();
println!("Apple Intelligence available: {}", available);
}
}

View file

@ -1,4 +1,6 @@
mod actions;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod apple_intelligence;
mod audio_feedback;
pub mod audio_toolkit;
mod clipboard;

View file

@ -6,6 +6,9 @@ use std::collections::HashMap;
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
pub const APPLE_INTELLIGENCE_PROVIDER_ID: &str = "apple_intelligence";
pub const APPLE_INTELLIGENCE_DEFAULT_MODEL_ID: &str = "Apple Intelligence";
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
@ -361,7 +364,7 @@ fn default_post_process_provider_id() -> String {
}
fn default_post_process_providers() -> Vec<PostProcessProvider> {
vec![
let mut providers = vec![
PostProcessProvider {
id: "openai".to_string(),
label: "OpenAI".to_string(),
@ -390,7 +393,22 @@ fn default_post_process_providers() -> Vec<PostProcessProvider> {
allow_base_url_edit: true,
models_endpoint: Some("/models".to_string()),
},
]
];
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
if crate::apple_intelligence::check_apple_intelligence_availability() {
providers.push(PostProcessProvider {
id: APPLE_INTELLIGENCE_PROVIDER_ID.to_string(),
label: "Apple Intelligence".to_string(),
base_url: "apple-intelligence://local".to_string(),
allow_base_url_edit: false,
models_endpoint: None,
});
}
}
providers
}
fn default_post_process_api_keys() -> HashMap<String, String> {
@ -401,10 +419,20 @@ fn default_post_process_api_keys() -> HashMap<String, String> {
map
}
fn default_model_for_provider(provider_id: &str) -> String {
if provider_id == APPLE_INTELLIGENCE_PROVIDER_ID {
return APPLE_INTELLIGENCE_DEFAULT_MODEL_ID.to_string();
}
String::new()
}
fn default_post_process_models() -> HashMap<String, String> {
let mut map = HashMap::new();
for provider in default_post_process_providers() {
map.insert(provider.id, String::new());
map.insert(
provider.id.clone(),
default_model_for_provider(&provider.id),
);
}
map
}
@ -417,6 +445,45 @@ fn default_post_process_prompts() -> Vec<LLMPrompt> {
}]
}
fn ensure_post_process_defaults(settings: &mut AppSettings) -> bool {
let mut changed = false;
for provider in default_post_process_providers() {
if settings
.post_process_providers
.iter()
.all(|existing| existing.id != provider.id)
{
settings.post_process_providers.push(provider.clone());
changed = true;
}
if !settings.post_process_api_keys.contains_key(&provider.id) {
settings
.post_process_api_keys
.insert(provider.id.clone(), String::new());
changed = true;
}
let default_model = default_model_for_provider(&provider.id);
match settings.post_process_models.get_mut(&provider.id) {
Some(existing) => {
if existing.is_empty() && !default_model.is_empty() {
*existing = default_model.clone();
changed = true;
}
}
None => {
settings
.post_process_models
.insert(provider.id.clone(), default_model);
changed = true;
}
}
}
changed
}
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
pub fn get_default_settings() -> AppSettings {
@ -518,7 +585,7 @@ pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
.store(SETTINGS_STORE_PATH)
.expect("Failed to initialize store");
let settings = if let Some(settings_value) = store.get("settings") {
let mut settings = if let Some(settings_value) = store.get("settings") {
// Parse the entire settings object
match serde_json::from_value::<AppSettings>(settings_value) {
Ok(mut settings) => {
@ -556,6 +623,10 @@ pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
default_settings
};
if ensure_post_process_defaults(&mut settings) {
store.set("settings", serde_json::to_value(&settings).unwrap());
}
settings
}
@ -564,7 +635,7 @@ pub fn get_settings(app: &AppHandle) -> AppSettings {
.store(SETTINGS_STORE_PATH)
.expect("Failed to initialize store");
if let Some(settings_value) = store.get("settings") {
let mut settings = if let Some(settings_value) = store.get("settings") {
serde_json::from_value::<AppSettings>(settings_value).unwrap_or_else(|_| {
let default_settings = get_default_settings();
store.set("settings", serde_json::to_value(&default_settings).unwrap());
@ -574,7 +645,13 @@ pub fn get_settings(app: &AppHandle) -> AppSettings {
let default_settings = get_default_settings();
store.set("settings", serde_json::to_value(&default_settings).unwrap());
default_settings
};
if ensure_post_process_defaults(&mut settings) {
store.set("settings", serde_json::to_value(&settings).unwrap());
}
settings
}
pub fn write_settings(app: &AppHandle, settings: AppSettings) {

View file

@ -11,6 +11,7 @@ use crate::managers::audio::AudioRecordingManager;
use crate::settings::ShortcutBinding;
use crate::settings::{
self, get_settings, ClipboardHandling, LLMPrompt, OverlayPosition, PasteMethod, SoundTheme,
APPLE_INTELLIGENCE_DEFAULT_MODEL_ID, APPLE_INTELLIGENCE_PROVIDER_ID,
};
use crate::ManagedToggleState;
@ -543,6 +544,18 @@ pub async fn fetch_post_process_models(
.find(|p| p.id == provider_id)
.ok_or_else(|| format!("Provider '{}' not found", provider_id))?;
if provider.id == APPLE_INTELLIGENCE_PROVIDER_ID {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
return Ok(vec![APPLE_INTELLIGENCE_DEFAULT_MODEL_ID.to_string()]);
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
return Err("Apple Intelligence is only available on Apple silicon Macs running macOS 15 or later.".to_string());
}
}
// Get API key
let api_key = settings
.post_process_api_keys

View file

@ -0,0 +1,139 @@
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_apple_llm")
public func processTextWithAppleLLM(
_ prompt: UnsafePointer<CChar>,
maxTokens: Int32
) -> UnsafeMutablePointer<AppleLLMResponse> {
let swiftPrompt = String(cString: prompt)
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)
var output: String
do {
let structured = try await session.respond(
to: swiftPrompt,
generating: CleanedTranscript.self
)
output = structured.content.cleanedText
} catch {
let fallbackGeneration = try await session.respond(to: swiftPrompt)
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()
}

View file

@ -0,0 +1,29 @@
#ifndef apple_intelligence_bridge_h
#define apple_intelligence_bridge_h
// C-compatible function declarations for Swift bridge
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char* response;
int success; // 0 for failure, 1 for success
char* error_message; // Only valid when success = 0
} AppleLLMResponse;
// Check if Apple Intelligence is available on the device
int is_apple_intelligence_available(void);
// Process text using Apple's on-device LLM
AppleLLMResponse* process_text_with_apple_llm(const char* prompt, int max_tokens);
// Free memory allocated by the Apple LLM response
void free_apple_llm_response(AppleLLMResponse* response);
#ifdef __cplusplus
}
#endif
#endif /* apple_intelligence_bridge_h */

View file

@ -0,0 +1,44 @@
import Foundation
// Stub implementation when FoundationModels is not available
// This file is compiled via Cargo build script when the build environment
// does not support Apple Intelligence (e.g. older Xcode/SDK).
private typealias ResponsePointer = UnsafeMutablePointer<AppleLLMResponse>
@_cdecl("is_apple_intelligence_available")
public func isAppleIntelligenceAvailable() -> Int32 {
return 0
}
@_cdecl("process_text_with_apple_llm")
public func processTextWithAppleLLM(
_ prompt: UnsafePointer<CChar>,
maxTokens: Int32
) -> UnsafeMutablePointer<AppleLLMResponse> {
let responsePtr = ResponsePointer.allocate(capacity: 1)
// Initialize with safe defaults
responsePtr.initialize(to: AppleLLMResponse(response: nil, success: 0, error_message: nil))
let msg = "Apple Intelligence is not available in this build (SDK requirement not met)."
// Duplicate the string for the C caller to own
responsePtr.pointee.error_message = strdup(msg)
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()
}

View file

@ -11,6 +11,7 @@ type PostProcessProviderState = {
selectedProviderId: string;
selectedProvider: PostProcessProvider | undefined;
isCustomProvider: boolean;
isAppleProvider: boolean;
baseUrl: string;
handleBaseUrlChange: (value: string) => void;
isBaseUrlUpdating: boolean;
@ -28,6 +29,8 @@ type PostProcessProviderState = {
handleRefreshModels: () => void;
};
const APPLE_PROVIDER_ID = "apple_intelligence";
export const usePostProcessProviderState = (): PostProcessProviderState => {
const {
settings,
@ -56,6 +59,8 @@ export const usePostProcessProviderState = (): PostProcessProviderState => {
);
}, [providers, selectedProviderId]);
const isAppleProvider = selectedProvider?.id === APPLE_PROVIDER_ID;
// Use settings directly as single source of truth
const baseUrl = selectedProvider?.base_url ?? "";
const apiKey = settings?.post_process_api_keys?.[selectedProviderId] ?? "";
@ -125,8 +130,9 @@ export const usePostProcessProviderState = (): PostProcessProviderState => {
);
const handleRefreshModels = useCallback(() => {
if (isAppleProvider) return;
void fetchPostProcessModels(selectedProviderId);
}, [fetchPostProcessModels, selectedProviderId]);
}, [fetchPostProcessModels, isAppleProvider, selectedProviderId]);
const availableModelsRaw = postProcessModelOptions[selectedProviderId] || [];
@ -175,6 +181,7 @@ export const usePostProcessProviderState = (): PostProcessProviderState => {
selectedProviderId,
selectedProvider,
isCustomProvider,
isAppleProvider,
baseUrl,
handleBaseUrlChange,
isBaseUrlUpdating,

View file

@ -16,12 +16,11 @@ import { ApiKeyField } from "../PostProcessingSettingsApi/ApiKeyField";
import { ModelSelect } from "../PostProcessingSettingsApi/ModelSelect";
import { usePostProcessProviderState } from "../PostProcessingSettingsApi/usePostProcessProviderState";
import { useSettings } from "../../../hooks/useSettings";
import type { LLMPrompt } from "@/bindings";
const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<div className="p-4 bg-mid-gray/5 rounded-lg border border-mid-gray/20 text-center">
<div className="p-4 bg-mid-gray/5 rounded-lg border border-mid-gray/20">
<p className="text-sm text-mid-gray">{children}</p>
</div>
);
@ -56,51 +55,70 @@ const PostProcessingSettingsApiComponent: React.FC = () => {
</div>
</SettingContainer>
<SettingContainer
title="Base URL"
description="API base URL for the selected provider. Only the custom provider can be edited."
descriptionMode="tooltip"
layout="horizontal"
grouped={true}
>
<div className="flex items-center gap-2">
<BaseUrlField
value={state.baseUrl}
onBlur={state.handleBaseUrlChange}
placeholder="https://api.openai.com/v1"
disabled={
!state.selectedProvider?.allow_base_url_edit ||
state.isBaseUrlUpdating
}
className="min-w-[380px]"
/>
</div>
</SettingContainer>
{state.isAppleProvider ? (
<SettingContainer
title="Apple Intelligence"
description="Runs fully on-device. No API key or network access is required."
descriptionMode="tooltip"
layout="stacked"
grouped={true}
>
<DisabledNotice>
Requires an Apple Silicon Mac running macOS Tahoe (26.0) or later.
Apple Intelligence must be enabled in System Settings.
</DisabledNotice>
</SettingContainer>
) : (
<>
<SettingContainer
title="Base URL"
description="API base URL for the selected provider. Only the custom provider can be edited."
descriptionMode="tooltip"
layout="horizontal"
grouped={true}
>
<div className="flex items-center gap-2">
<BaseUrlField
value={state.baseUrl}
onBlur={state.handleBaseUrlChange}
placeholder="https://api.openai.com/v1"
disabled={
!state.selectedProvider?.allow_base_url_edit ||
state.isBaseUrlUpdating
}
className="min-w-[380px]"
/>
</div>
</SettingContainer>
<SettingContainer
title="API Key"
description="API key for the selected provider."
descriptionMode="tooltip"
layout="horizontal"
grouped={true}
>
<div className="flex items-center gap-2">
<ApiKeyField
value={state.apiKey}
onBlur={state.handleApiKeyChange}
placeholder="sk-..."
disabled={state.isApiKeyUpdating}
className="min-w-[320px]"
/>
</div>
</SettingContainer>
<SettingContainer
title="API Key"
description="API key for the selected provider."
descriptionMode="tooltip"
layout="horizontal"
grouped={true}
>
<div className="flex items-center gap-2">
<ApiKeyField
value={state.apiKey}
onBlur={state.handleApiKeyChange}
placeholder="sk-..."
disabled={state.isApiKeyUpdating}
className="min-w-[320px]"
/>
</div>
</SettingContainer>
</>
)}
<SettingContainer
title="Model"
description={
state.isCustomProvider
? "Provide the model identifier expected by your custom endpoint."
: "Choose a model exposed by the selected provider."
state.isAppleProvider
? "Provide an optional numeric token limit or keep the default on-device preset."
: state.isCustomProvider
? "Provide the model identifier expected by your custom endpoint."
: "Choose a model exposed by the selected provider."
}
descriptionMode="tooltip"
layout="stacked"
@ -113,9 +131,11 @@ const PostProcessingSettingsApiComponent: React.FC = () => {
disabled={state.isModelUpdating}
isLoading={state.isFetchingModels}
placeholder={
state.modelOptions.length > 0
? "Search or select a model"
: "Type a model name"
state.isAppleProvider
? "Apple Intelligence"
: state.modelOptions.length > 0
? "Search or select a model"
: "Type a model name"
}
onSelect={state.handleModelSelect}
onCreate={state.handleModelCreate}
@ -124,7 +144,7 @@ const PostProcessingSettingsApiComponent: React.FC = () => {
/>
<ResetButton
onClick={state.handleRefreshModels}
disabled={state.isFetchingModels}
disabled={state.isFetchingModels || state.isAppleProvider}
ariaLabel="Refresh models"
className="flex h-10 w-10 items-center justify-center"
>