Skip to content

Fix OpenCode and LM Studio model refresh#317

Open
ethanMonico wants to merge 2 commits into
arul28:mainfrom
ethanMonico:codex-fix-opencode-refresh-cache
Open

Fix OpenCode and LM Studio model refresh#317
ethanMonico wants to merge 2 commits into
arul28:mainfrom
ethanMonico:codex-fix-opencode-refresh-cache

Conversation

@ethanMonico
Copy link
Copy Markdown

@ethanMonico ethanMonico commented May 18, 2026

Summary

  • clear the cached OpenCode binary lookup when provider status is force-refreshed
  • fall back to LM Studio's OpenAI-compatible /v1/models response when /api/v1/models returns an unexpected object shape
  • treat LM Studio OpenAI-compatible model listings as loaded so they surface in runtime/OpenCode model discovery

Tests

  • npx vitest run src/main/services/ai/localModelDiscovery.test.ts src/main/services/ai/aiIntegrationService.test.ts src/main/services/ai/authDetector.test.ts src/renderer/lib/modelOptions.test.ts
  • npm --prefix apps/desktop run typecheck

Summary by CodeRabbit

  • New Features

    • Enhanced LM Studio OpenAI-compatible model discovery and status reporting.
  • Bug Fixes

    • Improved fallback logic for LM Studio model detection when REST API responses are unexpected.
    • Models from local LM Studio instances are now correctly marked as available.
    • Fixed cache invalidation to ensure consistent model inventory updates.

Review Change Stack

Greptile Summary

This PR fixes two independent issues: a stale binary-path cache that prevented OpenCode from re-discovering its binary after a forced status refresh, and LM Studio model discovery not surfacing models found via the OpenAI-compatible /v1/models endpoint (both the missing fallback when /api/v1/models returns an unexpected shape, and the loaded: false flag that hid those models from runtime model lists).

  • aiIntegrationService.ts: adds clearOpenCodeBinaryCache() to invalidateProviderReadinessCaches(), consistent with the existing cache-clearing pattern.
  • localModelDiscovery.ts: tightens the REST-API guard to require a .models array (falling through to the OpenAI-compat path otherwise) and changes loaded from false to true for models discovered via the OpenAI-compat endpoint.
  • Tests cover the binary-cache-clear path, the new OpenAI-compat fallback shape, and the end-to-end surfacing of LM Studio compat models in getStatus.

Confidence Score: 4/5

Safe to merge; changes are well-scoped fixes to LM Studio model discovery and OpenCode binary caching with direct test coverage for each path.

Both changes are targeted and have corresponding tests. The main area to keep an eye on is the subtle state transition in inspectLmStudioProvider: a live LM Studio instance whose /api/v1/models returns an unexpected shape while /v1/models is also temporarily unreachable will now report as "unreachable" rather than "reachable_no_models". The test embedding an expect inside a mock implementation is also worth a second look before the pattern spreads.

localModelDiscovery.ts deserves a second look around the new fallback path and its effect on the reachable flag. aiIntegrationService.test.ts is worth reviewing for the expect-inside-mock pattern.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/ai/localModelDiscovery.ts Tightens the LM Studio REST API check so it only takes the native path when .models is an array, falls through to the OpenAI-compat endpoint otherwise; also fixes loaded: falseloaded: true for OpenAI-compat discovered models. Introduces a subtle edge case where an unexpected-but-reachable /api/v1/models response plus a failing /v1/models flips the health from "reachable_no_models" to "unreachable".
apps/desktop/src/main/services/ai/aiIntegrationService.ts Adds clearOpenCodeBinaryCache() to invalidateProviderReadinessCaches() so the binary path is re-probed on every forced status refresh. Change is minimal, correct, and consistent with the pattern used for other cache-clearing calls in the same function.
apps/desktop/src/main/services/ai/localModelDiscovery.test.ts Adds two new test cases: the OpenAI-compat fallback when REST returns unexpected shape, and loaded: true assertion on existing compat test. Coverage is good; the new "unexpected shape" test correctly mocks two sequential fetches.
apps/desktop/src/main/services/ai/aiIntegrationService.test.ts Adds two integration tests; the binary-cache-clear test is straightforward. The LM Studio end-to-end test embeds an expect assertion inside probeOpenCodeProviderInventory.mockImplementation, which can produce a false pass if upstream error handling swallows the rejection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[inspectLmStudioProvider] --> B[fetch /api/v1/models]
    B --> C{restPayload non-null\nAND .models is array?}
    C -- Yes --> D[Parse models array\nfrom REST payload]
    D --> E{Any loaded instances?}
    E -- Yes --> F[Return ready\ndiscoverySource: lmstudio-rest\nloaded: true]
    E -- No --> G[Return reachable_no_models]
    C -- No\nnew fallback path --> H[fetch /v1/models]
    H --> I{openAiPayload non-null?}
    I -- No --> J[Return unreachable]
    I -- Yes --> K{Any model entries\nin .data?}
    K -- Yes --> L[Return ready\ndiscoverySource: lmstudio-openai\nloaded: true ← was false]
    K -- No --> M[Return reachable_no_models]
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/services/ai/localModelDiscovery.ts, line 177-235 (link)

    P2 Behavior change: "reachable_no_models" can become "unreachable"

    Before this PR, any 200 OK response from /api/v1/models guaranteed reachable: true. Now, if that response has an unexpected shape (no .models array) and /v1/models also fails or is unreachable, the returned inspection will have reachable: false and health: "unreachable". A running LM Studio instance that returns an unexpected /api/v1/models body during startup would briefly appear completely unreachable in the UI instead of "reachable with no loaded models".

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/ai/localModelDiscovery.ts
    Line: 177-235
    
    Comment:
    **Behavior change: "reachable_no_models" can become "unreachable"**
    
    Before this PR, any 200 OK response from `/api/v1/models` guaranteed `reachable: true`. Now, if that response has an unexpected shape (no `.models` array) and `/v1/models` also fails or is unreachable, the returned inspection will have `reachable: false` and `health: "unreachable"`. A running LM Studio instance that returns an unexpected `/api/v1/models` body during startup would briefly appear completely unreachable in the UI instead of "reachable with no loaded models".
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Codex Fix in Claude Code

