Skip to content

(feat) Auto get free models from openrouter #2630

Closed
coderdylanbates wants to merge 23 commits into
dyad-sh:mainfrom
coderdylanbates:contrib-2
Closed

(feat) Auto get free models from openrouter #2630
coderdylanbates wants to merge 23 commits into
dyad-sh:mainfrom
coderdylanbates:contrib-2

Conversation

@coderdylanbates

@coderdylanbates coderdylanbates commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Sometimes the free router breaks and sometimes I want to use a specific model but theres just so many free models its a hassle adding them each individually so heres this PR

Screenshot 2026-02-12 110820
  • Initial functionality
  • Add e2e tests
  • Improve UX

Open with Devin

Summary by cubic

Automatically fetch and cache OpenRouter free models at startup, add a “Free models” submenu in ModelPicker, and build the free router fallback from dynamic free model IDs. The OpenRouter response is schema-validated, sanitized, and token limits are mapped for accurate caps.

  • New Features

    • Fetch OpenRouter models on startup; cache only $0-priced models with a safe default fallback.
    • Validate and sanitize /api/v1/models with Zod; strip HTML/script from names/descriptions; map context_length and max_completion_tokens to contextWindow/maxOutputTokens.
    • Merge and de-duplicate fetched free models with hardcoded options; remove “(free)” suffixes; hide duplicates from the main OpenRouter list with a “Free models” submenu.
    • Free routing now builds its fallback set from fetched free model IDs.
    • Added tests for free-model filtering, sanitization, display formatting, and token limit mapping.
  • Bug Fixes

    • Normalize paths to forward slashes in safeJoin and grep results; convert CRLF to LF in DSL test parsing.
    • Hide redundant “Free” badge when price is $0; update OpenRouter labels to remove “(free)”.
    • Build: include needed modules in Forge (better-sqlite3, bindings, esprima, source-map, tslib, recast, ast-types) and mark recast/ast-types as externals in Vite.

Written for commit fcd705f. Summary will update on new commits.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @coderdylanbates, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement by automating the discovery and integration of free models from OpenRouter. It fetches the latest list of free models directly from the OpenRouter API and presents them in a structured sub-menu within the model selection interface. This change aims to simplify model selection for users and ensure access to current free offerings. Additionally, it includes several foundational improvements to path handling utilities and build configurations, contributing to better cross-platform compatibility and overall system robustness.

Highlights

  • Dynamic OpenRouter Free Model Fetching: Implemented functionality to automatically fetch and cache a list of free models from the OpenRouter API, ensuring the application always has an up-to-date selection.
  • Enhanced Model Picker UI: Updated the Model Picker component to display dynamically fetched OpenRouter free models in a dedicated 'Free models' sub-menu, improving user experience and discoverability.
  • Improved Path Utility Consistency: Refined the safeJoin utility and its associated tests to ensure consistent path normalization across different operating systems, particularly handling Windows-style paths and line endings.
  • Build Configuration Updates: Adjusted build configurations (forge.config.ts, vite.main.config.mts) to properly handle new dependencies related to AST parsing and code transformation, ensuring smooth compilation and execution.
Changelog
  • forge.config.ts
    • Added recast, ast-types, esprima, source-map, and tslib to the node_modules ignore list to prevent bundling issues.
  • src/tests/openrouter_free_models.test.ts
    • Added a new test file to verify the buildOpenRouterFreeModels function correctly filters and decorates free models.
  • src/tests/path_utils.test.ts
    • Updated safeJoin test expectations to use template literals for clearer path comparisons.
    • Modified tests to reflect improved path normalization, including handling empty segments and . references.
  • src/components/ModelPicker.tsx
    • Imported LanguageModel type for better type safety.
    • Implemented logic to identify OpenRouter free models and display them within a new 'Free models' sub-menu.
    • Adjusted model rendering to differentiate between free and other models, including conditional tag display.
  • src/ipc/shared/language_model_constants.ts
    • Removed '(free)' suffix from displayName for 'Qwen3 Coder' and 'Devstral 2' models.
    • Simplified the displayName for the 'free' OpenRouter option.
    • Removed the FREE_OPENROUTER_MODEL_NAMES constant, as free models are now dynamically fetched.
  • src/ipc/shared/language_model_helpers.ts
    • Imported getOpenRouterFreeModels to dynamically retrieve free OpenRouter models.
    • Integrated dynamically fetched free OpenRouter models into the getLanguageModels function, ensuring they are included in the available model list.
  • src/ipc/shared/openrouter_free_models.ts
    • Added a new file containing types and functions for fetching, processing, and caching free OpenRouter models.
    • Implemented hydrateOpenRouterFreeModels to asynchronously fetch models from the OpenRouter API.
    • Defined buildOpenRouterFreeModels to filter and format models based on pricing and display names.
    • Included getOpenRouterFreeModels and getOpenRouterFreeModelNames for accessing the cached list.
  • src/ipc/utils/get_model_client.ts
    • Replaced the static FREE_OPENROUTER_MODEL_NAMES with the dynamic getOpenRouterFreeModelNames for model client creation.
  • src/ipc/utils/path_utils.ts
    • Applied normalizePath to the final safeJoin result to ensure consistent path formatting.
  • src/main.ts
    • Imported hydrateOpenRouterFreeModels.
    • Called hydrateOpenRouterFreeModels during application initialization to populate the free model cache.
  • src/pro/main/ipc/handlers/local_agent/tools/grep.ts
    • Added normalization for backslashes to forward slashes in ripgrep results for cross-platform consistency.
  • src/pro/main/ipc/processors/search_replace_dsl_test_runner.ts
    • Added replace(/\r\n/g, '\n') to normalize line endings in parsed test case content, diffs, and expected outputs for Windows compatibility.
  • vite.main.config.mts
    • Added recast and ast-types to rollupOptions.external to prevent them from being bundled.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

