[integrations] Fix silent metadata loss on OpenAI-compatible endpoints - #477
Open
mike-sanlon wants to merge 1 commit into
Open
Conversation
extractMetadata sends `response_format: { type: "json_object" }`, which not
every OpenAI-compatible endpoint accepts. LM Studio rejects it with HTTP 400
("'response_format.type' must be 'json_schema' or 'text'").
The response is never checked for `r.ok`, so that 400 body flows into
`d.choices[0]`, throws inside the try, and is swallowed by the catch, which
returns the "uncategorized" fallback. The capture reports success and stores
junk metadata. On an affected endpoint this happens for every capture, with
nothing in the logs.
Changes:
- Send a real `json_schema` response_format. Accepted by OpenAI and by the
compatible runtimes, and enforces the shape rather than requesting it.
Includes `additionalProperties: false` and lists every property in
`required`, both mandatory under strict mode.
- Check `r.ok` and validate the response shape, logging the status and body
on failure instead of failing silently.
- Tag the fallback with `metadata_degraded: true` so affected rows can be
found and re-extracted rather than being indistinguishable from thoughts
genuinely categorised as "uncategorized".
- Use constant-time comparison for the access key. `!==` short-circuits on
the first differing byte and leaks key material through response timing.
- Make the similarity threshold configurable via OPEN_BRAIN_MATCH_THRESHOLD.
The 0.5 default is unchanged; it is tuned for text-embedding-3-small and
is a poor fit for models with a compressed similarity range.
Verified with `deno check` against Deno 2.9.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hey @mike-sanlon — welcome to Open Brain Source! 👋 Thanks for submitting your first PR. The automated review will run shortly and check things like metadata, folder structure, and README completeness. If anything needs fixing, the review comment will tell you exactly what. Once the automated checks pass, a human admin will review for quality and clarity. Expect a response within a few days. If you have questions, check out CONTRIBUTING.md or open an issue. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
extractMetadatasendsresponse_format: { type: "json_object" }. Not every OpenAI-compatible endpoint accepts that. LM Studio rejects it with HTTP 400:The response is never checked for
r.ok, so the 400 body flows intod.choices[0], throws inside thetry, and is swallowed by thecatch, which returns the"uncategorized"fallback:capture_thoughtthen reports success. The thought is stored with junk metadata and nothing appears in the logs. On an affected endpoint this happens for every capture, and the resulting rows are indistinguishable from thoughts genuinely categorised as "uncategorized".The embedding and content are still correct, so search by meaning still works — which is likely why this hasn't been noticed. What breaks is everything driven by metadata:
list_thoughtsfiltering by type/topic/person, andthought_stats.Reproduction
Point
CHAT_API_BASEat any endpoint that rejectsjson_object(LM Studio's/v1is the easy one) and capture anything. It returns success; the row lands withtopics: ["uncategorized"].Changes
Send a real
json_schemaresponse_format. Accepted by OpenAI and by the compatible runtimes, and it enforces the shape rather than merely requesting it. IncludesadditionalProperties: falseand lists every property inrequired— both mandatory under strict mode, per the structured outputs guide.Check
r.okand validate the response shape. Logs status and body on failure instead of failing silently.Tag the fallback with
metadata_degraded: true, so degraded rows become findable and re-extractable:It still returns the fallback rather than throwing — losing the thought because the metadata model was down is worse than storing it with weak metadata.
Constant-time comparison for the access key.
!==short-circuits on the first differing byte, leaking key material through response timing. Length is compared first and non-secretly; that only reveals the key's length, and bailing early on a length mismatch is unavoidable for a fixed-width compare.Make the similarity threshold configurable via
OPEN_BRAIN_MATCH_THRESHOLD. The 0.5 default is unchanged. It is tuned fortext-embedding-3-smalland is a poor fit for models with a compressed similarity range — on bge-m3, 0.5 dropped 2 of 5 genuinely relevant queries in local testing. That is a config problem, not a default worth changing for everyone.Verification
deno checkclean against Deno 2.9.5. Items 1–3 exercised against a live LM Studio endpoint: captures that previously stored"uncategorized"now store correct extracted metadata, withmetadata_degradedabsent.Deliberately not included
The server also accepts the key as a
?key=query parameter. Query strings land in reverse-proxy access logs, browser history, andRefererheaders, so it is worth removing — but that is a breaking change for anyone relying on it, so it belongs in its own PR with maintainer input rather than buried in this one. Happy to open that separately if you want it.