Skip to content

Commit 719288e

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. Also keep explicit compaction suggestions on the same availability rules and stabilize the useRouting hook test so it waits for loaded routing state before asserting unavailable Copilot routes are hidden. --- _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 719288e

6 files changed

Lines changed: 172 additions & 20 deletions

File tree

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,

src/browser/hooks/useRouting.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,18 @@ describe("useRouting", () => {
6868
models: [KNOWN_MODELS.GPT_54_MINI.providerModelId],
6969
},
7070
};
71+
routePriority = ["github-copilot", "direct"];
7172

7273
const { result } = renderHook(() => useRouting(), { wrapper });
7374

74-
await waitFor(() =>
75+
await waitFor(() => {
76+
expect(result.current.routePriority).toEqual(["github-copilot", "direct"]);
7577
expect(
7678
result.current
7779
.availableRoutes(KNOWN_MODELS.GPT.id)
7880
.some((route) => route.route === "github-copilot")
79-
).toBe(true)
80-
);
81-
82-
result.current.setRoutePreferences(["github-copilot", "direct"], {});
81+
).toBe(false);
82+
});
8383

8484
expect(result.current.resolveRoute(KNOWN_MODELS.GPT.id)).toEqual({
8585
route: "direct",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { KNOWN_MODELS } from "@/common/constants/knownModels";
4+
import type { ProvidersConfigMap } from "@/common/orpc/types";
5+
6+
import { getExplicitCompactionSuggestion } from "./suggestion";
7+
8+
const COPILOT_ONLY_PROVIDERS_CONFIG: ProvidersConfigMap = {
9+
"github-copilot": {
10+
apiKeySet: true,
11+
isEnabled: true,
12+
isConfigured: true,
13+
models: [KNOWN_MODELS.GPT_54_MINI.providerModelId],
14+
},
15+
};
16+
17+
const COPILOT_ONLY_OPTIONS = {
18+
providersConfig: COPILOT_ONLY_PROVIDERS_CONFIG,
19+
policy: null,
20+
routePriority: ["direct"],
21+
routeOverrides: {},
22+
};
23+
24+
describe("getExplicitCompactionSuggestion", () => {
25+
test("rejects explicit Copilot models missing from the authoritative catalog", () => {
26+
expect(
27+
getExplicitCompactionSuggestion({
28+
...COPILOT_ONLY_OPTIONS,
29+
modelId: `github-copilot:${KNOWN_MODELS.GPT.providerModelId}`,
30+
})
31+
).toBeNull();
32+
});
33+
34+
test("keeps explicit Copilot models that the authoritative catalog exposes", () => {
35+
expect(
36+
getExplicitCompactionSuggestion({
37+
...COPILOT_ONLY_OPTIONS,
38+
modelId: `github-copilot:${KNOWN_MODELS.GPT_54_MINI.providerModelId}`,
39+
})
40+
).toMatchObject({
41+
kind: "preferred",
42+
modelId: `github-copilot:${KNOWN_MODELS.GPT_54_MINI.providerModelId}`,
43+
});
44+
});
45+
});

src/browser/utils/compaction/suggestion.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { isModelAvailable } from "@/common/routing";
1010
import type { EffectivePolicy, ProvidersConfigMap } from "@/common/orpc/types";
1111
import { normalizeToCanonical } from "@/common/utils/ai/models";
1212
import { formatModelDisplayName } from "@/common/utils/ai/modelDisplay";
13-
import { isGatewayModelAccessibleFromAuthoritativeCatalog } from "@/common/utils/providers/gatewayModelCatalog";
13+
import {
14+
isGatewayModelAccessibleFromAuthoritativeCatalog,
15+
isProviderModelAccessibleFromAuthoritativeCatalog,
16+
} from "@/common/utils/providers/gatewayModelCatalog";
1417
import { getModelStats } from "@/common/utils/tokens/modelStats";
1518

1619
export interface CompactionSuggestion {
@@ -47,6 +50,26 @@ function buildIsGatewayModelAccessible(
4750
);
4851
}
4952

53+
function buildIsAuthoritativeProviderModelAccessible(
54+
providersConfig: ProvidersConfigMap | null
55+
): (modelString: string) => boolean {
56+
return (modelString: string) => {
57+
const normalized = normalizeToCanonical(modelString);
58+
const colonIndex = normalized.indexOf(":");
59+
if (colonIndex <= 0 || colonIndex >= normalized.length - 1) {
60+
return true;
61+
}
62+
63+
const provider = normalized.slice(0, colonIndex);
64+
const providerModelId = normalized.slice(colonIndex + 1);
65+
return isProviderModelAccessibleFromAuthoritativeCatalog(
66+
provider,
67+
providerModelId,
68+
providersConfig?.[provider]?.models
69+
);
70+
};
71+
}
72+
5073
export interface CompactionRouteOptions {
5174
routePriority: string[];
5275
routeOverrides: Record<string, string>;
@@ -70,6 +93,13 @@ export function getExplicitCompactionSuggestion(
7093
const normalized = normalizeToCanonical(modelId);
7194
const isConfigured = buildIsConfigured(options.providersConfig);
7295
const isGatewayModelAccessible = buildIsGatewayModelAccessible(options.providersConfig);
96+
const isAuthoritativeProviderModelAccessible = buildIsAuthoritativeProviderModelAccessible(
97+
options.providersConfig
98+
);
99+
if (!isAuthoritativeProviderModelAccessible(normalized)) {
100+
return null;
101+
}
102+
73103
if (
74104
!isModelAvailable(
75105
normalized,
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)