Skip to content

fix(llm): normalize bare LM Studio / Ollama base URL to /v1 (#2922) - #2941

Merged
nicoloboschi merged 1 commit into
mainfrom
fix/lmstudio-bare-base-url
Jul 24, 2026
Merged

fix(llm): normalize bare LM Studio / Ollama base URL to /v1 (#2922)#2941
nicoloboschi merged 1 commit into
mainfrom
fix/lmstudio-bare-base-url

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

Problem

Hindsight fails against LM Studio when the base URL is set to the bare host that LM Studio's server UI advertises (http://localhost:1234, without /v1). The OpenAI SDK appends /chat/completions to the base URL, so it POSTs to http://localhost:1234/chat/completions, and LM Studio rejects it:

ProviderResponseError: Provider returned error payload
(lmstudio/..., scope=retain_extract_facts): Unexpected endpoint or method. (POST /chat/completions)

LM Studio's OpenAI-compatible routes live under /v1 (/v1/chat/completions) — the bare host has no such route. Its newer native REST endpoints (/api/v1/chat) are not OpenAI-compatible, so the OpenAI SDK can't target them anyway; /v1/chat/completions remains the correct endpoint and it still works.

Fixes #2922.

Fix

For lmstudio and ollama — the two local providers whose OpenAI-compatible surface is known to live under /v1 — normalize a base URL that has no meaningful path by appending /v1. Base URLs with an explicit path (an already-correct /v1, or a reverse-proxy mount) are left untouched, and cloud/proxy providers are not affected.

Input (lmstudio/ollama) Result
http://localhost:1234 http://localhost:1234/v1
http://localhost:1234/ http://localhost:1234/v1
http://localhost:1234/v1 unchanged
http://proxy.internal/lmstudio unchanged

Verification

  • New unit tests in test_local_provider_base_url.py cover the normalization matrix and assert the constructed OpenAI client targets /v1/chat/completions.
  • Verified end-to-end against a real LM Studio instance: a fact-extraction-style call configured with the bare host http://localhost:1234 (the exact config from the issue) now succeeds instead of returning Unexpected endpoint or method.

LM Studio's server UI advertises its address as a bare host
(http://localhost:1234), so users commonly set HINDSIGHT_API_LLM_BASE_URL
to that. The OpenAI SDK then POSTs to <host>/chat/completions and LM Studio
rejects it with 'Unexpected endpoint or method' — its OpenAI-compatible
routes live under /v1.

For lmstudio/ollama (whose OpenAI-compat surface is known to live under /v1)
append /v1 when the base URL has no meaningful path. Explicit paths (reverse
proxy mounts, already-correct /v1) are left untouched.

Fixes #2922
@nicoloboschi
nicoloboschi merged commit 47a7d43 into main Jul 24, 2026
198 of 203 checks passed
@nicoloboschi
nicoloboschi deleted the fix/lmstudio-bare-base-url branch July 24, 2026 09:55
nicoloboschi added a commit that referenced this pull request Jul 24, 2026
The three Oracle jobs run the Oracle 23ai `free` service image, which
together with the Python ML deps (torch) exhausts the runner's ~14 GB root
disk. Two symptoms, one cause:

- uv fails to extract a wheel with "No space left on device (os error 28)"
  (fast ~2 min failure), and
- a near-full disk starves I/O badly enough to trip the 30-minute job
  timeout.

test-python-client-oracle and test-typescript-client-oracle have been red on
every open PR (#2941, #2942, #2943) from this, independent of the code under
test. Reclaim ~20 GB of preinstalled tooling (the same jlumbroso action the
Docker build job already uses) before the Oracle setup step.

docker-images stays false here: unlike the Docker build job, the Oracle
service container is already running by the time steps execute, so pruning
images could disrupt it. The savings come from the tool cache, Android SDK,
.NET, Haskell, large apt packages and swap.
nicoloboschi added a commit that referenced this pull request Jul 24, 2026
…ain deadlock (#2948)

* ci(oracle): free runner disk space before Oracle jobs

The three Oracle jobs run the Oracle 23ai `free` service image, which
together with the Python ML deps (torch) exhausts the runner's ~14 GB root
disk. Two symptoms, one cause:

- uv fails to extract a wheel with "No space left on device (os error 28)"
  (fast ~2 min failure), and
- a near-full disk starves I/O badly enough to trip the 30-minute job
  timeout.

test-python-client-oracle and test-typescript-client-oracle have been red on
every open PR (#2941, #2942, #2943) from this, independent of the code under
test. Reclaim ~20 GB of preinstalled tooling (the same jlumbroso action the
Docker build job already uses) before the Oracle setup step.

docker-images stays false here: unlike the Docker build job, the Oracle
service container is already running by the time steps execute, so pruning
images could disrupt it. The savings come from the tool cache, Android SDK,
.NET, Haskell, large apt packages and swap.

* ci(oracle): trim disk reclaim to the fast, high-yield options

The first pass enabled every reclaim, which cost ~4 minutes of job time —
counterproductive on jobs that are already fighting a 30-minute limit.

android + dotnet + haskell + swap are a few rm -rf's worth ~16-21 GB, which
is ample headroom for the Oracle image plus torch. Dropped:
- large-packages: apt-get remove, costs minutes for little extra space;
- tool-cache: deletes the preinstalled Python that actions/setup-python then
  re-downloads, making the job slower rather than faster.

* fix(retain): flush entity stats after releasing the connection (Oracle hang)

Retain hung forever on the Oracle backend: every retain test burned its 120s
client timeout while the server sat idle, so test-python-client-oracle and
test-typescript-client-oracle only ever reached ~5% of the suite before the
30-minute job limit.

The server was not slow — it was deadlocked. flush_pending_stats() acquires
its own connection, but it was being called while the enclosing
acquire_with_retry(...) block still held one:

  async with acquire_with_retry(pool) as conn:   # conn checked out
      async with conn.transaction():             # SAVEPOINT only
          ...write facts/entities...
      await entity_resolver.flush_pending_stats()  # takes a 2nd connection

oracledb does not autocommit and OracleConnection.transaction() is only a
SAVEPOINT, so the write is committed by OracleBackend.acquire() when its block
exits. Connection #2's `UPDATE entities ...` therefore waits on row locks held
by the still-open connection #1, which cannot commit until the call returns —
a circular wait. Oracle never reports ORA-00060 because session #1 is blocked
in Python, not on the database, so it hangs indefinitely instead of erroring.

Move the flush after the acquire block in all three call sites (streaming
retain, delta retain, transfer importer), which is what its own docstring
already required ("must be called AFTER the retain transaction commits") and
which PostgreSQL satisfied only by accident via asyncpg autocommit.

Guarded with an AST lint test rather than a behavioural one: the deadlock
cannot be reproduced against PostgreSQL, which is what the suite runs on.

* test(repair): retry the concurrent index drop on deadlock

test_dry_run_creates_nothing still flaked in test-api shard 3. CONCURRENTLY
avoids ACCESS EXCLUSIVE but still takes ShareUpdateExclusive, which conflicts
with the ShareLock a fresh bank's plain CREATE INDEX holds — and that one
cannot be made concurrent, since it runs inside the bank-create transaction.
So _drop_bank_indexes can still be picked as the deadlock victim while another
xdist worker seeds a bank:

  Process A waits for ShareUpdateExclusiveLock on memory_units; blocked by B.
  Process B waits for ShareLock on virtual transaction; blocked by A.

The bank-create side already retries (#2943); give the drop the same treatment.
The drop is idempotent, so retrying is safe.
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.

hindsight does not work with the new LM-studio v1 endpoints

1 participant