11"""Pi coding agent: writes ~/.pi/agent/models.json with Databricks-backed providers.
22
3- Pi (https://pi.dev) is a multi-provider coding agent. We register three
3+ Pi (https://pi.dev) is a multi-provider coding agent. We register four
44providers in its `models.json`, each speaking the API dialect best suited to
55that family's gateway path:
66
77- `databricks-claude` (api: anthropic-messages) → /ai-gateway/anthropic
88- `databricks-openai` (api: openai-responses) → /ai-gateway/codex/v1
99- `databricks-gemini` (api: google-generative-ai) → /ai-gateway/gemini/v1beta
10+ - `databricks-mlflow` (api: openai-completions) → /ai-gateway/mlflow/v1
1011
1112Per-provider `compat` flags work around fields the gateway translators reject:
1213
1516 pi uses for every request. With this flag pi omits the per-tool field and
1617 sends the legacy `anthropic-beta: fine-grained-tool-streaming-...` header
1718 instead, which the gateway accepts.
18-
19- OSS / Databricks-foundation models (Llama, Qwen, etc.) are not exposed via
20- pi today — they live behind /ai-gateway/mlflow/v1 with per-model
21- `max_tokens` caps that pi has no global way to honor without per-model
22- config we don't currently maintain.
19+ - mlflow: `supportsStore: false` and `supportsStrictMode: false` — the MLflow
20+ chat-completions gateway rejects OpenAI's `store` field and
21+ `tools[].function.strict`.
22+
23+ The `databricks-mlflow` provider carries the validated OSS coding models
24+ (GLM and Kimi) discovered upstream. Per model it sets
25+ `contextWindow`/`maxTokens` from `databricks.model_token_limits` and
26+ `reasoning` from `databricks.model_is_reasoning` (so Pi renders the gateway's
27+ streamed reasoning_content as thinking). Inkling is intentionally not offered
28+ until the gateway emits a terminal `finish_reason` on natural completion
29+ (issue #215).
2330
2431The bearer token is baked into the file and refreshed by a background thread
2532while the session runs (same pattern as OpenCode/Copilot).
4552 TOKEN_REFRESH_INTERVAL_SECONDS ,
4653 build_pi_base_urls ,
4754 get_databricks_token ,
55+ model_is_reasoning ,
56+ model_token_limits ,
4857)
4958from ucode .state import mark_tool_managed , save_state
5059from ucode .telemetry import agent_version , ucode_version
6877 "databricks-claude" ,
6978 "databricks-openai" ,
7079 "databricks-gemini" ,
80+ "databricks-mlflow" ,
7181)
7282
7383PROVIDER_KEYS : list [list [str ]] = [["providers" , name ] for name in PROVIDER_NAMES ]
@@ -86,6 +96,7 @@ def _resolve_model_selector(
8696 claude_models : dict [str , str ],
8797 codex_models : list [str ],
8898 gemini_models : list [str ],
99+ oss_models : list [str ],
89100) -> str :
90101 """Return a Pi model selector in `<provider>/<model>` form when possible."""
91102 for name in PROVIDER_NAMES :
@@ -97,16 +108,37 @@ def _resolve_model_selector(
97108 return f"databricks-openai/{ model } "
98109 if model in gemini_models :
99110 return f"databricks-gemini/{ model } "
111+ if model in oss_models :
112+ return f"databricks-mlflow/{ model } "
100113 return model
101114
102115
116+ def _pi_oss_model_entry (model_id : str ) -> dict :
117+ """Build a Pi mlflow model entry enriched from the shared limits/reasoning
118+ tables: `reasoning:true` for reasoning models (Pi renders their streamed
119+ reasoning_content as thinking), and `contextWindow`/`maxTokens` from
120+ `model_token_limits`. Fields are omitted when unknown so Pi keeps its
121+ default."""
122+ entry : dict = {"id" : model_id }
123+ if model_is_reasoning (model_id ):
124+ entry ["reasoning" ] = True
125+ limits = model_token_limits (model_id )
126+ if limits :
127+ if limits .get ("context" ):
128+ entry ["contextWindow" ] = limits ["context" ]
129+ if limits .get ("output" ):
130+ entry ["maxTokens" ] = limits ["output" ]
131+ return entry
132+
133+
103134def render_overlay (
104135 model : str ,
105136 token : str ,
106137 pi_base_urls : dict [str , str ],
107138 claude_models : dict [str , str ],
108139 codex_models : list [str ],
109140 gemini_models : list [str ],
141+ oss_models : list [str ],
110142) -> tuple [dict , list [list [str ]]]:
111143 """Return (overlay, managed_key_paths) for ~/.pi/agent/models.json."""
112144 providers : dict = {}
@@ -150,8 +182,23 @@ def render_overlay(
150182 "models" : [{"id" : m } for m in gemini_models ],
151183 }
152184 keys .append (["providers" , "databricks-gemini" ])
185+ if oss_models :
186+ providers ["databricks-mlflow" ] = {
187+ "baseUrl" : pi_base_urls ["oss" ],
188+ "api" : "openai-completions" ,
189+ "apiKey" : token ,
190+ "authHeader" : True ,
191+ # MLflow chat-completions gateway rejects OpenAI's `store` field
192+ # and per-tool `strict`. Pi omits both when these are false.
193+ "compat" : {"supportsStore" : False , "supportsStrictMode" : False },
194+ "headers" : ua_headers ,
195+ "models" : [_pi_oss_model_entry (m ) for m in oss_models ],
196+ }
197+ keys .append (["providers" , "databricks-mlflow" ])
153198 overlay : dict = {
154- "model" : _resolve_model_selector (model , claude_models , codex_models , gemini_models ),
199+ "model" : _resolve_model_selector (
200+ model , claude_models , codex_models , gemini_models , oss_models
201+ ),
155202 }
156203 if providers :
157204 overlay ["providers" ] = providers
@@ -178,6 +225,7 @@ def write_tool_config(
178225 state .get ("claude_models" ) or {},
179226 state .get ("codex_models" ) or [],
180227 state .get ("gemini_models" ) or [],
228+ state .get ("oss_models" ) or [],
181229 )
182230 existing = read_json_safe (PI_CONFIG_PATH )
183231 providers = existing .get ("providers" )
@@ -206,7 +254,7 @@ def _write_settings(model_selector: str) -> None:
206254
207255
208256def default_model (state : dict ) -> str | None :
209- """Prefer Claude opus → sonnet → haiku; fall back to codex, gemini ."""
257+ """Prefer Claude opus → sonnet → haiku; fall back to codex, Gemini, then OSS ."""
210258 claude_models = state .get ("claude_models" ) or {}
211259 for family in ("opus" , "sonnet" , "haiku" ):
212260 if claude_models .get (family ):
@@ -215,7 +263,10 @@ def default_model(state: dict) -> str | None:
215263 if codex_models :
216264 return codex_models [0 ]
217265 gemini_models = state .get ("gemini_models" ) or []
218- return gemini_models [0 ] if gemini_models else None
266+ if gemini_models :
267+ return gemini_models [0 ]
268+ oss_models = state .get ("oss_models" ) or []
269+ return oss_models [0 ] if oss_models else None
219270
220271
221272def _refresh_token_once (state : dict , * , force_refresh : bool = False ) -> str :
0 commit comments