<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ti Gg</title>
    <description>The latest articles on DEV Community by Ti Gg (@ti_gg_043b3e06a4b7d5d0888).</description>
    <link>https://dev.to/ti_gg_043b3e06a4b7d5d0888</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4210925%2F1b12cfa9-134c-48d5-9656-b5808f5e21ae.png</url>
      <title>DEV Community: Ti Gg</title>
      <link>https://dev.to/ti_gg_043b3e06a4b7d5d0888</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ti_gg_043b3e06a4b7d5d0888"/>
    <language>en</language>
    <item>
      <title>ML Experiment Cockpit: read your MLflow, Optuna and W&amp;B runs without leaving your IDE</title>
      <dc:creator>Ti Gg</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:36:16 +0000</pubDate>
      <link>https://dev.to/ti_gg_043b3e06a4b7d5d0888/ml-experiment-cockpit-read-your-mlflow-optuna-and-wb-runs-without-leaving-your-ide-57aa</link>
      <guid>https://dev.to/ti_gg_043b3e06a4b7d5d0888/ml-experiment-cockpit-read-your-mlflow-optuna-and-wb-runs-without-leaving-your-ide-57aa</guid>
      <description>&lt;p&gt;Reading an ML experiment usually means leaving your editor. You open a browser tab for the MLflow UI, a second one for the W&amp;amp;B dashboard, maybe a notebook cell to call &lt;code&gt;mlflow.search_runs()&lt;/code&gt; and print a metric curve. Each of those is a context switch: a different window, a different login, a different rendering of the same numbers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plugins.jetbrains.com/plugin/35416-ml-experiment-cockpit" rel="noopener noreferrer"&gt;ML Experiment Cockpit&lt;/a&gt; is a free, Apache-2.0 JetBrains plugin that puts that information in a tool window inside PyCharm, IntelliJ IDEA, WebStorm, GoLand and CLion. It connects to servers you already run and renders their answers. It never writes anything back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;One tool window, four tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Experiments&lt;/strong&gt; — the MLflow experiment tree: experiments, their runs, each run's status, name and parameters. A &lt;strong&gt;Refresh&lt;/strong&gt; button reloads the tree. Selecting a run opens a detail pane where you pick a metric and read its history as a curve, with the last value and the step it was recorded at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optuna&lt;/strong&gt; — studies and trials, read from a running &lt;code&gt;optuna-dashboard&lt;/code&gt;. Multi-objective studies are only partially supported: the first objective is the one displayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models&lt;/strong&gt; — the MLflow Model Registry: registered models and their versions, with the stage each version sits in (&lt;code&gt;None&lt;/code&gt;, &lt;code&gt;Staging&lt;/code&gt;, &lt;code&gt;Production&lt;/code&gt;, &lt;code&gt;Archived&lt;/code&gt;). This tab reuses the MLflow tracking URI, so there is nothing extra to configure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sweeps&lt;/strong&gt; — Weights &amp;amp; Biases sweeps, listed by state with their run count. The tab asks for a W&amp;amp;B entity and a project name (both fields are pre-filled from the settings) and authenticates with a W&amp;amp;B API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: install it
&lt;/h2&gt;

&lt;p&gt;Search for "ML Experiment Cockpit" in &lt;em&gt;Settings | Plugins&lt;/em&gt; of any JetBrains IDE, or grab it from the &lt;a href="https://plugins.jetbrains.com/plugin/35416-ml-experiment-cockpit" rel="noopener noreferrer"&gt;Marketplace page&lt;/a&gt;. The published versions install on IDEs from 2024.3 (build 243) up to 2026.3 (build 263).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: point it at your servers
&lt;/h2&gt;

&lt;p&gt;Open &lt;em&gt;Settings | Tools | ML Experiment Cockpit&lt;/em&gt; and fill in what you use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MLflow tracking server URL&lt;/strong&gt;, for example &lt;code&gt;http://localhost:5000&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optuna dashboard URL&lt;/strong&gt;, for example &lt;code&gt;http://localhost:8080&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;W&amp;amp;B API key&lt;/strong&gt;, plus the default entity and project. The key comes from &lt;a href="https://www.wandb.ai/settings" rel="noopener noreferrer"&gt;https://www.wandb.ai/settings&lt;/a&gt; → Danger Zone → "API keys", and it is stored in the IDE's own credential store, not in a plaintext settings file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server-side parts are the ones you already run. The plugin does not start an MLflow server for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: read a run
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Refresh&lt;/strong&gt; in the Experiments tab and the tree fills with your experiments. Expand one, click a run, and the detail pane lists its parameters and metrics. Pick a metric from the drop-down and the plugin fetches its full history and draws the curve, labelled with the last value and its step.&lt;/p&gt;

