mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
community batch
This commit is contained in:
Generated
+14
-14
@@ -3875,7 +3875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3912,7 +3912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3944,7 +3944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3971,7 +3971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3997,7 +3997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -4025,7 +4025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -4042,7 +4042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4078,7 +4078,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4097,7 +4097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4116,7 +4116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4148,7 +4148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4171,7 +4171,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4190,7 +4190,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8818,7 +8818,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
@@ -196,6 +196,10 @@ pub async fn security_headers(request: Request<Body>, next: Next) -> Response<Bo
|
||||
"cache-control",
|
||||
"no-store, no-cache, must-revalidate".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"strict-transport-security",
|
||||
"max-age=63072000; includeSubDomains".parse().unwrap(),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
@@ -5030,6 +5030,90 @@ pub async fn update_agent(
|
||||
)
|
||||
}
|
||||
|
||||
/// PATCH /api/agents/{id} — Partial update of agent fields (name, description, model, system_prompt).
|
||||
pub async fn patch_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let agent_id: AgentId = match id.parse() {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "Invalid agent ID"})),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if state.kernel.registry.get(agent_id).is_none() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Agent not found"})),
|
||||
);
|
||||
}
|
||||
|
||||
// Apply partial updates using dedicated registry methods
|
||||
if let Some(name) = body.get("name").and_then(|v| v.as_str()) {
|
||||
if let Err(e) = state
|
||||
.kernel
|
||||
.registry
|
||||
.update_name(agent_id, name.to_string())
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(desc) = body.get("description").and_then(|v| v.as_str()) {
|
||||
if let Err(e) = state
|
||||
.kernel
|
||||
.registry
|
||||
.update_description(agent_id, desc.to_string())
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(model) = body.get("model").and_then(|v| v.as_str()) {
|
||||
if let Err(e) = state.kernel.set_agent_model(agent_id, model) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(system_prompt) = body.get("system_prompt").and_then(|v| v.as_str()) {
|
||||
if let Err(e) = state
|
||||
.kernel
|
||||
.registry
|
||||
.update_system_prompt(agent_id, system_prompt.to_string())
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist updated entry to SQLite
|
||||
if let Some(entry) = state.kernel.registry.get(agent_id) {
|
||||
let _ = state.kernel.memory.save_agent(&entry);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"status": "ok", "agent_id": entry.id.to_string(), "name": entry.name})),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": "Agent vanished during update"})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migration endpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -6113,6 +6197,12 @@ pub async fn clear_agent_history(
|
||||
)
|
||||
}
|
||||
};
|
||||
if state.kernel.registry.get(agent_id).is_none() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Agent not found"})),
|
||||
);
|
||||
}
|
||||
match state.kernel.clear_agent_history(agent_id) {
|
||||
Ok(()) => (
|
||||
StatusCode::OK,
|
||||
|
||||
@@ -126,7 +126,7 @@ pub async fn build_router(
|
||||
)
|
||||
.route(
|
||||
"/api/agents/{id}",
|
||||
axum::routing::get(routes::get_agent).delete(routes::kill_agent),
|
||||
axum::routing::get(routes::get_agent).delete(routes::kill_agent).patch(routes::patch_agent),
|
||||
)
|
||||
.route(
|
||||
"/api/agents/{id}/mode",
|
||||
|
||||
@@ -69,6 +69,32 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Card-based flex containers for agent chips and similar inline layouts */
|
||||
.card-flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Nested list indentation inside cards, detail panels, and modals */
|
||||
.card ul, .card ol,
|
||||
.detail-grid ul, .detail-grid ol,
|
||||
.modal ul, .modal ol,
|
||||
.info-card ul, .info-card ol {
|
||||
padding-left: 18px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.card ul ul, .card ol ol,
|
||||
.modal ul ul, .modal ol ol {
|
||||
padding-left: 16px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.card li, .modal li, .info-card li {
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Glow effect on card hover */
|
||||
.card-glow {
|
||||
overflow: hidden;
|
||||
@@ -90,13 +116,17 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.badge + .badge { margin-left: 4px; }
|
||||
|
||||
.badge-running { background: rgba(74,222,128,0.12); color: var(--success); }
|
||||
.badge-suspended { background: rgba(245,158,11,0.12); color: var(--warning); }
|
||||
@@ -110,7 +140,7 @@
|
||||
.badge-error { background: rgba(239,68,68,0.12); color: var(--error); }
|
||||
.badge-muted { background: rgba(148,163,184,0.12); color: var(--text-dim); }
|
||||
.badge-info { background: rgba(59,130,246,0.12); color: var(--info); }
|
||||
.badge-dim { background: rgba(148,163,184,0.08); color: var(--text-dim); font-size: 0.65rem; }
|
||||
.badge-dim { background: rgba(148,163,184,0.08); color: var(--text-dim); font-size: 0.65rem; padding: 2px 6px; }
|
||||
.text-danger { color: var(--error); }
|
||||
|
||||
/* Tables */
|
||||
@@ -949,6 +979,14 @@ mark.search-highlight {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.model-switcher-search select {
|
||||
max-width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.model-switcher-search select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.model-switcher-search input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
|
||||
@@ -769,6 +769,12 @@
|
||||
<div class="model-switcher-search">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0;opacity:0.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input id="model-switcher-search" type="text" x-model="modelSwitcherFilter" placeholder="Search models..." @keydown.escape.stop="showModelSwitcher = false" @keydown.arrow-down.prevent="modelSwitcherIdx = Math.min(modelSwitcherIdx + 1, filteredSwitcherModels.length - 1)" @keydown.arrow-up.prevent="modelSwitcherIdx = Math.max(modelSwitcherIdx - 1, 0)" @keydown.enter.prevent="filteredSwitcherModels[modelSwitcherIdx] && switchModel(filteredSwitcherModels[modelSwitcherIdx])">
|
||||
<select x-model="modelSwitcherProviderFilter" style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text-dim);font-size:11px;padding:2px 6px;cursor:pointer;font-family:var(--font-mono);flex-shrink:0">
|
||||
<option value="">All</option>
|
||||
<template x-for="pn in switcherProviders" :key="pn">
|
||||
<option :value="pn" x-text="pn"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="modelSwitching" style="display:flex;align-items:center;justify-content:center;padding:12px;gap:8px">
|
||||
<div class="tool-card-spinner"></div>
|
||||
@@ -841,6 +847,9 @@
|
||||
<div class="text-xs text-dim font-mono" style="font-size:11px" x-text="agent.model_name"></div>
|
||||
</div>
|
||||
<span class="badge" :class="'badge-' + agent.state.toLowerCase()" x-text="agent.state" style="font-size:10px"></span>
|
||||
<button class="agent-chip-config-btn" @click.stop="showDetail(agent)" title="Agent settings" style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;border:1px solid var(--border);background:transparent;cursor:pointer;color:var(--text-dim);transition:all 0.15s;flex-shrink:0" @mouseenter="$el.style.borderColor='var(--accent)';$el.style.color='var(--accent)';$el.style.background='var(--surface2)'" @mouseleave="$el.style.borderColor='var(--border)';$el.style.color='var(--text-dim)';$el.style.background='transparent'">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ function chatPage() {
|
||||
// Model switcher dropdown
|
||||
showModelSwitcher: false,
|
||||
modelSwitcherFilter: '',
|
||||
modelSwitcherProviderFilter: '',
|
||||
modelSwitcherIdx: 0,
|
||||
modelSwitching: false,
|
||||
_modelCache: null,
|
||||
@@ -99,14 +100,25 @@ function chatPage() {
|
||||
return short.length > 24 ? short.substring(0, 22) + '\u2026' : short;
|
||||
},
|
||||
|
||||
get switcherProviders() {
|
||||
var seen = {};
|
||||
(this._modelCache || []).forEach(function(m) { seen[m.provider] = true; });
|
||||
return Object.keys(seen).sort();
|
||||
},
|
||||
|
||||
get filteredSwitcherModels() {
|
||||
var models = this._modelCache || [];
|
||||
if (!this.modelSwitcherFilter) return models;
|
||||
var f = this.modelSwitcherFilter.toLowerCase();
|
||||
var provFilter = this.modelSwitcherProviderFilter;
|
||||
var textFilter = this.modelSwitcherFilter ? this.modelSwitcherFilter.toLowerCase() : '';
|
||||
if (!provFilter && !textFilter) return models;
|
||||
return models.filter(function(m) {
|
||||
return m.id.toLowerCase().indexOf(f) !== -1 ||
|
||||
(m.display_name || '').toLowerCase().indexOf(f) !== -1 ||
|
||||
m.provider.toLowerCase().indexOf(f) !== -1;
|
||||
if (provFilter && m.provider !== provFilter) return false;
|
||||
if (textFilter) {
|
||||
return m.id.toLowerCase().indexOf(textFilter) !== -1 ||
|
||||
(m.display_name || '').toLowerCase().indexOf(textFilter) !== -1 ||
|
||||
m.provider.toLowerCase().indexOf(textFilter) !== -1;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -220,6 +232,7 @@ function chatPage() {
|
||||
var now = Date.now();
|
||||
if (this._modelCache && (now - this._modelCacheTime) < 300000) {
|
||||
this.modelSwitcherFilter = '';
|
||||
this.modelSwitcherProviderFilter = '';
|
||||
this.modelSwitcherIdx = 0;
|
||||
this.showModelSwitcher = true;
|
||||
this.$nextTick(function() {
|
||||
@@ -234,6 +247,7 @@ function chatPage() {
|
||||
self._modelCacheTime = Date.now();
|
||||
self.modelPickerList = models;
|
||||
self.modelSwitcherFilter = '';
|
||||
self.modelSwitcherProviderFilter = '';
|
||||
self.modelSwitcherIdx = 0;
|
||||
self.showModelSwitcher = true;
|
||||
self.$nextTick(function() {
|
||||
|
||||
@@ -572,7 +572,12 @@ impl OpenFangKernel {
|
||||
}
|
||||
}
|
||||
|
||||
// Add fallback providers to the chain
|
||||
// Add fallback providers to the chain (with model names for cross-provider fallback)
|
||||
let mut model_chain: Vec<(Arc<dyn LlmDriver>, String)> = Vec::new();
|
||||
// Primary driver uses empty model name (uses the request's model field as-is)
|
||||
for d in &driver_chain {
|
||||
model_chain.push((d.clone(), String::new()));
|
||||
}
|
||||
for fb in &config.fallback_providers {
|
||||
let fb_config = DriverConfig {
|
||||
provider: fb.provider.clone(),
|
||||
@@ -593,7 +598,8 @@ impl OpenFangKernel {
|
||||
model = %fb.model,
|
||||
"Fallback provider configured"
|
||||
);
|
||||
driver_chain.push(d);
|
||||
driver_chain.push(d.clone());
|
||||
model_chain.push((d, fb.model.clone()));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -607,8 +613,8 @@ impl OpenFangKernel {
|
||||
|
||||
// Use the chain, or create a stub driver if everything failed
|
||||
let driver: Arc<dyn LlmDriver> = if driver_chain.len() > 1 {
|
||||
Arc::new(openfang_runtime::drivers::fallback::FallbackDriver::new(
|
||||
driver_chain,
|
||||
Arc::new(openfang_runtime::drivers::fallback::FallbackDriver::with_models(
|
||||
model_chain,
|
||||
))
|
||||
} else if let Some(single) = driver_chain.into_iter().next() {
|
||||
single
|
||||
|
||||
@@ -14,7 +14,8 @@ pub mod openai;
|
||||
use crate::llm_driver::{DriverConfig, LlmDriver, LlmError};
|
||||
use openfang_types::model_catalog::{
|
||||
AI21_BASE_URL, ANTHROPIC_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL,
|
||||
FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LMSTUDIO_BASE_URL,
|
||||
FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LEMONADE_BASE_URL,
|
||||
LMSTUDIO_BASE_URL,
|
||||
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL,
|
||||
OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
|
||||
REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
|
||||
@@ -89,6 +90,11 @@ fn provider_defaults(provider: &str) -> Option<ProviderDefaults> {
|
||||
api_key_env: "LMSTUDIO_API_KEY",
|
||||
key_required: false,
|
||||
}),
|
||||
"lemonade" => Some(ProviderDefaults {
|
||||
base_url: LEMONADE_BASE_URL,
|
||||
api_key_env: "LEMONADE_API_KEY",
|
||||
key_required: false,
|
||||
}),
|
||||
"perplexity" => Some(ProviderDefaults {
|
||||
base_url: PERPLEXITY_BASE_URL,
|
||||
api_key_env: "PERPLEXITY_API_KEY",
|
||||
|
||||
@@ -8,7 +8,8 @@ use openfang_types::model_catalog::{
|
||||
BEDROCK_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL,
|
||||
GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL,
|
||||
LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL,
|
||||
OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
|
||||
LEMONADE_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL,
|
||||
QIANFAN_BASE_URL, QWEN_BASE_URL,
|
||||
REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
|
||||
VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
|
||||
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
|
||||
@@ -499,6 +500,15 @@ fn builtin_providers() -> Vec<ProviderInfo> {
|
||||
auth_status: AuthStatus::NotRequired,
|
||||
model_count: 0,
|
||||
},
|
||||
ProviderInfo {
|
||||
id: "lemonade".into(),
|
||||
display_name: "Lemonade".into(),
|
||||
api_key_env: "LEMONADE_API_KEY".into(),
|
||||
base_url: LEMONADE_BASE_URL.into(),
|
||||
key_required: false,
|
||||
auth_status: AuthStatus::NotRequired,
|
||||
model_count: 0,
|
||||
},
|
||||
// ── New providers (8) ──────────────────────────────────────
|
||||
ProviderInfo {
|
||||
id: "perplexity".into(),
|
||||
@@ -768,7 +778,7 @@ fn builtin_aliases() -> HashMap<String, String> {
|
||||
("qwen", "qwen-plus"),
|
||||
("glm", "glm-5-20250605"),
|
||||
("ernie", "ernie-4.5-8k"),
|
||||
("kimi", "moonshot-v1-128k"),
|
||||
("kimi", "kimi-k2-0711"),
|
||||
("minimax", "MiniMax-M2.5"),
|
||||
("minimax-m2.5", "MiniMax-M2.5"),
|
||||
("minimax-m2.1", "MiniMax-M2.1"),
|
||||
@@ -2843,7 +2853,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec!["codegeex".into()],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Moonshot / Kimi (3)
|
||||
// Moonshot / Kimi (5)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "moonshot-v1-128k".into(),
|
||||
@@ -2857,7 +2867,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["kimi".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "moonshot-v1-32k".into(),
|
||||
@@ -2887,6 +2897,34 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "kimi-k2-0711".into(),
|
||||
display_name: "Kimi K2".into(),
|
||||
provider: "moonshot".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 131_072,
|
||||
max_output_tokens: 16_384,
|
||||
input_cost_per_m: 2.00,
|
||||
output_cost_per_m: 8.00,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["kimi-k2".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "kimi-k2.5-0711".into(),
|
||||
display_name: "Kimi K2.5".into(),
|
||||
provider: "moonshot".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 131_072,
|
||||
max_output_tokens: 16_384,
|
||||
input_cost_per_m: 2.00,
|
||||
output_cost_per_m: 8.00,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["kimi-k2.5".into()],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Baidu Qianfan / ERNIE (3)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -3243,7 +3281,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_catalog_has_providers() {
|
||||
let catalog = ModelCatalog::new();
|
||||
assert_eq!(catalog.list_providers().len(), 35);
|
||||
assert_eq!(catalog.list_providers().len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -296,6 +296,9 @@ pub async fn execute_tool(
|
||||
// Location tool
|
||||
"location_get" => tool_location_get().await,
|
||||
|
||||
// System time tool
|
||||
"system_time" => Ok(tool_system_time()),
|
||||
|
||||
// Cron scheduling tools
|
||||
"cron_create" => tool_cron_create(input, kernel, caller_agent_id).await,
|
||||
"cron_list" => tool_cron_list(kernel, caller_agent_id).await,
|
||||
@@ -1177,6 +1180,16 @@ pub fn builtin_tool_definitions() -> Vec<ToolDefinition> {
|
||||
"properties": {}
|
||||
}),
|
||||
},
|
||||
// --- System time tool ---
|
||||
ToolDefinition {
|
||||
name: "system_time".to_string(),
|
||||
description: "Get the current date, time, and timezone. Returns ISO 8601 timestamp, Unix epoch seconds, and timezone info.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
},
|
||||
// --- Canvas / A2UI tool ---
|
||||
ToolDefinition {
|
||||
name: "canvas_present".to_string(),
|
||||
@@ -2537,6 +2550,27 @@ async fn tool_location_get() -> Result<String, String> {
|
||||
serde_json::to_string_pretty(&result).map_err(|e| format!("Serialize error: {e}"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System time tool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return current date, time, timezone, and Unix epoch.
|
||||
fn tool_system_time() -> String {
|
||||
let now_utc = chrono::Utc::now();
|
||||
let now_local = chrono::Local::now();
|
||||
let result = serde_json::json!({
|
||||
"utc": now_utc.to_rfc3339(),
|
||||
"local": now_local.to_rfc3339(),
|
||||
"unix_epoch": now_utc.timestamp(),
|
||||
"timezone": now_local.format("%Z").to_string(),
|
||||
"utc_offset": now_local.format("%:z").to_string(),
|
||||
"date": now_local.format("%Y-%m-%d").to_string(),
|
||||
"time": now_local.format("%H:%M:%S").to_string(),
|
||||
"day_of_week": now_local.format("%A").to_string(),
|
||||
});
|
||||
serde_json::to_string_pretty(&result).unwrap_or_else(|_| now_utc.to_rfc3339())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Media understanding tools
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -3114,6 +3148,7 @@ mod tests {
|
||||
assert!(names.contains(&"schedule_delete"));
|
||||
assert!(names.contains(&"image_analyze"));
|
||||
assert!(names.contains(&"location_get"));
|
||||
assert!(names.contains(&"system_time"));
|
||||
// 6 browser tools
|
||||
assert!(names.contains(&"browser_navigate"));
|
||||
assert!(names.contains(&"browser_click"));
|
||||
|
||||
@@ -593,14 +593,25 @@ impl ClawHubClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal URL-encoding for query parameters.
|
||||
/// RFC 3986 percent-encoding for query parameters.
|
||||
/// Unreserved characters pass through, space becomes `+`, everything else is `%XX`.
|
||||
fn urlencoded(s: &str) -> String {
|
||||
s.replace(' ', "+")
|
||||
.replace('&', "%26")
|
||||
.replace('=', "%3D")
|
||||
.replace('?', "%3F")
|
||||
.replace('#', "%23")
|
||||
.replace('/', "%2F")
|
||||
const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
|
||||
let mut result = String::with_capacity(s.len() * 3);
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
result.push(b as char);
|
||||
}
|
||||
b' ' => result.push('+'),
|
||||
_ => {
|
||||
result.push('%');
|
||||
result.push(HEX_UPPER[(b >> 4) as usize] as char);
|
||||
result.push(HEX_UPPER[(b & 0xf) as usize] as char);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if a binary is available on PATH.
|
||||
@@ -786,6 +797,11 @@ mod tests {
|
||||
assert_eq!(urlencoded("hello world"), "hello+world");
|
||||
assert_eq!(urlencoded("a&b=c"), "a%26b%3Dc");
|
||||
assert_eq!(urlencoded("path/to#frag"), "path%2Fto%23frag");
|
||||
// Previously missed characters
|
||||
assert_eq!(urlencoded("100%"), "100%25");
|
||||
assert_eq!(urlencoded("a+b"), "a%2Bb");
|
||||
// Unreserved chars pass through
|
||||
assert_eq!(urlencoded("hello-world_2.0~test"), "hello-world_2.0~test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -474,7 +474,8 @@ pub struct AgentManifest {
|
||||
#[serde(default = "default_true")]
|
||||
pub generate_identity_files: bool,
|
||||
/// Per-agent exec policy override. If None, uses global exec_policy.
|
||||
#[serde(default)]
|
||||
/// Accepts string shorthand ("allow", "deny", "full", "allowlist") or full table.
|
||||
#[serde(default, deserialize_with = "crate::serde_compat::exec_policy_lenient")]
|
||||
pub exec_policy: Option<crate::config::ExecPolicy>,
|
||||
/// Tool allowlist — only these tools are available (empty = all tools).
|
||||
#[serde(default, deserialize_with = "crate::serde_compat::vec_lenient")]
|
||||
|
||||
@@ -20,6 +20,7 @@ pub const FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference/v1";
|
||||
pub const OLLAMA_BASE_URL: &str = "http://localhost:11434/v1";
|
||||
pub const VLLM_BASE_URL: &str = "http://localhost:8000/v1";
|
||||
pub const LMSTUDIO_BASE_URL: &str = "http://localhost:1234/v1";
|
||||
pub const LEMONADE_BASE_URL: &str = "http://localhost:8888/api/v1";
|
||||
pub const PERPLEXITY_BASE_URL: &str = "https://api.perplexity.ai";
|
||||
pub const COHERE_BASE_URL: &str = "https://api.cohere.com/v2";
|
||||
pub const AI21_BASE_URL: &str = "https://api.ai21.com/studio/v1";
|
||||
|
||||
@@ -157,6 +157,70 @@ where
|
||||
deserializer.deserialize_any(MapLenientVisitor(PhantomData))
|
||||
}
|
||||
|
||||
/// Deserialize an `Option<ExecPolicy>` leniently: accepts either a string
|
||||
/// shorthand (e.g., `"allow"`, `"deny"`, `"full"`, `"allowlist"`) which maps
|
||||
/// to `ExecPolicy { mode: <parsed>, ..Default::default() }`, or the full
|
||||
/// struct/table form. Returns `None` for null/missing.
|
||||
pub fn exec_policy_lenient<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<crate::config::ExecPolicy>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ExecPolicyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ExecPolicyVisitor {
|
||||
type Value = Option<crate::config::ExecPolicy>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str(
|
||||
"a string shorthand (\"allow\", \"deny\", \"full\", \"allowlist\") or an ExecPolicy table",
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
let mode = match v.to_lowercase().as_str() {
|
||||
"deny" | "none" | "disabled" => crate::config::ExecSecurityMode::Deny,
|
||||
"allowlist" | "restricted" => crate::config::ExecSecurityMode::Allowlist,
|
||||
"full" | "allow" | "all" | "unrestricted" => crate::config::ExecSecurityMode::Full,
|
||||
other => {
|
||||
return Err(de::Error::unknown_variant(
|
||||
other,
|
||||
&[
|
||||
"deny", "none", "disabled", "allowlist", "restricted", "full",
|
||||
"allow", "all", "unrestricted",
|
||||
],
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Some(crate::config::ExecPolicy {
|
||||
mode,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let policy = crate::config::ExecPolicy::deserialize(
|
||||
de::value::MapAccessDeserializer::new(map),
|
||||
)?;
|
||||
Ok(Some(policy))
|
||||
}
|
||||
|
||||
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ExecPolicyVisitor)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -303,4 +367,83 @@ mod tests {
|
||||
assert!(new.fallback_models.is_empty());
|
||||
assert!(new.skills.is_empty());
|
||||
}
|
||||
|
||||
// --- exec_policy_lenient tests ---
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestExecPolicy {
|
||||
#[serde(default, deserialize_with = "exec_policy_lenient")]
|
||||
exec_policy: Option<crate::config::ExecPolicy>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_string_allow() {
|
||||
let toml_str = r#"exec_policy = "allow""#;
|
||||
let parsed: TestExecPolicy = toml::from_str(toml_str).unwrap();
|
||||
let policy = parsed.exec_policy.unwrap();
|
||||
assert_eq!(policy.mode, crate::config::ExecSecurityMode::Full);
|
||||
// Should have default safe_bins, timeout, etc.
|
||||
assert!(!policy.safe_bins.is_empty());
|
||||
assert_eq!(policy.timeout_secs, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_string_deny() {
|
||||
let toml_str = r#"exec_policy = "deny""#;
|
||||
let parsed: TestExecPolicy = toml::from_str(toml_str).unwrap();
|
||||
let policy = parsed.exec_policy.unwrap();
|
||||
assert_eq!(policy.mode, crate::config::ExecSecurityMode::Deny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_string_full() {
|
||||
let toml_str = r#"exec_policy = "full""#;
|
||||
let parsed: TestExecPolicy = toml::from_str(toml_str).unwrap();
|
||||
let policy = parsed.exec_policy.unwrap();
|
||||
assert_eq!(policy.mode, crate::config::ExecSecurityMode::Full);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_string_allowlist() {
|
||||
let toml_str = r#"exec_policy = "allowlist""#;
|
||||
let parsed: TestExecPolicy = toml::from_str(toml_str).unwrap();
|
||||
let policy = parsed.exec_policy.unwrap();
|
||||
assert_eq!(policy.mode, crate::config::ExecSecurityMode::Allowlist);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_table_form() {
|
||||
let toml_str = r#"
|
||||
[exec_policy]
|
||||
mode = "full"
|
||||
timeout_secs = 60
|
||||
"#;
|
||||
let parsed: TestExecPolicy = toml::from_str(toml_str).unwrap();
|
||||
let policy = parsed.exec_policy.unwrap();
|
||||
assert_eq!(policy.mode, crate::config::ExecSecurityMode::Full);
|
||||
assert_eq!(policy.timeout_secs, 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_missing_is_none() {
|
||||
let toml_str = r#"other_field = true"#;
|
||||
// Use a struct with an extra ignored field
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(default, deserialize_with = "exec_policy_lenient")]
|
||||
exec_policy: Option<crate::config::ExecPolicy>,
|
||||
#[allow(dead_code)]
|
||||
#[serde(default)]
|
||||
other_field: bool,
|
||||
}
|
||||
let parsed: Wrapper = toml::from_str(toml_str).unwrap();
|
||||
assert!(parsed.exec_policy.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_policy_string_invalid_errors() {
|
||||
let toml_str = r#"exec_policy = "banana""#;
|
||||
let result = toml::from_str::<TestExecPolicy>(toml_str);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+25
-5
@@ -115,19 +115,39 @@ install() {
|
||||
codesign --force --sign - "$INSTALL_DIR/openfang" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Add to PATH
|
||||
# Add to PATH — detect the user's login shell
|
||||
USER_SHELL="${SHELL:-}"
|
||||
# Fallback: check /etc/passwd if $SHELL is unset (e.g. minimal containers)
|
||||
if [ -z "$USER_SHELL" ] && command -v getent &>/dev/null; then
|
||||
USER_SHELL=$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)
|
||||
fi
|
||||
if [ -z "$USER_SHELL" ] && [ -f /etc/passwd ]; then
|
||||
USER_SHELL=$(grep "^$(id -un):" /etc/passwd 2>/dev/null | cut -d: -f7)
|
||||
fi
|
||||
|
||||
SHELL_RC=""
|
||||
case "${SHELL:-}" in
|
||||
*/zsh) SHELL_RC="$HOME/.zshrc" ;;
|
||||
case "$USER_SHELL" in
|
||||
*/zsh) SHELL_RC="$HOME/.zshrc" ;;
|
||||
*/bash) SHELL_RC="$HOME/.bashrc" ;;
|
||||
*/fish) SHELL_RC="$HOME/.config/fish/config.fish" ;;
|
||||
esac
|
||||
# Also check for config files if shell detection failed
|
||||
if [ -z "$SHELL_RC" ]; then
|
||||
if [ -f "$HOME/.config/fish/config.fish" ]; then
|
||||
SHELL_RC="$HOME/.config/fish/config.fish"
|
||||
USER_SHELL="/usr/bin/fish"
|
||||
elif [ -f "$HOME/.zshrc" ]; then
|
||||
SHELL_RC="$HOME/.zshrc"
|
||||
elif [ -f "$HOME/.bashrc" ]; then
|
||||
SHELL_RC="$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$SHELL_RC" ] && ! grep -q "openfang" "$SHELL_RC" 2>/dev/null; then
|
||||
case "${SHELL:-}" in
|
||||
case "$USER_SHELL" in
|
||||
*/fish)
|
||||
mkdir -p "$(dirname "$SHELL_RC")"
|
||||
echo "set -gx PATH \"$INSTALL_DIR\" \$PATH" >> "$SHELL_RC"
|
||||
echo "fish_add_path \"$INSTALL_DIR\"" >> "$SHELL_RC"
|
||||
;;
|
||||
*)
|
||||
echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$SHELL_RC"
|
||||
|
||||
Reference in New Issue
Block a user