Skip to content

LM Studio: send completions to /v1, keep discovery on the raw address - #1885

Merged
igordayen merged 2 commits into
mainfrom
fix/lmstudio-completions-v1
Aug 8, 2026
Merged

LM Studio: send completions to /v1, keep discovery on the raw address#1885
igordayen merged 2 commits into
mainfrom
fix/lmstudio-completions-v1

Conversation

@johnsonr

@johnsonr johnsonr commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Every completion against an LM Studio model fails with `choices` is not set — a baffling message for what is really a 404.

What is wrong

The two halves of LmStudioModelsConfig disagree about what baseUrl means:

  • Discovery (loadModelsFromUrl) appends /v1/models, so it wants the plain server address.
  • Chat goes through OpenAiCompatibleModelFactory, where the SDK appends /chat/completions — the factory's own KDoc says "bake the full path into baseUrl" — so it wants the address to already end in /v1.

No single configured value satisfies both. With the documented default (http://127.0.0.1:1234) models list perfectly and then nothing can be called, which is the worst shape for a bug: the surface that proves the integration works is the one that works.

What this does

Normalises at each use. The SDK gets the /v1 root it expects; discovery keeps reading the configured address. The configured value stays the plain server address an operator would naturally write, and an operator who has already written /v1 is not given two.

Doing only the first half is a trap I fell into: discovery reads the same inherited field, so normalising it there built …/v1/api/v1/models, registered nothing, and left an appliance whose default model was local unable to start at all. Both paths are now explicit about which form they want.

How it was found

A logging proxy in front of LM Studio, because the error message pointed nowhere useful:

POST /chat/completions  →  {"error": "Unexpected endpoint or method. (POST /chat/completions)"}

Retries 40ms apart were the tell — not a model thinking, an instant rejection.

Verified

On a real machine: 17 LM Studio models discovered at boot, chat completions answered, and an agentic RAG loop driven end to end by a local model. 8 unit tests cover the normalisation, including the trailing-slash and already-has-/v1 cases.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C7Etsae7WnXfshf5wjDELB

Every completion against an LM Studio model failed with '`choices` is
not set' — a baffling message for what was really a 404. The two halves
of this class disagreed about what baseUrl means: discovery appends
/v1/models to it, while the OpenAI SDK appends /chat/completions (the
factory's own docs say to bake the full path into the base URL). No
single configured value satisfies both, so models listed perfectly and
then nothing could be called.

Normalised at each use: the SDK gets the /v1 root it expects, discovery
keeps reading the configured address. Doing only the first broke
discovery — it read the same inherited field and built
/v1/api/v1/models, which registered nothing and left an appliance with a
local default model unable to start at all. Both paths are now explicit
about which form they want.

Found with a logging proxy in front of LM Studio: the request went to
POST /chat/completions and came back 'Unexpected endpoint or method'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7Etsae7WnXfshf5wjDELB

@igordayen igordayen 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.

@johnsonr - perhaps use localhost and an externalized port, or port as a constant.
Let me know if you'd like me to clean it up for you.
Thank you

* the OpenAI surface under `/v1`, so append it unless the operator has
* already done so.
*/
internal fun chatBaseUrl(baseUrl: String?): String {

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.

hardcoded URL, should be localhost?

Is it always local URL?


@Test
fun `chat base url gains the v1 the SDK expects`() {
assertEquals("http://127.0.0.1:1234/v1", LmStudioModelsConfig.chatBaseUrl("http://127.0.0.1:1234"))

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.

127.... hardcoding in many places....

@johnsonr johnsonr Aug 7, 2026

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.

Yes, please clean up. Thank you!

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.

resolved

@igordayen igordayen 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.

@johnsonr - completed clean-up. thanks

@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@igordayen
igordayen merged commit cd66f23 into main Aug 8, 2026
17 checks passed
@igordayen
igordayen deleted the fix/lmstudio-completions-v1 branch August 8, 2026 01:03
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