Skip to content

Commit c6ce621

Browse files
committed
🤖 fix: gate direct Copilot model entries
Apply the authoritative Copilot catalog check to direct provider model strings so hidden and custom model lists do not surface Copilot-only entries that the signed-in account cannot use. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$23.81`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=23.81 -->
1 parent 2fd1440 commit c6ce621

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

src/browser/hooks/useModelsFromSettings.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import { isModelAvailable } from "@/common/routing";
2121
import type { ProviderModelEntry, ProvidersConfigMap } from "@/common/orpc/types";
2222
import { DEFAULT_MODEL_KEY, HIDDEN_MODELS_KEY } from "@/common/constants/storage";
2323

24-
import { isGatewayModelAccessibleFromAuthoritativeCatalog } from "@/common/utils/providers/gatewayModelCatalog";
24+
import {
25+
isGatewayModelAccessibleFromAuthoritativeCatalog,
26+
isProviderModelAccessibleFromAuthoritativeCatalog,
27+
} from "@/common/utils/providers/gatewayModelCatalog";
2528
import { getProviderModelEntryId } from "@/common/utils/providers/modelEntries";
2629

2730
const BUILT_IN_MODELS: string[] = Object.values(KNOWN_MODELS).map((m) => m.id);
@@ -162,10 +165,30 @@ export function useModelsFromSettings() {
162165
[config]
163166
);
164167

168+
const isAuthoritativeProviderModelAccessible = useCallback(
169+
(modelString: string) => {
170+
const colonIndex = modelString.indexOf(":");
171+
if (colonIndex <= 0 || colonIndex >= modelString.length - 1) {
172+
return true;
173+
}
174+
175+
const provider = modelString.slice(0, colonIndex);
176+
const providerModelId = modelString.slice(colonIndex + 1);
177+
return isProviderModelAccessibleFromAuthoritativeCatalog(
178+
provider,
179+
providerModelId,
180+
config?.[provider]?.models
181+
);
182+
},
183+
[config]
184+
);
185+
165186
const customModels = useMemo(() => {
166-
const next = filterHiddenModels(getCustomModels(config), hiddenModels);
187+
const next = filterHiddenModels(getCustomModels(config), hiddenModels).filter(
188+
isAuthoritativeProviderModelAccessible
189+
);
167190
return effectivePolicy ? next.filter((m) => isModelAllowedByPolicy(effectivePolicy, m)) : next;
168-
}, [config, hiddenModels, effectivePolicy]);
191+
}, [config, hiddenModels, effectivePolicy, isAuthoritativeProviderModelAccessible]);
169192

170193
const openaiApiKeySet = config === null ? null : config.openai?.apiKeySet === true;
171194
const codexOauthSet = config === null ? null : config.openai?.codexOauthSet === true;
@@ -186,6 +209,10 @@ export function useModelsFromSettings() {
186209
return false;
187210
}
188211

212+
if (!isAuthoritativeProviderModelAccessible(modelId)) {
213+
return true;
214+
}
215+
189216
if (
190217
isModelAvailable(
191218
modelId,
@@ -221,6 +248,7 @@ export function useModelsFromSettings() {
221248
effectivePolicy,
222249
isConfigured,
223250
isGatewayModelAccessible,
251+
isAuthoritativeProviderModelAccessible,
224252
routePriority,
225253
routeOverrides,
226254
openaiApiKeySet,
@@ -240,14 +268,16 @@ export function useModelsFromSettings() {
240268
const providerFiltered =
241269
config == null
242270
? suggested
243-
: suggested.filter((modelId) =>
244-
isModelAvailable(
245-
modelId,
246-
routePriority,
247-
routeOverrides,
248-
isConfigured,
249-
isGatewayModelAccessible
250-
)
271+
: suggested.filter(
272+
(modelId) =>
273+
isAuthoritativeProviderModelAccessible(modelId) &&
274+
isModelAvailable(
275+
modelId,
276+
routePriority,
277+
routeOverrides,
278+
isConfigured,
279+
isGatewayModelAccessible
280+
)
251281
);
252282

253283
if (config == null) {
@@ -286,6 +316,7 @@ export function useModelsFromSettings() {
286316
effectivePolicy,
287317
isConfigured,
288318
isGatewayModelAccessible,
319+
isAuthoritativeProviderModelAccessible,
289320
routePriority,
290321
routeOverrides,
291322
openaiApiKeySet,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import {
4+
isGatewayModelAccessibleFromAuthoritativeCatalog,
5+
isProviderModelAccessibleFromAuthoritativeCatalog,
6+
} from "./gatewayModelCatalog";
7+
8+
describe("gatewayModelCatalog", () => {
9+
test("treats non-Copilot providers as permissive even with custom model lists", () => {
10+
expect(
11+
isProviderModelAccessibleFromAuthoritativeCatalog("openrouter", "openai/gpt-5", [
12+
"team-only-model",
13+
])
14+
).toBe(true);
15+
});
16+
17+
test("treats an empty Copilot catalog as permissive", () => {
18+
expect(isProviderModelAccessibleFromAuthoritativeCatalog("github-copilot", "gpt-5.4", [])).toBe(
19+
true
20+
);
21+
});
22+
23+
test("rejects direct Copilot model ids missing from the authoritative catalog", () => {
24+
expect(
25+
isProviderModelAccessibleFromAuthoritativeCatalog("github-copilot", "gpt-5.4", [
26+
"gpt-5.4-mini",
27+
])
28+
).toBe(false);
29+
});
30+
31+
test("keeps the gateway-specific helper behavior aligned", () => {
32+
expect(
33+
isGatewayModelAccessibleFromAuthoritativeCatalog("github-copilot", "gpt-5.4", [
34+
"gpt-5.4-mini",
35+
])
36+
).toBe(false);
37+
});
38+
});

src/common/utils/providers/gatewayModelCatalog.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import type { ProviderModelEntry } from "@/common/orpc/types";
22

33
import { getProviderModelEntryId } from "@/common/utils/providers/modelEntries";
44

5-
export function isGatewayModelAccessibleFromAuthoritativeCatalog(
6-
gateway: string,
5+
export function isProviderModelAccessibleFromAuthoritativeCatalog(
6+
provider: string,
77
modelId: string,
88
models: ProviderModelEntry[] | undefined
99
): boolean {
1010
// Most provider config model lists are user-managed custom entries, not exhaustive
1111
// server catalogs. GitHub Copilot is the current exception because OAuth refresh
1212
// stores the full model catalog returned by Copilot's /models endpoint.
13-
if (gateway !== "github-copilot") {
13+
if (provider !== "github-copilot") {
1414
return true;
1515
}
1616

@@ -20,3 +20,11 @@ export function isGatewayModelAccessibleFromAuthoritativeCatalog(
2020

2121
return models.some((entry) => getProviderModelEntryId(entry) === modelId);
2222
}
23+
24+
export function isGatewayModelAccessibleFromAuthoritativeCatalog(
25+
gateway: string,
26+
modelId: string,
27+
models: ProviderModelEntry[] | undefined
28+
): boolean {
29+
return isProviderModelAccessibleFromAuthoritativeCatalog(gateway, modelId, models);
30+
}

0 commit comments

Comments
 (0)