Skip to content

Commit 23d261f

Browse files
committed
fix: filter model dropdowns by policy
1 parent 68070bf commit 23d261f

1 file changed

Lines changed: 36 additions & 3 deletions

File tree

src/browser/components/Settings/sections/ModelsSection.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useGateway } from "@/browser/hooks/useGatewayModels";
1414
import { usePersistedState } from "@/browser/hooks/usePersistedState";
1515
import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
1616
import { SearchableModelSelect } from "../components/SearchableModelSelect";
17+
import type { EffectivePolicy } from "@/common/orpc/types";
1718
import { KNOWN_MODELS } from "@/common/constants/knownModels";
1819
import { PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers";
1920
import { usePolicy } from "@/browser/contexts/PolicyContext";
@@ -24,6 +25,33 @@ import {
2425
} from "@/common/constants/storage";
2526
import { ModelRow } from "./ModelRow";
2627

28+
function isModelAllowedByPolicy(policy: EffectivePolicy | null, modelString: string): boolean {
29+
const providerAccess = policy?.providerAccess;
30+
if (providerAccess == null) {
31+
return true;
32+
}
33+
34+
const colonIndex = modelString.indexOf(":");
35+
if (colonIndex <= 0 || colonIndex === modelString.length - 1) {
36+
return true;
37+
}
38+
39+
const provider = modelString.slice(0, colonIndex);
40+
const modelId = modelString.slice(colonIndex + 1);
41+
42+
const providerPolicy = providerAccess.find((p) => p.id === provider);
43+
if (!providerPolicy) {
44+
return false;
45+
}
46+
47+
const allowedModels = providerPolicy.allowedModels ?? null;
48+
if (allowedModels === null) {
49+
return true;
50+
}
51+
52+
return allowedModels.includes(modelId);
53+
}
54+
2755
// Providers to exclude from the custom models UI (handled specially or internal)
2856
const HIDDEN_PROVIDERS = new Set(["mux-gateway"]);
2957

@@ -80,8 +108,13 @@ export function ModelsSection() {
80108
{ listener: true }
81109
);
82110

83-
// All models (including hidden) for the settings dropdowns
111+
// All models (including hidden) for the settings dropdowns.
112+
// PolicyService enforces model access on the backend, but we also filter here so users can't
113+
// select models that will be denied at send time.
84114
const allModels = getSuggestedModels(config);
115+
const selectableModels = effectivePolicy
116+
? allModels.filter((model) => isModelAllowedByPolicy(effectivePolicy, model))
117+
: allModels;
85118

86119
// Check if a model already exists (for duplicate prevention)
87120
const modelExists = useCallback(
@@ -232,7 +265,7 @@ export function ModelsSection() {
232265
<SearchableModelSelect
233266
value={defaultModel}
234267
onChange={setDefaultModel}
235-
models={allModels}
268+
models={selectableModels}
236269
placeholder="Select model"
237270
/>
238271
</div>
@@ -247,7 +280,7 @@ export function ModelsSection() {
247280
<SearchableModelSelect
248281
value={compactionModel}
249282
onChange={setCompactionModel}
250-
models={allModels}
283+
models={selectableModels}
251284
emptyOption={{ value: "", label: "Use workspace model" }}
252285
/>
253286
</div>

0 commit comments

Comments
 (0)