fix: strip provider prefix from all AI provider models and instant URL refresh

Backend fixes:
- Strip provider prefix (anthropic:, openai:, deepseek:, ollama:) in all
  provider Chat methods and constructors for robust handling
- Models are now correctly parsed regardless of caller format

Frontend fixes:
- Tool cards now persist in AI chat after approval execution by adding
  to streamEvents array
- Dashboard now listens for pulse:metadata-changed custom event
- AI Chat emits this event when set_resource_url tool completes
- Guest URL icons now update instantly when AI sets them
This commit is contained in:
rcourtman 2025-12-15 19:18:09 +00:00
parent 43d658556b
commit a3909f1b39
5 changed files with 58 additions and 4 deletions

View file

@ -557,6 +557,14 @@ export const AIChat: Component<AIChatProps> = (props) => {
}; };
// Add to both toolCalls and streamEvents for chronological display // Add to both toolCalls and streamEvents for chronological display
const events = msg.streamEvents || []; const events = msg.streamEvents || [];
// Emit event for metadata refresh if AI set a resource URL
if (data.name === 'set_resource_url' && data.success) {
window.dispatchEvent(new CustomEvent('pulse:metadata-changed', {
detail: { source: 'ai', tool: data.name }
}));
}
return { return {
...msg, ...msg,
pendingTools: updatedPending, pendingTools: updatedPending,
@ -748,11 +756,14 @@ export const AIChat: Component<AIChatProps> = (props) => {
}; };
const remainingApprovals = m.pendingApprovals?.filter((a) => a.toolId !== approval.toolId) || []; const remainingApprovals = m.pendingApprovals?.filter((a) => a.toolId !== approval.toolId) || [];
const existingEvents = m.streamEvents || [];
return { return {
...m, ...m,
pendingApprovals: remainingApprovals, pendingApprovals: remainingApprovals,
toolCalls: [...(m.toolCalls || []), newToolCall], toolCalls: [...(m.toolCalls || []), newToolCall],
// Also add to streamEvents so it appears in the chronological view
streamEvents: [...existingEvents, { type: 'tool' as const, tool: newToolCall }],
// Clear the stale "I need approval" content after the last approval is processed // Clear the stale "I need approval" content after the last approval is processed
// The tool output will show the result instead // The tool output will show the result instead
content: remainingApprovals.length === 0 ? '' : m.content, content: remainingApprovals.length === 0 ? '' : m.content,

View file

@ -290,15 +290,31 @@ export function Dashboard(props: DashboardProps) {
// Total columns for colspan calculations // Total columns for colspan calculations
const totalColumns = createMemo(() => visibleColumns().length); const totalColumns = createMemo(() => visibleColumns().length);
// Load all guest metadata on mount (single API call for all guests) // Helper function to refresh guest metadata from server
onMount(async () => { const refreshGuestMetadata = async () => {
try { try {
const metadata = await GuestMetadataAPI.getAllMetadata(); const metadata = await GuestMetadataAPI.getAllMetadata();
updateGuestMetadataState(() => metadata || {}); updateGuestMetadataState(() => metadata || {});
logger.debug('Guest metadata refreshed');
} catch (err) { } catch (err) {
// Silently fail - metadata is optional for display logger.debug('Failed to refresh guest metadata', err);
logger.debug('Failed to load guest metadata', err);
} }
};
// Load all guest metadata on mount (single API call for all guests)
onMount(async () => {
await refreshGuestMetadata();
});
// Listen for metadata changes from AI or other sources
createEffect(() => {
const handleMetadataChanged = () => {
logger.debug('Metadata changed event received, refreshing...');
refreshGuestMetadata();
};
window.addEventListener('pulse:metadata-changed', handleMetadataChanged);
return () => window.removeEventListener('pulse:metadata-changed', handleMetadataChanged);
}); });
// Callback to update a guest's custom URL in metadata // Callback to update a guest's custom URL in metadata

View file

@ -166,6 +166,10 @@ func (c *AnthropicClient) Chat(ctx context.Context, req ChatRequest) (*ChatRespo
// Use provided model or fall back to client default // Use provided model or fall back to client default
model := req.Model model := req.Model
// Strip provider prefix if present - callers may pass the full "provider:model" string
if len(model) > 10 && model[:10] == "anthropic:" {
model = model[10:]
}
if model == "" { if model == "" {
model = c.model model = c.model
} }

View file

@ -431,6 +431,10 @@ func (c *AnthropicOAuthClient) Chat(ctx context.Context, req ChatRequest) (*Chat
} }
model := req.Model model := req.Model
// Strip provider prefix if present - callers may pass the full "provider:model" string
if len(model) > 10 && model[:10] == "anthropic:" {
model = model[10:]
}
if model == "" { if model == "" {
model = c.model model = c.model
} }

View file

@ -28,11 +28,18 @@ type OpenAIClient struct {
client *http.Client client *http.Client
} }
// NewOpenAIClient creates a new OpenAI API client // NewOpenAIClient creates a new OpenAI API client
func NewOpenAIClient(apiKey, model, baseURL string) *OpenAIClient { func NewOpenAIClient(apiKey, model, baseURL string) *OpenAIClient {
if baseURL == "" { if baseURL == "" {
baseURL = openaiAPIURL baseURL = openaiAPIURL
} }
// Strip provider prefix if present - the model should be just the model name
if strings.HasPrefix(model, "openai:") {
model = strings.TrimPrefix(model, "openai:")
} else if strings.HasPrefix(model, "deepseek:") {
model = strings.TrimPrefix(model, "deepseek:")
}
return &OpenAIClient{ return &OpenAIClient{
apiKey: apiKey, apiKey: apiKey,
model: model, model: model,
@ -205,10 +212,19 @@ func (c *OpenAIClient) Chat(ctx context.Context, req ChatRequest) (*ChatResponse
// Use provided model or fall back to client default // Use provided model or fall back to client default
model := req.Model model := req.Model
// Strip provider prefix if present - callers may pass the full "provider:model" string
if strings.HasPrefix(model, "openai:") {
model = strings.TrimPrefix(model, "openai:")
} else if strings.HasPrefix(model, "deepseek:") {
model = strings.TrimPrefix(model, "deepseek:")
}
if model == "" { if model == "" {
model = c.model model = c.model
} }
// Debug log to trace model issues
log.Debug().Str("model", model).Str("req_model", req.Model).Str("c_model", c.model).Str("base_url", c.baseURL).Msg("OpenAI/DeepSeek Chat request")
// Build request // Build request
openaiReq := openaiRequest{ openaiReq := openaiRequest{
Model: model, Model: model,
@ -245,6 +261,9 @@ func (c *OpenAIClient) Chat(ctx context.Context, req ChatRequest) (*ChatResponse
} }
} }
// Log actual model being sent (INFO level for visibility)
log.Info().Str("model_in_request", openaiReq.Model).Str("base_url", c.baseURL).Msg("Sending OpenAI/DeepSeek request")
body, err := json.Marshal(openaiReq) body, err := json.Marshal(openaiReq)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err) return nil, fmt.Errorf("failed to marshal request: %w", err)