From c1b2ff20de98236c3f2bff4af578619eec1003a2 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:02:19 +0700 Subject: [PATCH 01/15] feat: Add method to fetch model info from openrouter pages --- aider/models.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/aider/models.py b/aider/models.py index cc54f03d3..8289a424a 100644 --- a/aider/models.py +++ b/aider/models.py @@ -211,7 +211,7 @@ class ModelInfoManager: def get_model_info(self, model): cached_info = self.get_model_from_cached_json_db(model) - + litellm_info = None if litellm._lazy_module or not cached_info: try: @@ -219,13 +219,52 @@ class ModelInfoManager: except Exception as ex: if "model_prices_and_context_window.json" not in str(ex): print(str(ex)) - + if litellm_info: return litellm_info - + + if not cached_info and model.startswith("openrouter/"): + openrouter_info = self.fetch_openrouter_model_info(model) + if openrouter_info: + return openrouter_info + return cached_info + def fetch_openrouter_model_info(self, model): + """ + Fetch model info by scraping the openrouter model page. + Expected URL: https://openrouter.ai/ + Example: openrouter/qwen/qwen-2.5-72b-instruct:free + Returns a dict with keys: max_input_tokens, input_cost, output_cost. + """ + url_part = model[len("openrouter/"):] + url = "https://openrouter.ai/" + url_part + import requests + try: + response = requests.get(url, timeout=5) + if response.status_code != 200: + return {} + html = response.text + from bs4 import BeautifulSoup + soup = BeautifulSoup(html, "html.parser") + text = soup.get_text() + import re + context_match = re.search(r"([\d,]+)\s*context", text) + if context_match: + context_str = context_match.group(1).replace(",", "") + context_size = int(context_str) + else: + context_size = None + input_cost_match = re.search(r"\$\s*0\s*/M input tokens", text, re.IGNORECASE) + output_cost_match = re.search(r"\$\s*0\s*/M output tokens", text, re.IGNORECASE) + input_cost = 0 if input_cost_match else None + output_cost = 0 if output_cost_match else None + return {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} + except Exception as e: + print("Error fetching openrouter info:", str(e)) + return {} + model_info_manager = ModelInfoManager() From 4600dbcda5c991b9f630cfd571bb8f884c5e41ab Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:05:55 +0700 Subject: [PATCH 02/15] feat: Print parsed model parameters in fetch_openrouter_model_info() method --- aider/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aider/models.py b/aider/models.py index 8289a424a..108dd9a97 100644 --- a/aider/models.py +++ b/aider/models.py @@ -260,7 +260,9 @@ class ModelInfoManager: output_cost_match = re.search(r"\$\s*0\s*/M output tokens", text, re.IGNORECASE) input_cost = 0 if input_cost_match else None output_cost = 0 if output_cost_match else None - return {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} + params = {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} + print("Parsed model parameters:", params) + return params except Exception as e: print("Error fetching openrouter info:", str(e)) return {} From 08d48f42ad6c826ad108d5d3becef2c602c27fe3 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:08:57 +0700 Subject: [PATCH 03/15] refactor: Remove BeautifulSoup dependency and use regex to strip HTML tags --- aider/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aider/models.py b/aider/models.py index 108dd9a97..28bc05f85 100644 --- a/aider/models.py +++ b/aider/models.py @@ -246,10 +246,8 @@ class ModelInfoManager: if response.status_code != 200: return {} html = response.text - from bs4 import BeautifulSoup - soup = BeautifulSoup(html, "html.parser") - text = soup.get_text() import re + text = re.sub(r'<[^>]+>', ' ', html) context_match = re.search(r"([\d,]+)\s*context", text) if context_match: context_str = context_match.group(1).replace(",", "") From 5e48f6898d17163e94a30c15df4507a6c23711cb Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:10:23 +0700 Subject: [PATCH 04/15] fix: Improve print statement to include model name in parameters output --- aider/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/models.py b/aider/models.py index 28bc05f85..fbe6ed181 100644 --- a/aider/models.py +++ b/aider/models.py @@ -259,7 +259,7 @@ class ModelInfoManager: input_cost = 0 if input_cost_match else None output_cost = 0 if output_cost_match else None params = {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} - print("Parsed model parameters:", params) + print(f"Model '{model}': Parsed parameters: {params}") return params except Exception as e: print("Error fetching openrouter info:", str(e)) From 44e5525e6f46f04b020b3feea19b63669d214dd7 Mon Sep 17 00:00:00 2001 From: Stefan Hladnik Date: Tue, 18 Mar 2025 03:17:56 +0700 Subject: [PATCH 05/15] style: Remove unnecessary blank lines in ModelInfoManager class --- aider/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aider/models.py b/aider/models.py index fbe6ed181..1bf2168b6 100644 --- a/aider/models.py +++ b/aider/models.py @@ -211,7 +211,7 @@ class ModelInfoManager: def get_model_info(self, model): cached_info = self.get_model_from_cached_json_db(model) - + litellm_info = None if litellm._lazy_module or not cached_info: try: @@ -219,15 +219,15 @@ class ModelInfoManager: except Exception as ex: if "model_prices_and_context_window.json" not in str(ex): print(str(ex)) - + if litellm_info: return litellm_info - + if not cached_info and model.startswith("openrouter/"): openrouter_info = self.fetch_openrouter_model_info(model) if openrouter_info: return openrouter_info - + return cached_info @@ -264,7 +264,7 @@ class ModelInfoManager: except Exception as e: print("Error fetching openrouter info:", str(e)) return {} - + model_info_manager = ModelInfoManager() From 2651d9967681accad317e1b7023a7303e20c1a0a Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:17:57 +0700 Subject: [PATCH 06/15] feat: Update cost extraction to capture non-zero input/output costs --- aider/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aider/models.py b/aider/models.py index 1bf2168b6..53b99c772 100644 --- a/aider/models.py +++ b/aider/models.py @@ -254,10 +254,10 @@ class ModelInfoManager: context_size = int(context_str) else: context_size = None - input_cost_match = re.search(r"\$\s*0\s*/M input tokens", text, re.IGNORECASE) - output_cost_match = re.search(r"\$\s*0\s*/M output tokens", text, re.IGNORECASE) - input_cost = 0 if input_cost_match else None - output_cost = 0 if output_cost_match else None + input_cost_match = re.search(r"\$\s*([\d.]+)\s*/M input tokens", text, re.IGNORECASE) + output_cost_match = re.search(r"\$\s*([\d.]+)\s*/M output tokens", text, re.IGNORECASE) + input_cost = float(input_cost_match.group(1)) if input_cost_match else None + output_cost = float(output_cost_match.group(1)) if output_cost_match else None params = {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} print(f"Model '{model}': Parsed parameters: {params}") return params From 29587cd07cc824ce2795f9b255540dd4c7f66575 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:24:04 +0700 Subject: [PATCH 07/15] fix: Update return keys in fetch_openrouter_model_info() to match JSON metadata --- aider/models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aider/models.py b/aider/models.py index 53b99c772..1ca6276fd 100644 --- a/aider/models.py +++ b/aider/models.py @@ -236,7 +236,7 @@ class ModelInfoManager: Fetch model info by scraping the openrouter model page. Expected URL: https://openrouter.ai/ Example: openrouter/qwen/qwen-2.5-72b-instruct:free - Returns a dict with keys: max_input_tokens, input_cost, output_cost. + Returns a dict with keys: max_tokens, max_input_tokens, max_output_tokens, input_cost_per_token, output_cost_per_token. """ url_part = model[len("openrouter/"):] url = "https://openrouter.ai/" + url_part @@ -258,7 +258,13 @@ class ModelInfoManager: output_cost_match = re.search(r"\$\s*([\d.]+)\s*/M output tokens", text, re.IGNORECASE) input_cost = float(input_cost_match.group(1)) if input_cost_match else None output_cost = float(output_cost_match.group(1)) if output_cost_match else None - params = {"max_input_tokens": context_size, "input_cost": input_cost, "output_cost": output_cost} + params = { + "max_input_tokens": context_size, + "max_tokens": context_size, + "max_output_tokens": context_size, + "input_cost_per_token": input_cost, + "output_cost_per_token": output_cost, + } print(f"Model '{model}': Parsed parameters: {params}") return params except Exception as e: From 4765a90f97722b124ea9166c7442b4baec6d48ef Mon Sep 17 00:00:00 2001 From: Stefan Hladnik Date: Tue, 18 Mar 2025 03:38:24 +0700 Subject: [PATCH 08/15] fix: Adjust input and output cost calculations to use million scale --- aider/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/models.py b/aider/models.py index 1ca6276fd..3499e7820 100644 --- a/aider/models.py +++ b/aider/models.py @@ -256,8 +256,8 @@ class ModelInfoManager: context_size = None input_cost_match = re.search(r"\$\s*([\d.]+)\s*/M input tokens", text, re.IGNORECASE) output_cost_match = re.search(r"\$\s*([\d.]+)\s*/M output tokens", text, re.IGNORECASE) - input_cost = float(input_cost_match.group(1)) if input_cost_match else None - output_cost = float(output_cost_match.group(1)) if output_cost_match else None + input_cost = float(input_cost_match.group(1)) / 1000000 if input_cost_match else None + output_cost = float(output_cost_match.group(1)) / 1000000 if output_cost_match else None params = { "max_input_tokens": context_size, "max_tokens": context_size, From b37773c6300428c5d7ac277f927862431601b01b Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:38:25 +0700 Subject: [PATCH 09/15] style: Update import location and add SSL verification in fetch_openrouter_model_info() --- aider/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/models.py b/aider/models.py index 3499e7820..ed014bb54 100644 --- a/aider/models.py +++ b/aider/models.py @@ -240,9 +240,9 @@ class ModelInfoManager: """ url_part = model[len("openrouter/"):] url = "https://openrouter.ai/" + url_part - import requests try: - response = requests.get(url, timeout=5) + import requests + response = requests.get(url, timeout=5, verify=self.verify_ssl) if response.status_code != 200: return {} html = response.text From 87ccacb99f79aeab6d3747d3b6c05d55377d9e30 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:46:36 +0700 Subject: [PATCH 10/15] fix: Return empty dict if any required parameters are missing --- aider/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/models.py b/aider/models.py index ed014bb54..749b2c79c 100644 --- a/aider/models.py +++ b/aider/models.py @@ -258,6 +258,8 @@ class ModelInfoManager: output_cost_match = re.search(r"\$\s*([\d.]+)\s*/M output tokens", text, re.IGNORECASE) input_cost = float(input_cost_match.group(1)) / 1000000 if input_cost_match else None output_cost = float(output_cost_match.group(1)) / 1000000 if output_cost_match else None + if context_size is None or input_cost is None or output_cost is None: + return {} params = { "max_input_tokens": context_size, "max_tokens": context_size, From d64427d726685f8687bd7400041b16fce2170c95 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:49:59 +0700 Subject: [PATCH 11/15] feat: Add error handling for unavailable model in response text --- aider/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/models.py b/aider/models.py index 749b2c79c..dc55e0a8a 100644 --- a/aider/models.py +++ b/aider/models.py @@ -246,6 +246,9 @@ class ModelInfoManager: if response.status_code != 200: return {} html = response.text + if f'The model "{url_part}" is not available' in html: + print(f"Error: Model '{url_part}' is not available") + sys.exit(1) import re text = re.sub(r'<[^>]+>', ' ', html) context_match = re.search(r"([\d,]+)\s*context", text) From 21cca34392a8bac0bb6ffbc1226a8877578944ad Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:53:27 +0700 Subject: [PATCH 12/15] fix: Allow arbitrary characters in model availability check regex --- aider/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/models.py b/aider/models.py index dc55e0a8a..fe92e8aa5 100644 --- a/aider/models.py +++ b/aider/models.py @@ -246,10 +246,10 @@ class ModelInfoManager: if response.status_code != 200: return {} html = response.text - if f'The model "{url_part}" is not available' in html: + import re + if re.search(rf'The model\s*.*{re.escape(url_part)}.* is not available', html, re.IGNORECASE): print(f"Error: Model '{url_part}' is not available") sys.exit(1) - import re text = re.sub(r'<[^>]+>', ' ', html) context_match = re.search(r"([\d,]+)\s*context", text) if context_match: From cdd730e627e7e9f2a18d77e72c96b1600706e6eb Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 03:54:27 +0700 Subject: [PATCH 13/15] feat: Print error message in red for unavailable models --- aider/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/models.py b/aider/models.py index fe92e8aa5..f78376948 100644 --- a/aider/models.py +++ b/aider/models.py @@ -248,7 +248,7 @@ class ModelInfoManager: html = response.text import re if re.search(rf'The model\s*.*{re.escape(url_part)}.* is not available', html, re.IGNORECASE): - print(f"Error: Model '{url_part}' is not available") + print(f"\033[91mError: Model '{url_part}' is not available\033[0m") sys.exit(1) text = re.sub(r'<[^>]+>', ' ', html) context_match = re.search(r"([\d,]+)\s*context", text) From 7c3d96d0e7a9360f60e4148c585f6553bb73b2c5 Mon Sep 17 00:00:00 2001 From: Stefan Hladnik Date: Tue, 18 Mar 2025 04:13:40 +0700 Subject: [PATCH 14/15] fix: Remove debug print statement from ModelInfoManager class --- aider/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aider/models.py b/aider/models.py index f78376948..a190f5f97 100644 --- a/aider/models.py +++ b/aider/models.py @@ -270,7 +270,6 @@ class ModelInfoManager: "input_cost_per_token": input_cost, "output_cost_per_token": output_cost, } - print(f"Model '{model}': Parsed parameters: {params}") return params except Exception as e: print("Error fetching openrouter info:", str(e)) From b3d9e0d1b058da0f9a560e3a6637b0cd446da7d8 Mon Sep 17 00:00:00 2001 From: "Stefan Hladnik (aider)" Date: Tue, 18 Mar 2025 12:18:48 +0700 Subject: [PATCH 15/15] fix: Return empty dict instead of exiting on model availability error --- aider/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/models.py b/aider/models.py index a190f5f97..bc1d54e12 100644 --- a/aider/models.py +++ b/aider/models.py @@ -249,7 +249,7 @@ class ModelInfoManager: import re if re.search(rf'The model\s*.*{re.escape(url_part)}.* is not available', html, re.IGNORECASE): print(f"\033[91mError: Model '{url_part}' is not available\033[0m") - sys.exit(1) + return {} text = re.sub(r'<[^>]+>', ' ', html) context_match = re.search(r"([\d,]+)\s*context", text) if context_match: