diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html
index 0a87a94e..29ed4b76 100644
--- a/crates/openfang-api/static/index_body.html
+++ b/crates/openfang-api/static/index_body.html
@@ -922,7 +922,23 @@
Profile
- Provider
+ Provider
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Model
diff --git a/crates/openfang-api/static/js/pages/agents.js b/crates/openfang-api/static/js/pages/agents.js
index 5a3c7df4..86ecfaf3 100644
--- a/crates/openfang-api/static/js/pages/agents.js
+++ b/crates/openfang-api/static/js/pages/agents.js
@@ -77,6 +77,8 @@ function agentsPage() {
// -- Model switch --
editingModel: false,
newModelValue: '',
+ editingProvider: false,
+ newProviderValue: '',
modelSaving: false,
// -- Fallback chain --
editingFallback: false,
@@ -638,6 +640,26 @@ function agentsPage() {
this.modelSaving = false;
},
+ // ── Provider switch ──
+ async changeProvider() {
+ if (!this.detailAgent || !this.newProviderValue.trim()) return;
+ this.modelSaving = true;
+ try {
+ var combined = this.newProviderValue.trim() + '/' + this.detailAgent.model_name;
+ var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: combined });
+ OpenFangToast.success('Provider changed to ' + (resp && resp.provider ? resp.provider : this.newProviderValue.trim()));
+ this.editingProvider = false;
+ await Alpine.store('app').refreshAgents();
+ var agents = Alpine.store('app').agents;
+ for (var i = 0; i < agents.length; i++) {
+ if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; }
+ }
+ } catch(e) {
+ OpenFangToast.error('Failed to change provider: ' + e.message);
+ }
+ this.modelSaving = false;
+ },
+
// ── Fallback model chain ──
async addFallback() {
if (!this.detailAgent || !this.newFallbackValue.trim()) return;
diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs
index 573e7c6f..3f906583 100644
--- a/crates/openfang-kernel/src/kernel.rs
+++ b/crates/openfang-kernel/src/kernel.rs
@@ -5318,6 +5318,12 @@ fn infer_provider_from_model(model: &str) -> Option {
(lower.as_str(), false)
};
if has_delim {
+ // Two or more slashes (e.g. "mlx-lm-lg/mlx-community/Qwen3-4B") means
+ // the first segment is explicitly a provider prefix — HuggingFace repo
+ // IDs only have one slash, so extra slashes are unambiguous.
+ if lower.chars().filter(|&c| c == '/').count() >= 2 {
+ return Some(prefix.to_string());
+ }
match prefix {
"minimax" | "gemini" | "anthropic" | "openai" | "groq" | "deepseek" | "mistral"
| "cohere" | "xai" | "ollama" | "together" | "fireworks" | "perplexity"
diff --git a/packages/whatsapp-gateway/index.js b/packages/whatsapp-gateway/index.js
index 8da94bf4..973d6b3f 100644
--- a/packages/whatsapp-gateway/index.js
+++ b/packages/whatsapp-gateway/index.js
@@ -124,7 +124,9 @@ async function startConnection() {
if (msg.key.fromMe) continue;
if (msg.key.remoteJid === 'status@broadcast') continue;
- const sender = msg.key.remoteJid || '';
+ const remoteJid = msg.key.remoteJid || '';
+ const isGroup = remoteJid.endsWith('@g.us');
+
let text = msg.message?.conversation
|| msg.message?.extendedTextMessage?.text
|| msg.message?.imageMessage?.caption
@@ -141,19 +143,32 @@ async function startConnection() {
else continue; // Only skip truly empty messages
}
- // Extract phone number from JID (e.g. "1234567890@s.whatsapp.net" → "+1234567890")
- const phone = '+' + sender.replace(/@.*$/, '');
+ // For groups: real sender is in participant; for DMs: it's remoteJid
+ const senderJid = isGroup ? (msg.key.participant || '') : remoteJid;
+ const phone = '+' + senderJid.replace(/@.*$/, '');
const pushName = msg.pushName || phone;
- console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`);
+ const metadata = {
+ channel: 'whatsapp',
+ sender: phone,
+ sender_name: pushName,
+ };
+ if (isGroup) {
+ metadata.group_jid = remoteJid;
+ metadata.is_group = true;
+ console.log(`[gateway] Group msg from ${pushName} (${phone}) in ${remoteJid}: ${text.substring(0, 80)}`);
+ } else {
+ console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`);
+ }
// Forward to OpenFang agent
try {
- const response = await forwardToOpenFang(text, phone, pushName);
+ const response = await forwardToOpenFang(text, phone, pushName, metadata);
if (response && sock) {
- // Send agent response back to WhatsApp
- await sock.sendMessage(sender, { text: response });
- console.log(`[gateway] Replied to ${pushName}`);
+ // Reply in the same context: group → group, DM → DM
+ const replyJid = isGroup ? remoteJid : senderJid.replace(/@.*$/, '') + '@s.whatsapp.net';
+ await sock.sendMessage(replyJid, { text: response });
+ console.log(`[gateway] Replied to ${pushName}${isGroup ? ' in group ' + remoteJid : ''}`);
}
} catch (err) {
console.error(`[gateway] Forward/reply failed:`, err.message);
@@ -165,11 +180,11 @@ async function startConnection() {
// ---------------------------------------------------------------------------
// Forward incoming message to OpenFang API, return agent response
// ---------------------------------------------------------------------------
-function forwardToOpenFang(text, phone, pushName) {
+function forwardToOpenFang(text, phone, pushName, metadata) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify({
message: text,
- metadata: {
+ metadata: metadata || {
channel: 'whatsapp',
sender: phone,
sender_name: pushName,
@@ -223,8 +238,8 @@ async function sendMessage(to, text) {
throw new Error('WhatsApp not connected');
}
- // Normalize phone → JID: "+1234567890" → "1234567890@s.whatsapp.net"
- const jid = to.replace(/^\+/, '').replace(/@.*$/, '') + '@s.whatsapp.net';
+ // If already a full JID (group or user), use as-is; otherwise normalize phone → JID
+ const jid = to.includes('@') ? to : to.replace(/^\+/, '') + '@s.whatsapp.net';
await sock.sendMessage(jid, { text });
}