@@ -215,6 +215,89 @@ def test_custom_provider_prompts_for_base_url(self):
215215 assert [call .kwargs ["message" ] for call in mock_text_fn .call_args_list ] == ["Base URL:" , "Model ID:" ]
216216
217217
218+ class TestModelPricingDisplay :
219+ """Model list pricing display must distinguish free from unknown."""
220+
221+ def test_format_price_missing_is_na_zero_is_free (self ):
222+ from onemancompany .onboard import PRICE_FREE , PRICE_NA , _format_price
223+
224+ assert _format_price (None ) == PRICE_NA
225+ assert _format_price ("" ) == PRICE_NA
226+ assert _format_price ("0" ) == PRICE_FREE
227+
228+ def test_select_model_uses_existing_price_labels_without_reformatting (self ):
229+ from onemancompany .onboard import (
230+ MODEL_KEY_COMPLETION_PRICE ,
231+ MODEL_KEY_CONTEXT ,
232+ MODEL_KEY_ID ,
233+ MODEL_KEY_NAME ,
234+ MODEL_KEY_PROMPT_PRICE ,
235+ PRICE_FREE ,
236+ PRICE_NA ,
237+ _select_model_interactive ,
238+ )
239+
240+ mock_console = MagicMock ()
241+ fuzzy_prompt = MagicMock ()
242+ fuzzy_prompt .execute .return_value = "claude-haiku-4-5-20251001"
243+ models = [
244+ {
245+ MODEL_KEY_ID : "claude-haiku-4-5-20251001" ,
246+ MODEL_KEY_NAME : "Claude Haiku 4.5" ,
247+ MODEL_KEY_PROMPT_PRICE : "" ,
248+ MODEL_KEY_COMPLETION_PRICE : "" ,
249+ MODEL_KEY_CONTEXT : 0 ,
250+ },
251+ {
252+ MODEL_KEY_ID : "openrouter/free-model" ,
253+ MODEL_KEY_NAME : "Free Model" ,
254+ MODEL_KEY_PROMPT_PRICE : PRICE_FREE ,
255+ MODEL_KEY_COMPLETION_PRICE : PRICE_FREE ,
256+ MODEL_KEY_CONTEXT : 0 ,
257+ },
258+ {
259+ MODEL_KEY_ID : "priced-model" ,
260+ MODEL_KEY_NAME : "Priced Model" ,
261+ MODEL_KEY_PROMPT_PRICE : "$0.25/M" ,
262+ MODEL_KEY_COMPLETION_PRICE : "$1.25/M" ,
263+ MODEL_KEY_CONTEXT : 0 ,
264+ },
265+ ]
266+
267+ with patch ("InquirerPy.inquirer.fuzzy" , return_value = fuzzy_prompt ) as mock_fuzzy :
268+ selected = _select_model_interactive (mock_console , models )
269+
270+ choices = mock_fuzzy .call_args .kwargs ["choices" ]
271+ assert selected == "claude-haiku-4-5-20251001"
272+ assert choices [0 ]["name" ] == f"claude-haiku-4-5-20251001 [{ PRICE_NA } / { PRICE_NA } ]"
273+ assert choices [1 ]["name" ] == "openrouter/free-model [free / free]"
274+ assert choices [2 ]["name" ] == "priced-model [$0.25/M / $1.25/M]"
275+
276+ def test_fetch_provider_models_missing_pricing_is_na (self ):
277+ from onemancompany .onboard import (
278+ MODEL_KEY_COMPLETION_PRICE ,
279+ MODEL_KEY_PROMPT_PRICE ,
280+ PRICE_NA ,
281+ _fetch_provider_models ,
282+ )
283+
284+ mock_console = MagicMock ()
285+ mock_console .status .return_value .__enter__ .return_value = None
286+ mock_console .status .return_value .__exit__ .return_value = None
287+ mock_resp = MagicMock ()
288+ mock_resp .json .return_value = {
289+ "data" : [
290+ {"id" : "claude-haiku-4-5-20251001" , "display_name" : "Claude Haiku 4.5" }
291+ ]
292+ }
293+
294+ with patch ("httpx.get" , return_value = mock_resp ):
295+ models = _fetch_provider_models (mock_console , "anthropic" , "sk-ant-test" )
296+
297+ assert models [0 ][MODEL_KEY_PROMPT_PRICE ] == PRICE_NA
298+ assert models [0 ][MODEL_KEY_COMPLETION_PRICE ] == PRICE_NA
299+
300+
218301class TestOpenclawLaunchShErrorHandling :
219302 """launch.sh must surface errors instead of silently returning 'No output returned'."""
220303
0 commit comments