sneakoscope 10.3.7 → 10.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -181
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +61 -1
- package/dist/commands/codex-app.js +4 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +55 -0
- package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
- package/dist/core/codex-app/codex-context-window.js +61 -24
- package/dist/core/codex-app/menubar/constants.js +4 -0
- package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
- package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
- package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
- package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
- package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
- package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
- package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
- package/dist/core/codex-lb/desktop-service.js +13 -2
- package/dist/core/commands/naruto-command.js +1 -1
- package/dist/core/decisions/child-model-choice.js +107 -0
- package/dist/core/decisions/cli.js +19 -2
- package/dist/core/decisions/routing.js +5 -4
- package/dist/core/hooks-runtime/hook-context.js +21 -7
- package/dist/core/hooks-runtime/hook-io.js +3 -2
- package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
- package/dist/core/hooks-runtime/subagent-context.js +13 -2
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
- package/dist/core/hooks-runtime.js +12 -5
- package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
- package/dist/core/subagents/child-model-allowlist.js +179 -0
- package/dist/core/subagents/child-model-plan.js +117 -0
- package/dist/core/subagents/naruto-host-credentials.js +82 -13
- package/dist/core/subagents/official-subagent-config.js +66 -15
- package/dist/core/subagents/official-subagent-preparation.js +45 -12
- package/dist/core/subagents/official-subagent-prompt.js +82 -11
- package/dist/core/subagents/official-subagent-runner.js +3 -0
- package/dist/core/subagents/read-only-list-role.js +73 -0
- package/dist/core/subagents/role-model-preferences.js +54 -17
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
- package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
- package/package.json +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Exact `sks bridge …` argv for OpenRouter Only and its subagent model list.
|
|
4
|
+
/// Criteria are free text, so `set` sends the whole list on stdin, never argv.
|
|
5
|
+
enum OpenRouterOnlyCommand {
|
|
6
|
+
static let resultSchema = "sks.desktop-bridge-command-result.v1"
|
|
7
|
+
|
|
8
|
+
static var status: [String] { ["bridge", "openrouter-only", "status", "--json"] }
|
|
9
|
+
static func set(enabled: Bool) -> [String] { ["bridge", "openrouter-only", enabled ? "on" : "off", "--json"] }
|
|
10
|
+
static var listSubagentModels: [String] { ["bridge", "subagent-models", "list", "--json"] }
|
|
11
|
+
static var setSubagentModels: [String] { ["bridge", "subagent-models", "set", "--stdin", "--json"] }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Mirrors src/core/subagents/child-model-allowlist.ts so a bad list is caught
|
|
15
|
+
/// before Apply; the CLI validates again and stays authoritative.
|
|
16
|
+
enum SubagentModelRules {
|
|
17
|
+
static let maxModels = 16
|
|
18
|
+
static let maxCriteriaCharacters = 240
|
|
19
|
+
static let efforts = ["low", "medium", "high", "xhigh"]
|
|
20
|
+
|
|
21
|
+
/// Same pattern as isOpenRouterModelId: `vendor/model[:variant]`, at most 160 characters.
|
|
22
|
+
static func isModelId(_ id: String) -> Bool {
|
|
23
|
+
guard id.count <= 160 else { return false }
|
|
24
|
+
return id.range(of: #"^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._:-]*$"#, options: [.regularExpression, .caseInsensitive]) != nil
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// One row of `result.openrouter_only.subagent_models`.
|
|
29
|
+
struct SubagentModelEntry: Equatable {
|
|
30
|
+
var model: String
|
|
31
|
+
var criteria: String
|
|
32
|
+
var reasoningEffort: String?
|
|
33
|
+
var isDefault: Bool
|
|
34
|
+
/// The CLI's answer: the model has an OpenRouter route now. nil for local drafts.
|
|
35
|
+
var routable: Bool?
|
|
36
|
+
|
|
37
|
+
static func decode(_ value: Any) -> SubagentModelEntry? {
|
|
38
|
+
guard let row = value as? [String: Any], let model = OpenRouterOnlyJSON.text(row["model"]) else { return nil }
|
|
39
|
+
let effort = OpenRouterOnlyJSON.text(row["reasoning_effort"]).flatMap { SubagentModelRules.efforts.contains($0) ? $0 : nil }
|
|
40
|
+
return SubagentModelEntry(
|
|
41
|
+
model: model,
|
|
42
|
+
criteria: row["criteria"] as? String ?? "",
|
|
43
|
+
reasoningEffort: effort,
|
|
44
|
+
isDefault: row["default"] as? Bool == true,
|
|
45
|
+
routable: row["routable"] as? Bool
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// ProcessClient redacts text that looks like a secret in every answer, so
|
|
50
|
+
/// these criteria are not the saved text and must never be sent back.
|
|
51
|
+
var criteriaUnreadable: Bool { criteria.range(of: "[redacted]", options: .caseInsensitive) != nil }
|
|
52
|
+
|
|
53
|
+
/// The user-editable part; routability never counts as an edit.
|
|
54
|
+
var submitted: SubagentModelEntry {
|
|
55
|
+
var copy = self
|
|
56
|
+
copy.routable = nil
|
|
57
|
+
return copy
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// `result.openrouter_only` of the openrouter-only and subagent-models answers
|
|
62
|
+
/// (and of `auth-priority on`, which turns this mode off first). Like the
|
|
63
|
+
/// Codex-LB preference, "on but unavailable" is a distinct saved state.
|
|
64
|
+
struct OpenRouterOnlyState: Equatable {
|
|
65
|
+
let enabled: Bool
|
|
66
|
+
let state: String
|
|
67
|
+
let error: String?
|
|
68
|
+
let mainModel: String?
|
|
69
|
+
let defaultSubagentModel: String?
|
|
70
|
+
let subagentModels: [SubagentModelEntry]
|
|
71
|
+
let jevEnabled: Bool
|
|
72
|
+
let warnings: [String]
|
|
73
|
+
|
|
74
|
+
static func decode(_ payload: [String: Any]) -> OpenRouterOnlyState? {
|
|
75
|
+
let result = payload["result"] as? [String: Any]
|
|
76
|
+
guard let raw = (result?["openrouter_only"] ?? payload["openrouter_only"]) as? [String: Any],
|
|
77
|
+
let enabled = raw["enabled"] as? Bool,
|
|
78
|
+
let state = raw["state"] as? String,
|
|
79
|
+
["off", "active", "unavailable"].contains(state),
|
|
80
|
+
!(enabled && state == "off") else { return nil }
|
|
81
|
+
return OpenRouterOnlyState(
|
|
82
|
+
enabled: enabled,
|
|
83
|
+
state: enabled ? state : "off",
|
|
84
|
+
error: OpenRouterOnlyJSON.text(raw["error"]),
|
|
85
|
+
mainModel: OpenRouterOnlyJSON.text(raw["main_model"]),
|
|
86
|
+
defaultSubagentModel: OpenRouterOnlyJSON.text(raw["default_subagent_model"]),
|
|
87
|
+
subagentModels: (raw["subagent_models"] as? [Any] ?? []).compactMap(SubagentModelEntry.decode),
|
|
88
|
+
jevEnabled: raw["jev_enabled"] as? Bool == true,
|
|
89
|
+
warnings: OpenRouterOnlyJSON.strings(raw["warnings"])
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var message: String {
|
|
94
|
+
switch state {
|
|
95
|
+
case "active": return "On · Codex and its subagents use OpenRouter models only"
|
|
96
|
+
case "off":
|
|
97
|
+
// A mode that is off can still report why turning it on would fail.
|
|
98
|
+
return error.map { "Off · before turning on: " + OpenRouterOnlyMessages.describe($0) }
|
|
99
|
+
?? "Off · Codex uses its configured model routes"
|
|
100
|
+
default:
|
|
101
|
+
return "On, unavailable · " + OpenRouterOnlyMessages.describe(error ?? "openrouter_only_unavailable")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var modelSummary: String? {
|
|
106
|
+
guard enabled else { return nil }
|
|
107
|
+
let main = mainModel ?? "not set"
|
|
108
|
+
let child = defaultSubagentModel ?? "none"
|
|
109
|
+
return "Main model: \(main) · default subagent model: \(child) · Jev routing \(jevEnabled ? "on" : "off")"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var unroutableModels: [String] { subagentModels.filter { $0.routable == false }.map(\.model) }
|
|
113
|
+
/// `unroutableModels` already names each unroutable list model; its
|
|
114
|
+
/// per-model warning would say the same thing again.
|
|
115
|
+
var warningMessages: [String] {
|
|
116
|
+
warnings.filter { !$0.hasPrefix(OpenRouterOnlyMessages.unroutableWarningPrefix) }.map(OpenRouterOnlyMessages.describe)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Human wording for blocker, error and warning codes. Unknown codes stay readable.
|
|
121
|
+
enum OpenRouterOnlyMessages {
|
|
122
|
+
/// Codes an SKS build without these commands answers with.
|
|
123
|
+
static let olderCliCodes: Set<String> = ["bridge_command_invalid", "bridge_command_unknown_option", "unknown_command"]
|
|
124
|
+
static let unroutableWarningPrefix = "openrouter_only_subagent_model_unroutable:"
|
|
125
|
+
|
|
126
|
+
static func describe(_ raw: String) -> String {
|
|
127
|
+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
128
|
+
let parts = trimmed.split(separator: ":", maxSplits: 1).map(String.init)
|
|
129
|
+
let code = parts.first ?? trimmed
|
|
130
|
+
let row = parts.count > 1 ? Int(parts[1]).map { "Row \($0 + 1): " } ?? "" : ""
|
|
131
|
+
switch code {
|
|
132
|
+
case "openrouter_only_provider_disabled":
|
|
133
|
+
return "OpenRouter is turned off. Enable it under Accounts on the Connections page."
|
|
134
|
+
case "openrouter_only_credential_missing":
|
|
135
|
+
return "OpenRouter is not connected. Connect it under Accounts on the Connections page."
|
|
136
|
+
case "openrouter_only_no_openrouter_models":
|
|
137
|
+
return "No OpenRouter model is selected. Choose models under Models in Codex on the Connections page first."
|
|
138
|
+
case "openrouter_only_subagent_list_empty":
|
|
139
|
+
return "The subagent model list is empty. Add at least one model on the Subagent Models page."
|
|
140
|
+
case "openrouter_only_main_model_not_openrouter":
|
|
141
|
+
return "Codex's main model is not an OpenRouter model. Turn OpenRouter Only off and on again to reapply it."
|
|
142
|
+
case "desktop_bridge_not_running", "desktop_bridge_runtime_not_ready":
|
|
143
|
+
return "The Desktop Bridge is not running. Open Bridge diagnostics on the Connections page and repair it."
|
|
144
|
+
case "openrouter_only_no_routable_subagent_model":
|
|
145
|
+
return "No model on the subagent list has an OpenRouter route. Add a model Codex offers under Models in Codex, then try again."
|
|
146
|
+
case "codex_restart_failed_manual_restart_required":
|
|
147
|
+
return "Codex could not be restarted. Quit and reopen Codex to load the change."
|
|
148
|
+
case "openrouter_only_main_model_unparsed":
|
|
149
|
+
return "SKS cannot read the model line in ~/.codex/config.toml. Write it as model = \"vendor/model\", then turn OpenRouter Only on again."
|
|
150
|
+
case "openrouter_only_main_model_unexposed":
|
|
151
|
+
return "Codex's main model is not in the Codex model list. Add it under Models in Codex on the Connections page."
|
|
152
|
+
case "openrouter_only_main_model_switch_failed", "openrouter_only_main_model_write_not_applied":
|
|
153
|
+
return "SKS could not change Codex's main model. Pick an OpenRouter model in Codex's model picker."
|
|
154
|
+
case "openrouter_only_read_only_role_install_failed":
|
|
155
|
+
return "SKS could not install the read-only subagent role in ~/.codex/agents. Check that folder's permissions."
|
|
156
|
+
case let value where value.hasPrefix("openrouter_only_main_model_write_"):
|
|
157
|
+
return "SKS did not change Codex's model line because config.toml changed at the same time. Try again."
|
|
158
|
+
case "openrouter_only_codex_image_mode_unavailable":
|
|
159
|
+
return "Codex's built-in image generation does not work on OpenRouter models. Choose a custom image model on the Image Generation page."
|
|
160
|
+
case "subagent_model_id_invalid":
|
|
161
|
+
return row + "not an OpenRouter model id (vendor/model)."
|
|
162
|
+
case "subagent_model_duplicate":
|
|
163
|
+
return row + "this model is already on the list."
|
|
164
|
+
case "subagent_model_effort_invalid":
|
|
165
|
+
return row + "effort must be Default, low, medium, high, or xhigh."
|
|
166
|
+
case "subagent_model_list_too_long":
|
|
167
|
+
return row + "the list holds at most \(SubagentModelRules.maxModels) models."
|
|
168
|
+
case "subagent_model_criteria_redacted":
|
|
169
|
+
return row + "part of these criteria looks like a secret, so SKS output hid it. Type the criteria again before applying."
|
|
170
|
+
case let value where olderCliCodes.contains(value):
|
|
171
|
+
return "This SKS build does not include OpenRouter Only. Update SKS, then reopen this page."
|
|
172
|
+
case "native_process_timeout":
|
|
173
|
+
return "SKS did not answer in time."
|
|
174
|
+
case "native_process_output_limit":
|
|
175
|
+
return "SKS returned more output than Control Center accepts."
|
|
176
|
+
case "native_process_empty_output":
|
|
177
|
+
return "SKS returned no output."
|
|
178
|
+
default:
|
|
179
|
+
// Only the code is humanized; a value after ':' (a model id) stays exact.
|
|
180
|
+
let words = code.replacingOccurrences(of: "_", with: " ")
|
|
181
|
+
guard let first = words.first else { return "Unknown issue." }
|
|
182
|
+
let text = first.uppercased() + words.dropFirst()
|
|
183
|
+
return parts.count > 1 ? text + ": " + parts[1] : text
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Schema-agnostic reading of any answer: command results carry blockers in
|
|
189
|
+
/// `execution`, CLI refusals and native process failures at the top level.
|
|
190
|
+
struct OpenRouterOnlyReceipt: Equatable {
|
|
191
|
+
let ok: Bool
|
|
192
|
+
let blockers: [String]
|
|
193
|
+
|
|
194
|
+
static func decode(_ payload: [String: Any]?) -> OpenRouterOnlyReceipt {
|
|
195
|
+
let execution = payload?["execution"] as? [String: Any]
|
|
196
|
+
var seen = Set<String>()
|
|
197
|
+
let blockers = (OpenRouterOnlyJSON.strings(execution?["blockers"])
|
|
198
|
+
+ OpenRouterOnlyJSON.strings(payload?["blockers"])
|
|
199
|
+
+ [OpenRouterOnlyJSON.text(payload?["reason"]), OpenRouterOnlyJSON.text(payload?["error"])].compactMap { $0 })
|
|
200
|
+
.filter { seen.insert($0).inserted }
|
|
201
|
+
return OpenRouterOnlyReceipt(ok: payload?["ok"] as? Bool == true, blockers: blockers)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var primaryIssue: String? { blockers.first.map(OpenRouterOnlyMessages.describe) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Command execution and the saved mode are separate facts, exactly as for the
|
|
208
|
+
/// Codex-LB preference: a setup failure after the write is not a rollback.
|
|
209
|
+
enum OpenRouterOnlyMutationOutcome: Equatable {
|
|
210
|
+
case saved(OpenRouterOnlyState)
|
|
211
|
+
case savedWithSetupIssue(OpenRouterOnlyState)
|
|
212
|
+
case notApplied(OpenRouterOnlyState)
|
|
213
|
+
case unconfirmed
|
|
214
|
+
|
|
215
|
+
static func resolve(payload: [String: Any]?, desired: Bool, commandSucceeded: Bool, responseComplete: Bool) -> OpenRouterOnlyMutationOutcome {
|
|
216
|
+
guard responseComplete, let payload = payload, let state = OpenRouterOnlyState.decode(payload) else { return .unconfirmed }
|
|
217
|
+
guard state.enabled == desired else { return .notApplied(state) }
|
|
218
|
+
return commandSucceeded ? .saved(state) : .savedWithSetupIssue(state)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var observedState: OpenRouterOnlyState? {
|
|
222
|
+
switch self {
|
|
223
|
+
case .saved(let state), .savedWithSetupIssue(let state), .notApplied(let state): return state
|
|
224
|
+
case .unconfirmed: return nil
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
var succeeded: Bool {
|
|
229
|
+
if case .saved = self { return true }
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var operationSummary: String {
|
|
234
|
+
switch self {
|
|
235
|
+
case .saved: return "OpenRouter Only setting saved"
|
|
236
|
+
case .savedWithSetupIssue: return "OpenRouter Only setting saved; setup needs attention"
|
|
237
|
+
case .notApplied: return "Requested OpenRouter Only setting was not saved; current setting confirmed"
|
|
238
|
+
case .unconfirmed: return "OpenRouter Only setting could not be confirmed"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// ProcessClient merges stdout and stderr, and a timeout appends a native
|
|
244
|
+
/// failure object. Prefer the command result over banners or trailing noise.
|
|
245
|
+
enum OpenRouterOnlyJSON {
|
|
246
|
+
private static let preferredSchemas = [OpenRouterOnlyCommand.resultSchema, "sks.bridge-command-error.v1", "sks.native-process-error.v1"]
|
|
247
|
+
|
|
248
|
+
static func object(from text: String) -> [String: Any]? {
|
|
249
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
250
|
+
if let whole = parse(trimmed) { return whole }
|
|
251
|
+
var objects: [[String: Any]] = []
|
|
252
|
+
var search = trimmed.startIndex
|
|
253
|
+
while search < trimmed.endIndex, let start = trimmed[search...].firstIndex(of: "{") {
|
|
254
|
+
if let slice = completeObject(in: trimmed, from: start), let object = parse(slice) {
|
|
255
|
+
objects.append(object)
|
|
256
|
+
search = trimmed.index(start, offsetBy: slice.count)
|
|
257
|
+
} else {
|
|
258
|
+
search = trimmed.index(after: start)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for schema in preferredSchemas {
|
|
262
|
+
if let match = objects.first(where: { $0["schema"] as? String == schema }) { return match }
|
|
263
|
+
}
|
|
264
|
+
return objects.first { $0["ok"] != nil }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Why a read failed, in words a user can act on; an older CLI says "update SKS".
|
|
268
|
+
static func unavailableReason(code: Int32, output: String, payload: [String: Any]?) -> String {
|
|
269
|
+
let lower = output.lowercased()
|
|
270
|
+
if code == 127 || lower.contains("sks command not found") {
|
|
271
|
+
return "SKS is not on this Menu Bar path. Update SKS, then reopen this page."
|
|
272
|
+
}
|
|
273
|
+
let receipt = OpenRouterOnlyReceipt.decode(payload)
|
|
274
|
+
if receipt.blockers.contains(where: OpenRouterOnlyMessages.olderCliCodes.contains)
|
|
275
|
+
|| lower.contains("unknown command") || lower.contains("unknown subcommand") {
|
|
276
|
+
return OpenRouterOnlyMessages.describe("unknown_command")
|
|
277
|
+
}
|
|
278
|
+
return receipt.primaryIssue ?? "SKS returned no readable answer. Update SKS, then reopen this page."
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static func text(_ value: Any?) -> String? {
|
|
282
|
+
guard let string = (value as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else { return nil }
|
|
283
|
+
return string
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static func strings(_ value: Any?) -> [String] { (value as? [Any] ?? []).compactMap(text) }
|
|
287
|
+
|
|
288
|
+
private static func parse(_ text: String) -> [String: Any]? {
|
|
289
|
+
guard let data = text.data(using: .utf8) else { return nil }
|
|
290
|
+
return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private static func completeObject(in text: String, from start: String.Index) -> String? {
|
|
294
|
+
var depth = 0
|
|
295
|
+
var inString = false
|
|
296
|
+
var escape = false
|
|
297
|
+
var index = start
|
|
298
|
+
while index < text.endIndex {
|
|
299
|
+
let ch = text[index]
|
|
300
|
+
if inString {
|
|
301
|
+
if escape { escape = false } else if ch == "\\" { escape = true } else if ch == "\"" { inString = false }
|
|
302
|
+
} else if ch == "\"" {
|
|
303
|
+
inString = true
|
|
304
|
+
} else if ch == "{" {
|
|
305
|
+
depth += 1
|
|
306
|
+
} else if ch == "}" {
|
|
307
|
+
depth -= 1
|
|
308
|
+
if depth == 0 { return String(text[start...index]) }
|
|
309
|
+
}
|
|
310
|
+
index = text.index(after: index)
|
|
311
|
+
}
|
|
312
|
+
return nil
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -406,7 +406,8 @@ final class ProcessClient {
|
|
|
406
406
|
text = text.replacingOccurrences(of: sensitiveValue, with: "[redacted]")
|
|
407
407
|
}
|
|
408
408
|
let patterns = [
|
|
409
|
-
|
|
409
|
+
// Anchored like the CLI (secret-redaction.ts): "risk-assessment" is not a key.
|
|
410
|
+
#"\bsk-(?:proj|or-v1|clb)?-?[A-Za-z0-9_-]{12,}"#,
|
|
410
411
|
#"gh[pousr]_[A-Za-z0-9_]{20,}"#,
|
|
411
412
|
#"github_pat_[A-Za-z0-9_]{20,}"#,
|
|
412
413
|
#"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}"#,
|
|
@@ -78,7 +78,7 @@ extension ProvidersViewController: NSTableViewDataSource, NSTableViewDelegate {
|
|
|
78
78
|
clear.setAccessibilityIdentifier("sks-provider-exposure-clear")
|
|
79
79
|
let card = NativeView.card(
|
|
80
80
|
title: "Models in Codex",
|
|
81
|
-
subtitle: "
|
|
81
|
+
subtitle: "Select OpenRouter models, apply, then relaunch Codex to update its picker. Codex-LB models are included unless OpenRouter Only is on.",
|
|
82
82
|
views: [exposureStatus, exposureSearchField, scroll, ControlKit.actionRow([exposureApplyButton, clear])]
|
|
83
83
|
)
|
|
84
84
|
card.setAccessibilityIdentifier("sks-provider-card-model-exposure")
|
|
@@ -97,12 +97,22 @@ extension ProvidersViewController: NSTableViewDataSource, NSTableViewDelegate {
|
|
|
97
97
|
self.exposureRows = decoded.rows
|
|
98
98
|
self.exposurePending = Set(decoded.rows.filter(\.selected).map(\.publicId))
|
|
99
99
|
self.exposureMaxSelected = decoded.max
|
|
100
|
-
self.exposureStatus.stringValue =
|
|
100
|
+
self.exposureStatus.stringValue = self.exposureModeSummary + "OpenRouter: \(decoded.selected) of \(decoded.available) selected (max \(decoded.max))."
|
|
101
101
|
self.exposureStatus.textColor = .secondaryLabelColor
|
|
102
102
|
self.exposureTable.reloadData()
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/// What the picker holds besides the OpenRouter selection depends on the
|
|
107
|
+
/// routing mode; while the mode is unknown, nothing is claimed.
|
|
108
|
+
var exposureModeSummary: String {
|
|
109
|
+
switch openRouterOnlyEnabled {
|
|
110
|
+
case true?: return "OpenRouter Only: Codex-LB models are hidden; subagent list models are always exposed · "
|
|
111
|
+
case false?: return "Codex-LB: all models exposed · "
|
|
112
|
+
case nil: return ""
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
func controlTextDidChange(_ notification: Notification) {
|
|
107
117
|
if notification.object as? NSTextField === exposureSearchField { exposureSearchChanged() }
|
|
108
118
|
}
|
|
@@ -142,8 +152,10 @@ extension ProvidersViewController: NSTableViewDataSource, NSTableViewDelegate {
|
|
|
142
152
|
}
|
|
143
153
|
let completed = result.code == 0 && truth?.completed == true
|
|
144
154
|
let blocker = truth?.blockers.first.map(ProviderSecretRedactor.redact)
|
|
155
|
+
let extra = self.openRouterOnlyEnabled == true ? " plus the subagent list models; Codex-LB models stay hidden while OpenRouter Only is on"
|
|
156
|
+
: self.openRouterOnlyEnabled == false ? " plus every Codex-LB model" : ""
|
|
145
157
|
let summary = completed
|
|
146
|
-
? "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)
|
|
158
|
+
? "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Restart Codex to refresh its picker."
|
|
147
159
|
: blocker.map { "Exposure not applied · \($0)" } ?? "Exposure result schema invalid"
|
|
148
160
|
_ = self.operations.update(snapshot, state: completed ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
|
|
149
161
|
self.exposureStatus.stringValue = summary
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import Cocoa
|
|
2
|
+
|
|
3
|
+
/// "OpenRouter Only" routing mode in the Default connection card. It sits next
|
|
4
|
+
/// to "Prefer Codex-LB": the CLI turns one off when the other turns on, so
|
|
5
|
+
/// both switches lock while either changes and both re-read afterwards. The
|
|
6
|
+
/// OpenRouter profile itself stays a coexisting account under Accounts.
|
|
7
|
+
extension ProvidersViewController {
|
|
8
|
+
var routingModeBusy: Bool { authPriorityBusy || openRouterOnlyBusy }
|
|
9
|
+
|
|
10
|
+
func makeOpenRouterOnlyRows() -> [NSView] {
|
|
11
|
+
openRouterOnlyToggle.target = self
|
|
12
|
+
openRouterOnlyToggle.action = #selector(toggleOpenRouterOnly)
|
|
13
|
+
openRouterOnlyToggle.isEnabled = false
|
|
14
|
+
openRouterOnlyToggle.setAccessibilityLabel("OpenRouter Only")
|
|
15
|
+
openRouterOnlyToggle.setAccessibilityIdentifier("sks-provider-openrouter-only-toggle")
|
|
16
|
+
openRouterOnlyStatus.setAccessibilityIdentifier("sks-provider-openrouter-only-status")
|
|
17
|
+
openRouterOnlyDetail.setAccessibilityIdentifier("sks-provider-openrouter-only-detail")
|
|
18
|
+
openRouterOnlyDetail.isHidden = true
|
|
19
|
+
return [
|
|
20
|
+
NativeView.row([openRouterOnlyToggle, NativeView.sectionTitle("OpenRouter Only")]),
|
|
21
|
+
openRouterOnlyStatus, openRouterOnlyDetail,
|
|
22
|
+
NativeView.detail("Run Codex and every subagent on OpenRouter models. Subagents may use only the models on the Subagent Models page. Turning this on turns Prefer Codex-LB off; turning Prefer Codex-LB on turns this off.")
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Both switches stay locked while either mode changes or a page-wide operation runs.
|
|
27
|
+
func updateRoutingModeSwitches() {
|
|
28
|
+
authPriorityToggle.isEnabled = !busy && !routingModeBusy && authPriorityEnabled != nil
|
|
29
|
+
openRouterOnlyToggle.isEnabled = !busy && !routingModeBusy && openRouterOnlyEnabled != nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Reads that started before a mode change must not render over it.
|
|
33
|
+
func beginRoutingModeMutation() {
|
|
34
|
+
authPriorityGeneration += 1
|
|
35
|
+
openRouterOnlyGeneration += 1
|
|
36
|
+
updateRoutingModeSwitches()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// The two modes exclude each other, so a change to either re-reads both,
|
|
40
|
+
/// and the catalog, routes and picker exposure it rebuilt.
|
|
41
|
+
func rereadRoutingModes(authPriorityNotice: String?, openRouterOnlyNotice: String?) {
|
|
42
|
+
refreshAuthPriority(notice: authPriorityNotice)
|
|
43
|
+
refreshOpenRouterOnly(notice: openRouterOnlyNotice)
|
|
44
|
+
refreshBridgeStatus()
|
|
45
|
+
refreshModelExposure()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Either mode's answer also carries the other mode. Showing it at once
|
|
49
|
+
/// keeps the other switch from offering its pre-change state while the
|
|
50
|
+
/// re-reads run; without it that switch stays locked until they land.
|
|
51
|
+
func renderAuthPriority(from payload: [String: Any]?, responseComplete: Bool) {
|
|
52
|
+
if responseComplete, let payload = payload, let state = AuthPriorityState.decode(payload) {
|
|
53
|
+
renderAuthPriority(state)
|
|
54
|
+
} else {
|
|
55
|
+
authPriorityEnabled = nil
|
|
56
|
+
updateRoutingModeSwitches()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func renderOpenRouterOnly(from payload: [String: Any]?, responseComplete: Bool) {
|
|
61
|
+
if responseComplete, let payload = payload, let state = OpenRouterOnlyState.decode(payload) {
|
|
62
|
+
renderOpenRouterOnly(state)
|
|
63
|
+
} else {
|
|
64
|
+
openRouterOnlyEnabled = nil
|
|
65
|
+
updateRoutingModeSwitches()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func refreshOpenRouterOnly(notice: String? = nil) {
|
|
70
|
+
guard !routingModeBusy else { return }
|
|
71
|
+
openRouterOnlyGeneration += 1
|
|
72
|
+
let generation = openRouterOnlyGeneration
|
|
73
|
+
processClient.run(OpenRouterOnlyCommand.status, timeout: NativeView.statusTimeout) { [weak self] result in
|
|
74
|
+
guard let self = self, !self.routingModeBusy, generation == self.openRouterOnlyGeneration else { return }
|
|
75
|
+
let payload = OpenRouterOnlyJSON.object(from: result.output)
|
|
76
|
+
guard !result.timedOut, !result.truncated, let payload = payload, let state = OpenRouterOnlyState.decode(payload) else {
|
|
77
|
+
self.openRouterOnlyEnabled = nil
|
|
78
|
+
self.openRouterOnlyToggle.isHidden = true
|
|
79
|
+
self.openRouterOnlyDetail.isHidden = true
|
|
80
|
+
let reason = OpenRouterOnlyJSON.unavailableReason(code: result.code, output: result.output, payload: payload)
|
|
81
|
+
self.openRouterOnlyStatus.stringValue = "OpenRouter Only could not be confirmed · \(ProviderSecretRedactor.redact(reason))"
|
|
82
|
+
self.openRouterOnlyStatus.textColor = .systemOrange
|
|
83
|
+
self.updateRoutingModeSwitches()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
self.renderOpenRouterOnly(state)
|
|
87
|
+
if let notice = notice {
|
|
88
|
+
self.openRouterOnlyStatus.stringValue += "\n" + notice
|
|
89
|
+
self.openRouterOnlyStatus.textColor = .systemOrange
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func renderOpenRouterOnly(_ state: OpenRouterOnlyState) {
|
|
95
|
+
openRouterOnlyEnabled = state.enabled
|
|
96
|
+
openRouterOnlyToggle.isHidden = false
|
|
97
|
+
openRouterOnlyToggle.state = state.enabled ? .on : .off
|
|
98
|
+
openRouterOnlyStatus.stringValue = ProviderSecretRedactor.redact(state.message)
|
|
99
|
+
openRouterOnlyStatus.textColor = state.state == "active" ? .systemGreen : state.state == "unavailable" ? .systemOrange : .secondaryLabelColor
|
|
100
|
+
var lines = [state.modelSummary].compactMap { $0 }
|
|
101
|
+
if !state.unroutableModels.isEmpty {
|
|
102
|
+
lines.append("Not routable yet: \(state.unroutableModels.joined(separator: ", ")). Refresh the combined catalog, or apply the list again on the Subagent Models page.")
|
|
103
|
+
}
|
|
104
|
+
lines += state.warningMessages
|
|
105
|
+
openRouterOnlyDetail.stringValue = ProviderSecretRedactor.redact(lines.joined(separator: "\n"))
|
|
106
|
+
openRouterOnlyDetail.isHidden = lines.isEmpty
|
|
107
|
+
updateRoutingModeSwitches()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@objc func toggleOpenRouterOnly() {
|
|
111
|
+
guard !routingModeBusy, let previous = openRouterOnlyEnabled else {
|
|
112
|
+
openRouterOnlyToggle.state = openRouterOnlyEnabled == true ? .on : .off
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
let desired = openRouterOnlyToggle.state == .on
|
|
116
|
+
let summary = desired ? "Turn OpenRouter Only on" : "Turn OpenRouter Only off"
|
|
117
|
+
guard let operation = operations.begin(kind: "bridge-openrouter-only", mutationGroup: "codex-config", summary: summary) else {
|
|
118
|
+
openRouterOnlyToggle.state = previous ? .on : .off
|
|
119
|
+
openRouterOnlyStatus.stringValue = "Another configuration change is running. Try again when it finishes."
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
openRouterOnlyBusy = true
|
|
123
|
+
beginRoutingModeMutation()
|
|
124
|
+
openRouterOnlyStatus.stringValue = desired
|
|
125
|
+
? "Turning OpenRouter Only on · updating the Codex model list and bridge routes…"
|
|
126
|
+
: "Turning OpenRouter Only off · restoring the Codex model list…"
|
|
127
|
+
openRouterOnlyStatus.textColor = .secondaryLabelColor
|
|
128
|
+
_ = operations.update(operation, state: .running, stage: "saving", progress: nil, summary: summary)
|
|
129
|
+
processClient.run(OpenRouterOnlyCommand.set(enabled: desired), timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
130
|
+
guard let self = self else { return }
|
|
131
|
+
self.openRouterOnlyBusy = false
|
|
132
|
+
let payload = OpenRouterOnlyJSON.object(from: result.output)
|
|
133
|
+
let complete = !result.truncated && !result.timedOut
|
|
134
|
+
let outcome = OpenRouterOnlyMutationOutcome.resolve(
|
|
135
|
+
payload: payload, desired: desired,
|
|
136
|
+
commandSucceeded: result.code == 0 && payload?["ok"] as? Bool == true
|
|
137
|
+
&& OpenRouterOnlyReceipt.decode(payload).blockers.isEmpty,
|
|
138
|
+
responseComplete: complete
|
|
139
|
+
)
|
|
140
|
+
// Turning this mode on turned Prefer Codex-LB off in the same operation.
|
|
141
|
+
self.renderAuthPriority(from: payload, responseComplete: complete)
|
|
142
|
+
let issue = OpenRouterOnlyReceipt.decode(payload).primaryIssue.map(ProviderSecretRedactor.redact)
|
|
143
|
+
let detail = outcome.succeeded ? nil : [outcome.operationSummary, issue].compactMap { $0 }.joined(separator: " · ")
|
|
144
|
+
let operationState: OperationState = outcome.succeeded ? .succeeded : outcome == .unconfirmed ? .terminalUncertain : .failed
|
|
145
|
+
_ = self.operations.update(operation, state: operationState, stage: "complete", progress: 1, summary: detail ?? outcome.operationSummary)
|
|
146
|
+
if let state = outcome.observedState {
|
|
147
|
+
self.renderOpenRouterOnly(state)
|
|
148
|
+
if let detail = detail {
|
|
149
|
+
self.openRouterOnlyStatus.stringValue = detail + "\n" + state.message
|
|
150
|
+
self.openRouterOnlyStatus.textColor = .systemOrange
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
self.openRouterOnlyEnabled = nil
|
|
154
|
+
self.openRouterOnlyToggle.isHidden = true
|
|
155
|
+
self.openRouterOnlyStatus.stringValue = "Operation result is uncertain. Checking the saved setting…"
|
|
156
|
+
self.openRouterOnlyStatus.textColor = .systemOrange
|
|
157
|
+
self.updateRoutingModeSwitches()
|
|
158
|
+
}
|
|
159
|
+
self.rereadRoutingModes(authPriorityNotice: nil, openRouterOnlyNotice: detail)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|