File size: 4,213 Bytes
3d700dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import AgentServerConversationService from "#/api/conversation-service/agent-server-conversation-service.api";
import SettingsService from "#/api/settings-service/settings-service.api";
import {
  LLM_PROFILES_QUERY_KEYS,
  SETTINGS_QUERY_KEYS,
} from "#/hooks/query/query-keys";
import { getLastRenderableEventId } from "#/hooks/chat/model-command-event-anchor";
import {
  recordModelSwitchMessage,
  stampActiveLlmProfile,
} from "#/hooks/chat/record-model-switch-message";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
import { I18nKey } from "#/i18n/declaration";
import { invalidateConversationQueries } from "./conversation-mutation-utils";

interface SwitchLlmProfileVars {
  /**
   * When set, the conversation's running LLM is swapped via /switch_profile and
   * the user's global default profile is untouched. When null (home page),
   * the profile is activated globally instead.
   */
  conversationId: string | null;
  profileName: string;
}

/**
 * Shared key so any picker instance can observe an in-flight switch via
 * `useIsMutating` — the pill button and the menu that fires the switch are
 * separate hook instances, so per-observer `isPending` wouldn't line up.
 */
export const SWITCH_LLM_PROFILE_MUTATION_KEY = ["switch-llm-profile"];

/**
 * Switches the LLM profile. Per-conversation when called from inside a
 * conversation; globally activates the profile when called from the home page.
 *
 * The confirmation message, #1082 metadata persist, and error reporting all
 * live in the mutation-level callbacks (not `mutate(..., callbacks)`) so they
 * still run after the switcher menu closes on select — React Query drops
 * mutate-scoped callbacks when the calling component unmounts.
 *
 * `meta.disableToast` + a tailored `onError` (rather than the global mutation
 * toast) so a failed switch keeps the specific "Switched to {name} failed"
 * message instead of a generic error (#1571 review) — unlike
 * use-switch-acp-model, which has no per-action message worth tailoring.
 */
export const useSwitchLlmProfile = () => {
  const queryClient = useQueryClient();
  const { t } = useTranslation();

  return useMutation({
    mutationKey: SWITCH_LLM_PROFILE_MUTATION_KEY,
    mutationFn: ({ conversationId, profileName }: SwitchLlmProfileVars) =>
      AgentServerConversationService.switchProfile(conversationId, profileName),
    meta: { disableToast: true },
    // Anchor the inline message where the user issued the switch; captured
    // synchronously here because the menu unmounts before the switch resolves.
    onMutate: ({ conversationId }) => ({
      anchorEventId: conversationId ? getLastRenderableEventId() : null,
    }),
    onError: (error, { profileName }) => {
      const fallback = t(I18nKey.MODEL$SWITCH_FAILED, { name: profileName });
      displayErrorToast(retrieveAxiosErrorMessage(error) || fallback);
    },
    onSuccess: (_data, { conversationId, profileName }, context) => {
      queryClient.invalidateQueries({
        queryKey: LLM_PROFILES_QUERY_KEYS.all,
      });
      if (conversationId) {
        invalidateConversationQueries(queryClient, conversationId);
        recordModelSwitchMessage(
          conversationId,
          profileName,
          context?.anchorEventId ?? null,
        );
        // Keep the per-conversation profile identity fresh so the chat-header
        // switcher shows the right name after a reload (the agent-server only
        // round-trips the model string). #1082
        stampActiveLlmProfile(conversationId, profileName);
      } else {
        // Home-page activate path (same server endpoint as
        // useActivateLlmProfile): clear the SettingsService cache so the next
        // conversation-start reads the newly activated profile's LLM config
        // instead of the stale encrypted settings.
        SettingsService.invalidateCache();
        queryClient.invalidateQueries({
          queryKey: SETTINGS_QUERY_KEYS.personal(),
        });
      }
    },
  });
};