working version of the extension

This commit is contained in:
Salman Paracha 2025-07-02 14:52:32 -07:00
parent c9539d21f7
commit a872f9eec2
4 changed files with 228 additions and 158 deletions

View file

@ -129,17 +129,24 @@ export default function PreferenceBasedModelSelector() {
// Save settings: generate name slug and store tuples
const handleSave = () => {
const tuples = preferences.map((p) => {
const slug = p.usage.split(/\s+/).slice(0, 3).join('-').toLowerCase() || 'route';
return { name: slug, usage: p.usage, model: p.model };
});
// Only keep valid preferences with non-empty usage
const tuples = preferences
.filter(p => p.usage?.trim())
.map((p) => {
const slug = p.usage.split(/\s+/).slice(0, 3).join('-').toLowerCase() || 'route';
return { name: slug, usage: p.usage, model: p.model };
});
chrome.storage.sync.set({ routingEnabled, preferences: tuples, defaultModel }, () => {
console.log('[PBMS] Saved tuples:', tuples);
});
// Notify content script
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'applyModelSelection', model: defaultModel });
});
// Close the modal
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
};

View file

@ -1,36 +0,0 @@
// background.js
chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg.type === 'ARCHGW_STREAM') {
const { url, init, port } = msg;
fetch(url, {
method: init.method || 'POST',
headers: init.headers,
body: init.body,
credentials: init.credentials ?? 'same-origin',
// ...copy other init options as needed
}).then(res => {
const reader = res.body.getReader();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
port.postMessage({ done: true });
port.close();
} else {
// Send each chunk (Uint8Array) to the content script
port.postMessage({ chunk: value.buffer }, [value.buffer]);
read();
}
});
}
read();
}).catch(err => {
// In case of error, signal done
port.postMessage({ done: true });
port.close();
});
// Indicate were handling this asynchronously
return true;
}
});

View file

