fix: Add pony-alpha model and fix model lookup for prefixed model IDs

- Add openrouter/pony-alpha model to models-api.json fixture
- Fix getModel() to lookup models with provider prefix (e.g., openrouter/pony-alpha)
  When user specifies openrouter/pony-alpha, the code now correctly looks for
  the full model ID including prefix in the provider's models object

This fixes the 'ModelNotFoundError' when using OpenRouter models that have
prefixed IDs in the database.
This commit is contained in:
southseact-3d
2026-02-08 16:21:21 +00:00
parent 1fbf5abce6
commit 26c6f5f6c7
4 changed files with 218 additions and 20 deletions

View File

@@ -300,6 +300,26 @@
box-shadow: 0 10px 25px rgba(0, 128, 96, 0.25);
}
/* Cancel mode for send button */
.primary.cancel-mode {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: #fff;
}
.primary.cancel-mode:hover {
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.25);
}
/* Cancelled message styling */
.message.assistant.cancelled {
opacity: 0.7;
}
.message.assistant.cancelled .body {
background: #fef2f2;
border: 1px solid #fecaca;
}
.back-home {
color: var(--muted);
text-decoration: none;
@@ -1932,11 +1952,16 @@
}
const miniBtn = el.miniSendBtn;
const originalLabel = miniBtn ? miniBtn.textContent : '';
if (miniBtn) {
miniBtn.disabled = true;
miniBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>'; // Clock icon for planning
// Set sending state for cancellation support
if (typeof state !== 'undefined') {
state.isSending = true;
state.currentSendingMessageId = tempMessageId;
}
if (typeof updateSendButtonState === 'function') {
updateSendButtonState();
}
try {
const response = await api('/api/plan', { method: 'POST', body: JSON.stringify(payload) });
@@ -1986,14 +2011,13 @@
renderMessages(session);
}
} finally {
if (miniBtn) {
miniBtn.disabled = false;
// Restore original SVG icon
miniBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>`;
// Reset sending state
if (typeof state !== 'undefined') {
state.isSending = false;
state.currentSendingMessageId = null;
}
if (typeof updateSendButtonState === 'function') {
updateSendButtonState();
}
}
}
@@ -2278,7 +2302,15 @@
};
// document.getElementById('send-btn') removed
document.getElementById('mini-send-btn').addEventListener('click', handleSend, true);
// Use handleSendButtonClick which handles both send and cancel modes
document.getElementById('mini-send-btn').addEventListener('click', (e) => {
if (typeof handleSendButtonClick === 'function') {
handleSendButtonClick(e);
} else {
// Fallback to old behavior if function not loaded yet
handleSend(e);
}
}, true);
// Initialize UI
updateBuildModeUI();

View File

@@ -384,6 +384,8 @@ const state = {
usageSummary: null,
todos: [], // Current todos from OpenCode
currentMessageId: null, // Track which message we're displaying todos for
isSending: false, // Track if a message is currently being sent
currentSendingMessageId: null, // Track the ID of the message being sent for cancellation
};
// Expose state for builder.html
@@ -1824,7 +1826,7 @@ function renderMessages(session) {
// Don't hide loading indicator during streaming - only hide when completely done
// This allows the spinner to continue until the OpenCode session is fully complete
const assistantCard = document.createElement('div');
assistantCard.className = 'message assistant';
assistantCard.className = 'message assistant' + (status === 'cancelled' ? ' cancelled' : '');
const assistantMeta = document.createElement('div');
assistantMeta.className = 'meta';
const msgCliLabel = (msg.cli || session.cli || 'opencode');
@@ -1841,10 +1843,10 @@ function renderMessages(session) {
rawBtn.textContent = 'Plugin Compass';
assistantMeta.appendChild(rawBtn);
// Add Undo/Redo buttons - only show for latest message and for opencode messages when done
// Add Undo/Redo buttons - show for latest message when done, errored, or cancelled
const isOpencodeMsg = msg.cli === 'opencode' || msg.phase === 'build';
const isLatestMessage = latestMessage && latestMessage.id === msg.id;
const shouldShowUndoRedo = isOpencodeMsg && isLatestMessage && (status === 'done' || status === 'error');
const shouldShowUndoRedo = isOpencodeMsg && isLatestMessage && (status === 'done' || status === 'error' || status === 'cancelled');
if (shouldShowUndoRedo) {
const undoBtn = document.createElement('button');
@@ -3721,7 +3723,9 @@ async function sendMessage() {
}
}
if (el.miniSendBtn) el.miniSendBtn.disabled = true;
// Set sending state and update button
state.isSending = true;
updateSendButtonState();
// Ensure we have a valid current session before proceeding
if (!state.currentSessionId) {
@@ -3801,6 +3805,12 @@ async function sendMessage() {
body: JSON.stringify(payload),
});
// Track the message ID being sent for potential cancellation
if (response?.message?.id) {
state.currentSendingMessageId = response.message.id;
console.log('[BUILDER] Tracking message for cancellation:', response.message.id);
}
// Clear attachments after successful send
pendingAttachments.length = 0;
renderAttachmentPreview();
@@ -3862,12 +3872,143 @@ async function sendMessage() {
renderMessages(currentSession);
}
} finally {
if (el.miniSendBtn) el.miniSendBtn.disabled = false;
// Reset sending state when message completes, is cancelled, or errors out
// Only reset if we're not currently in a cancellation (cancelMessage handles that)
if (state.isSending && state.currentSendingMessageId) {
const session = state.sessions.find(s => s.id === state.currentSessionId);
const messageId = state.currentSendingMessageId;
const message = session?.messages.find(m => m.id === messageId);
// Reset if message is done, errored, or cancelled
if (!message || message.status === 'done' || message.status === 'error' || message.status === 'cancelled') {
state.isSending = false;
state.currentSendingMessageId = null;
updateSendButtonState();
}
}
}
}
// Cancel the current message being sent
async function cancelMessage() {
if (!state.isSending || !state.currentSendingMessageId) {
console.log('[CANCEL] No message to cancel');
return;
}
const messageId = state.currentSendingMessageId;
const sessionId = state.currentSessionId;
console.log('[CANCEL] Cancelling message:', messageId);
setStatus('Cancelling...');
// Close any active SSE streams for this message
if (state.activeStreams.has(messageId)) {
const stream = state.activeStreams.get(messageId);
stream.close();
state.activeStreams.delete(messageId);
console.log('[CANCEL] Closed SSE stream for message:', messageId);
}
// Find and update the message status
const session = state.sessions.find(s => s.id === sessionId);
if (session) {
const message = session.messages.find(m => m.id === messageId);
if (message) {
message.status = 'cancelled';
message.reply = message.reply || '(Cancelled by user)';
message.cancelled = true;
// Show undo/redo buttons for the cancelled message
message.showUndoRedo = true;
console.log('[CANCEL] Message marked as cancelled:', messageId);
}
// Remove temp messages
const tempMessages = session.messages.filter(m => m.id.startsWith('temp-') && m.status === 'queued');
tempMessages.forEach(tempMsg => {
const idx = session.messages.indexOf(tempMsg);
if (idx > -1) {
session.messages.splice(idx, 1);
}
});
renderMessages(session);
}
// Reset sending state
state.isSending = false;
state.currentSendingMessageId = null;
// Reset send button
updateSendButtonState();
hideLoadingIndicator();
setStatus('Message cancelled');
// Refresh session to sync with server
try {
await refreshCurrentSession();
} catch (err) {
console.warn('[CANCEL] Failed to refresh session:', err.message);
}
}
// Update send button appearance based on state
function updateSendButtonState() {
const btn = el.miniSendBtn;
if (!btn) return;
if (state.isSending) {
// Show cancel button (square icon)
btn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect>
</svg>
`;
btn.title = 'Cancel message';
btn.classList.add('cancel-mode');
btn.disabled = false;
} else {
// Show send button (paper plane icon)
btn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
`;
btn.title = 'Send message';
btn.classList.remove('cancel-mode');
btn.disabled = false;
}
}
// Handle send button click - either send or cancel
function handleSendButtonClick(e) {
if (state.isSending) {
// Cancel the current message
cancelMessage();
} else {
// Check if we're in plan mode and call the appropriate handler
if (builderState.mode === 'plan') {
// Use handleSend from builder.html which handles plan mode
if (typeof handleSend === 'function') {
handleSend(e);
} else {
console.warn('[BUILDER] handleSend not available');
}
} else {
// Send a new message in build mode
sendMessage();
}
}
}
// Expose for builder.html
window.sendMessage = sendMessage;
window.cancelMessage = cancelMessage;
window.handleSendButtonClick = handleSendButtonClick;
function hookEvents() {
if (el.newChat) {
@@ -4205,7 +4346,11 @@ function hookEvents() {
el.messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
sendMessage();
if (state.isSending) {
cancelMessage();
} else {
sendMessage();
}
}
});

View File

@@ -1200,7 +1200,11 @@ export namespace Provider {
throw new ModelNotFoundError({ providerID, modelID, suggestions })
}
const info = provider.models[modelID]
let info = provider.models[modelID]
// Try with provider prefix if not found (e.g., "openrouter/pony-alpha")
if (!info && !modelID.includes("/")) {
info = provider.models[`${providerID}/${modelID}`]
}
if (!info) {
const availableModels = Object.keys(provider.models)
const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })

View File

@@ -27310,6 +27310,23 @@
"cost": { "input": 0, "output": 0 },
"limit": { "context": 1840000, "output": 0 }
},
"openrouter/pony-alpha": {
"id": "openrouter/pony-alpha",
"name": "Stealth",
"family": "pony",
"attachment": false,
"reasoning": true,
"tool_call": true,
"structured_output": true,
"temperature": true,
"release_date": "2026-02-06",
"last_updated": "2026-02-06",
"modalities": { "input": ["text"], "output": ["text"] },
"open_weights": false,
"cost": { "input": 0, "output": 0 },
"limit": { "context": 200000, "output": 131000 },
"status": "alpha"
},
"z-ai/glm-4.7": {
"id": "z-ai/glm-4.7",
"name": "GLM-4.7",