devin-ai-integration[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

gemini-code-assist[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds automatic fetching and caching of free OpenRouter models at startup, surfacing them in a dedicated "Free models" submenu within ModelPicker, and wiring the free router fallback to the dynamically fetched list instead of the previous static constant.

Key changes:

  • New openrouter_free_models.ts module — fetches /api/v1/models on startup with a 5-second abort timeout, validates with Zod, and caches $0-priced models. Sanitization removes HTML tags but preserves tag body text (e.g., <script>alert(1)</script>alert(1) in tooltips) — while not an XSS risk since content is rendered as text, displaying script body content as model description is confusing UX.
  • Free model detection and UI split — the merge separates API-fetched free models into a distinct submenu, with hardcoded :free models as fallback.
  • Duplicate display names — both qwen/qwen3-coder:free and qwen/qwen3-coder use the same display name "Qwen3 Coder", which can confuse users seeing both in different submenus.
  • Dead exportFREE_OPENROUTER_MODEL_NAMES in constants is no longer consumed by get_model_client.ts and represents an orphaned constant.

The core fetch/cache/submenu flow is solid and the dynamic free model list is a good improvement. Remaining concerns are style and UX clarity issues around naming, sanitization, and dead exports.

Confidence Score: 4/5

  • Safe to merge — all identified issues are style and UX improvements, with no blocking bugs.
  • The PR's core functionality (fetch, validate, cache, and display free models) is sound. All three remaining findings are valid code quality observations: removing dead exports, improving sanitization clarity for edge cases, and distinguishing duplicate model names in the UI. None are critical bugs, and the feature enhances the user experience by dynamically sourcing free models from the OpenRouter API.
  • src/ipc/shared/language_model_constants.ts (dead export and duplicate display names should be addressed), src/ipc/shared/openrouter_free_models.ts (optional: improve script/style tag sanitization for UX clarity).

Sequence Diagram

sequenceDiagram
    participant Main as main.ts (onReady)
    participant Hydrate as hydrateOpenRouterFreeModels
    participant OR as OpenRouter API
    participant Cache as cachedFreeModels (module state)
    participant IPC as getLanguageModels IPC
    participant UI as ModelPicker (React)

    Main->>Hydrate: hydrateOpenRouterFreeModels() (no await)
    activate Hydrate
    Note over Cache: initialised with DEFAULT_FREE_MODELS
    Hydrate->>OR: GET /api/v1/models (5s timeout)
    OR-->>Hydrate: { data: [...] }
    Hydrate->>Hydrate: Zod parse → buildOpenRouterFreeModels()
    Hydrate->>Cache: cachedFreeModels = freeModels (or DEFAULT on error)
    deactivate Hydrate

    UI->>IPC: getLanguageModels({ providerId: "openrouter" })
    IPC->>Cache: getOpenRouterFreeModels()
    Cache-->>IPC: LanguageModel[]
    IPC->>IPC: dedupe([...freeModels, ...hardcodedModels])
    IPC-->>UI: merged LanguageModel[]
    UI->>UI: split into openRouterFreeModels / displayModels
    UI-->>UI: render "Free models" submenu + main list
Loading

Last reviewed commit: 383f7e0

greptile-apps[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Feb 12, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/main.ts
Unhandled async startup fetch

hydrateOpenRouterFreeModels() is invoked without await, so any fetch/parse errors become an unhandled rejection unless hydrateOpenRouterFreeModels fully swallows them. Since it does fetch(...) and can throw before entering its catch only if something synchronous fails (rare), this still means the app can proceed with stale DEFAULT_FREE_MODELS while the UI may render before the cache is hydrated. If startup ordering matters, await this (or explicitly void hydrateOpenRouterFreeModels() and ensure it never rejects).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main.ts
Line: 91:96

Comment:
**Unhandled async startup fetch**

`hydrateOpenRouterFreeModels()` is invoked without `await`, so any fetch/parse errors become an unhandled rejection unless `hydrateOpenRouterFreeModels` fully swallows them. Since it does `fetch(...)` and can throw before entering its `catch` only if something synchronous fails (rare), this still means the app can proceed with stale `DEFAULT_FREE_MODELS` while the UI may render before the cache is hydrated. If startup ordering matters, `await` this (or explicitly `void hydrateOpenRouterFreeModels()` and ensure it never rejects).


How can I resolve this? If you propose a fix, please make it concise.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Feb 12, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/ipc/utils/get_model_client.ts
Empty fallback model list
createFallback({ models: getOpenRouterFreeModelNames().map(...) }) can be called with an empty array if the OpenRouter free-model hydration fails and DEFAULT_FREE_MODELS is empty (e.g., if the hardcoded MODEL_OPTIONS.openrouter list ever drops all :free entries). createFallback typically expects at least one underlying model; with an empty list this will fail at runtime when the user selects auto/free.

Even if this is unlikely today, the current code makes the runtime safety depend on the hardcoded constants always containing at least one :free model.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ipc/utils/get_model_client.ts
Line: 136:154

Comment:
**Empty fallback model list**
`createFallback({ models: getOpenRouterFreeModelNames().map(...) })` can be called with an empty array if the OpenRouter free-model hydration fails and `DEFAULT_FREE_MODELS` is empty (e.g., if the hardcoded `MODEL_OPTIONS.openrouter` list ever drops all `:free` entries). `createFallback` typically expects at least one underlying model; with an empty list this will fail at runtime when the user selects `auto/free`.

Even if this is unlikely today, the current code makes the runtime safety depend on the hardcoded constants always containing at least one `:free` model.


How can I resolve this? If you propose a fix, please make it concise.

@coderdylanbates coderdylanbates requested a review from a team February 16, 2026 22:17

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps

greptile-apps Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/components/ModelPicker.tsx
Model count is misleading

models.length includes free models that have been moved into the "Free models" sub-submenu. Users will see e.g. "30 models" in the OpenRouter trigger but only find the non-free subset when they click through. For OpenRouter, this should use displayModels.length + (openRouterFreeModels.length > 0 ? 1 : 0) or simply displayModels.length to accurately reflect what's shown in the main submenu, or the total with a note.

                          {displayModels.length + openRouterFreeModels.length} models
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/ModelPicker.tsx
Line: 343:343

Comment:
**Model count is misleading**

`models.length` includes free models that have been moved into the "Free models" sub-submenu. Users will see e.g. "30 models" in the OpenRouter trigger but only find the non-free subset when they click through. For OpenRouter, this should use `displayModels.length + (openRouterFreeModels.length > 0 ? 1 : 0)` or simply `displayModels.length` to accurately reflect what's shown in the main submenu, or the total with a note.

```suggestion
                          {displayModels.length + openRouterFreeModels.length} models
```

How can I resolve this? If you propose a fix, please make it concise.

@wwwillchen wwwillchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @coderdylanbates thanks for the PR

i think it's overall a good change, but needs a few changes before we can land it:

UX-wise

  • Let's pin the Free (OpenRouter) to the top since it's a good default option.
  • We should plumb the model description too

Testing
Our project primarily relies on e2e tests for coverage. here's some docs on e2e testing: https://github.com/dyad-sh/dyad/blob/main/CONTRIBUTING.md#e2e-tests - the main thing would be to create a fake OpenRouter endpoint which we do for other things like github, llm servers, etc. in https://github.com/dyad-sh/dyad/tree/main/testing/fake-llm-server

There's also a lot of other seemingly unrelated changes. Let's revert those changes and focus only on openrouter-specific changes for this PR.

Comment thread forge.config.ts Outdated
"esprima",
"source-map",
"tslib",
"bindings",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason this file is changing? it doesn't look related to the openrouter changes. there's also a few other unrelated changes in this PR as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @wwwillchen the other seemingly unrelated changes are related to tests that were not passing on my machine. I wanted to make sure all of the tests passed so I changed them a bit, however I understand those changes may not be in the scope of this PR so I will remove the changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @wwwillchen I've been trying to add e2e tests and followed the instructions in Contributing.md but it seems the testing suit always errors out for me with exit code 2, even though I have all dependencies and build tools installed. This seems to happen even without my new tests added, and I'm not sure how you want me to go about it since you don't want me to edit the tests. Of course, I could edit the testing suite on my local version of the project, but I worry the results won't accurately reflect those in the proper production test suite.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @coderdylanbates - sorry for the delay, could you share more details on the error you're getting when running e2e tests? e.g. error logs, etc.

i've rebased the PR and going to run the e2e tests in CI. if it's clean, i'll land it - thanks

Comment thread package-lock.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this file

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="forge.config.ts">

<violation number="1">
P2: Removed whitelist entries can exclude externalized runtime deps (recast/ast-types/etc.), causing packaged app to miss required modules.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

devin-ai-integration[bot]

This comment was marked as resolved.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

const logger = log.scope("openrouter_free_models");

const DEFAULT_FREE_MODELS: LanguageModel[] = MODEL_OPTIONS.openrouter
.filter((model) => model.name.endsWith(":free"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter won't match openrouter/free (ends with /free, not :free), so DEFAULT_FREE_MODELS will be empty

Suggested change
.filter((model) => model.name.endsWith(":free"))
.filter((model) => model.name.endsWith(":free") || model.name.endsWith("/free"))
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ipc/shared/openrouter_free_models.ts
Line: 42

Comment:
Filter won't match `openrouter/free` (ends with `/free`, not `:free`), so `DEFAULT_FREE_MODELS` will be empty

```suggestion
  .filter((model) => model.name.endsWith(":free") || model.name.endsWith("/free"))
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

).toBeVisible();

await expect(
po.page.getByRole("menuitem").filter({ hasText: "Free (OpenRouter)" }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test expects "Free (OpenRouter)" but language_model_constants.ts:234 now shows displayName: "Free" - the test will fail.

Suggested change
po.page.getByRole("menuitem").filter({ hasText: "Free (OpenRouter)" }),
po.page.getByRole("menuitem").filter({ hasText: "Free" }),
Prompt To Fix With AI
This is a comment left during a code review.
Path: e2e-tests/openrouter_free_models.spec.ts
Line: 29

Comment:
Test expects "Free (OpenRouter)" but `language_model_constants.ts:234` now shows `displayName: "Free"` - the test will fail.

```suggestion
      po.page.getByRole("menuitem").filter({ hasText: "Free" }),
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 276 to +303
@@ -280,6 +291,16 @@ export const MODEL_OPTIONS: Record<string, ModelOption[]> = {
temperature: 0.7,
dollarSigns: 2,
},
// https://openrouter.ai/mistralai/devstral-2512
{
name: "mistralai/devstral-2512:free",
displayName: "Devstral 2",
description: "Use for free (data may be used for training)",
maxOutputTokens: 32_000,
contextWindow: 200_000,
temperature: 0.7,
dollarSigns: 2,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect dollarSigns on new :free hardcoded models

Both new free-tier models are given non-zero dollarSigns values (1 and 2), but because the deduplication in language_model_helpers.ts uses [...freeModels, ...hardcodedModels], hardcoded entries always overwrite the API-fetched entries with dollarSigns: 0. The result is that these two models end up with dollarSigns: 1 / dollarSigns: 2 in the merged list, so PriceBadge will display $ / $$ for them inside the "Free models" submenu — contradicting the "free" label.

Both entries should use dollarSigns: 0:

Suggested change
// https://openrouter.ai/qwen/qwen3-coder
{
name: "qwen/qwen3-coder:free",
displayName: "Qwen3 Coder",
description: "Use for free (data may be used for training)",
maxOutputTokens: 32_000,
contextWindow: 196_608,
temperature: 0,
dollarSigns: 0,
},

And for mistralai/devstral-2512:free:

Suggested change
dollarSigns: 0,
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ipc/shared/language_model_constants.ts
Line: 276-303

Comment:
**Incorrect `dollarSigns` on new `:free` hardcoded models**

Both new free-tier models are given non-zero `dollarSigns` values (`1` and `2`), but because the deduplication in `language_model_helpers.ts` uses `[...freeModels, ...hardcodedModels]`, hardcoded entries always **overwrite** the API-fetched entries with `dollarSigns: 0`. The result is that these two models end up with `dollarSigns: 1` / `dollarSigns: 2` in the merged list, so `PriceBadge` will display `$` / `$$` for them inside the "Free models" submenu — contradicting the "free" label.

Both entries should use `dollarSigns: 0`:

```suggestion
    // https://openrouter.ai/qwen/qwen3-coder
    {
      name: "qwen/qwen3-coder:free",
      displayName: "Qwen3 Coder",
      description: "Use for free (data may be used for training)",
      maxOutputTokens: 32_000,
      contextWindow: 196_608,
      temperature: 0,
      dollarSigns: 0,
    },
```

And for `mistralai/devstral-2512:free`:
```suggestion
      dollarSigns: 0,
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +152 to +159
if (providerId === "openrouter") {
const freeModels = getOpenRouterFreeModels();
const dedupe = new Map<string, LanguageModel>();
[...freeModels, ...hardcodedModels].forEach((model) => {
dedupe.set(model.apiName, model);
});
hardcodedModels = Array.from(dedupe.values());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplication direction discards API-fetched data

The array spread [...freeModels, ...hardcodedModels] means that when the same apiName exists in both lists, Map.set is called last with the hardcoded entry, so hardcoded values always win. The API-fetched contextWindow, maxOutputTokens, dollarSigns: 0, and tag: "Free" metadata are silently discarded in favour of the static constants file.

This defeats the PR's goal of pulling current metadata from OpenRouter at runtime. The spread order should be reversed so that API data overrides the hardcoded fallback:

Suggested change
if (providerId === "openrouter") {
const freeModels = getOpenRouterFreeModels();
const dedupe = new Map<string, LanguageModel>();
[...freeModels, ...hardcodedModels].forEach((model) => {
dedupe.set(model.apiName, model);
});
hardcodedModels = Array.from(dedupe.values());
}
if (providerId === "openrouter") {
const freeModels = getOpenRouterFreeModels();
const dedupe = new Map<string, LanguageModel>();
[...hardcodedModels, ...freeModels].forEach((model) => {
dedupe.set(model.apiName, model);
});
hardcodedModels = Array.from(dedupe.values());
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ipc/shared/language_model_helpers.ts
Line: 152-159

Comment:
**Deduplication direction discards API-fetched data**

The array spread `[...freeModels, ...hardcodedModels]` means that when the same `apiName` exists in both lists, `Map.set` is called last with the **hardcoded** entry, so hardcoded values always win. The API-fetched `contextWindow`, `maxOutputTokens`, `dollarSigns: 0`, and `tag: "Free"` metadata are silently discarded in favour of the static constants file.

This defeats the PR's goal of pulling current metadata from OpenRouter at runtime. The spread order should be reversed so that API data overrides the hardcoded fallback:

```suggestion
  if (providerId === "openrouter") {
    const freeModels = getOpenRouterFreeModels();
    const dedupe = new Map<string, LanguageModel>();
    [...hardcodedModels, ...freeModels].forEach((model) => {
      dedupe.set(model.apiName, model);
    });
    hardcodedModels = Array.from(dedupe.values());
  }
```

How can I resolve this? If you propose a fix, please make it concise.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

maxOutputTokens: 32_000,
contextWindow: 196_608,
temperature: 0,
dollarSigns: 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Free model qwen/qwen3-coder:free has incorrect dollarSigns: 1 instead of 0

The newly added qwen/qwen3-coder:free model has dollarSigns: 1 despite being a free model (:free suffix, description says "Use for free"). This causes the PriceBadge component (src/components/PriceBadge.tsx:10) to render "$" instead of "Free". Furthermore, the dedupe logic in src/ipc/shared/language_model_helpers.ts:155 gives hardcoded models precedence over dynamically fetched free models (which correctly have dollarSigns: 0), so the incorrect value persists even after hydration.

Suggested change
dollarSigns: 1,
dollarSigns: 0,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

maxOutputTokens: 32_000,
contextWindow: 200_000,
temperature: 0.7,
dollarSigns: 2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Free model mistralai/devstral-2512:free has incorrect dollarSigns: 2 instead of 0

The newly added mistralai/devstral-2512:free model has dollarSigns: 2 despite being a free model (:free suffix, description says "Use for free"). This causes the PriceBadge component (src/components/PriceBadge.tsx:10) to render "$$" instead of "Free". The same dedupe issue as with qwen/qwen3-coder:free applies: src/ipc/shared/language_model_helpers.ts:155 gives hardcoded models precedence, so the incorrect dollarSigns value overrides the correct 0 from the dynamic free model list.

Suggested change
dollarSigns: 2,
dollarSigns: 0,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@wwwillchen

Copy link
Copy Markdown
Collaborator

hi @coderdylanbates - sorry for the delay, we've updated our language model catalog to be on the server-side so we can push out new LLMs quicker than our binary release so i'm going to try to port this logic over to our server (this is currently not open-source). thanks again for the PR

@wwwillchen wwwillchen closed this Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants