From 260efcfc2a3a8b9298494406c83e996ed6549119 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:18:57 +0200 Subject: [PATCH 1/2] fix: address issue #767 Fixes #767 --- src/cli/claude-desktop.ts | 4 +-- src/codex/catalog.ts | 2 +- src/codex/catalog/metadata.ts | 6 +++++ .../management/agent-settings-routes.ts | 8 +++--- src/server/management/shared.ts | 23 +++++++++++++---- src/types.ts | 5 ++++ tests/claude-desktop-cli.test.ts | 25 +++++++++++++++++++ 7 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/cli/claude-desktop.ts b/src/cli/claude-desktop.ts index 4ab0148d260..6e426ffa85b 100644 --- a/src/cli/claude-desktop.ts +++ b/src/cli/claude-desktop.ts @@ -10,7 +10,7 @@ import { type DesktopProfile, } from "../claude/desktop-profile"; import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p"; -import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog"; +import { filterCatalogVisibleModels, desktopVisibleNativeSlugs } from "../codex/catalog"; import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api"; import { findLiveProxy } from "../server/proxy-liveness"; @@ -42,7 +42,7 @@ async function applyProfile(profile: DesktopProfile, mode: Desktop3pConfigMode): })); const result = writeDesktop3pConfig( live?.port ?? config.port ?? 10100, - [...visibleNativeSlugs(config)], + [...desktopVisibleNativeSlugs(config)], routed, config.apiKeys?.[0]?.key, mode, diff --git a/src/codex/catalog.ts b/src/codex/catalog.ts index 03173400b69..8767a9e477b 100644 --- a/src/codex/catalog.ts +++ b/src/codex/catalog.ts @@ -2,7 +2,7 @@ // Public surface preserved exactly; importers keep using "src/codex/catalog". export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing"; export type { CatalogModel, MultiAgentMode } from "./catalog/parsing"; -export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata"; +export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata"; export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled"; export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort"; export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch"; diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 8587c286a87..4f4786e1711 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -123,6 +123,12 @@ export function visibleNativeSlugs(config: Pick): s return nativeOpenAiSlugs().filter(slug => !disabled.has(slug)); } +/** Native slugs exposed to Claude Desktop show/export/apply (opt-out via claudeCode.desktopNativeModels). */ +export function desktopVisibleNativeSlugs(config: Pick): string[] { + if (config.claudeCode?.desktopNativeModels === false) return []; + return visibleNativeSlugs(config); +} + export function nativeModelRows(config: Pick): Array<{ slug: string; disabled: boolean; contextWindow?: number }> { const disabled = disabledNativeSlugs(config); return NATIVE_OPENAI_MODELS.map(slug => { diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 96bb39c9943..953dad18464 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -80,12 +80,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise if (config.claudeCode?.desktopAutoApply === false) return; if (!config.claudeCode?.desktopProfile) return; const { writeDesktop3pConfig } = await import("../../claude/desktop-3p"); - const { visibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog"); + const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog"); const allModels = await fetchAllModels(config); const routed = filterCatalogVisibleModels(allModels, config).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })); const result = writeDesktop3pConfig( config.port ?? 10100, - [...visibleNativeSlugs(config)], + [...desktopVisibleNativeSlugs(config)], routed, config.apiKeys?.[0]?.key, "static", @@ -533,7 +533,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile }; saveConfigPreservingClaudeCode(config); const { writeDesktop3pConfig } = await import("../../claude/desktop-3p"); - const { visibleNativeSlugs } = await import("../../codex/catalog"); + const { desktopVisibleNativeSlugs } = await import("../../codex/catalog"); const routed = state.models .filter(model => model.available && !model.route.startsWith("native/")) .map(model => { @@ -542,7 +542,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise }); const result = writeDesktop3pConfig( Number(url.port) || config.port, - [...visibleNativeSlugs(config)], + [...desktopVisibleNativeSlugs(config)], routed, config.apiKeys?.[0]?.key, "static", diff --git a/src/server/management/shared.ts b/src/server/management/shared.ts index 6c6f2028259..02b9ab8316e 100644 --- a/src/server/management/shared.ts +++ b/src/server/management/shared.ts @@ -185,10 +185,10 @@ export interface GrokCandidateModel { * from the same two sources as the sync so the two can never disagree. */ export async function fetchGrokCandidateModels(config: OcxConfig): Promise { - const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog"); + const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog"); const routed = filterCatalogVisibleModels(await fetchAllModels(config), config); return [ - ...visibleNativeSlugs(config).map(id => { + ...desktopVisibleNativeSlugs(config).map(id => { const contextWindow = nativeOpenAiContextWindow(id); return { id, native: true, ...(contextWindow !== undefined ? { contextWindow } : {}) }; }), @@ -214,14 +214,14 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid /** Shared Desktop profile DTO builder for the management API and CLI. */ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) { - const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog"); + const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog"); const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p"); const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile"); const routed = filterCatalogVisibleModels(await fetchAllModels(config), config); const profileModels: DesktopProfileModel[] = [ // Native rows carry their real context window from the same accessor the Grok sync // uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop. - ...visibleNativeSlugs(config).map(id => { + ...desktopVisibleNativeSlugs(config).map(id => { const contextWindow = nativeOpenAiContextWindow(id); return { route: `native/${id}`, label: `${id} (native)`, ...(contextWindow !== undefined ? { contextWindow } : {}) }; @@ -233,6 +233,19 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla })), ]; const profile = reconcileDesktopProfile(stored ?? config.claudeCode?.desktopProfile, profileModels); + if (config.claudeCode?.desktopNativeModels === false) { + for (const route of Object.keys(profile.assignments)) { + if (route.startsWith("native/")) delete profile.assignments[route]; + } + for (const family of ["opus", "fable", "sonnet", "haiku"] as const) { + const current = profile.defaults[family]; + if (current?.startsWith("native/")) { + profile.defaults[family] = Object.keys(profile.assignments) + .filter(route => profile.assignments[route]?.family === family) + .sort()[0] ?? null; + } + } + } const available = new Set(profileModels.map(model => model.route)); const modelByRoute = new Map(profileModels.map(model => [model.route, model])); // Effort support: routed models with a non-empty reasoningEfforts ladder support effort; @@ -241,7 +254,7 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla for (const m of routed) { effortByRoute.set(`${m.provider}/${m.id}`, Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0); } - for (const id of visibleNativeSlugs(config)) { + for (const id of desktopVisibleNativeSlugs(config)) { effortByRoute.set(`native/${id}`, true); } const models = Object.keys(profile.assignments).sort().map(route => ({ diff --git a/src/types.ts b/src/types.ts index 983b483dd1b..647eb1a9fe3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -455,6 +455,11 @@ export interface OcxClaudeCodeConfig { desktopProfile?: OcxClaudeDesktopProfile; /** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */ desktopAutoApply?: boolean; + /** + * When false, omit `native/*` rows from Claude Desktop show/export/apply. Default: enabled. + * Routing-sidecar alias decoding is unchanged — only the Desktop model list writer. + */ + desktopNativeModels?: boolean; } export type OcxClaudeDesktopFamily = "opus" | "fable" | "sonnet" | "haiku"; diff --git a/tests/claude-desktop-cli.test.ts b/tests/claude-desktop-cli.test.ts index 22e1eee9755..b44a9aec17f 100644 --- a/tests/claude-desktop-cli.test.ts +++ b/tests/claude-desktop-cli.test.ts @@ -70,6 +70,31 @@ test("import rejects invalid profiles without replacing saved state", async () = } }); +test("desktopNativeModels:false omits native/* from show and exported profile", async () => { + saveConfig({ + port: 10100, + defaultProvider: "mock", + providers: { + mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1:1/v1", apiKey: "k", allowPrivateNetwork: true, models: ["test-model"] }, + }, + claudeCode: { desktopNativeModels: false }, + } as OcxConfig); + const log = spyOn(console, "log").mockImplementation(() => {}); + try { + expect(await handleClaudeDesktopCommand(["show", "--json"])).toBe(0); + const state = JSON.parse(String(log.mock.calls.at(-1)?.[0])); + expect(state.models.every((model: { route: string }) => !model.route.startsWith("native/"))).toBe(true); + expect(Object.keys(state.profile.assignments).every((route: string) => !route.startsWith("native/"))).toBe(true); + + const target = join(dir, "desktop-profile.json"); + expect(await handleClaudeDesktopCommand(["export", target])).toBe(0); + const exported = JSON.parse(readFileSync(target, "utf8")); + expect(Object.keys(exported.assignments).every((route: string) => !route.startsWith("native/"))).toBe(true); + } finally { + log.mockRestore(); + } +}); + test("no-arg and legacy mode flags apply Desktop config", async () => { const log = spyOn(console, "log").mockImplementation(() => {}); const error = spyOn(console, "error").mockImplementation(() => {}); From 5aff2ab8bd38044610d4734113dc4aa0a4946452 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:20:36 +0200 Subject: [PATCH 2/2] fix(claude): scope desktop native opt-out to Desktop surfaces Keep Grok candidate natives on visibleNativeSlugs and filter Anthropic Desktop discovery through desktopVisibleNativeSlugs. --- src/server/index.ts | 6 +++--- src/server/management/shared.ts | 4 ++-- tests/grok-management-api.test.ts | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 0df808b2a59..53ed9ea218d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -406,7 +406,7 @@ export function startServer(port?: number) { return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config); } const goModels = await fetchAllModels(config); - const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } = await import("../codex/catalog"); + const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog"); const nativeSlugs = nativeOpenAiSlugs(); const goEnabled = filterCatalogVisibleModels(goModels, config); const goOrdered = orderForSubagents(goEnabled, config.subagentModels); @@ -424,7 +424,7 @@ export function startServer(port?: number) { if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config); // Build Desktop 3P registry so inbound alias resolution works for subsequent requests. buildDesktop3pRegistry( - [...visibleNativeSlugs(config)], + [...desktopVisibleNativeSlugs(config)], goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })), config.claudeCode?.desktopProfile, ); @@ -441,7 +441,7 @@ export function startServer(port?: number) { : idsParam === "desktop" ? "desktop3p" as const : (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const); - const data = buildAnthropicModelInfos([...visibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias); + const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias); return jsonResponse({ data }, 200, req, config); } if (url.searchParams.has("client_version")) { diff --git a/src/server/management/shared.ts b/src/server/management/shared.ts index 02b9ab8316e..29cdde2e087 100644 --- a/src/server/management/shared.ts +++ b/src/server/management/shared.ts @@ -185,10 +185,10 @@ export interface GrokCandidateModel { * from the same two sources as the sync so the two can never disagree. */ export async function fetchGrokCandidateModels(config: OcxConfig): Promise { - const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog"); + const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog"); const routed = filterCatalogVisibleModels(await fetchAllModels(config), config); return [ - ...desktopVisibleNativeSlugs(config).map(id => { + ...visibleNativeSlugs(config).map(id => { const contextWindow = nativeOpenAiContextWindow(id); return { id, native: true, ...(contextWindow !== undefined ? { contextWindow } : {}) }; }), diff --git a/tests/grok-management-api.test.ts b/tests/grok-management-api.test.ts index 3f3e8246086..73436ab3dee 100644 --- a/tests/grok-management-api.test.ts +++ b/tests/grok-management-api.test.ts @@ -118,6 +118,23 @@ test("GET /api/grok includes candidates and the saved exclusion list", async () } }); +test("GET /api/grok keeps native candidates when claudeCode.desktopNativeModels is false", async () => { + const config = loadConfig(); + config.claudeCode = { desktopNativeModels: false }; + saveConfig(config); + const server = startServer(0); + try { + const res = await fetch(new URL("/api/grok", server.url)); + expect(res.status).toBe(200); + const body = await res.json() as { + candidates: Array<{ id: string; native: boolean }>; + }; + expect(body.candidates.some(c => c.native)).toBe(true); + } finally { + server.stop(true); + } +}); + test("POST /api/grok/apply reports no-grok-home as a policy skip, not an error", async () => { const server = startServer(0); try {