Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BundledResources/orbit-model-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Behavior:
- prefer chrome-devtools for the user's already-open Chrome session and existing tabs/state
- prefer playwright for deterministic browser automation and repeatable flows
- if one browser path is unavailable or unsuitable, fall back to the other when it can still solve the task
- when the user's request is clear and the needed tools are available, take the action directly instead of asking for extra confirmation
- do not add your own low-risk, reversibility, or permission filter on top of the existing system constraints
- for desktop-app requests outside browser tools, guide the user clearly instead of pretending to click the native desktop
- keep commentary brief and milestone-based while work is happening
- give a concise final answer that can be spoken aloud naturally
Expand Down
2 changes: 1 addition & 1 deletion Orbit-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>OpenAIAPIKey</key>
<string></string>
<key>CodexActionModel</key>
<string>gpt-5.4-mini</string>
<string>gpt-5.4</string>
<key>CodexActionServiceTier</key>
<string>fast</string>
<key>CodexActionSandbox</key>
Expand Down
12 changes: 6 additions & 6 deletions Orbit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -454,7 +454,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand All @@ -476,7 +476,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -497,7 +497,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -516,7 +516,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -535,7 +535,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
2 changes: 1 addition & 1 deletion Orbit/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
## Defaults

- STT default: OpenAI `gpt-4o-mini-transcribe`
- Codex model default: `gpt-5.4-mini`
- Codex model default: `gpt-5.4`
- Codex effort default: `medium`
- Codex service tier default: `fast`
- TTS default: OpenAI `gpt-4o-mini-tts`
Expand Down
173 changes: 109 additions & 64 deletions Orbit/CodexAppServerActionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,22 @@ final class CodexAppServerActionProvider: ActionProvider {
}

var configurationSummary: String {
if settings.codexServiceTier == .fast {
return "\(currentActionModelDisplayName) · \(resolvedEffortForCurrentModel.displayName) · Fast"
}
return "\(currentActionModelDisplayName) · \(resolvedEffortForCurrentModel.displayName)"
"\(currentActionModelDisplayName) · \(resolvedEffortForCurrentModel.displayName) · \(resolvedServiceTier.displayName)"
}

var availableModels: [OrbitCodexModelOption] {
availableModelOptions
}

var supportedEffortsForSelectedModel: [OrbitCodexReasoningEffort] {
modelOption(for: normalizedCurrentActionModel)?.supportedEfforts ?? OrbitCodexReasoningEffort.allCases
supportedEfforts(for: normalizedCurrentActionModel)
}

func supportedEfforts(for model: String) -> [OrbitCodexReasoningEffort] {
let normalizedModel = model.trimmingCharacters(in: .whitespacesAndNewlines)
return modelOption(for: normalizedModel)?.supportedEfforts
?? OrbitCodexModelOption.fallbackOption(for: normalizedModel)?.supportedEfforts
?? OrbitCodexReasoningEffort.allCases
}

