working version with fixed styling and better tested

This commit is contained in:
Salman Paracha 2025-07-02 16:45:56 -07:00
parent a872f9eec2
commit bb2eb02030
3 changed files with 118 additions and 74 deletions

View file

@ -6,8 +6,9 @@ export default function App() {
<div className="bg-gray-100 min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-6xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-800">ChatGPT Model Selector</h1>
<p className="text-gray-600 mt-2">Define usage preferences to optimize model selection.</p>
<h1 className="text-3xl font-bold text-gray-800">RouteGPT</h1>
<p className="text-gray-600 mt-2">Dynamically route to GPT models based on usage preferences.</p>
<a target="_blank" href="https://github.com/katanemo/archgw" className="text-blue-500 hover:underline">powered by Arch Router</a>
</div>
<PreferenceBasedModelSelector />
</div>

View file

@ -5,8 +5,8 @@ import React, { useState, useEffect } from 'react';
const MODEL_LIST = [
'gpt-4o',
'gpt-4.1',
'gpt-4.1 mini',
'gpt-4.5 preview',
'gpt-4.1-mini',
'gpt-4.5-preview',
'o3',
'o3-pro',
'o4-mini',
@ -129,35 +129,52 @@ export default function PreferenceBasedModelSelector() {
// Save settings: generate name slug and store tuples
const handleSave = () => {
// Only keep valid preferences with non-empty usage
const tuples = preferences
const slugCounts = {};
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 };
.forEach(p => {
const baseSlug = p.usage
.split(/\s+/)
.slice(0, 3)
.join('-')
.toLowerCase()
.replace(/[^\w-]/g, '');
const count = slugCounts[baseSlug] || 0;
slugCounts[baseSlug] = count + 1;
const dedupedSlug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
tuples.push({
name: dedupedSlug,
usage: p.usage.trim(),
model: p.model?.trim?.() || ''
});
});
chrome.storage.sync.set({ routingEnabled, preferences: tuples, defaultModel }, () => {
console.log('[PBMS] Saved tuples:', tuples);
if (chrome.runtime.lastError) {
console.error('[PBMS] Storage error:', chrome.runtime.lastError);
} else {
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' }, '*');
};
const handleCancel = () => {
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
};
return (
<div className="w-full max-w-[600px] bg-gray-50 p-4 mx-auto">
<h2 className="text-lg font-semibold text-center mb-4">Model Preferences</h2>
<div className="space-y-4">
<Card className="w-full">
<CardContent>

View file

@ -1,11 +1,26 @@
(() => {
const TAG = '[ModelSelector]';
/**─────────────────────── 🔧 Utility: Scrape Current Messages ───────────────────────**/
function getMessagesFromDom() {
async function streamToPort(response, port) {
const reader = response.body?.getReader();
if (!reader) {
port.postMessage({ done: true });
return;
}
while (true) {
const { done, value } = await reader.read();
if (done) {
port.postMessage({ done: true });
break;
}
port.postMessage({ chunk: value.buffer }, [value.buffer]);
}
}
function getMessagesFromDom(requestMessages = null) {
const bubbles = [...document.querySelectorAll('[data-message-author-role]')];
return bubbles
const domMessages = bubbles
.map(b => {
const role = b.getAttribute('data-message-author-role');
const content =
@ -15,29 +30,44 @@
return content ? { role, content } : null;
})
.filter(Boolean);
// Fallback: If DOM is empty but we have requestMessages, use those
if (domMessages.length === 0 && requestMessages?.length > 0) {
return requestMessages
.map(msg => {
const role = msg.author?.role;
const parts = msg.content?.parts ?? [];
const textPart = parts.find(p => typeof p === 'string');
return role && textPart ? { role, content: textPart.trim() } : null;
})
.filter(Boolean);
}
return domMessages;
}
/**─────────────────────── 🔧 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>
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>
<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>.
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"}
`;
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(
@ -72,7 +102,6 @@
return systemPrompt;
}
/**─────────────────────── 🔧 Get routes from storage ───────────────────────**/
function getRoutesFromStorage() {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
@ -91,8 +120,6 @@
});
}
/**─────────────────────── 🔧 Get model ID by route name ───────────────────────**/
function getModelIdForRoute(routeName) {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
@ -103,7 +130,6 @@
});
}
/**─────────────────────── 1⃣ Inject page-context fetch override ───────────────────────**/
(function injectPageFetchOverride() {
const injectorTag = '[ModelSelector][Injector]';
const s = document.createElement('script');
@ -115,7 +141,6 @@
(document.head || document.documentElement).appendChild(s);
})();
/**─────────────────────── 2⃣ Intercept fetch and reroute via Ollama ───────────────────────**/
window.addEventListener('message', ev => {
if (ev.source !== window || ev.data?.type !== 'ARCHGW_FETCH') return;
@ -133,11 +158,23 @@
console.warn(`${TAG} Could not parse original fetch body`);
}
const scrapedMessages = getMessagesFromDom();
const routes = await getRoutesFromStorage();
const { routingEnabled, preferences } = await new Promise(resolve => {
chrome.storage.sync.get(['routingEnabled', 'preferences'], resolve);
});
if (!routingEnabled) {
console.log(`${TAG} Routing disabled — forwarding original request`);
await streamToPort(await fetch(url, init), port);
return;
}
const scrapedMessages = getMessagesFromDom(originalBody.messages);
const routes = (preferences || []).map(p => ({
name: p.name,
description: p.usage
}));
const prompt = prepareProxyRequest(scrapedMessages, routes);
// 🔁 Call Ollama router
let selectedRoute = null;
try {
const res = await fetch('http://localhost:11434/api/generate', {
@ -146,32 +183,32 @@
body: JSON.stringify({
model: 'hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M',
prompt: prompt,
temperature: 0.1,
temperature: 0.01,
top_p: 0.95,
top_k: 20,
stream: false
})
});
if (res.ok) {
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);
let parsed = data.response;
if (typeof data.response === 'string') {
try {
parsed = JSON.parse(data.response);
} catch (jsonErr) {
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);
}
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 {
} else {
console.warn(`${TAG} Ollama router failed:`, res.status);
}
} catch (err) {
@ -184,7 +221,6 @@
console.log(`${TAG} Resolved model for route "${selectedRoute}" →`, targetModel);
}
// 🧠 Replace model if we found one
const modifiedBody = { ...originalBody };
if (targetModel) {
modifiedBody.model = targetModel;
@ -193,22 +229,12 @@
console.warn(`${TAG} No route/model override applied`);
}
const upstreamRes = await fetch(url, {
await streamToPort(await fetch(url, {
method: init.method,
headers: init.headers,
credentials: init.credentials,
body: JSON.stringify(modifiedBody)
});
const reader = upstreamRes.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
port.postMessage({ done: true });
break;
}
port.postMessage({ chunk: value.buffer }, [value.buffer]);
}
}), port);
} catch (err) {
console.error(`${TAG} Proxy fetch error`, err);
port.postMessage({ done: true });
@ -216,7 +242,6 @@
})();
});
/**─────────────────────── 3⃣ DOM patch for model selector label ───────────────────────**/
let desiredModel = null;
function patchDom() {
if (!desiredModel) return;
@ -252,7 +277,6 @@
}
});
/**─────────────────────── 4⃣ Modal / dropdown interception ───────────────────────**/
function showModal() {
if (document.getElementById('pbms-overlay')) return;
const overlay = document.createElement('div');
@ -278,7 +302,9 @@
}
function interceptDropdown(ev) {
if (!ev.target.closest('button[aria-haspopup="menu"]')) return;
const btn = ev.target.closest('button[data-testid="model-switcher-dropdown-button"]');
if (!btn) return;
ev.preventDefault();
ev.stopPropagation();
showModal();