Skip to content

Commit ea04eb9

Browse files
authored
ENG-502: Add direct OpenAI provider support (#388)
* feat: add direct OpenAI provider support * test: update OpenAI provider story snapshots * test: add setup step story snapshot * fix(ai): consolidate OpenAI model capabilities * fix(ai): defer cloud credential probes
1 parent 484a574 commit ea04eb9

27 files changed

Lines changed: 1561 additions & 241 deletions

apps/native/src-tauri/src/ai/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod log_summarizer;
2+
pub mod model_capabilities;
23
pub mod provider_errors;
34
pub mod providers;
45

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
const DEFAULT_CONTEXT_WINDOW_TOKENS: u32 = 8192;
2+
const GPT_4O_MAX_COMPLETION_TOKENS: u32 = 16_384;
3+
4+
#[derive(Debug, Clone, PartialEq, Eq)]
5+
pub struct ModelCapabilities {
6+
/// Conservative local budget cap. A future server registry can replace this
7+
/// source without changing provider call sites.
8+
pub context_window_tokens: u32,
9+
pub max_completion_tokens: Option<u32>,
10+
pub supports_custom_temperature: bool,
11+
}
12+
13+
impl ModelCapabilities {
14+
pub fn clamp_max_completion_tokens(&self, requested: u32) -> u32 {
15+
self.max_completion_tokens
16+
.map_or(requested, |limit| requested.min(limit))
17+
}
18+
}
19+
20+
fn normalized_model_name(model: &str) -> String {
21+
model
22+
.strip_prefix("openai/")
23+
.unwrap_or(model)
24+
.to_ascii_lowercase()
25+
}
26+
27+
fn supports_custom_temperature_for_normalized_model(model: &str) -> bool {
28+
!(model == "o1"
29+
|| model == "o3"
30+
|| model == "o4"
31+
|| model == "gpt-5"
32+
|| model.starts_with("o1-")
33+
|| model.starts_with("o3-")
34+
|| model.starts_with("o4-")
35+
|| model.starts_with("gpt-5-")
36+
|| model.starts_with("gpt-5."))
37+
}
38+
39+
fn max_completion_tokens_for_normalized_model(model: &str) -> Option<u32> {
40+
if matches!(model, "gpt-4o" | "gpt-4o-mini")
41+
|| model.starts_with("gpt-4o-")
42+
{
43+
Some(GPT_4O_MAX_COMPLETION_TOKENS)
44+
} else {
45+
None
46+
}
47+
}
48+
49+
fn context_window_tokens(model: &str) -> u32 {
50+
let model = model.to_ascii_lowercase();
51+
52+
if model.contains("gpt-oss")
53+
|| model.contains("o1")
54+
|| model.contains("o3")
55+
|| model.contains("gpt-4.1")
56+
|| model.contains("claude-3")
57+
|| model.contains("gemini-1.5")
58+
|| model.contains("gemini-2")
59+
{
60+
return 32768;
61+
}
62+
63+
if model.contains("gpt-4o")
64+
|| model.contains("llama3")
65+
|| model.contains("qwen")
66+
|| model.contains("mistral")
67+
|| model.contains("codellama")
68+
{
69+
return 16384;
70+
}
71+
72+
DEFAULT_CONTEXT_WINDOW_TOKENS
73+
}
74+
75+
pub fn capabilities_for_model(model: &str) -> ModelCapabilities {
76+
let normalized_model = normalized_model_name(model);
77+
78+
ModelCapabilities {
79+
context_window_tokens: context_window_tokens(model),
80+
max_completion_tokens: max_completion_tokens_for_normalized_model(&normalized_model),
81+
supports_custom_temperature: supports_custom_temperature_for_normalized_model(
82+
&normalized_model,
83+
),
84+
}
85+
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::{capabilities_for_model, DEFAULT_CONTEXT_WINDOW_TOKENS};
90+
91+
#[test]
92+
fn reasoning_models_do_not_support_custom_temperature() {
93+
for model in [
94+
"o1",
95+
"o1-2024-12-17",
96+
"o3",
97+
"o3-mini",
98+
"o3-2025-04-16",
99+
"o4-mini",
100+
"openai/o4-mini",
101+
"gpt-5",
102+
"gpt-5-mini",
103+
"gpt-5.1",
104+
"gpt-5.2",
105+
"openai/gpt-5-nano",
106+
] {
107+
assert!(
108+
!capabilities_for_model(model).supports_custom_temperature,
109+
"{model}"
110+
);
111+
}
112+
}
113+
114+
#[test]
115+
fn gpt_models_support_custom_temperature() {
116+
for model in ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "openai/gpt-4.1-mini"] {
117+
assert!(
118+
capabilities_for_model(model).supports_custom_temperature,
119+
"{model}"
120+
);
121+
}
122+
}
123+
124+
#[test]
125+
fn gpt_4o_models_cap_max_completion_tokens() {
126+
for model in [
127+
"gpt-4o",
128+
"gpt-4o-mini",
129+
"gpt-4o-2024-08-06",
130+
"openai/gpt-4o-mini",
131+
] {
132+
let capabilities = capabilities_for_model(model);
133+
134+
assert_eq!(capabilities.max_completion_tokens, Some(16_384), "{model}");
135+
assert_eq!(capabilities.clamp_max_completion_tokens(32_768), 16_384);
136+
assert_eq!(capabilities.clamp_max_completion_tokens(4_096), 4_096);
137+
}
138+
}
139+
140+
#[test]
141+
fn non_gpt_4o_models_do_not_cap_max_completion_tokens() {
142+
for model in [
143+
"gpt-4.1",
144+
"gpt-5",
145+
"custom-openai-model",
146+
"openai/gpt-5-nano",
147+
] {
148+
let capabilities = capabilities_for_model(model);
149+
150+
assert_eq!(capabilities.max_completion_tokens, None, "{model}");
151+
assert_eq!(capabilities.clamp_max_completion_tokens(32_768), 32_768);
152+
}
153+
}
154+
155+
#[test]
156+
fn context_window_budget_preserves_existing_model_groups() {
157+
for model in [
158+
"gpt-oss-120b",
159+
"o1-preview",
160+
"o3-mini",
161+
"gpt-4.1-mini",
162+
"claude-3-5-sonnet",
163+
"gemini-1.5-pro",
164+
"gemini-2.5-pro",
165+
"my-qwen-finetune",
166+
] {
167+
let expected = if model.contains("qwen") {
168+
16_384
169+
} else {
170+
32_768
171+
};
172+
173+
assert_eq!(
174+
capabilities_for_model(model).context_window_tokens,
175+
expected,
176+
"{model}"
177+
);
178+
}
179+
180+
for model in [
181+
"openai/gpt-4o-mini",
182+
"llama3:8b-instruct",
183+
"qwen2.5-coder",
184+
"mistral-small",
185+
"codellama:13b",
186+
] {
187+
assert_eq!(
188+
capabilities_for_model(model).context_window_tokens,
189+
16_384,
190+
"{model}"
191+
);
192+
}
193+
194+
assert_eq!(
195+
capabilities_for_model("unknown-model").context_window_tokens,
196+
DEFAULT_CONTEXT_WINDOW_TOKENS
197+
);
198+
}
199+
}

0 commit comments

Comments
 (0)