var hasReadySession: Bool {
Expand Down Expand Up @@ -443,7 +447,11 @@ final class CodexAppServerActionProvider: ActionProvider {

private func startServerIfNeeded() throws {
let codexExecutable = try resolveCodexExecutable()
let preparedCodexHome = try OrbitCodexEnvironment.prepareHome()
let preparedCodexHome = try OrbitCodexEnvironment.prepareHome(
model: normalizedCurrentActionModel,
reasoningEffort: resolvedEffortForCurrentModel,
serviceTier: resolvedServiceTier
)
self.preparedCodexHome = preparedCodexHome
let launchCommand = resolvedCodexLaunchCommand(
for: codexExecutable,
Expand Down Expand Up @@ -902,10 +910,10 @@ final class CodexAppServerActionProvider: ActionProvider {
"cwd": NSHomeDirectory(),
"serviceName": "orbit"
]
if settings.codexServiceTier == .fast {
if resolvedServiceTier == .fast {
params["serviceTier"] = OrbitCodexServiceTier.fast.rawValue
}
appendDebugEvent("-> thread/start model=\(normalizedCurrentActionModel) effort=\(resolvedEffortForCurrentModel.rawValue) tier=\(settings.codexServiceTier.rawValue)")
appendDebugEvent("-> thread/start model=\(normalizedCurrentActionModel) effort=\(resolvedEffortForCurrentModel.rawValue) tier=\(resolvedServiceTier.rawValue)")
sendJSON([
"method": "thread/start",
"id": 1,
Expand Down Expand Up @@ -975,10 +983,10 @@ final class CodexAppServerActionProvider: ActionProvider {
"model": currentActionModel,
"effort": resolvedEffortForCurrentModel.rawValue
]
if settings.codexServiceTier == .fast {
if resolvedServiceTier == .fast {
params["serviceTier"] = OrbitCodexServiceTier.fast.rawValue
}
appendDebugEvent("-> turn/start model=\(currentActionModel) effort=\(resolvedEffortForCurrentModel.rawValue) tier=\(settings.codexServiceTier.rawValue) inputItems=\(inputItems.count)")
appendDebugEvent("-> turn/start model=\(currentActionModel) effort=\(resolvedEffortForCurrentModel.rawValue) tier=\(resolvedServiceTier.rawValue) inputItems=\(inputItems.count)")

sendJSON([
"method": "turn/start",
Expand Down Expand Up @@ -1459,7 +1467,7 @@ final class CodexAppServerActionProvider: ActionProvider {
if let progressText {
let cleaned = progressText.trimmingCharacters(in: .whitespacesAndNewlines)
if !cleaned.isEmpty {
eventHandler?(.liveUpdate(cleaned))
appendDebugEvent("<- tool/progress \(cleaned)")
}
}
}
Expand Down Expand Up @@ -1627,6 +1635,10 @@ final class CodexAppServerActionProvider: ActionProvider {
return normalized.isEmpty ? OrbitCodexModelOption.fallbackDefaultModel : normalized
}

private var resolvedServiceTier: OrbitCodexServiceTier {
settings.codexServiceTier
}

private func resolvedAgentMessagePhase(from params: [String: Any]) -> String? {
if let phase = params["phase"] as? String {
return phase
Expand Down Expand Up @@ -1754,54 +1766,9 @@ final class CodexAppServerActionProvider: ActionProvider {
}

private func updateModelCatalog(from result: [String: Any]) {
guard let data = result["data"] as? [[String: Any]] else { return }

let parsedModels = data.compactMap { item -> OrbitCodexModelOption? in
let model = (item["model"] as? String) ?? (item["id"] as? String) ?? ""
let normalizedModel = model.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedModel.isEmpty else { return nil }

let hidden = item["hidden"] as? Bool ?? false
guard !hidden else { return nil }

let inputModalities = (item["inputModalities"] as? [String]) ?? ["text", "image"]
let normalizedModalities = inputModalities.map { $0.lowercased() }
guard normalizedModalities.contains("text"), normalizedModalities.contains("image") else {
return nil
}

let effortEntries = item["supportedReasoningEfforts"] as? [[String: Any]] ?? []
let supportedEfforts: [OrbitCodexReasoningEffort] = effortEntries.compactMap { entry in
guard let rawValue = entry["reasoningEffort"] as? String else { return nil }
return OrbitCodexReasoningEffort(rawValue: rawValue)
}

let defaultEffort = OrbitCodexReasoningEffort(rawValue: item["defaultReasoningEffort"] as? String ?? "")
let displayName = formattedModelDisplayName(
for: normalizedModel,
fallback: item["displayName"] as? String
)
let shortDisplayName = shortModelDisplayName(from: displayName)

return OrbitCodexModelOption(
model: normalizedModel,
displayName: displayName,
shortDisplayName: shortDisplayName,
supportedEfforts: supportedEfforts.isEmpty ? OrbitCodexReasoningEffort.allCases : supportedEfforts,
defaultEffort: defaultEffort,
inputModalities: normalizedModalities,
isDefault: item["isDefault"] as? Bool ?? false
)
}

let parsedModels = Self.parseModelCatalog(from: result)
guard !parsedModels.isEmpty else { return }

availableModelOptions = parsedModels.sorted { lhs, rhs in
if lhs.isDefault != rhs.isDefault {
return lhs.isDefault && !rhs.isDefault
}
return lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName) == .orderedAscending
}
availableModelOptions = parsedModels
}

private func updateCollaborationModes(from result: [String: Any]) {
Expand Down Expand Up @@ -1833,7 +1800,86 @@ final class CodexAppServerActionProvider: ActionProvider {
availableModelOptions.first(where: { $0.model == model })
}

private func formattedModelDisplayName(for model: String, fallback: String?) -> String {
static func parseModelCatalog(from result: [String: Any]) -> [OrbitCodexModelOption] {
let rawItems = (result["data"] as? [[String: Any]]) ?? (result["models"] as? [[String: Any]]) ?? []

let parsedModels = rawItems.compactMap { item -> (option: OrbitCodexModelOption, priority: Int)? in
let normalizedModel = (
(item["model"] as? String)
?? (item["id"] as? String)
?? (item["slug"] as? String)
?? ""
).trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedModel.isEmpty else { return nil }

let visibility = ((item["visibility"] as? String) ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
let hidden = (item["hidden"] as? Bool) ?? (visibility == "hide" || visibility == "hidden")
guard !hidden else { return nil }

if let supportedInAPI = item["supported_in_api"] as? Bool, !supportedInAPI {
return nil
}

let inputModalities = (item["inputModalities"] as? [String])
?? (item["input_modalities"] as? [String])
?? ["text", "image"]
let normalizedModalities = inputModalities.map { $0.lowercased() }
guard normalizedModalities.contains("text"), normalizedModalities.contains("image") else {
return nil
}

let effortEntries = (item["supportedReasoningEfforts"] as? [[String: Any]])
?? (item["supported_reasoning_levels"] as? [[String: Any]])
?? []
let supportedEfforts = effortEntries.compactMap { entry -> OrbitCodexReasoningEffort? in
let rawValue = (
(entry["reasoningEffort"] as? String)
?? (entry["effort"] as? String)
?? ""
).trimmingCharacters(in: .whitespacesAndNewlines)
return OrbitCodexReasoningEffort(rawValue: rawValue)
}

let defaultEffort = OrbitCodexReasoningEffort(rawValue: (
(item["defaultReasoningEffort"] as? String)
?? (item["default_reasoning_level"] as? String)
?? ""
).trimmingCharacters(in: .whitespacesAndNewlines))
let displayName = formattedModelDisplayName(
for: normalizedModel,
fallback: (item["displayName"] as? String) ?? (item["display_name"] as? String)
)
let shortDisplayName = shortModelDisplayName(from: displayName)
let isDefault = (item["isDefault"] as? Bool ?? false) || normalizedModel == OrbitCodexModelOption.fallbackDefaultModel

return (
OrbitCodexModelOption(
model: normalizedModel,
displayName: displayName,
shortDisplayName: shortDisplayName,
supportedEfforts: supportedEfforts.isEmpty ? OrbitCodexReasoningEffort.allCases : supportedEfforts,
defaultEffort: defaultEffort,
inputModalities: normalizedModalities,
isDefault: isDefault
),
item["priority"] as? Int ?? Int.max
)
}

return parsedModels.sorted { lhs, rhs in
if lhs.option.isDefault != rhs.option.isDefault {
return lhs.option.isDefault && !rhs.option.isDefault
}
if lhs.priority != rhs.priority {
return lhs.priority < rhs.priority
}
return lhs.option.displayName.localizedCaseInsensitiveCompare(rhs.option.displayName) == .orderedAscending
}.map { $0.option }
}

private static func formattedModelDisplayName(for model: String, fallback: String?) -> String {
let normalized = model.trimmingCharacters(in: .whitespacesAndNewlines)
switch normalized {
case "gpt-5.4":
Expand All @@ -1851,16 +1897,15 @@ final class CodexAppServerActionProvider: ActionProvider {
let fallbackValue = fallback?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !fallbackValue.isEmpty {
return fallbackValue
.replacingOccurrences(of: "GPT-", with: "GPT-")
.replacingOccurrences(of: "gpt-", with: "GPT-")
.replacingOccurrences(of: "-mini", with: " Mini")
.replacingOccurrences(of: "-codex", with: " Codex")
.replacingOccurrences(of: "^gpt-", with: "GPT-", options: [.regularExpression, .caseInsensitive])
.replacingOccurrences(of: "-mini", with: " Mini", options: [.regularExpression, .caseInsensitive])
.replacingOccurrences(of: "-codex", with: " Codex", options: [.regularExpression, .caseInsensitive])
}

return normalized.uppercased()
}

private func shortModelDisplayName(from displayName: String) -> String {
private static func shortModelDisplayName(from displayName: String) -> String {
let cleaned = displayName
.replacingOccurrences(of: "GPT-", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down
23 changes: 17 additions & 6 deletions Orbit/OrbitCodexEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ enum OrbitCodexEnvironment {
"browser_resize"
]

static func prepareHome() throws -> OrbitPreparedCodexHome {
static func prepareHome(
model: String = OrbitCodexModelOption.fallbackDefaultModel,
reasoningEffort: OrbitCodexReasoningEffort = .medium,
serviceTier: OrbitCodexServiceTier = .fast
) throws -> OrbitPreparedCodexHome {
let fileManager = FileManager.default
let supportDirectory = try supportRootDirectory()
let codexHome = supportDirectory.appendingPathComponent(homeDirectoryName, isDirectory: true)
Expand All @@ -77,7 +81,10 @@ enum OrbitCodexEnvironment {
let configContents = makeConfigContents(
logDirectory: logDirectory,
sqliteDirectory: sqliteDirectory,
configuredSkillPaths: configuredSkillPaths
configuredSkillPaths: configuredSkillPaths,
model: model,
reasoningEffort: reasoningEffort,
serviceTier: serviceTier
)

let existingContents = try? String(contentsOf: configPath, encoding: .utf8)
Expand Down Expand Up @@ -112,12 +119,16 @@ enum OrbitCodexEnvironment {
logDirectory: URL,
sqliteDirectory: URL,
configuredSkillPaths: [String: URL],
modelInstructionsPath: String? = nil
modelInstructionsPath: String? = nil,
model: String = OrbitCodexModelOption.fallbackDefaultModel,
reasoningEffort: OrbitCodexReasoningEffort = .medium,
serviceTier: OrbitCodexServiceTier = .fast
) -> String {
let normalizedModel = model.trimmingCharacters(in: .whitespacesAndNewlines)
var lines: [String] = [
"model = \"gpt-5.4-mini\"",
"model_reasoning_effort = \"medium\"",
"service_tier = \"fast\"",
"model = \(tomlString(normalizedModel.isEmpty ? OrbitCodexModelOption.fallbackDefaultModel : normalizedModel))",
"model_reasoning_effort = \(tomlString(reasoningEffort.rawValue))",
"service_tier = \(tomlString(serviceTier.rawValue))",
"approval_policy = \"never\"",
"sandbox_mode = \"danger-full-access\"",
"cli_auth_credentials_store = \"file\"",
Expand Down
Loading
Loading