File size: 5,065 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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import React from "react";
import { useQuery } from "@tanstack/react-query";
import ConfigService from "#/api/config-service/config-service.api";
import type { LLMModel } from "#/api/config-service/config-service.types";
import type { FreeModelSet } from "#/utils/format-model-name";
import { useFreeModelsStore } from "#/stores/free-models-store";
import { useActiveBackend } from "#/contexts/active-backend-context";
import {
  VERIFIED_MODELS_GC_TIME,
  VERIFIED_MODELS_QUERY_KEY,
  VERIFIED_MODELS_STALE_TIME,
  fetchVerifiedModelsByProvider,
} from "./use-verified-models";

/**
 * Provider whose models carry free / default metadata. Both are
 * OpenHands-managed concepts, so they are scoped to the `openhands` provider.
 */
const OPENHANDS_PROVIDER = "openhands";
const CLOUD_MODEL_SEARCH_PAGE_LIMIT = 100;
const MAX_PAGINATION_DEPTH = 10;

async function fetchAllOpenHandsModels(
  verifiedByProvider: Record<string, string[]>,
  pageId: string | null | undefined,
  seenPageIds: Set<string>,
  depth: number,
): Promise<LLMModel[]> {
  if (depth >= MAX_PAGINATION_DEPTH) {
    throw new Error(
      `Too many pagination requests while fetching OpenHands models (depth=${depth})`,
    );
  }

  const page = await ConfigService.searchModels(
    {
      provider__eq: OPENHANDS_PROVIDER,
      limit: CLOUD_MODEL_SEARCH_PAGE_LIMIT,
      ...(pageId ? { page_id: pageId } : {}),
    },
    verifiedByProvider,
  );

  if (!page.next_page_id) {
    return page.items;
  }

  if (seenPageIds.has(page.next_page_id)) {
    throw new Error(
      `Repeated page id while fetching OpenHands models: ${page.next_page_id}`,
    );
  }
  seenPageIds.add(page.next_page_id);

  const rest = await fetchAllOpenHandsModels(
    verifiedByProvider,
    page.next_page_id,
    seenPageIds,
    depth + 1,
  );
  return [...page.items, ...rest];
}

/**
 * Fetches the `openhands` provider's models with their DB-driven `free` /
 * `default` flags (the same channel that carries `verified`).
 */
const useOpenHandsModels = () => {
  const { backend, orgId } = useActiveBackend();
  const backendScope = [
    backend.id,
    backend.connectionRevision ?? 0,
    orgId,
  ] as const;

  return useQuery({
    queryKey: [
      "config",
      "models",
      OPENHANDS_PROVIDER,
      "flags",
      ...backendScope,
    ],
    queryFn: async ({ client }): Promise<LLMModel[]> => {
      const verifiedByProvider = await client.fetchQuery({
        queryKey: [...VERIFIED_MODELS_QUERY_KEY, ...backendScope],
        queryFn: fetchVerifiedModelsByProvider,
        staleTime: VERIFIED_MODELS_STALE_TIME,
      });
      return fetchAllOpenHandsModels(verifiedByProvider, null, new Set(), 0);
    },
    staleTime: VERIFIED_MODELS_STALE_TIME,
    gcTime: VERIFIED_MODELS_GC_TIME,
  });
};

/**
 * Fetches the DB-driven free / default flags once and mirrors them into the
 * {@link useFreeModelsStore}. Mount this high in the tree (inside the query
 * provider). Leaf display components then read the flags synchronously via the
 * {@link useFreeModels} / {@link useDefaultModel} zustand selectors, so they
 * stay renderable in isolation without a QueryClientProvider in scope.
 */
export const useHydrateFreeModels = (): void => {
  const { backend, orgId } = useActiveBackend();
  const { data, isError } = useOpenHandsModels();
  const setFlags = useFreeModelsStore((state) => state.setFlags);
  const markDefaultModelReady = useFreeModelsStore(
    (state) => state.markDefaultModelReady,
  );
  const resetFlags = useFreeModelsStore((state) => state.resetFlags);

  React.useEffect(() => {
    resetFlags();
  }, [backend.id, backend.connectionRevision, orgId, resetFlags]);

  React.useEffect(() => {
    if (!data) return;
    const freeModels = new Set(
      data
        .filter((model) => model.free)
        .map((model) => `${OPENHANDS_PROVIDER}/${model.name}`),
    );
    const defaultEntry = data.find((model) => model.default);
    setFlags({
      freeModels,
      defaultModel: defaultEntry
        ? `${OPENHANDS_PROVIDER}/${defaultEntry.name}`
        : null,
    });
  }, [data, setFlags]);

  React.useEffect(() => {
    if (isError) markDefaultModelReady();
  }, [isError, markDefaultModelReady]);
};

/**
 * Set of free ``openhands/<model>`` ids, sourced from the backend model list
 * (DB-driven on cloud via the `free` flag). Returns an empty set on backends
 * without free metadata (e.g. the local agent-server), so callers uniformly
 * treat "not in set" as paid and the frontend keeps no hardcoded free list.
 */
export const useFreeModels = (): FreeModelSet =>
  useFreeModelsStore((state) => state.freeModels);

/**
 * DB-driven default OpenHands model id (``openhands/<model>``), used to
 * preselect a model on onboarding and when creating a new OpenHands model.
 * Returns `null` on backends without default metadata.
 */
export const useDefaultModel = (): string | null =>
  useFreeModelsStore((state) => state.defaultModel);

export const useDefaultModelReady = (): boolean =>
  useFreeModelsStore((state) => state.defaultModelReady);