feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
use boosapi_core::types::ChatRequest;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ReasoningStyle {
|
||||
None,
|
||||
OpenAIReasoningEffort,
|
||||
OpenAIResponsesReasoning,
|
||||
GlmThinking,
|
||||
GeminiThinkingLevel,
|
||||
GeminiThinkingBudget,
|
||||
AnthropicAdaptive,
|
||||
AnthropicBudgetTokens,
|
||||
SiliconFlowEnableThinking,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ResolvedReasoning {
|
||||
pub style: ReasoningStyle,
|
||||
pub level: String,
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub thinking_level: Option<String>,
|
||||
pub budget_tokens: Option<u32>,
|
||||
pub enable_thinking: Option<bool>,
|
||||
pub suppress_sampling_params: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_reasoning(
|
||||
request: &ChatRequest,
|
||||
default_style: ReasoningStyle,
|
||||
) -> Option<ResolvedReasoning> {
|
||||
let style = request
|
||||
.reasoning_profile
|
||||
.as_deref()
|
||||
.map(reasoning_style_from_profile)
|
||||
.unwrap_or(default_style);
|
||||
if style == ReasoningStyle::None {
|
||||
return None;
|
||||
}
|
||||
|
||||
let level = request
|
||||
.thinking_level
|
||||
.clone()
|
||||
.or_else(|| request.thinking_budget.map(legacy_budget_to_level))?;
|
||||
|
||||
if level == "default" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let suppress_sampling_params = !matches!(level.as_str(), "off" | "none");
|
||||
let budget_tokens = level_to_budget(&level);
|
||||
let reasoning_effort = level_to_effort(&level).map(str::to_string);
|
||||
|
||||
match style {
|
||||
ReasoningStyle::OpenAIReasoningEffort | ReasoningStyle::OpenAIResponsesReasoning => {
|
||||
Some(ResolvedReasoning {
|
||||
style,
|
||||
level,
|
||||
reasoning_effort,
|
||||
thinking_level: None,
|
||||
budget_tokens: None,
|
||||
enable_thinking: None,
|
||||
suppress_sampling_params,
|
||||
})
|
||||
}
|
||||
ReasoningStyle::GlmThinking => Some(ResolvedReasoning {
|
||||
style,
|
||||
level,
|
||||
reasoning_effort: None,
|
||||
thinking_level: None,
|
||||
budget_tokens: None,
|
||||
enable_thinking: None,
|
||||
suppress_sampling_params,
|
||||
}),
|
||||
ReasoningStyle::GeminiThinkingLevel => {
|
||||
if !matches!(level.as_str(), "minimal" | "low" | "medium" | "high") {
|
||||
return None;
|
||||
}
|
||||
Some(ResolvedReasoning {
|
||||
style,
|
||||
thinking_level: Some(level.clone()),
|
||||
level,
|
||||
reasoning_effort: None,
|
||||
budget_tokens: None,
|
||||
enable_thinking: None,
|
||||
suppress_sampling_params: false,
|
||||
})
|
||||
}
|
||||
ReasoningStyle::GeminiThinkingBudget | ReasoningStyle::AnthropicBudgetTokens => {
|
||||
Some(ResolvedReasoning {
|
||||
style,
|
||||
level,
|
||||
reasoning_effort: None,
|
||||
thinking_level: None,
|
||||
budget_tokens,
|
||||
enable_thinking: None,
|
||||
suppress_sampling_params,
|
||||
})
|
||||
}
|
||||
ReasoningStyle::AnthropicAdaptive => Some(ResolvedReasoning {
|
||||
style,
|
||||
level,
|
||||
reasoning_effort,
|
||||
thinking_level: None,
|
||||
budget_tokens: None,
|
||||
enable_thinking: None,
|
||||
suppress_sampling_params,
|
||||
}),
|
||||
ReasoningStyle::SiliconFlowEnableThinking => Some(ResolvedReasoning {
|
||||
style,
|
||||
level,
|
||||
reasoning_effort: None,
|
||||
thinking_level: None,
|
||||
budget_tokens,
|
||||
enable_thinking: Some(suppress_sampling_params),
|
||||
suppress_sampling_params,
|
||||
}),
|
||||
ReasoningStyle::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_style_from_profile(profile: &str) -> ReasoningStyle {
|
||||
match profile {
|
||||
"openai_reasoning_effort" => ReasoningStyle::OpenAIReasoningEffort,
|
||||
"openai_responses_reasoning" => ReasoningStyle::OpenAIResponsesReasoning,
|
||||
"glm_thinking" => ReasoningStyle::GlmThinking,
|
||||
"gemini_thinking_level" => ReasoningStyle::GeminiThinkingLevel,
|
||||
"gemini_thinking_budget" => ReasoningStyle::GeminiThinkingBudget,
|
||||
"anthropic_adaptive" => ReasoningStyle::AnthropicAdaptive,
|
||||
"anthropic_budget_tokens" => ReasoningStyle::AnthropicBudgetTokens,
|
||||
"siliconflow_enable_thinking" => ReasoningStyle::SiliconFlowEnableThinking,
|
||||
"none" => ReasoningStyle::None,
|
||||
// Backward-compatible aliases from the old model settings UI.
|
||||
"reasoning_effort" => ReasoningStyle::OpenAIReasoningEffort,
|
||||
"enable_thinking" => ReasoningStyle::SiliconFlowEnableThinking,
|
||||
_ => ReasoningStyle::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_budget_to_level(budget: u32) -> String {
|
||||
match budget {
|
||||
0 => "none",
|
||||
1..=2048 => "low",
|
||||
2049..=6144 => "medium",
|
||||
6145..=12288 => "high",
|
||||
_ => "xhigh",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn level_to_effort(level: &str) -> Option<&'static str> {
|
||||
match level {
|
||||
"off" => Some("none"),
|
||||
"none" => Some("none"),
|
||||
"minimal" => Some("minimal"),
|
||||
"low" => Some("low"),
|
||||
"medium" => Some("medium"),
|
||||
"high" => Some("high"),
|
||||
"xhigh" => Some("xhigh"),
|
||||
"max" => Some("max"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn level_to_budget(level: &str) -> Option<u32> {
|
||||
match level {
|
||||
"off" | "none" => Some(0),
|
||||
"low" => Some(1024),
|
||||
"medium" => Some(4096),
|
||||
"high" => Some(8192),
|
||||
"xhigh" => Some(16384),
|
||||
"max" => Some(32768),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use boosapi_core::types::{ChatContent, ChatMessage};
|
||||
|
||||
fn request(thinking_level: Option<&str>, thinking_budget: Option<u32>) -> ChatRequest {
|
||||
ChatRequest {
|
||||
model: "gemini-3-flash-preview".to_string(),
|
||||
messages: vec![ChatMessage {
|
||||
role: "user".to_string(),
|
||||
content: ChatContent::Text("hi".to_string()),
|
||||
reasoning_content: None,
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
}],
|
||||
stream: false,
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
max_tokens: None,
|
||||
tools: None,
|
||||
thinking_budget,
|
||||
thinking_level: thinking_level.map(str::to_string),
|
||||
reasoning_profile: None,
|
||||
use_max_completion_tokens: None,
|
||||
thinking_param_style: None,
|
||||
extra_body: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_thinking_level_rejects_invalid_off_level() {
|
||||
let resolved = resolve_reasoning(
|
||||
&request(Some("off"), None),
|
||||
ReasoningStyle::GeminiThinkingLevel,
|
||||
);
|
||||
|
||||
assert!(resolved.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_treats_off_alias_as_none_effort() {
|
||||
let resolved = resolve_reasoning(
|
||||
&request(Some("off"), None),
|
||||
ReasoningStyle::OpenAIReasoningEffort,
|
||||
)
|
||||
.expect("reasoning config");
|
||||
|
||||
assert_eq!(resolved.reasoning_effort.as_deref(), Some("none"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glm_thinking_profile_is_resolved_without_openai_reasoning_effort() {
|
||||
let mut req = request(Some("high"), None);
|
||||
req.reasoning_profile = Some("glm_thinking".to_string());
|
||||
|
||||
let resolved = resolve_reasoning(&req, ReasoningStyle::OpenAIReasoningEffort)
|
||||
.expect("reasoning config");
|
||||
|
||||
assert_eq!(resolved.style, ReasoningStyle::GlmThinking);
|
||||
assert_eq!(resolved.level, "high");
|
||||
assert_eq!(resolved.reasoning_effort, None);
|
||||
assert!(resolved.suppress_sampling_params);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user