Fix All in Cursor Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/main/services/ai/localModelDiscovery.ts:177-235
**Behavior change: "reachable_no_models" can become "unreachable"**

Before this PR, any 200 OK response from `/api/v1/models` guaranteed `reachable: true`. Now, if that response has an unexpected shape (no `.models` array) and `/v1/models` also fails or is unreachable, the returned inspection will have `reachable: false` and `health: "unreachable"`. A running LM Studio instance that returns an unexpected `/api/v1/models` body during startup would briefly appear completely unreachable in the UI instead of "reachable with no loaded models".

### Issue 2 of 2
apps/desktop/src/main/services/ai/aiIntegrationService.test.ts:430-440
**`expect` inside mock implementation can silently pass if the service catches errors**

Placing an `expect` assertion inside `mockImplementation` means a failed assertion throws inside the mock, which will propagate as a rejected promise from `probeOpenCodeProviderInventory`. If any upstream `timePhase` or similar utility in the service swallows that rejection (e.g., falls back to an empty result), the inner assertion failure is invisible and the test could produce a false pass. The safer pattern is to capture the call arguments and assert on them after `getStatus` resolves: `const spy = vi.spyOn(...)` or `expect(mockState.probeOpenCodeProviderInventory).toHaveBeenCalledWith(expect.objectContaining({ discoveredLocalModels: expect.arrayContaining([...]) }))`.

Reviews (1): Last reviewed commit: "Fix LM Studio OpenAI model discovery" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@ethanMonico ethanMonico requested a review from arul28 as a code owner May 18, 2026 03:18
@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

@codex is attempting to deploy a commit to the arul28's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR refines LM Studio model discovery to handle REST API payload shape variations and improves fallback to OpenAI-compatible endpoints. It marks discovered models as loaded and integrates OpenCode binary cache clearing into the provider readiness cache invalidation flow with corresponding test coverage.

Changes

Local Model Discovery and Cache Management

Layer / File(s) Summary
LM Studio Model Discovery Validation and Loaded State
apps/desktop/src/main/services/ai/localModelDiscovery.ts, apps/desktop/src/main/services/ai/localModelDiscovery.test.ts
LM Studio REST payload validation now requires models array; unexpected shapes fall through to OpenAI-compatible discovery. Discovered models are marked loaded: true instead of false. Tests cover the fallback scenario with assertions on provider, model fields, and loaded state.
OpenCode Binary Cache Invalidation Routing
apps/desktop/src/main/services/ai/aiIntegrationService.ts, apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
Imports and wires clearOpenCodeBinaryCache into invalidateProviderReadinessCaches. Test infrastructure mocks and initializes the cache-clearing function, verifies it is called on force, and asserts LM Studio models surface in getStatus output when refreshOpenCodeInventory is enabled.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

desktop

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix OpenCode and LM Studio model refresh' directly summarizes the main changes: clearing OpenCode binary cache and fixing LM Studio model detection/refresh behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 18, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Comment on lines +430 to +440
discoverySource: "lmstudio-openai",
loaded: true,
}]
: [],
}));
mockState.probeOpenCodeProviderInventory.mockImplementation(async (args: any) => {
expect(args.discoveredLocalModels).toContainEqual({ provider: "lmstudio", modelId });
return {
modelIds: [`opencode/lmstudio/${modelId}`],
providers: [{ id: "lmstudio", name: "LM Studio", connected: true, modelCount: 1 }],
error: null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 expect inside mock implementation can silently pass if the service catches errors

Placing an expect assertion inside mockImplementation means a failed assertion throws inside the mock, which will propagate as a rejected promise from probeOpenCodeProviderInventory. If any upstream timePhase or similar utility in the service swallows that rejection (e.g., falls back to an empty result), the inner assertion failure is invisible and the test could produce a false pass. The safer pattern is to capture the call arguments and assert on them after getStatus resolves: const spy = vi.spyOn(...) or expect(mockState.probeOpenCodeProviderInventory).toHaveBeenCalledWith(expect.objectContaining({ discoveredLocalModels: expect.arrayContaining([...]) })).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
Line: 430-440

Comment:
**`expect` inside mock implementation can silently pass if the service catches errors**

Placing an `expect` assertion inside `mockImplementation` means a failed assertion throws inside the mock, which will propagate as a rejected promise from `probeOpenCodeProviderInventory`. If any upstream `timePhase` or similar utility in the service swallows that rejection (e.g., falls back to an empty result), the inner assertion failure is invisible and the test could produce a false pass. The safer pattern is to capture the call arguments and assert on them after `getStatus` resolves: `const spy = vi.spyOn(...)` or `expect(mockState.probeOpenCodeProviderInventory).toHaveBeenCalledWith(expect.objectContaining({ discoveredLocalModels: expect.arrayContaining([...]) }))`.

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

Fix in Cursor Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants