Add Ollama provider options
This commit is contained in:
@@ -6,18 +6,20 @@ use rig::{
|
||||
client::completion::CompletionClient,
|
||||
completion::{Message, Prompt},
|
||||
message::ToolChoice,
|
||||
providers::openai,
|
||||
providers::{anthropic, openai},
|
||||
streaming::{StreamedAssistantContent, StreamingPrompt},
|
||||
};
|
||||
|
||||
use crate::agent::stream_events::AgentStreamEmitter;
|
||||
use crate::agent::tools::terminal_command::{AgentCommandExecutor, RunTerminalCommandTool};
|
||||
use crate::agent::AgentRuntimeConfig;
|
||||
use crate::agent::{AgentProviderKind, AgentRuntimeConfig};
|
||||
use crate::error::AppError;
|
||||
use crate::state::PendingAgentToolApprovals;
|
||||
|
||||
const SYSTEM_PROMPT: &str = "You are MosaicIQ's terminal chat assistant. Answer concisely in plain text. Use the available terminal command tool whenever current workspace data or live MosaicIQ terminal actions would improve the answer. Never claim to have run a command unless the tool actually ran it. If the request is unclear, ask a short clarifying question.";
|
||||
const MAX_TOOL_TURNS: usize = 4;
|
||||
const CHAT_MAX_TOKENS: u64 = 1_024;
|
||||
const SUMMARY_MAX_TOKENS: u64 = 512;
|
||||
const SUMMARY_SYSTEM_PROMPT: &str = "You maintain a rolling summary for MosaicIQ terminal chats. Return plain text only. Keep the summary under 1200 characters and roughly 10 bullets. Preserve concrete facts, user preferences, decisions, portfolio actions, unresolved questions, and symbols/tickers. Use exactly these sections: User goals, Established facts, Actions taken, Open threads.";
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -62,22 +64,18 @@ impl ChatGateway for RigChatGateway {
|
||||
tool_runtime: AgentToolRuntimeContext,
|
||||
) -> BoxFuture<'static, Result<String, AppError>> {
|
||||
Box::pin(async move {
|
||||
let api_key = runtime.api_key.unwrap_or_default();
|
||||
let client = openai::CompletionsClient::builder()
|
||||
.api_key(api_key)
|
||||
.base_url(&runtime.base_url)
|
||||
.build()
|
||||
.map_err(|error| AppError::ProviderInit(error.to_string()))?;
|
||||
|
||||
let history = compose_request_messages(context_messages, history);
|
||||
let tool = RunTerminalCommandTool {
|
||||
stream_emitter: tool_runtime.stream_emitter.clone(),
|
||||
command_executor: tool_runtime.command_executor,
|
||||
pending_approvals: tool_runtime.pending_approvals,
|
||||
workspace_id: tool_runtime.workspace_id,
|
||||
command_executor: tool_runtime.command_executor.clone(),
|
||||
pending_approvals: tool_runtime.pending_approvals.clone(),
|
||||
workspace_id: tool_runtime.workspace_id.clone(),
|
||||
};
|
||||
|
||||
let mut rig_stream = client
|
||||
match runtime.provider_kind {
|
||||
AgentProviderKind::Remote | AgentProviderKind::OllamaOpenAI => {
|
||||
let client = build_openai_client(&runtime)?;
|
||||
let rig_stream = client
|
||||
.agent(runtime.model)
|
||||
.preamble(SYSTEM_PROMPT)
|
||||
.temperature(0.2)
|
||||
@@ -90,15 +88,120 @@ impl ChatGateway for RigChatGateway {
|
||||
.multi_turn(MAX_TOOL_TURNS)
|
||||
.await;
|
||||
|
||||
consume_stream(rig_stream, &tool_runtime).await
|
||||
}
|
||||
AgentProviderKind::OllamaAnthropic => {
|
||||
let client = build_anthropic_client(&runtime)?;
|
||||
let rig_stream = client
|
||||
.agent(runtime.model)
|
||||
.preamble(SYSTEM_PROMPT)
|
||||
.temperature(0.2)
|
||||
.max_tokens(CHAT_MAX_TOKENS)
|
||||
.tool(tool)
|
||||
.tool_choice(ToolChoice::Auto)
|
||||
.default_max_turns(MAX_TOOL_TURNS)
|
||||
.build()
|
||||
.stream_prompt(prompt)
|
||||
.with_history(history)
|
||||
.multi_turn(MAX_TOOL_TURNS)
|
||||
.await;
|
||||
|
||||
consume_stream(rig_stream, &tool_runtime).await
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn summarize_history(
|
||||
&self,
|
||||
runtime: AgentRuntimeConfig,
|
||||
existing_summary: Option<String>,
|
||||
messages: Vec<Message>,
|
||||
) -> BoxFuture<'static, Result<String, AppError>> {
|
||||
Box::pin(async move {
|
||||
let mut history = Vec::new();
|
||||
|
||||
if let Some(summary) = existing_summary.filter(|value| !value.trim().is_empty()) {
|
||||
history.push(Message::system(format!(
|
||||
"Existing rolling summary:\n{}",
|
||||
summary.trim()
|
||||
)));
|
||||
}
|
||||
history.extend(messages);
|
||||
|
||||
match runtime.provider_kind {
|
||||
AgentProviderKind::Remote | AgentProviderKind::OllamaOpenAI => {
|
||||
let client = build_openai_client(&runtime)?;
|
||||
let response = client
|
||||
.agent(runtime.model)
|
||||
.preamble(SUMMARY_SYSTEM_PROMPT)
|
||||
.temperature(0.1)
|
||||
.build()
|
||||
.prompt("Update the rolling summary using the conversation history.")
|
||||
.with_history(history)
|
||||
.await
|
||||
.map_err(map_prompt_error)?;
|
||||
|
||||
Ok(response.trim().to_string())
|
||||
}
|
||||
AgentProviderKind::OllamaAnthropic => {
|
||||
let client = build_anthropic_client(&runtime)?;
|
||||
let response = client
|
||||
.agent(runtime.model)
|
||||
.preamble(SUMMARY_SYSTEM_PROMPT)
|
||||
.temperature(0.1)
|
||||
.max_tokens(SUMMARY_MAX_TOKENS)
|
||||
.build()
|
||||
.prompt("Update the rolling summary using the conversation history.")
|
||||
.with_history(history)
|
||||
.await
|
||||
.map_err(map_prompt_error)?;
|
||||
|
||||
Ok(response.trim().to_string())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_openai_client(runtime: &AgentRuntimeConfig) -> Result<openai::CompletionsClient, AppError> {
|
||||
let api_key = runtime.api_key.clone().unwrap_or_default();
|
||||
|
||||
openai::CompletionsClient::builder()
|
||||
.api_key(api_key)
|
||||
.base_url(&runtime.base_url)
|
||||
.build()
|
||||
.map_err(|error| AppError::ProviderInit(error.to_string()))
|
||||
}
|
||||
|
||||
fn build_anthropic_client(runtime: &AgentRuntimeConfig) -> Result<anthropic::Client, AppError> {
|
||||
let api_key = runtime.api_key.clone().unwrap_or_default();
|
||||
|
||||
anthropic::Client::builder()
|
||||
.api_key(api_key)
|
||||
.base_url(&runtime.base_url)
|
||||
.build()
|
||||
.map_err(|error| AppError::ProviderInit(error.to_string()))
|
||||
}
|
||||
|
||||
fn compose_request_messages(context_messages: Vec<Message>, history: Vec<Message>) -> Vec<Message> {
|
||||
context_messages.into_iter().chain(history).collect()
|
||||
}
|
||||
|
||||
async fn consume_stream<R, S>(
|
||||
mut rig_stream: S,
|
||||
tool_runtime: &AgentToolRuntimeContext,
|
||||
) -> Result<String, AppError>
|
||||
where
|
||||
S: futures::Stream<Item = Result<MultiTurnStreamItem<R>, rig::agent::StreamingError>> + Unpin,
|
||||
{
|
||||
let mut reply = String::new();
|
||||
let mut saw_text = false;
|
||||
let mut saw_reasoning_delta = false;
|
||||
|
||||
while let Some(item) = rig_stream.next().await {
|
||||
match item {
|
||||
Ok(MultiTurnStreamItem::StreamAssistantItem(
|
||||
StreamedAssistantContent::Text(text),
|
||||
)) => {
|
||||
Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Text(text))) => {
|
||||
saw_text = true;
|
||||
reply.push_str(&text.text);
|
||||
tool_runtime.stream_emitter.text_delta(text.text)?;
|
||||
@@ -144,54 +247,6 @@ impl ChatGateway for RigChatGateway {
|
||||
}
|
||||
|
||||
Ok(reply)
|
||||
})
|
||||
}
|
||||
|
||||
fn summarize_history(
|
||||
&self,
|
||||
runtime: AgentRuntimeConfig,
|
||||
existing_summary: Option<String>,
|
||||
messages: Vec<Message>,
|
||||
) -> BoxFuture<'static, Result<String, AppError>> {
|
||||
Box::pin(async move {
|
||||
let client = build_client(&runtime)?;
|
||||
let mut history = Vec::new();
|
||||
|
||||
if let Some(summary) = existing_summary.filter(|value| !value.trim().is_empty()) {
|
||||
history.push(Message::system(format!(
|
||||
"Existing rolling summary:\n{}",
|
||||
summary.trim()
|
||||
)));
|
||||
}
|
||||
history.extend(messages);
|
||||
|
||||
let response = client
|
||||
.agent(runtime.model)
|
||||
.preamble(SUMMARY_SYSTEM_PROMPT)
|
||||
.temperature(0.1)
|
||||
.build()
|
||||
.prompt("Update the rolling summary using the conversation history.")
|
||||
.with_history(history)
|
||||
.await
|
||||
.map_err(map_prompt_error)?;
|
||||
|
||||
Ok(response.trim().to_string())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_client(runtime: &AgentRuntimeConfig) -> Result<openai::CompletionsClient, AppError> {
|
||||
let api_key = runtime.api_key.clone().unwrap_or_default();
|
||||
|
||||
openai::CompletionsClient::builder()
|
||||
.api_key(api_key)
|
||||
.base_url(&runtime.base_url)
|
||||
.build()
|
||||
.map_err(|error| AppError::ProviderInit(error.to_string()))
|
||||
}
|
||||
|
||||
fn compose_request_messages(context_messages: Vec<Message>, history: Vec<Message>) -> Vec<Message> {
|
||||
context_messages.into_iter().chain(history).collect()
|
||||
}
|
||||
|
||||
fn reasoning_text(reasoning: &rig::message::Reasoning) -> String {
|
||||
|
||||
@@ -14,9 +14,10 @@ pub use service::AgentService;
|
||||
pub(crate) use settings::AgentSettingsService;
|
||||
pub use stream_events::AgentStreamEmitter;
|
||||
pub use types::{
|
||||
default_task_defaults, AgentConfigStatus, AgentRuntimeConfig, AgentStoredSettings,
|
||||
AgentStreamItemEvent, AgentStreamItemKind, AgentTaskRoute, ChatPanelContext, ChatPromptRequest,
|
||||
ChatStreamStart, PreparedChatTurn, RemoteProviderSettings, ResolveAgentToolApprovalRequest,
|
||||
SaveAgentRuntimeConfigRequest, TaskProfile, UpdateRemoteApiKeyRequest,
|
||||
AGENT_SETTINGS_STORE_PATH, DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
default_task_defaults, AgentConfigStatus, AgentProviderKind, AgentRuntimeConfig,
|
||||
AgentStoredSettings, AgentStreamItemEvent, AgentStreamItemKind, AgentTaskRoute,
|
||||
ChatPanelContext, ChatPromptRequest, ChatStreamStart, PreparedChatTurn,
|
||||
RemoteProviderSettings, ResolveAgentToolApprovalRequest, SaveAgentRuntimeConfigRequest,
|
||||
TaskProfile, UpdateRemoteApiKeyRequest, AGENT_SETTINGS_STORE_PATH,
|
||||
DEFAULT_OLLAMA_COMPAT_API_KEY, DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::agent::{AgentRuntimeConfig, AgentStoredSettings, AgentTaskRoute, TaskProfile};
|
||||
use crate::agent::{
|
||||
AgentProviderKind, AgentRuntimeConfig, AgentStoredSettings, AgentTaskRoute, TaskProfile,
|
||||
DEFAULT_OLLAMA_COMPAT_API_KEY,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
|
||||
pub fn resolve_runtime(
|
||||
@@ -16,14 +19,24 @@ pub fn resolve_runtime(
|
||||
}
|
||||
|
||||
let api_key = settings.remote.api_key.trim().to_string();
|
||||
if api_key.is_empty() {
|
||||
if settings.remote.provider_kind.uses_api_key() && api_key.is_empty() {
|
||||
return Err(AppError::RemoteApiKeyMissing);
|
||||
}
|
||||
|
||||
Ok(AgentRuntimeConfig {
|
||||
provider_kind: settings.remote.provider_kind,
|
||||
base_url: settings.remote.base_url.clone(),
|
||||
model: resolve_model(settings, task_profile, route, model_override)?,
|
||||
api_key: Some(api_key),
|
||||
api_key: Some(match settings.remote.provider_kind {
|
||||
AgentProviderKind::Remote => api_key,
|
||||
AgentProviderKind::OllamaOpenAI | AgentProviderKind::OllamaAnthropic => {
|
||||
if api_key.is_empty() {
|
||||
DEFAULT_OLLAMA_COMPAT_API_KEY.to_string()
|
||||
} else {
|
||||
api_key
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,8 +85,9 @@ pub fn normalize_routes(settings: &mut AgentStoredSettings) -> Result<(), AppErr
|
||||
pub fn compute_remote_configured(settings: &AgentStoredSettings) -> bool {
|
||||
settings.remote.enabled
|
||||
&& !settings.remote.base_url.trim().is_empty()
|
||||
&& !settings.remote.api_key.trim().is_empty()
|
||||
&& !settings.default_remote_model.trim().is_empty()
|
||||
&& (!settings.remote.provider_kind.uses_api_key()
|
||||
|| !settings.remote.api_key.trim().is_empty())
|
||||
}
|
||||
|
||||
pub fn compute_overall_configured(settings: &AgentStoredSettings) -> bool {
|
||||
|
||||
@@ -292,6 +292,7 @@ impl<R: Runtime, G: ChatGateway> AgentService<R, G> {
|
||||
let mut settings = self.settings.load()?;
|
||||
settings.remote = RemoteProviderSettings {
|
||||
enabled: request.remote_enabled,
|
||||
provider_kind: request.provider_kind,
|
||||
base_url: request.remote_base_url.trim().to_string(),
|
||||
api_key: settings.remote.api_key,
|
||||
};
|
||||
@@ -328,6 +329,7 @@ impl<R: Runtime, G: ChatGateway> AgentService<R, G> {
|
||||
configured: compute_overall_configured(&settings),
|
||||
remote_configured: compute_remote_configured(&settings),
|
||||
remote_enabled: settings.remote.enabled,
|
||||
provider_kind: settings.remote.provider_kind,
|
||||
has_remote_api_key: !settings.remote.api_key.trim().is_empty(),
|
||||
has_sec_edgar_user_agent: !settings.sec_edgar_user_agent.trim().is_empty(),
|
||||
remote_base_url: settings.remote.base_url,
|
||||
@@ -364,9 +366,9 @@ mod tests {
|
||||
|
||||
use super::SessionManager;
|
||||
use crate::agent::{
|
||||
default_task_defaults, AgentRuntimeConfig, AgentService, ChatPanelContext,
|
||||
ChatPromptRequest, SaveAgentRuntimeConfigRequest, TaskProfile, UpdateRemoteApiKeyRequest,
|
||||
DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
default_task_defaults, AgentProviderKind, AgentRuntimeConfig, AgentService,
|
||||
ChatPanelContext, ChatPromptRequest, SaveAgentRuntimeConfigRequest, TaskProfile,
|
||||
UpdateRemoteApiKeyRequest, DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
use crate::terminal::{Company, CompanyProfile, PanelPayload};
|
||||
@@ -462,6 +464,7 @@ mod tests {
|
||||
let saved = service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults: default_task_defaults("glm-test"),
|
||||
@@ -498,6 +501,7 @@ mod tests {
|
||||
service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults: default_task_defaults("glm-test"),
|
||||
@@ -528,6 +532,7 @@ mod tests {
|
||||
service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults: default_task_defaults("glm-test"),
|
||||
@@ -569,6 +574,7 @@ mod tests {
|
||||
let saved = service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults,
|
||||
@@ -591,6 +597,7 @@ mod tests {
|
||||
service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults: default_task_defaults("glm-test"),
|
||||
@@ -604,6 +611,37 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ollama_openai_provider_is_configured_without_a_stored_api_key() {
|
||||
with_test_home("ollama-openai-config", || {
|
||||
let app = build_test_app();
|
||||
let mut service = AgentService::new(app.handle()).unwrap();
|
||||
|
||||
let saved = service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::OllamaOpenAI,
|
||||
remote_base_url:
|
||||
super::super::types::DEFAULT_OLLAMA_OPENAI_BASE_URL.to_string(),
|
||||
default_remote_model: "qwen3-coder".to_string(),
|
||||
task_defaults: default_task_defaults("qwen3-coder"),
|
||||
sec_edgar_user_agent: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(saved.configured);
|
||||
assert!(saved.remote_configured);
|
||||
assert!(!saved.has_remote_api_key);
|
||||
|
||||
let prepared = prepare_turn(&mut service, request("hello")).unwrap();
|
||||
assert_eq!(prepared.runtime.provider_kind, AgentProviderKind::OllamaOpenAI);
|
||||
assert_eq!(
|
||||
prepared.runtime.api_key.as_deref(),
|
||||
Some(crate::agent::DEFAULT_OLLAMA_COMPAT_API_KEY)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_turn_without_panel_context_yields_no_context_messages() {
|
||||
with_test_home("context-none", || {
|
||||
@@ -678,6 +716,7 @@ mod tests {
|
||||
|
||||
fn sample_runtime() -> AgentRuntimeConfig {
|
||||
AgentRuntimeConfig {
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
base_url: "https://example.com".to_string(),
|
||||
model: "glm-5.1".to_string(),
|
||||
api_key: Some("key".to_string()),
|
||||
@@ -731,6 +770,7 @@ mod tests {
|
||||
service
|
||||
.save_runtime_config(SaveAgentRuntimeConfigRequest {
|
||||
remote_enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
remote_base_url: "https://example.test/v4".to_string(),
|
||||
default_remote_model: "glm-test".to_string(),
|
||||
task_defaults: default_task_defaults("glm-test"),
|
||||
|
||||
@@ -3,12 +3,13 @@ use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
use crate::agent::{
|
||||
default_task_defaults, AgentStoredSettings, RemoteProviderSettings, AGENT_SETTINGS_STORE_PATH,
|
||||
DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
default_task_defaults, AgentProviderKind, AgentStoredSettings, RemoteProviderSettings,
|
||||
AGENT_SETTINGS_STORE_PATH, DEFAULT_REMOTE_BASE_URL, DEFAULT_REMOTE_MODEL,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
|
||||
const REMOTE_ENABLED_KEY: &str = "remoteEnabled";
|
||||
const PROVIDER_KIND_KEY: &str = "providerKind";
|
||||
const REMOTE_BASE_URL_KEY: &str = "remoteBaseUrl";
|
||||
const REMOTE_API_KEY_KEY: &str = "remoteApiKey";
|
||||
const DEFAULT_REMOTE_MODEL_KEY: &str = "defaultRemoteModel";
|
||||
@@ -69,6 +70,10 @@ impl<R: Runtime> AgentSettingsService<R> {
|
||||
.get(REMOTE_ENABLED_KEY)
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(true),
|
||||
provider_kind: store
|
||||
.get(PROVIDER_KIND_KEY)
|
||||
.and_then(|value| serde_json::from_value(value.clone()).ok())
|
||||
.unwrap_or(AgentProviderKind::Remote),
|
||||
base_url: store
|
||||
.get(REMOTE_BASE_URL_KEY)
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
@@ -119,6 +124,10 @@ impl<R: Runtime> AgentSettingsService<R> {
|
||||
REMOTE_ENABLED_KEY.to_string(),
|
||||
json!(settings.remote.enabled),
|
||||
);
|
||||
store.set(
|
||||
PROVIDER_KIND_KEY.to_string(),
|
||||
json!(settings.remote.provider_kind),
|
||||
);
|
||||
store.set(
|
||||
REMOTE_BASE_URL_KEY.to_string(),
|
||||
json!(settings.remote.base_url),
|
||||
|
||||
@@ -8,9 +8,31 @@ use crate::terminal::{PanelPayload, TerminalCommandResponse};
|
||||
pub const DEFAULT_REMOTE_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4";
|
||||
/// Default model used for plain-text terminal chat.
|
||||
pub const DEFAULT_REMOTE_MODEL: &str = "glm-5.1";
|
||||
/// Default Ollama OpenAI compatibility endpoint used by the app.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub const DEFAULT_OLLAMA_OPENAI_BASE_URL: &str = "http://localhost:11434/v1";
|
||||
/// Default Ollama Anthropic compatibility endpoint used by the app.
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_OLLAMA_ANTHROPIC_BASE_URL: &str = "http://localhost:11434";
|
||||
/// Placeholder token required by Ollama compatibility endpoints but ignored by Ollama.
|
||||
pub const DEFAULT_OLLAMA_COMPAT_API_KEY: &str = "ollama";
|
||||
/// Store file used for agent settings and plaintext API key storage.
|
||||
pub const AGENT_SETTINGS_STORE_PATH: &str = "agent-settings.json";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentProviderKind {
|
||||
Remote,
|
||||
OllamaOpenAI,
|
||||
OllamaAnthropic,
|
||||
}
|
||||
|
||||
impl AgentProviderKind {
|
||||
pub const fn uses_api_key(self) -> bool {
|
||||
matches!(self, Self::Remote)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable harness task profiles that can be routed independently.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -49,6 +71,7 @@ pub struct ChatPanelContext {
|
||||
/// Runtime provider configuration after settings resolution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentRuntimeConfig {
|
||||
pub provider_kind: AgentProviderKind,
|
||||
pub base_url: String,
|
||||
pub model: String,
|
||||
pub api_key: Option<String>,
|
||||
@@ -145,6 +168,7 @@ pub struct AgentConfigStatus {
|
||||
pub configured: bool,
|
||||
pub remote_configured: bool,
|
||||
pub remote_enabled: bool,
|
||||
pub provider_kind: AgentProviderKind,
|
||||
pub has_remote_api_key: bool,
|
||||
pub has_sec_edgar_user_agent: bool,
|
||||
pub remote_base_url: String,
|
||||
@@ -158,6 +182,7 @@ pub struct AgentConfigStatus {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveAgentRuntimeConfigRequest {
|
||||
pub remote_enabled: bool,
|
||||
pub provider_kind: AgentProviderKind,
|
||||
pub remote_base_url: String,
|
||||
pub default_remote_model: String,
|
||||
pub task_defaults: HashMap<TaskProfile, AgentTaskRoute>,
|
||||
@@ -176,6 +201,7 @@ pub struct UpdateRemoteApiKeyRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteProviderSettings {
|
||||
pub enabled: bool,
|
||||
pub provider_kind: AgentProviderKind,
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
@@ -184,6 +210,7 @@ impl Default for RemoteProviderSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
provider_kind: AgentProviderKind::Remote,
|
||||
base_url: DEFAULT_REMOTE_BASE_URL.to_string(),
|
||||
api_key: String::new(),
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { agentSettingsBridge } from '../../lib/agentSettingsBridge';
|
||||
import {
|
||||
AGENT_PROVIDER_LABELS,
|
||||
AgentProviderKind,
|
||||
AgentConfigStatus,
|
||||
AgentTaskRoute,
|
||||
TASK_LABELS,
|
||||
@@ -20,7 +22,7 @@ import {
|
||||
TaskProfile,
|
||||
} from '../../types/agentSettings';
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { DEFAULT_MODEL_OPTIONS, ModelSelector, OLLAMA_MODEL_OPTIONS } from './ModelSelector';
|
||||
import { ValidatedInput, ValidationStatus } from './ValidatedInput';
|
||||
import { HelpIcon } from './Tooltip';
|
||||
|
||||
@@ -31,6 +33,7 @@ interface AgentSettingsFormProps {
|
||||
|
||||
interface FormState {
|
||||
remoteEnabled: boolean;
|
||||
providerKind: AgentProviderKind;
|
||||
remoteBaseUrl: string;
|
||||
defaultRemoteModel: string;
|
||||
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
|
||||
@@ -56,6 +59,47 @@ const mergeTaskDefaults = (
|
||||
return acc;
|
||||
}, {} as Record<TaskProfile, AgentTaskRoute>);
|
||||
|
||||
const REMOTE_DEFAULT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
||||
const OLLAMA_OPENAI_BASE_URL = 'http://localhost:11434/v1';
|
||||
const OLLAMA_ANTHROPIC_BASE_URL = 'http://localhost:11434';
|
||||
|
||||
const PROVIDER_BASE_URLS: Record<AgentProviderKind, string> = {
|
||||
remote: REMOTE_DEFAULT_BASE_URL,
|
||||
ollama_openai: OLLAMA_OPENAI_BASE_URL,
|
||||
ollama_anthropic: OLLAMA_ANTHROPIC_BASE_URL,
|
||||
};
|
||||
|
||||
const PROVIDER_MODEL_OPTIONS = {
|
||||
remote: DEFAULT_MODEL_OPTIONS,
|
||||
ollama_openai: OLLAMA_MODEL_OPTIONS,
|
||||
ollama_anthropic: OLLAMA_MODEL_OPTIONS,
|
||||
} satisfies Record<AgentProviderKind, typeof DEFAULT_MODEL_OPTIONS>;
|
||||
|
||||
const PROVIDER_HELPERS: Record<
|
||||
AgentProviderKind,
|
||||
{ description: string; baseUrlHelper: string; baseUrlPlaceholder: string }
|
||||
> = {
|
||||
remote: {
|
||||
description: 'Connect to an external OpenAI-compatible AI service.',
|
||||
baseUrlHelper: 'The API endpoint URL for your AI provider.',
|
||||
baseUrlPlaceholder: 'https://api.example.com/v1',
|
||||
},
|
||||
ollama_openai: {
|
||||
description: 'Use Ollama through its OpenAI-compatible `/v1/chat/completions` interface.',
|
||||
baseUrlHelper: 'Default Ollama OpenAI compatibility URL: `http://localhost:11434/v1`.',
|
||||
baseUrlPlaceholder: OLLAMA_OPENAI_BASE_URL,
|
||||
},
|
||||
ollama_anthropic: {
|
||||
description: 'Use Ollama through its Anthropic-compatible `/v1/messages` interface.',
|
||||
baseUrlHelper: 'Default Ollama Anthropic compatibility URL: `http://localhost:11434`.',
|
||||
baseUrlPlaceholder: OLLAMA_ANTHROPIC_BASE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
const providerUsesApiKey = (providerKind: AgentProviderKind) => providerKind === 'remote';
|
||||
const shouldReplaceBaseUrl = (currentBaseUrl: string) =>
|
||||
!currentBaseUrl.trim() || Object.values(PROVIDER_BASE_URLS).includes(currentBaseUrl.trim());
|
||||
|
||||
const validateUrl = (url: string): { valid: boolean; error?: string } => {
|
||||
if (!url.trim()) {
|
||||
return { valid: false, error: 'Base URL is required' };
|
||||
@@ -92,6 +136,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
}) => {
|
||||
const [formState, setFormState] = useState<FormState>({
|
||||
remoteEnabled: true,
|
||||
providerKind: 'remote',
|
||||
remoteBaseUrl: '',
|
||||
defaultRemoteModel: '',
|
||||
taskDefaults: mergeTaskDefaults({}, ''),
|
||||
@@ -120,6 +165,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
|
||||
const newState: FormState = {
|
||||
remoteEnabled: status.remoteEnabled,
|
||||
providerKind: status.providerKind,
|
||||
remoteBaseUrl: status.remoteBaseUrl,
|
||||
defaultRemoteModel: status.defaultRemoteModel,
|
||||
taskDefaults: mergeTaskDefaults(status.taskDefaults, status.defaultRemoteModel),
|
||||
@@ -146,6 +192,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
|
||||
const hasChanges =
|
||||
formState.remoteEnabled !== initialState.remoteEnabled ||
|
||||
formState.providerKind !== initialState.providerKind ||
|
||||
formState.remoteBaseUrl !== initialState.remoteBaseUrl ||
|
||||
formState.defaultRemoteModel !== initialState.defaultRemoteModel ||
|
||||
JSON.stringify(formState.taskDefaults) !== JSON.stringify(initialState.taskDefaults) ||
|
||||
@@ -217,6 +264,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
|
||||
const runtimeRequest = {
|
||||
remoteEnabled: formState.remoteEnabled,
|
||||
providerKind: formState.providerKind,
|
||||
remoteBaseUrl: formState.remoteBaseUrl,
|
||||
defaultRemoteModel: formState.defaultRemoteModel,
|
||||
taskDefaults: formState.taskDefaults,
|
||||
@@ -335,9 +383,29 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleProviderKindChange = (nextProviderKind: AgentProviderKind) => {
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
providerKind: nextProviderKind,
|
||||
remoteBaseUrl: shouldReplaceBaseUrl(current.remoteBaseUrl)
|
||||
? PROVIDER_BASE_URLS[nextProviderKind]
|
||||
: current.remoteBaseUrl,
|
||||
}));
|
||||
};
|
||||
|
||||
const isFormValid =
|
||||
(validation.baseUrl === 'valid' || validation.baseUrl === 'idle') &&
|
||||
(validation.secEdgarUserAgent === 'valid' || validation.secEdgarUserAgent === 'idle');
|
||||
const providerHelpers = PROVIDER_HELPERS[formState.providerKind];
|
||||
const providerModelOptions = PROVIDER_MODEL_OPTIONS[formState.providerKind];
|
||||
const usesApiKey = providerUsesApiKey(formState.providerKind);
|
||||
const apiKeyStatusLabel = usesApiKey
|
||||
? status.hasRemoteApiKey
|
||||
? 'Stored'
|
||||
: 'Not set'
|
||||
: status.hasRemoteApiKey
|
||||
? 'Unused stored key'
|
||||
: 'Not required';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -353,11 +421,14 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
<div className="text-right text-xs font-mono">
|
||||
<div className="flex items-center justify-end gap-2 text-term-text-muted">
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
<span>Remote ready: {status.remoteConfigured ? 'Yes' : 'No'}</span>
|
||||
<span>
|
||||
Provider: {AGENT_PROVIDER_LABELS[status.providerKind]}
|
||||
{status.remoteConfigured ? ' / Ready' : ' / Needs setup'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-end gap-2 text-term-text-muted">
|
||||
<KeyRound className="h-3.5 w-3.5" />
|
||||
<span>API key: {status.hasRemoteApiKey ? 'Stored' : 'Not set'}</span>
|
||||
<span>API key: {apiKeyStatusLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,11 +468,11 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
<div className="mb-6 flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-mono font-semibold text-term-text">Remote Provider</h3>
|
||||
<HelpIcon tooltip="Configure your OpenAI-compatible AI provider endpoint" />
|
||||
<h3 className="text-base font-mono font-semibold text-term-text">AI Provider</h3>
|
||||
<HelpIcon tooltip="Choose the chat provider runtime and configure its endpoint." />
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-mono leading-6 text-term-text-muted">
|
||||
Connect to an external AI service for enhanced capabilities
|
||||
{providerHelpers.description}
|
||||
</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs font-mono text-term-text">
|
||||
@@ -417,19 +488,36 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-xs font-mono text-term-text-muted">Provider</span>
|
||||
<select
|
||||
value={formState.providerKind}
|
||||
onChange={(event) => handleProviderKindChange(event.target.value as AgentProviderKind)}
|
||||
disabled={isBusy}
|
||||
className="w-full border border-term-border bg-term-surface px-3 py-2 text-sm font-mono text-term-text outline-none transition-all hover:border-term-border-focus focus:border-info disabled:cursor-not-allowed disabled:opacity-50 border-l-2 focus:border-l-info hover:border-l-term-border-focus"
|
||||
>
|
||||
<option value="remote">{AGENT_PROVIDER_LABELS.remote}</option>
|
||||
<option value="ollama_openai">{AGENT_PROVIDER_LABELS.ollama_openai}</option>
|
||||
<option value="ollama_anthropic">{AGENT_PROVIDER_LABELS.ollama_anthropic}</option>
|
||||
</select>
|
||||
<p className="mt-1.5 text-xs font-mono text-term-text-muted">
|
||||
Switches the backend client and auth requirements.
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<ValidatedInput
|
||||
label="Remote Base URL"
|
||||
value={formState.remoteBaseUrl}
|
||||
onChange={(e) => setFormState((prev) => ({ ...prev, remoteBaseUrl: e.target.value }))}
|
||||
placeholder="https://api.example.com/v1"
|
||||
placeholder={providerHelpers.baseUrlPlaceholder}
|
||||
validationStatus={validation.baseUrl}
|
||||
errorMessage={validation.baseUrlError}
|
||||
helperText="The API endpoint URL for your AI provider"
|
||||
helperText={providerHelpers.baseUrlHelper}
|
||||
disabled={isBusy}
|
||||
aria-required="true"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="mb-2 block text-xs font-mono text-term-text-muted">
|
||||
Default Remote Model
|
||||
</label>
|
||||
@@ -437,6 +525,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
value={formState.defaultRemoteModel}
|
||||
onChange={handleDefaultRemoteModelChange}
|
||||
placeholder="Select a model"
|
||||
options={providerModelOptions}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
<p className="mt-1.5 text-xs font-mono text-term-text-muted">
|
||||
@@ -475,6 +564,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
value={formState.taskDefaults[task].model}
|
||||
onChange={(value) => setTaskRoute(task, () => ({ model: value }))}
|
||||
placeholder={formState.defaultRemoteModel || 'Use default model'}
|
||||
options={providerModelOptions}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
</div>
|
||||
@@ -529,14 +619,24 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
<div className="mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-mono font-semibold text-term-text">API Key</h3>
|
||||
<HelpIcon tooltip="Your API key is stored securely and used to authenticate with the remote provider" />
|
||||
<HelpIcon
|
||||
tooltip={
|
||||
usesApiKey
|
||||
? 'Your API key is stored securely and used to authenticate with the selected remote provider.'
|
||||
: 'Ollama compatibility mode ignores API keys. MosaicIQ injects the placeholder token expected by the compatibility layer.'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-mono leading-6 text-term-text-muted">
|
||||
Authentication credential for your AI provider
|
||||
{usesApiKey
|
||||
? 'Authentication credential for your AI provider.'
|
||||
: 'Optional for Ollama. The documented placeholder token is supplied automatically.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{usesApiKey ? (
|
||||
<>
|
||||
<div className="relative">
|
||||
<ValidatedInput
|
||||
label={status.hasRemoteApiKey ? 'Update API Key' : 'API Key'}
|
||||
@@ -548,7 +648,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
helperText={
|
||||
status.hasRemoteApiKey && !formState.remoteApiKey
|
||||
? 'A remote API key is currently stored.'
|
||||
: 'Required for API requests'
|
||||
: 'Required for API requests.'
|
||||
}
|
||||
disabled={isBusy}
|
||||
fullWidth
|
||||
@@ -598,6 +698,26 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-term-border-subtle bg-term-bg px-4 py-3 text-xs font-mono text-term-text-muted">
|
||||
<span>
|
||||
Ollama compatibility mode ignores stored API keys and uses the placeholder token
|
||||
documented by Ollama.
|
||||
</span>
|
||||
{status.hasRemoteApiKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearApiKeyClick}
|
||||
disabled={isBusy}
|
||||
className="flex items-center gap-2 rounded border border-[var(--term-status-error-border)] bg-[var(--term-status-error)] px-4 py-2 text-xs font-mono text-negative transition-colors hover:border-negative hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
Clear Stored Key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface ModelSelectorProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
||||
export const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
||||
{ value: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
|
||||
{ value: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
|
||||
@@ -29,6 +29,13 @@ const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
||||
{ value: 'deepseek-chat', label: 'DeepSeek Chat', provider: 'DeepSeek' },
|
||||
];
|
||||
|
||||
export const OLLAMA_MODEL_OPTIONS: ModelOption[] = [
|
||||
{ value: 'qwen3-coder', label: 'Qwen3 Coder', provider: 'Ollama' },
|
||||
{ value: 'llama3.2', label: 'Llama 3.2', provider: 'Ollama' },
|
||||
{ value: 'mistral', label: 'Mistral', provider: 'Ollama' },
|
||||
{ value: 'deepseek-r1', label: 'DeepSeek R1', provider: 'Ollama' },
|
||||
];
|
||||
|
||||
export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
id,
|
||||
value,
|
||||
|
||||
@@ -91,6 +91,7 @@ export const SecEdgarSettingsCard: React.FC<SecEdgarSettingsCardProps> = ({
|
||||
try {
|
||||
const nextStatus = await agentSettingsBridge.saveRuntimeConfig({
|
||||
remoteEnabled: status.remoteEnabled,
|
||||
providerKind: status.providerKind,
|
||||
remoteBaseUrl: status.remoteBaseUrl,
|
||||
defaultRemoteModel: status.defaultRemoteModel,
|
||||
taskDefaults: status.taskDefaults,
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { AgentConfigStatus } from "../../types/agentSettings";
|
||||
import {
|
||||
AGENT_PROVIDER_LABELS,
|
||||
AgentConfigStatus,
|
||||
} from "../../types/agentSettings";
|
||||
import { AgentSettingsForm } from "./AgentSettingsForm";
|
||||
import { NewsFeedSettingsCard } from "./NewsFeedSettingsCard";
|
||||
import { SecEdgarSettingsCard } from "./SecEdgarSettingsCard";
|
||||
@@ -129,8 +132,12 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Remote provider",
|
||||
value: status?.remoteEnabled ? "Enabled" : "Disabled",
|
||||
label: "AI provider",
|
||||
value: status?.remoteEnabled
|
||||
? status
|
||||
? AGENT_PROVIDER_LABELS[status.providerKind]
|
||||
: "Enabled"
|
||||
: "Disabled",
|
||||
icon: status?.remoteEnabled ? (
|
||||
<Wifi className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -139,7 +146,15 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
},
|
||||
{
|
||||
label: "API key",
|
||||
value: status?.hasRemoteApiKey ? "Stored" : "Missing",
|
||||
value: status
|
||||
? status.providerKind === "remote"
|
||||
? status.hasRemoteApiKey
|
||||
? "Stored"
|
||||
: "Missing"
|
||||
: status.hasRemoteApiKey
|
||||
? "Unused"
|
||||
: "Optional"
|
||||
: "Missing",
|
||||
icon: status?.hasRemoteApiKey ? (
|
||||
<KeyRound className="h-4 w-4" />
|
||||
) : (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type AgentProviderKind = 'remote' | 'ollama_openai' | 'ollama_anthropic';
|
||||
|
||||
export type TaskProfile =
|
||||
| 'interactiveChat'
|
||||
| 'analysis'
|
||||
@@ -16,6 +18,7 @@ export interface AgentConfigStatus {
|
||||
configured: boolean;
|
||||
remoteConfigured: boolean;
|
||||
remoteEnabled: boolean;
|
||||
providerKind: AgentProviderKind;
|
||||
hasRemoteApiKey: boolean;
|
||||
hasSecEdgarUserAgent: boolean;
|
||||
remoteBaseUrl: string;
|
||||
@@ -26,6 +29,7 @@ export interface AgentConfigStatus {
|
||||
|
||||
export interface SaveAgentRuntimeConfigRequest {
|
||||
remoteEnabled: boolean;
|
||||
providerKind: AgentProviderKind;
|
||||
remoteBaseUrl: string;
|
||||
defaultRemoteModel: string;
|
||||
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
|
||||
@@ -47,6 +51,12 @@ export const TASK_PROFILES: TaskProfile[] = [
|
||||
'memoStructuring',
|
||||
];
|
||||
|
||||
export const AGENT_PROVIDER_LABELS: Record<AgentProviderKind, string> = {
|
||||
remote: 'Remote',
|
||||
ollama_openai: 'Ollama (OpenAI Compat)',
|
||||
ollama_anthropic: 'Ollama (Anthropic Compat)',
|
||||
};
|
||||
|
||||
export const TASK_LABELS: Record<TaskProfile, string> = {
|
||||
interactiveChat: 'Interactive Chat',
|
||||
analysis: 'Analysis',
|
||||
|
||||
Reference in New Issue
Block a user