Skip to content

Commit cd4ff81

Browse files
steipetePeter Steinberger
andauthored
refactor: namespace provider-specific config fields into provider folders (#2661)
* test: characterize provider config JSON bytes * refactor: namespace provider config fields --------- Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
1 parent 8399c3c commit cd4ff81

22 files changed

Lines changed: 508 additions & 159 deletions

Sources/CodexBarCore/Config/CodexBarConfig.swift

Lines changed: 8 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ public struct CodexBarConfig: Codable, Sendable {
5454

5555
/// User plugins exist only where JavaScriptCore does; other platforms drop their config entries.
5656
private static func isKnownProviderInstance(_ instanceID: ProviderInstanceID) -> Bool {
57-
if instanceID.firstPartyProvider != nil { return true }
57+
if instanceID.firstPartyProvider != nil {
58+
return true
59+
}
5860
#if canImport(JavaScriptCore)
5961
return UserProviderPluginRegistry.plugin(for: instanceID) != nil
6062
#else
@@ -107,15 +109,8 @@ public struct CodexBarConfig: Codable, Sendable {
107109
for var provider in self.providers {
108110
guard !seen.contains(provider.id) else { continue }
109111
seen.insert(provider.id)
110-
if provider.id.firstPartyProvider == .deepseek {
111-
provider.deepseekProfileID = provider.sanitizedDeepSeekProfileID
112-
provider.deepseekProfileScope = provider.sanitizedDeepSeekProfileScope
113-
}
114-
if provider.id.firstPartyProvider == .moonshot,
115-
provider.sanitizedAPIKey != nil,
116-
provider.sanitizedAPIKeyRegion == nil
117-
{
118-
provider.apiKeyRegion = provider.sanitizedRegion ?? MoonshotRegion.international.rawValue
112+
if let firstPartyProvider = provider.id.firstPartyProvider {
113+
ProviderDescriptorRegistry.descriptor(for: firstPartyProvider).normalizeConfig(&provider)
119114
}
120115
normalized.append(provider)
121116
}
@@ -191,24 +186,11 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
191186
public var workspaceID: String?
192187
public var enterpriseHost: String?
193188
public var tokenAccounts: ProviderTokenAccountData?
194-
public var claudeSwapEnabled: Bool?
195-
public var claudeSwapShowSingleAccount: Bool?
196-
public var claudeSwapExecutablePath: String?
197-
public var codexActiveSource: CodexActiveSource?
198-
public var codexProfileHomePaths: [String]?
199-
public var antigravityPrioritizeExhaustedQuotas: Bool?
200189
public var quotaWarnings: QuotaWarningConfig?
201-
public var kiloKnownOrganizations: [KiloOrganization]?
202-
public var kiloEnabledOrganizationIDs: [String]?
203-
public var awsProfile: String?
204-
public var awsAuthMode: String?
205-
public var deepseekProfileID: String?
206-
public var deepseekProfileScope: String?
207-
/// Region that owns `apiKey`. Region-routed providers use this to keep credentials host-scoped.
208-
public var apiKeyRegion: String?
209190
/// Arbitrary user-plugin values stay scoped to the provider instance. Secure values are redacted from config dumps.
210191
public var pluginSettings: [String: String]?
211192
public var pluginSecrets: [String: String]?
193+
var extensionValues: [String: ProviderConfigExtensionValue]
212194

213195
public init(
214196
id: ProviderInstanceID,
@@ -223,20 +205,7 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
223205
workspaceID: String? = nil,
224206
enterpriseHost: String? = nil,
225207
tokenAccounts: ProviderTokenAccountData? = nil,
226-
claudeSwapEnabled: Bool? = nil,
227-
claudeSwapShowSingleAccount: Bool? = nil,
228-
claudeSwapExecutablePath: String? = nil,
229-
codexActiveSource: CodexActiveSource? = nil,
230-
codexProfileHomePaths: [String]? = nil,
231-
antigravityPrioritizeExhaustedQuotas: Bool? = nil,
232208
quotaWarnings: QuotaWarningConfig? = nil,
233-
kiloKnownOrganizations: [KiloOrganization]? = nil,
234-
kiloEnabledOrganizationIDs: [String]? = nil,
235-
awsProfile: String? = nil,
236-
awsAuthMode: String? = nil,
237-
deepseekProfileID: String? = nil,
238-
deepseekProfileScope: String? = nil,
239-
apiKeyRegion: String? = nil,
240209
pluginSettings: [String: String]? = nil,
241210
pluginSecrets: [String: String]? = nil)
242211
{
@@ -252,22 +221,10 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
252221
self.workspaceID = workspaceID
253222
self.enterpriseHost = enterpriseHost
254223
self.tokenAccounts = tokenAccounts
255-
self.claudeSwapEnabled = claudeSwapEnabled
256-
self.claudeSwapShowSingleAccount = claudeSwapShowSingleAccount
257-
self.claudeSwapExecutablePath = claudeSwapExecutablePath
258-
self.codexActiveSource = codexActiveSource
259-
self.codexProfileHomePaths = codexProfileHomePaths
260-
self.antigravityPrioritizeExhaustedQuotas = antigravityPrioritizeExhaustedQuotas
261224
self.quotaWarnings = quotaWarnings
262-
self.kiloKnownOrganizations = kiloKnownOrganizations
263-
self.kiloEnabledOrganizationIDs = kiloEnabledOrganizationIDs
264-
self.awsProfile = awsProfile
265-
self.awsAuthMode = awsAuthMode
266-
self.deepseekProfileID = deepseekProfileID
267-
self.deepseekProfileScope = deepseekProfileScope
268-
self.apiKeyRegion = apiKeyRegion
269225
self.pluginSettings = pluginSettings
270226
self.pluginSecrets = pluginSecrets
227+
self.extensionValues = [:]
271228
}
272229

273230
public var sanitizedAPIKey: String? {
@@ -286,10 +243,6 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
286243
Self.clean(self.region)
287244
}
288245

289-
public var sanitizedAPIKeyRegion: String? {
290-
Self.clean(self.apiKeyRegion)
291-
}
292-
293246
public var sanitizedWorkspaceID: String? {
294247
Self.clean(self.workspaceID)
295248
}
@@ -298,26 +251,6 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
298251
Self.clean(self.enterpriseHost)
299252
}
300253

301-
public var sanitizedClaudeSwapExecutablePath: String? {
302-
Self.clean(self.claudeSwapExecutablePath)
303-
}
304-
305-
public var sanitizedAWSProfile: String? {
306-
Self.clean(self.awsProfile)
307-
}
308-
309-
public var sanitizedAWSAuthMode: String? {
310-
Self.clean(self.awsAuthMode)
311-
}
312-
313-
public var sanitizedDeepSeekProfileID: String? {
314-
Self.clean(self.deepseekProfileID).map(DeepSeekSettingsReader.canonicalProfileID)
315-
}
316-
317-
public var sanitizedDeepSeekProfileScope: String? {
318-
Self.clean(self.deepseekProfileScope)
319-
}
320-
321254
public func sanitizedForDump() -> ProviderConfig {
322255
var copy = self
323256
if copy.apiKey != nil {
@@ -338,7 +271,7 @@ public struct ProviderConfig: Codable, Sendable, Identifiable {
338271
return copy
339272
}
340273

341-
private static func clean(_ raw: String?) -> String? {
274+
static func clean(_ raw: String?) -> String? {
342275
guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
343276
return nil
344277
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
private enum CodingKeys: String, CodingKey, CaseIterable {
5+
case id
6+
case enabled
7+
case source
8+
case extrasEnabled
9+
case apiKey
10+
case secretKey
11+
case cookieHeader
12+
case cookieSource
13+
case region
14+
case workspaceID
15+
case enterpriseHost
16+
case tokenAccounts
17+
case quotaWarnings
18+
case pluginSettings
19+
case pluginSecrets
20+
}
21+
22+
public init(from decoder: any Decoder) throws {
23+
let container = try decoder.container(keyedBy: ProviderConfigCodingKey.self)
24+
self.id = try container.decode(ProviderInstanceID.self, forKey: .init(CodingKeys.id.rawValue))
25+
self.enabled = try container.decodeIfPresent(Bool.self, forKey: .init(CodingKeys.enabled.rawValue))
26+
self.source = try container.decodeIfPresent(ProviderSourceMode.self, forKey: .init(CodingKeys.source.rawValue))
27+
self.extrasEnabled = try container.decodeIfPresent(Bool.self, forKey: .init(CodingKeys.extrasEnabled.rawValue))
28+
self.apiKey = try container.decodeIfPresent(String.self, forKey: .init(CodingKeys.apiKey.rawValue))
29+
self.secretKey = try container.decodeIfPresent(String.self, forKey: .init(CodingKeys.secretKey.rawValue))
30+
self.cookieHeader = try container.decodeIfPresent(String.self, forKey: .init(CodingKeys.cookieHeader.rawValue))
31+
self.cookieSource = try container.decodeIfPresent(
32+
ProviderCookieSource.self,
33+
forKey: .init(CodingKeys.cookieSource.rawValue))
34+
self.region = try container.decodeIfPresent(String.self, forKey: .init(CodingKeys.region.rawValue))
35+
self.workspaceID = try container.decodeIfPresent(String.self, forKey: .init(CodingKeys.workspaceID.rawValue))
36+
self.enterpriseHost = try container.decodeIfPresent(
37+
String.self,
38+
forKey: .init(CodingKeys.enterpriseHost.rawValue))
39+
self.tokenAccounts = try container.decodeIfPresent(
40+
ProviderTokenAccountData.self,
41+
forKey: .init(CodingKeys.tokenAccounts.rawValue))
42+
self.quotaWarnings = try container.decodeIfPresent(
43+
QuotaWarningConfig.self,
44+
forKey: .init(CodingKeys.quotaWarnings.rawValue))
45+
self.pluginSettings = try container.decodeIfPresent(
46+
[String: String].self,
47+
forKey: .init(CodingKeys.pluginSettings.rawValue))
48+
self.pluginSecrets = try container.decodeIfPresent(
49+
[String: String].self,
50+
forKey: .init(CodingKeys.pluginSecrets.rawValue))
51+
52+
let genericKeys = Set(CodingKeys.allCases.map(\.rawValue))
53+
self.extensionValues = try container.allKeys.reduce(into: [:]) { values, key in
54+
guard !genericKeys.contains(key.stringValue), try !container.decodeNil(forKey: key) else { return }
55+
values[key.stringValue] = try container.decode(ProviderConfigExtensionValue.self, forKey: key)
56+
}
57+
}
58+
59+
public func encode(to encoder: any Encoder) throws {
60+
var container = encoder.container(keyedBy: ProviderConfigCodingKey.self)
61+
try container.encode(self.id, forKey: .init(CodingKeys.id.rawValue))
62+
try container.encodeIfPresent(self.enabled, forKey: .init(CodingKeys.enabled.rawValue))
63+
try container.encodeIfPresent(self.source, forKey: .init(CodingKeys.source.rawValue))
64+
try container.encodeIfPresent(self.extrasEnabled, forKey: .init(CodingKeys.extrasEnabled.rawValue))
65+
try container.encodeIfPresent(self.apiKey, forKey: .init(CodingKeys.apiKey.rawValue))
66+
try container.encodeIfPresent(self.secretKey, forKey: .init(CodingKeys.secretKey.rawValue))
67+
try container.encodeIfPresent(self.cookieHeader, forKey: .init(CodingKeys.cookieHeader.rawValue))
68+
try container.encodeIfPresent(self.cookieSource, forKey: .init(CodingKeys.cookieSource.rawValue))
69+
try container.encodeIfPresent(self.region, forKey: .init(CodingKeys.region.rawValue))
70+
try container.encodeIfPresent(self.workspaceID, forKey: .init(CodingKeys.workspaceID.rawValue))
71+
try container.encodeIfPresent(self.enterpriseHost, forKey: .init(CodingKeys.enterpriseHost.rawValue))
72+
try container.encodeIfPresent(self.tokenAccounts, forKey: .init(CodingKeys.tokenAccounts.rawValue))
73+
try container.encodeIfPresent(self.quotaWarnings, forKey: .init(CodingKeys.quotaWarnings.rawValue))
74+
try container.encodeIfPresent(self.pluginSettings, forKey: .init(CodingKeys.pluginSettings.rawValue))
75+
try container.encodeIfPresent(self.pluginSecrets, forKey: .init(CodingKeys.pluginSecrets.rawValue))
76+
for (key, value) in self.extensionValues {
77+
try container.encode(value, forKey: .init(key))
78+
}
79+
}
80+
81+
func extensionValue<Value: Codable>(_ type: Value.Type = Value.self, forKey key: String) -> Value? {
82+
guard let value = self.extensionValues[key],
83+
let data = try? JSONEncoder().encode(value)
84+
else { return nil }
85+
return try? JSONDecoder().decode(type, from: data)
86+
}
87+
88+
mutating func setExtensionValue(_ value: (some Codable)?, forKey key: String) {
89+
precondition(
90+
!CodingKeys.allCases.map(\.rawValue).contains(key),
91+
"Provider extension key collides with a generic key")
92+
guard let value else {
93+
self.extensionValues[key] = nil
94+
return
95+
}
96+
guard let data = try? JSONEncoder().encode(value),
97+
let encoded = try? JSONDecoder().decode(ProviderConfigExtensionValue.self, from: data)
98+
else {
99+
assertionFailure("Provider config extension value must be JSON encodable")
100+
return
101+
}
102+
self.extensionValues[key] = encoded
103+
}
104+
}
105+
106+
private struct ProviderConfigCodingKey: CodingKey {
107+
let stringValue: String
108+
let intValue: Int? = nil
109+
110+
init(_ stringValue: String) {
111+
self.stringValue = stringValue
112+
}
113+
114+
init?(stringValue: String) {
115+
self.init(stringValue)
116+
}
117+
118+
init?(intValue: Int) {
119+
nil
120+
}
121+
}
122+
123+
enum ProviderConfigExtensionValue: Codable, Sendable {
124+
case bool(Bool)
125+
case integer(Int64)
126+
case number(Double)
127+
case string(String)
128+
case array([Self])
129+
case object([String: Self])
130+
131+
init(from decoder: any Decoder) throws {
132+
let container = try decoder.singleValueContainer()
133+
if let value = try? container.decode(Bool.self) {
134+
self = .bool(value)
135+
} else if let value = try? container.decode(Int64.self) {
136+
self = .integer(value)
137+
} else if let value = try? container.decode(Double.self) {
138+
self = .number(value)
139+
} else if let value = try? container.decode(String.self) {
140+
self = .string(value)
141+
} else if let value = try? container.decode([Self].self) {
142+
self = .array(value)
143+
} else {
144+
self = try .object(container.decode([String: Self].self))
145+
}
146+
}
147+
148+
func encode(to encoder: any Encoder) throws {
149+
var container = encoder.singleValueContainer()
150+
switch self {
151+
case let .bool(value): try container.encode(value)
152+
case let .integer(value): try container.encode(value)
153+
case let .number(value): try container.encode(value)
154+
case let .string(value): try container.encode(value)
155+
case let .array(value): try container.encode(value)
156+
case let .object(value): try container.encode(value)
157+
}
158+
}
159+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
public var antigravityPrioritizeExhaustedQuotas: Bool? {
5+
get { self.extensionValue(forKey: "antigravityPrioritizeExhaustedQuotas") }
6+
set { self.setExtensionValue(newValue, forKey: "antigravityPrioritizeExhaustedQuotas") }
7+
}
8+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
public var awsProfile: String? {
5+
get { self.extensionValue(forKey: "awsProfile") }
6+
set { self.setExtensionValue(newValue, forKey: "awsProfile") }
7+
}
8+
9+
public var awsAuthMode: String? {
10+
get { self.extensionValue(forKey: "awsAuthMode") }
11+
set { self.setExtensionValue(newValue, forKey: "awsAuthMode") }
12+
}
13+
14+
public var sanitizedAWSProfile: String? {
15+
Self.clean(self.awsProfile)
16+
}
17+
18+
public var sanitizedAWSAuthMode: String? {
19+
Self.clean(self.awsAuthMode)
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
public var claudeSwapEnabled: Bool? {
5+
get { self.extensionValue(forKey: "claudeSwapEnabled") }
6+
set { self.setExtensionValue(newValue, forKey: "claudeSwapEnabled") }
7+
}
8+
9+
public var claudeSwapShowSingleAccount: Bool? {
10+
get { self.extensionValue(forKey: "claudeSwapShowSingleAccount") }
11+
set { self.setExtensionValue(newValue, forKey: "claudeSwapShowSingleAccount") }
12+
}
13+
14+
public var claudeSwapExecutablePath: String? {
15+
get { self.extensionValue(forKey: "claudeSwapExecutablePath") }
16+
set { self.setExtensionValue(newValue, forKey: "claudeSwapExecutablePath") }
17+
}
18+
19+
public var sanitizedClaudeSwapExecutablePath: String? {
20+
Self.clean(self.claudeSwapExecutablePath)
21+
}
22+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
public var codexActiveSource: CodexActiveSource? {
5+
get { self.extensionValue(forKey: "codexActiveSource") }
6+
set { self.setExtensionValue(newValue, forKey: "codexActiveSource") }
7+
}
8+
9+
public var codexProfileHomePaths: [String]? {
10+
get { self.extensionValue(forKey: "codexProfileHomePaths") }
11+
set { self.setExtensionValue(newValue, forKey: "codexProfileHomePaths") }
12+
}
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
extension ProviderConfig {
4+
public var deepseekProfileID: String? {
5+
get { self.extensionValue(forKey: "deepseekProfileID") }
6+
set { self.setExtensionValue(newValue, forKey: "deepseekProfileID") }
7+
}
8+
9+
public var deepseekProfileScope: String? {
10+
get { self.extensionValue(forKey: "deepseekProfileScope") }
11+
set { self.setExtensionValue(newValue, forKey: "deepseekProfileScope") }
12+
}
13+
14+
public var sanitizedDeepSeekProfileID: String? {
15+
Self.clean(self.deepseekProfileID).map(DeepSeekSettingsReader.canonicalProfileID)
16+
}
17+
18+
public var sanitizedDeepSeekProfileScope: String? {
19+
Self.clean(self.deepseekProfileScope)
20+
}
21+
}

0 commit comments

Comments
 (0)