@ -1,17 +1,107 @@
// content.js
(() => {
const TAG = '[ModelSelector]';
/**─────────────────────── 0⃣ Broadcast initial settings ───────────────────────**/
chrome.storage.sync.get(['preferences','defaultModel'], settings => {
window.postMessage({ type: 'PBMS_SETTINGS', settings }, '*');
});
chrome.storage.onChanged.addListener(() => {
chrome.storage.sync.get(['preferences','defaultModel'], settings => {
window.postMessage({ type: 'PBMS_SETTINGS', settings }, '*');
/**─────────────────────── 🔧 Utility: Scrape Current Messages ───────────────────────**/
function getMessagesFromDom() {
const bubbles = [...document.querySelectorAll('[data-message-author-role]')];
return bubbles
.map(b => {
const role = b.getAttribute('data-message-author-role');
const content =
role === 'assistant'
? (b.querySelector('.markdown')?.innerText ?? b.innerText ?? '').trim()
: (b.innerText ?? '').trim();
return content ? { role, content } : null;
})
.filter(Boolean);
}
/**─────────────────────── 🔧 Utility: Prepare Request to Proxy ───────────────────────**/
function prepareProxyRequest(messages, routes, maxTokenLength = 2048) {
const SYSTEM_PROMPT_TEMPLATE = `
You are a helpful assistant designed to find the best suited route.
You are provided with route description within <routes></routes> XML tags:
<routes>
{routes}
</routes>
<conversation>
{conversation}
</conversation>
Your task is to decide which route is best suit with user intent on the conversation in <conversation></conversation> XML tags. Follow the instruction:
1. If the latest intent from user is irrelevant or user intent is full filled, response with other route {"route": "other"}.
2. You must analyze the route descriptions and find the best match route for user latest intent.
3. You only response the name of the route that best matches the user's request, use the exact name in the <routes></routes>.
Based on your analysis, provide your response in the following JSON formats if you decide to match any route:
{"route": "route_name"}
`;
const TOKEN_DIVISOR = 4;
const filteredMessages = messages.filter(
m => m.role !== 'system' && m.role !== 'tool' && m.content?.trim()
);
let tokenCount = SYSTEM_PROMPT_TEMPLATE.length / TOKEN_DIVISOR;
const selected = [];
for (let i = filteredMessages.length - 1; i >= 0; i--) {
const msg = filteredMessages[i];
tokenCount += msg.content.length / TOKEN_DIVISOR;
if (tokenCount > maxTokenLength) {
if (msg.role === 'user') selected.push(msg);
break;
}
selected.push(msg);
}
if (selected.length === 0 && filteredMessages.length > 0) {
selected.push(filteredMessages[filteredMessages.length - 1]);
}
const selectedOrdered = selected.reverse();
const systemPrompt = SYSTEM_PROMPT_TEMPLATE
.replace('{routes}', JSON.stringify(routes, null, 2))
.replace('{conversation}', JSON.stringify(selectedOrdered, null, 2));
return systemPrompt;
}
/**─────────────────────── 🔧 Get routes from storage ───────────────────────**/
function getRoutesFromStorage() {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
if (!preferences || !Array.isArray(preferences)) {
console.warn('[ModelSelector] No preferences found in storage');
return resolve([]);
}
const routes = preferences.map(p => ({
name: p.name,
description: p.usage
}));
resolve(routes);
});
});
});
}
/**─────────────────────── 🔧 Get model ID by route name ───────────────────────**/
function getModelIdForRoute(routeName) {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
const match = (preferences || []).find(p => p.name === routeName);
if (match) resolve(match.model);
else resolve(null);
});
});
}
/**─────────────────────── 1⃣ Inject page-context fetch override ───────────────────────**/
(function injectPageFetchOverride() {
@ -25,89 +115,136 @@
(document.head || document.documentElement).appendChild(s);
})();
/**─────────────────────── 2Handle proxied fetch from the page ───────────────────────**/
/**─────────────────────── 2Intercept fetch and reroute via Ollama ───────────────────────**/
window.addEventListener('message', ev => {
console.log('[ModelSelector] page→content message', ev.data, ev.ports);
if (ev.source !== window || ev.data?.type !== 'ARCHGW_FETCH') return;
const { url, init, originalRequestUrl } = ev.data;
const { url, init } = ev.data;
const port = ev.ports[0];
(async () => {
try {
console.log('[ModelSelector] Fetching model recommendation from local proxy...');
//const res = await fetch(url, init);
//const json = await res.json();
console.log(`${TAG} Intercepted fetch from page:`, url);
//console.log('[ModelSelector] Proxy responded with:', json);
const targetModel = 'o4-mini-high';
if (!targetModel) {
console.warn('[ModelSelector] No model returned from proxy, using default fetch');
port.postMessage({ done: true });
return;
}
// ✅ Extract the original fetch request body from init.body
let originalBody = {};
try {
originalBody = JSON.parse(init.body);
} catch {
console.warn('[ModelSelector] Could not parse original fetch body');
console.warn(`${TAG} Could not parse original fetch body`);
}
// ✅ Patch the model in the request
originalBody.model = targetModel;
const scrapedMessages = getMessagesFromDom();
const routes = await getRoutesFromStorage();
const prompt = prepareProxyRequest(scrapedMessages, routes);
console.log(`[ModelSelector] Updating model in request → ${targetModel}`);
// 🔁 Call Ollama router
let selectedRoute = null;
try {
const res = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M',
prompt: prompt,
temperature: 0.1,
stream: false
})
});
// ✅ Resume the request to the real backend
const upstreamRes = await fetch('/backend-api/conversation', {
if (res.ok) {
const data = await res.json();
console.log(`${TAG} Ollama router response:`, data.response);
try {
let parsed = data.response;
if (typeof data.response === 'string') {
try {
parsed = JSON.parse(data.response);
} catch (jsonErr) {
// Try to recover from single quotes
const safe = data.response.replace(/'/g, '"');
parsed = JSON.parse(safe);
}
}
selectedRoute = parsed.route || null;
if (!selectedRoute) console.warn(`${TAG} Route missing in parsed response`);
} catch (e) {
console.warn(`${TAG} Failed to parse or extract route from response`, e);
}
}
else {
console.warn(`${TAG} Ollama router failed:`, res.status);
}
} catch (err) {
console.error(`${TAG} Ollama request error`, err);
}
let targetModel = null;
if (selectedRoute) {
targetModel = await getModelIdForRoute(selectedRoute);
console.log(`${TAG} Resolved model for route "${selectedRoute}" →`, targetModel);
}
// 🧠 Replace model if we found one
const modifiedBody = { ...originalBody };
if (targetModel) {
modifiedBody.model = targetModel;
console.log(`${TAG} Overriding request with model: ${targetModel}`);
} else {
console.warn(`${TAG} No route/model override applied`);
}
const upstreamRes = await fetch(url, {
method: init.method,
headers: init.headers,
credentials: init.credentials,
body: JSON.stringify(originalBody)
body: JSON.stringify(modifiedBody)
});
// ✅ Stream the upstream response back to the page
const reader = upstreamRes.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
port.postMessage({ done: true });
break;
} else {
port.postMessage({ chunk: value.buffer }, [value.buffer]);
}
port.postMessage({ chunk: value.buffer }, [value.buffer]);
}
} catch (err) {
console.error('[ModelSelector] proxy fetch error', err);
console.error(`${TAG} Proxy fetch error`, err);
port.postMessage({ done: true });
}
})();
});
/**─────────────────────── 3⃣ DOM patch for model selector label ───────────────────────**/
let desiredModel = null;
function patchDom() {
if (desiredModel == null) return;
if (!desiredModel) return;
const btn = document.querySelector('[data-testid="model-switcher-dropdown-button"]');
if (!btn) return;
const span = btn.querySelector('span.text-token-text-tertiary') || btn.querySelector('span');
const span = btn.querySelector('div > span');
const wantLabel = `Model selector, current model is ${desiredModel}`;
if (span && span.textContent !== desiredModel) span.textContent = desiredModel;
if (btn.getAttribute('aria-label') !== wantLabel) btn.setAttribute('aria-label', wantLabel);
if (btn.getAttribute('aria-label') !== wantLabel) {
btn.setAttribute('aria-label', wantLabel);
}
}
const observer = new MutationObserver(patchDom);
observer.observe(document.body || document.documentElement, {
subtree: true, childList: true, characterData: true, attributes: true
});
chrome.storage.sync.get(['defaultModel'], ({ defaultModel }) => {
if (defaultModel) { desiredModel = defaultModel; patchDom(); }
if (defaultModel) {
desiredModel = defaultModel;
patchDom();
}
});
chrome.runtime.onMessage.addListener(msg => {
if (msg.action === 'applyModelSelection' && msg.model) {
desiredModel = msg.model;
@ -121,31 +258,35 @@
const overlay = document.createElement('div');
overlay.id = 'pbms-overlay';
Object.assign(overlay.style, {
position:'fixed', top:0, left:0,
width:'100vw', height:'100vh',
background:'rgba(0,0,0,0.4)',
display:'flex', alignItems:'center', justifyContent:'center',
zIndex:2147483647
position: 'fixed', top: 0, left: 0,
width: '100vw', height: '100vh',
background: 'rgba(0,0,0,0.4)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 2147483647
});
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL('index.html');
Object.assign(iframe.style,{
width:'500px', height:'600px',
border:0, borderRadius:'8px',
boxShadow:'0 4px 16px rgba(0,0,0,0.2)',
background:'white', zIndex:2147483648
Object.assign(iframe.style, {
width: '500px', height: '600px',
border: 0, borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
background: 'white', zIndex: 2147483648
});
overlay.addEventListener('click', e => e.target===overlay && overlay.remove());
overlay.addEventListener('click', e => e.target === overlay && overlay.remove());
overlay.appendChild(iframe);
document.body.appendChild(overlay);
}
function interceptDropdown(ev) {
if (!ev.target.closest('button[aria-haspopup="menu"]')) return;
ev.preventDefault(); ev.stopPropagation();
ev.preventDefault();
ev.stopPropagation();
showModal();
}
document.addEventListener('pointerdown', interceptDropdown, true);
document.addEventListener('mousedown', interceptDropdown, true);
window.addEventListener('message', ev => {
if (ev.data?.action === 'CLOSE_PBMS_MODAL') {
document.getElementById('pbms-overlay')?.remove();

View file

@ -1,82 +1,39 @@
// pageFetchOverride.js
(function() {
const TAG = '[ModelSelector][Page]';
console.log(`${TAG} installing fetch override`);
window.archgwSettings = window.archgwSettings || { preferences: [], defaultModel: null };
window.addEventListener('message', ev => {
if (ev.source === window && ev.data?.type === 'PBMS_SETTINGS') {
window.archgwSettings = ev.data.settings;
console.log(`${TAG} got updated settings`, window.archgwSettings);
}
});
// New function: scrape current messages from the DOM
function get_messages() {
const bubbles = [...document.querySelectorAll('[data-message-author-role]')];
const messages = bubbles
.map(b => {
const role = b.getAttribute('data-message-author-role'); // "user" | "assistant"
const content =
role === 'assistant'
? (b.querySelector('.markdown')?.innerText ?? b.innerText ?? '').trim()
: (b.innerText ?? '').trim();
return content ? { role, content } : null;
})
.filter(Boolean);
return { messages };
}
const origFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const urlString = typeof input === 'string' ? input : input.url;
console.log(`${TAG} fetch →`, urlString);
let pathname;
try {
pathname = new URL(urlString).pathname;
} catch {
pathname = urlString;
const urlString = typeof input === 'string' ? input : input.url;
const urlObj = new URL(urlString, window.location.origin);
const pathname = urlObj.pathname;
console.log(`${TAG} fetch →`, pathname);
const method = (init.method || 'GET').toUpperCase();
if (method === 'OPTIONS') {
console.log(`${TAG} OPTIONS request → bypassing completely`);
return origFetch(input, init);
}
// Only intercept conversation fetches
if (pathname === '/backend-api/conversation') {
console.log(`${TAG} matched conversation → proxy via content script`);
let body = {};
try { body = JSON.parse(init.body); } catch {}
const currentMessages = get_messages();
console.log(`${TAG} scraped messages →`, currentMessages);
// Patch metadata with current preferences
body.metadata = {
archgw_preference_config: window.archgwSettings.preferences
.map(p => `- name: ${p.name}\n model: ${p.model}\n usage: ${p.usage}`)
.join('\n'),
// Add current messages dynamically
archgw_current_messages: JSON.stringify(currentMessages)
};
init.body = JSON.stringify(body);
const safeInit = {
method: init.method,
headers: init.headers,
body: init.body,
credentials: init.credentials
};
console.log(`${TAG} matched → proxy via content script`);
const { port1, port2 } = new MessageChannel();
// ✅ Remove non-cloneable properties like 'signal'
const safeInit = { ...init };
delete safeInit.signal;
// Forward the fetch details to the content script
window.postMessage({
type: 'ARCHGW_FETCH',
url: 'http://localhost:12000/v1/chat/completions',
url: urlString,
init: safeInit
}, '*', [port2]);
// Return a stream response that the content script will fulfill
return new Response(new ReadableStream({
start(controller) {
port1.onmessage = ({ data }) => {
@ -96,6 +53,7 @@
});
}
// Otherwise, pass through to the original fetch
return origFetch(input, init);
};