Skip to content

Commit 6fd577c

Browse files
committed
Fix iOS provider reasoning effort defaults
1 parent 9493062 commit 6fd577c

5 files changed

Lines changed: 219 additions & 12 deletions

File tree

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileConversationViews.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,8 @@ private struct GaryxThreadRuntimeSettingsSheet: View {
947947
private var effortFilterModel: String? {
948948
GaryxThreadModelOverridePresentation.effortFilterModel(
949949
override: modelOverride,
950-
agentConfiguredModel: effectiveModel
950+
agentConfiguredModel: effectiveModel,
951+
providerModels: providerModels
951952
)
952953
}
953954

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel+AgentsWorkspaces.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,8 @@ extension GaryxMobileModel {
474474
var newThreadEffortFilterModel: String? {
475475
GaryxThreadModelOverridePresentation.effortFilterModel(
476476
override: newThreadModelOverride,
477-
agentConfiguredModel: newThreadAgentTarget?.model
477+
agentConfiguredModel: newThreadAgentTarget?.model,
478+
providerModels: newThreadProviderModels
478479
)
479480
}
480481

mobile/garyx-mobile/Sources/GaryxMobileCore/GaryxGatewayAgentModels.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ public struct GaryxProviderModels: Decodable, Equatable, Sendable {
280280
public var supportsServiceTierSelection: Bool
281281
public var serviceTiers: [GaryxProviderModelOption]
282282
public var defaultModel: String?
283+
public var defaultReasoningEffort: String?
283284
public var source: String
284285
public var error: String?
285286

@@ -299,6 +300,8 @@ public struct GaryxProviderModels: Decodable, Equatable, Sendable {
299300
case serviceTiersSnake = "service_tiers"
300301
case defaultModel
301302
case defaultModelSnake = "default_model"
303+
case defaultReasoningEffort
304+
case defaultReasoningEffortSnake = "default_reasoning_effort"
302305
case source
303306
case error
304307
}
@@ -327,6 +330,10 @@ public struct GaryxProviderModels: Decodable, Equatable, Sendable {
327330
?? container.decodeIfPresent([GaryxProviderModelOption].self, forKey: .serviceTiersSnake)
328331
?? []
329332
defaultModel = try container.garyxDecodeFirstString(.defaultModel, .defaultModelSnake)
333+
defaultReasoningEffort = try container.garyxDecodeFirstString(
334+
.defaultReasoningEffort,
335+
.defaultReasoningEffortSnake
336+
)
330337
source = try container.garyxDecodeFirstString(.source) ?? ""
331338
error = try container.garyxDecodeFirstString(.error)
332339
}

mobile/garyx-mobile/Sources/GaryxMobileCore/GaryxThreadModelOverridePresentation.swift

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ public enum GaryxThreadModelOverridePresentation {
1010
}
1111

1212
/// The model that will actually run and should filter thinking levels:
13-
/// the per-thread override when chosen, else the agent's configured model.
13+
/// the per-thread override when chosen, else the agent's configured model,
14+
/// else the provider's default model.
1415
public static func effortFilterModel(
1516
override modelOverride: String?,
16-
agentConfiguredModel: String?
17+
agentConfiguredModel: String?,
18+
providerModels: GaryxProviderModels? = nil
1719
) -> String? {
18-
normalized(modelOverride) ?? normalized(agentConfiguredModel)
20+
normalized(modelOverride)
21+
?? normalized(agentConfiguredModel)
22+
?? normalized(providerModels?.defaultModel)
1923
}
2024

2125
/// Thinking levels valid for the current selection: the chosen model's own
@@ -27,7 +31,7 @@ public enum GaryxThreadModelOverridePresentation {
2731
guard let providerModels, providerModels.supportsReasoningEffortSelection else {
2832
return []
2933
}
30-
if let model = normalized(model),
34+
if let model = effortScopedModel(providerModels: providerModels, model: model),
3135
let modelOption = providerModels.models.first(where: { $0.id == model }),
3236
!modelOption.supportedReasoningEfforts.isEmpty {
3337
return modelOption.supportedReasoningEfforts
@@ -41,15 +45,23 @@ public enum GaryxThreadModelOverridePresentation {
4145
providerModels: GaryxProviderModels?,
4246
model: String?
4347
) -> String? {
44-
guard let model = normalized(model) else {
48+
let explicitModel = normalized(model)
49+
if let configuredDefault = supportedConfiguredDefaultReasoningEffort(
50+
providerModels: providerModels,
51+
model: model
52+
) {
53+
return configuredDefault
54+
}
55+
guard let model = explicitModel else {
4556
return nil
4657
}
4758
if let modelOption = providerModels?.models.first(where: { $0.id == model }),
4859
let effort = normalized(modelOption.defaultReasoningEffort) {
4960
return effort
5061
}
51-
return providerModels?.reasoningEfforts.first(where: { $0.recommended }).flatMap { normalized($0.id) }
52-
?? providerModels?.reasoningEfforts.first.flatMap { normalized($0.id) }
62+
let options = reasoningEffortOptions(providerModels: providerModels, model: model)
63+
return options.first(where: { $0.recommended }).flatMap { normalized($0.id) }
64+
?? options.first.flatMap { normalized($0.id) }
5365
}
5466

5567
/// The option id a model / thinking-level picker should mark as selected,
@@ -155,6 +167,18 @@ public enum GaryxThreadModelOverridePresentation {
155167
reasoningEffort: String?,
156168
fallback: String
157169
) -> String {
170+
if normalized(model) == nil,
171+
normalized(reasoningEffort) == nil,
172+
let defaultModel = normalized(providerModels?.defaultModel),
173+
let defaultEffort = supportedConfiguredDefaultReasoningEffort(providerModels: providerModels, model: nil) {
174+
let defaultEffortLabel = reasoningEffortLabel(
175+
providerModels: providerModels,
176+
model: defaultModel,
177+
reasoningEffort: defaultEffort
178+
) ?? defaultEffort
179+
let defaultModelLabel = modelLabel(providerModels: providerModels, model: defaultModel) ?? defaultModel
180+
return "\(defaultModelLabel) · \(defaultEffortLabel)"
181+
}
158182
let modelLabel = modelLabel(providerModels: providerModels, model: model)
159183
let effortLabel = reasoningEffortLabel(
160184
providerModels: providerModels,
@@ -179,4 +203,22 @@ public enum GaryxThreadModelOverridePresentation {
179203
}
180204
return value
181205
}
206+
207+
private static func effortScopedModel(
208+
providerModels: GaryxProviderModels?,
209+
model: String?
210+
) -> String? {
211+
normalized(model) ?? normalized(providerModels?.defaultModel)
212+
}
213+
214+
private static func supportedConfiguredDefaultReasoningEffort(
215+
providerModels: GaryxProviderModels?,
216+
model: String?
217+
) -> String? {
218+
guard let configuredDefault = normalized(providerModels?.defaultReasoningEffort) else {
219+
return nil
220+
}
221+
let options = reasoningEffortOptions(providerModels: providerModels, model: model)
222+
return options.contains(where: { $0.id == configuredDefault }) ? configuredDefault : nil
223+
}
182224
}

mobile/garyx-mobile/Tests/GaryxMobileCoreTests/GaryxThreadModelOverridePresentationTests.swift

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ final class GaryxThreadModelOverridePresentationTests: XCTestCase {
1616
)
1717
}
1818

19+
func testProviderLevelDefaultReasoningEffortDecodesSnakeAndCamelKeys() throws {
20+
XCTAssertEqual(
21+
try decodeProviderModels(#"{ "default_reasoning_effort": "max" }"#).defaultReasoningEffort,
22+
"max"
23+
)
24+
XCTAssertEqual(
25+
try decodeProviderModels(#"{ "defaultReasoningEffort": "high" }"#).defaultReasoningEffort,
26+
"high"
27+
)
28+
}
29+
1930
func testReasoningEffortOptionsFollowSelectedModel() throws {
2031
let providerModels = try decodeProviderModels(claudeProviderJSON)
2132

@@ -38,6 +49,76 @@ final class GaryxThreadModelOverridePresentationTests: XCTestCase {
3849
XCTAssertEqual(unknownModelOptions.map(\.id), ["low", "high"])
3950
}
4051

52+
func testDefaultStateUsesProviderDefaultModelAndConfiguredReasoningEffort() throws {
53+
let providerModels = try decodeProviderModels(configuredClaudeProviderJSON)
54+
55+
let defaultOptions = GaryxThreadModelOverridePresentation.reasoningEffortOptions(
56+
providerModels: providerModels,
57+
model: nil
58+
)
59+
XCTAssertEqual(defaultOptions.map(\.id), ["low", "high", "max"])
60+
61+
XCTAssertEqual(
62+
GaryxThreadModelOverridePresentation.defaultReasoningEffort(
63+
providerModels: providerModels,
64+
model: nil
65+
),
66+
"max"
67+
)
68+
69+
XCTAssertEqual(
70+
GaryxThreadModelOverridePresentation.controlLabel(
71+
providerModels: providerModels,
72+
model: nil,
73+
reasoningEffort: nil,
74+
fallback: "Model"
75+
),
76+
"Claude Opus 4.8 · Max"
77+
)
78+
}
79+
80+
func testConfiguredProviderDefaultReasoningEffortMustBeSupportedByCurrentModel() throws {
81+
let providerModels = try decodeProviderModels(configuredClaudeProviderJSON)
82+
83+
let sonnetOptions = GaryxThreadModelOverridePresentation.reasoningEffortOptions(
84+
providerModels: providerModels,
85+
model: "claude-sonnet-4-6"
86+
)
87+
XCTAssertEqual(sonnetOptions.map(\.id), ["low", "high"])
88+
89+
XCTAssertEqual(
90+
GaryxThreadModelOverridePresentation.defaultReasoningEffort(
91+
providerModels: providerModels,
92+
model: "claude-sonnet-4-6"
93+
),
94+
"high"
95+
)
96+
}
97+
98+
func testTraexPerModelReasoningEffortsRenderThroughGatewayShape() throws {
99+
let providerModels = try decodeProviderModels(traexProviderJSON)
100+
101+
let reasonerOptions = GaryxThreadModelOverridePresentation.reasoningEffortOptions(
102+
providerModels: providerModels,
103+
model: "traex-reasoner"
104+
)
105+
XCTAssertEqual(reasonerOptions.map(\.id), ["medium", "max"])
106+
XCTAssertEqual(reasonerOptions.map(\.label), ["Medium", "max"])
107+
XCTAssertTrue(
108+
GaryxThreadModelOverridePresentation.reasoningEffortOptions(
109+
providerModels: providerModels,
110+
model: "traex-fast"
111+
).isEmpty
112+
)
113+
XCTAssertEqual(
114+
GaryxThreadModelOverridePresentation.modelLabel(
115+
providerModels: providerModels,
116+
model: "traex-reasoner"
117+
),
118+
"traex-reasoner"
119+
)
120+
}
121+
41122
func testReasoningEffortOptionsEmptyWhenSelectionUnsupported() throws {
42123
let providerModels = try decodeProviderModels(geminiProviderJSON)
43124
XCTAssertTrue(
@@ -158,21 +239,33 @@ final class GaryxThreadModelOverridePresentationTests: XCTestCase {
158239
)
159240
}
160241

161-
func testEffortFilterModelPrefersOverrideThenAgentModel() {
242+
func testEffortFilterModelPrefersOverrideThenAgentModel() throws {
243+
let providerModels = try decodeProviderModels(configuredClaudeProviderJSON)
244+
162245
XCTAssertEqual(
163246
GaryxThreadModelOverridePresentation.effortFilterModel(
164247
override: "claude-opus-4-8",
165-
agentConfiguredModel: "claude-haiku-4-5"
248+
agentConfiguredModel: "claude-haiku-4-5",
249+
providerModels: providerModels
166250
),
167251
"claude-opus-4-8"
168252
)
169253
XCTAssertEqual(
170254
GaryxThreadModelOverridePresentation.effortFilterModel(
171255
override: " ",
172-
agentConfiguredModel: "claude-haiku-4-5"
256+
agentConfiguredModel: "claude-haiku-4-5",
257+
providerModels: providerModels
173258
),
174259
"claude-haiku-4-5"
175260
)
261+
XCTAssertEqual(
262+
GaryxThreadModelOverridePresentation.effortFilterModel(
263+
override: nil,
264+
agentConfiguredModel: "",
265+
providerModels: providerModels
266+
),
267+
"claude-opus-4-8"
268+
)
176269
XCTAssertNil(
177270
GaryxThreadModelOverridePresentation.effortFilterModel(
178271
override: nil,
@@ -241,6 +334,69 @@ final class GaryxThreadModelOverridePresentationTests: XCTestCase {
241334
}
242335
"""
243336

337+
private let configuredClaudeProviderJSON = """
338+
{
339+
"provider_type": "claude_code",
340+
"supports_model_selection": true,
341+
"supports_reasoning_effort_selection": true,
342+
"default_model": "claude-opus-4-8",
343+
"default_reasoning_effort": "max",
344+
"source": "claude_code_builtin",
345+
"reasoning_efforts": [
346+
{ "id": "low", "label": "Low", "recommended": false },
347+
{ "id": "high", "label": "High", "recommended": true }
348+
],
349+
"models": [
350+
{
351+
"id": "claude-sonnet-4-6",
352+
"label": "Claude Sonnet 4.6",
353+
"recommended": true,
354+
"supported_reasoning_efforts": [
355+
{ "id": "low", "label": "Low", "recommended": false },
356+
{ "id": "high", "label": "High", "recommended": true }
357+
]
358+
},
359+
{
360+
"id": "claude-opus-4-8",
361+
"label": "Claude Opus 4.8",
362+
"recommended": false,
363+
"supported_reasoning_efforts": [
364+
{ "id": "low", "label": "Low", "recommended": false },
365+
{ "id": "high", "label": "High", "recommended": true },
366+
{ "id": "max", "label": "Max", "recommended": false }
367+
]
368+
}
369+
]
370+
}
371+
"""
372+
373+
private let traexProviderJSON = """
374+
{
375+
"provider_type": "traex",
376+
"supports_model_selection": true,
377+
"supports_reasoning_effort_selection": true,
378+
"default_model": "traex-fast",
379+
"source": "traex_builtin",
380+
"reasoning_efforts": [],
381+
"models": [
382+
{
383+
"id": "traex-fast",
384+
"label": "TRAE Fast",
385+
"recommended": true,
386+
"supported_reasoning_efforts": []
387+
},
388+
{
389+
"id": "traex-reasoner",
390+
"recommended": false,
391+
"supported_reasoning_efforts": [
392+
{ "id": "medium", "label": "Medium", "recommended": true },
393+
{ "id": "max", "recommended": false }
394+
]
395+
}
396+
]
397+
}
398+
"""
399+
244400
private let geminiProviderJSON = """
245401
{
246402
"provider_type": "gemini_cli",

0 commit comments

Comments
 (0)