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);
|