&lt;p&gt;Here is the code that does the reading — &lt;code&gt;MlflowClient.getRunMetrics()&lt;/code&gt;, in &lt;code&gt;src/main/kotlin/com/slot5/mlexp/MlflowClient.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getRunMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;out&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="c1"&gt;// MLflow (protobuf JSON) writes NaN / Infinity as *strings*:&lt;/span&gt;
            &lt;span class="c1"&gt;// `m.dbl("value")` would silently turn them into 0.0.&lt;/span&gt;
            &lt;span class="k"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;num&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;asDouble&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"NaN"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NaN&lt;/span&gt;
                &lt;span class="s"&gt;"Infinity"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;POSITIVE_INFINITY&lt;/span&gt;
                &lt;span class="s"&gt;"-Infinity"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NEGATIVE_INFINITY&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are worth reading twice. First, &lt;code&gt;runGet(runId)&lt;/code&gt; is a plain &lt;code&gt;GET /api/2.0/mlflow/runs/get&lt;/code&gt;: MLflow 3.x answers HTTP 405 to a POST on that endpoint, so the verb is measured, not assumed. Second, the NaN branch — MLflow writes &lt;code&gt;NaN&lt;/code&gt; and &lt;code&gt;Infinity&lt;/code&gt; as &lt;em&gt;strings&lt;/em&gt; in its protobuf JSON, and a naive conversion would silently turn a diverged run into a flat 0.0 on your chart. A reader that lies about the shape of your metrics is worse than no reader at all.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;mlflow-client-java&lt;/code&gt; dependency in the plugin. The client is the JDK's &lt;code&gt;HttpClient&lt;/code&gt; plus a small hand-rolled JSON parser of about 6 KB, which avoids pulling in a library with megabytes of transitive dependencies. That parser is deliberately strict: a body that is not JSON raises an error instead of producing an empty list, so "0 experiment(s)" never silently means "the server answered with an HTML error page".&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-only, on purpose
&lt;/h2&gt;

&lt;p&gt;This is a design constraint, not a missing feature. Across the whole plugin there is no &lt;code&gt;PUT&lt;/code&gt;, no &lt;code&gt;DELETE&lt;/code&gt; and no GraphQL mutation. The only requests carrying a body are MLflow's &lt;code&gt;experiments/search&lt;/code&gt; and &lt;code&gt;runs/search&lt;/code&gt;, the "Connect to MLflow Tracking Server" probe (a &lt;code&gt;search&lt;/code&gt; as well), and the W&amp;amp;B GraphQL &lt;code&gt;POST /graphql&lt;/code&gt;, which carries a &lt;code&gt;query&lt;/code&gt; — GraphQL sends queries by POST, and a mutation would be a different document.&lt;/p&gt;

&lt;p&gt;Concretely, the plugin can show you that a run is training, which metric is climbing, whether the model you just registered made it to the Registry, which sweep is running and how many trials it has. It cannot start a run, delete one, tag a model, download an artifact or touch a deployment. If you want to change something, you change it in your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only.&lt;/strong&gt; No writes, no artifacts, no run control, no model serving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No local MLflow.&lt;/strong&gt; The plugin speaks HTTP to a tracking server you already run (&lt;code&gt;mlflow server&lt;/code&gt;, a container, whatever you use). It does not read the &lt;code&gt;mlruns/&lt;/code&gt; folder of a local run, and it does not ship a server: point the tracking URI at a running instance, otherwise the tabs stay empty. The same goes for Optuna, which needs &lt;code&gt;optuna-dashboard&lt;/code&gt; running as a separate service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query limits.&lt;/strong&gt; Up to 500 experiments, 100 runs per experiment, 500 registered models, 500 model versions and 5 000 metric points per run; W&amp;amp;B sweeps are read without pagination, so you get the first 50. These are the limits the plugin asks the server for, not a promise of completeness — with 4 000 runs in an experiment you are looking at the first 100.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write Python against MLflow every day and would rather read the state of your experiments from the editor you are already in, give it a try and tell us what is missing — the next versions are decided by what you hit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this article was written with the help of AI and reviewed before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jetbrains</category>
      <category>mlflow</category>
      <category>python</category>
    </item>
  </channel>
</rss>
