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
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 {
...msg,
pendingTools: updatedPending,
@ -748,11 +756,14 @@ export const AIChat: Component<AIChatProps> = (props) => {
};
const remainingApprovals = m.pendingApprovals?.filter((a) => a.toolId !== approval.toolId) || [];
const existingEvents = m.streamEvents || [];
return {
...m,
pendingApprovals: remainingApprovals,
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
// The tool output will show the result instead
content: remainingApprovals.length === 0 ? '' : m.content,

View file

@ -290,15 +290,31 @@ export function Dashboard(props: DashboardProps) {
// Total columns for colspan calculations
const totalColumns = createMemo(() => visibleColumns().length);
// Load all guest metadata on mount (single API call for all guests)
onMount(async () => {
// Helper function to refresh guest metadata from server
const refreshGuestMetadata = async () => {
try {
const metadata = await GuestMetadataAPI.getAllMetadata();
updateGuestMetadataState(() => metadata || {});
logger.debug('Guest metadata refreshed');
} catch (err) {
// Silently fail - metadata is optional for display
logger.debug('Failed to load guest metadata', err);
logger.debug('Failed to refresh 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

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
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 == "" {
model = c.model
}

View file

@ -431,6 +431,10 @@ func (c *AnthropicOAuthClient) Chat(ctx context.Context, req ChatRequest) (*Chat
}
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 == "" {
model = c.model
}

View file

@ -28,11 +28,18 @@ type OpenAIClient struct {
client *http.Client
}
// NewOpenAIClient creates a new OpenAI API client
func NewOpenAIClient(apiKey, model, baseURL string) *OpenAIClient {
if baseURL == "" {
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{
apiKey: apiKey,
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
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 == "" {
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
openaiReq := openaiRequest{
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)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)