Consume LLM Singleton registry everywhere + add per-corpus LLM admin GUI#2035
Conversation
…pus GUI Audit + completion of runtime LLM configuration so the Singleton registry (resolve_model_spec: per-call -> agent -> corpus -> PipelineSettings.default_llm -> Django settings) is consumed everywhere and is admin-controllable. Consumption fix: - Conversation-title generation was the one runtime path bypassing the registry: it used the OpenAI-only SimpleLLMClient hardcoded to gpt-4o-mini, ignoring corpus.preferred_llm and the install-wide default. Add opencontractserver.llms.completions.agenerate_text, a provider-agnostic, registry-backed one-shot completion helper, and route title generation through it (honouring the corpus default). - Remove the dead, registry-bypassing SimpleLLMClient (client.py) and its LLM_CLIENT_* settings; update docs and tests accordingly. Admin GUI: - Surface the previously unreachable Corpus.preferred_llm override in the Corpus Settings panel via a new shared LlmModelPicker component (also adopted by the admin System Settings screen, de-duplicating its inline picker). Shows the inherited system default when unset. Threaded preferredLlm through the corpus resolution query, UPDATE_CORPUS, and the TS types; added two slim, secret-free login_required queries for the provider list + system default.
Code Review — PR #2035: Consume LLM Singleton registry everywhere + add per-corpus LLM admin GUIOverviewThis is a well-scoped, well-executed audit PR. It closes two genuine gaps: the last runtime path that bypassed the LLM registry (conversation-title generation was hardcoded to IssuesMedium — Disabled providers appear in the picker
Fix options (pick one):
Low — Changelog fragment slugs don't match the PR numberThe three fragments use Low — Missing test: empty-string return from
|
…mpty-title test - CorpusSettings: drop admin-disabled LLM providers from the picker so users aren't offered models they can't use (enabled !== false). - Rename changelog fragments 2030-* -> 2035-* to match the PR number. - Add a test covering the empty-string title -> generated-fallback branch in _generate_conversation_title.
CI's pyupgrade (--py39-plus) rewrote Optional[X] -> X | None in opencontractserver/llms/completions.py, after which flake8 flagged the now-unused typing.Optional import. Apply the pyupgrade rewrite and drop the import.
|
Thanks for the review. Addressed in
Skipped the combined- Separately, the Generated by Claude Code |
Code ReviewOverviewThis PR closes two real gaps in the LLM configuration story: (1) conversation-title generation was the only production LLM path that bypassed the Singleton registry entirely, and (2) What works well
Issues and suggestionsMedium —
|
| Surface | Coverage |
|---|---|
agenerate_text resolution chain (3 cases) |
✅ |
| Consumer title fallback on error / empty string | ✅ |
LlmModelPicker chip click, inherited hint, clear hint, hint hide |
✅ |
validate_preferred_llm serializer normalisation |
Not in this diff — presumably covered by existing corpus serializer tests |
GET_LLM_PROVIDERS / GET_SYSTEM_DEFAULT_LLM resolvers |
Not visible in this diff |
One gap worth noting: test_llms_completions.py doesn't test the path where resolve_model_spec receives explicit=None, corpus_preferred=None, and settings_default=None/"". The intent is that the agent factory has a hard fallback in that case, but a fourth test case would pin the contract explicitly.
Overall
The core changes — agenerate_text, the removal of SimpleLLMClient, and the per-corpus LLM picker — are well-designed and follow project conventions. The temperature-always-set issue is the only change I'd recommend before merge; the rest are improvements for a follow-up.
… skip, tests - completions.py: make temperature optional (float | None = 0.7) and only inject it into model_settings when set, so backends that reject an explicit temperature (e.g. reasoning models) aren't forced to receive one (mirrors max_tokens handling). - queries.ts: LlmProvidersQueryResult now reuses the exported LlmProviderOption shape instead of an inline duplicate, so the GraphQL projection and the picker component can't silently drift. - CorpusSettings: skip GET_LLM_PROVIDERS for read-only viewers (chips are only used when editing); the inherited-default hint still loads. - tests: add hard-fallback (everything unset -> openai:gpt-4o) and temperature=None-omitted cases to test_llms_completions.
|
Thanks — round-2 items addressed in
Declined:
Note: the Generated by Claude Code |
Code Review: LLM Singleton Registry + Per-Corpus LLM Admin GUIOverviewThis is a well-scoped audit-and-complete pass that closes two genuine gaps: (1) conversation-title generation was the only runtime LLM path bypassing the Singleton registry (hardcoded Issues & SuggestionsMedium — LlmModelPicker input not disabled during save (CorpusSettings.tsx)The picker and input remain interactive while <LlmModelPicker
disabled={!canUpdate} {/* missing: || updatingLlm */}
/>If the user clicks a chip or edits the input while a save is in-flight, the draft diverges from what was sent, making the subsequent Medium — Magic colour literals in LlmModelPicker.tsx violate the no-magic-numbers ruleThe selected-chip highlight uses two raw hex values not in
Minor —
|
…ors, trim
- LlmModelPicker: replace hardcoded #e0e7ff/#6366f1 selected-chip literals with
the existing OS_LEGAL_COLORS.selectedBg/selectedBorder tokens (no-magic-numbers),
and trim the value before chip-selection comparison so a stray trailing space
still highlights the matching chip.
- CorpusSettings: disable the picker while a save is in-flight
(disabled={!canUpdate || updatingLlm}) to prevent draft/baseline divergence.
- completions.py: comment that str(result.output) is type-narrowing (bare-string
output, no output_type), not defensive coercion.
|
Round-3 items addressed in
Left as-is: the per-call agent construction note — acknowledged as acceptable for the no-action-needed cases; not adding a docstring caveat for a hypothetical high-frequency caller that doesn't exist yet. Generated by Claude Code |
Code ReviewOverviewThis is a well-scoped, well-motivated PR that closes two concrete gaps: (1) conversation-title generation was the only LLM call that hardcoded a provider/model and ignored the Singleton registry, and (2) What's Good
Issues & Suggestions1. Import direction in
|
- Move LlmProviderOption to types/graphql-api.ts (canonical home); LlmModelPicker re-exports it and queries.ts imports from types/ instead of the component, fixing the graphql->components import-direction inversion. - LlmModelPicker: default the field id via React useId() so two pickers on one page can't collide on the label<->input association. - CorpusSettings: clarify the enabled-filter comment (null = not disabled). - completions.py: docstring note that a fresh Agent is built per call (one-shot helper; hot paths should reuse an agent). - tests: add consumer corpus-preferred-llm threading test and a LlmModelPicker disabled-chip (no onChange) test. Declined: replacing the raw <label> with 'FormLabel from @os-legal/ui' — that component is not exported by @os-legal/ui (only FormField is); the SystemSettings FormLabel is an admin-local styled component, so coupling the shared picker to it would be a worse dependency than the self-contained label.
Code Review: PR #2035 — Consume LLM Singleton registry everywhere + add per-corpus LLM admin GUIOverviewA solid, well-scoped PR that closes two concrete gaps in LLM registry adoption: (1) reroutes the hardcoded OpenAI-only title-generation path through the canonical registry via a new Backend
|
| Area | Verdict |
|---|---|
| Architecture / design | Correct — closes both registry gaps cleanly |
| Dead-code removal | Complete |
| DRY | LlmModelPicker extraction is a clear improvement |
| Security | Permission design looks correct |
| Backend tests | Missing max_tokens and whitespace-output cases; str(None) edge case |
| Frontend tests | Minor fireEvent vs userEvent nit |
| Constants | Two hex colour literals should be extracted to constants |
| Loading states | Consider skeleton for provider/default-LLM queries in CorpusSettings |
uuid import |
Verify uuid is already imported in the consumer file |
The items most worth addressing before merge: the str(None) bug in completions.py and the uuid import verification in the consumer. The colour constants and missing test cases are recommended improvements but can land in a quick fixup.
- completions.py: return '' when result.output is None (model refusal / future API shape) instead of str(None)='None', so a caller's 'value or fallback' guard fires correctly (e.g. conversation-title generation). - tests: add max_tokens-forwarded and None-output->'' cases to test_llms_completions. Verified (no change needed): uuid is already imported in the consumer (unified_agent_conversation.py:31); the selected-chip colour literals were already replaced with OS_LEGAL_COLORS.selectedBg/selectedBorder tokens in an earlier round; CorpusSerializer.validate_preferred_llm already normalises ''->None.
Code ReviewOverviewThis PR closes two real gaps in the LLM singleton registry: (1) conversation-title generation was silently hardcoding `gpt-4o-mini` via the now-deleted `SimpleLLMClient`, and (2) `Corpus.preferred_llm` had no GUI surface. Both changes are well-motivated and clearly described. The overall approach is sound; the notes below are mostly minor. Backend`opencontractserver/llms/completions.py` — new `agenerate_text` helper
One note: the default `temperature=0.7` is reasonable for general use but is on the high end for a deterministic infra task like title generation. `0.3` would produce more consistent results. Minor opinion. `config/websocket/consumers/unified_agent_conversation.py`
`opencontractserver/tests/test_llms_completions.py` Five targeted mock-based tests pin the entire resolution-chain contract without hitting the network. Good coverage. Minor nit: `test_hard_fallback_when_everything_unset` uses `@override_settings(DEFAULT_LLM="", OPENAI_MODEL="")` but the function under test reads `settings_default` from the patched `get_default_llm_spec` return value, not from Django settings directly. The decorator is redundant — not wrong, just noise. Frontend`frontend/src/components/common/LlmModelPicker.tsx`
Type precision issue: `LlmProviderOption` in `frontend/src/types/graphql-api.ts` does not include an `enabled` field. The filter in `CorpusSettings.tsx` accesses `p.enabled`: ```ts This works at runtime because the query result carries `enabled`, and `LlmProvidersQueryResult` widens the type with `& { enabled?: boolean | null }`. But once the filtered array is passed to `LlmModelPicker` as `LlmProviderOption[]`, the `enabled` field is erased from the static type. TypeScript won't error, but the base type is imprecise. Adding `enabled?: boolean | null` to `LlmProviderOption` in `graphql-api.ts` would make the contract explicit. Re-export path: The component re-exports `LlmProviderOption` from `types/`, creating a secondary import path for the type. Importing a type from a component module is backwards from the usual direction — consider whether callers should import directly from `../../types/graphql-api`. `frontend/src/components/corpuses/CorpusSettings.tsx`
UX gap — missing API-key indicator: The corpus picker is mounted without `showApiKeyBadge` (defaults to `false`). A corpus editor can click a chip for a provider whose API key isn't configured — the save succeeds but the corpus silently uses a broken LLM. The `showApiKeyBadge` prop and `requiresApiKey` field are already in the data; passing `showApiKeyBadge` here would surface this at selection time with zero additional cost. `frontend/src/graphql/queries.ts` — new queries
PR description accuracy: The description says "new `LlmModelPicker` unit test (4 cases)" but the file contains 5 `it()` blocks (the fifth is "hides the inherited hint once a value is set"). Minor. SummaryThe core change — removing `SimpleLLMClient`, routing title generation through `agenerate_text`, adding the corpus LLM picker — is correct, well-tested, and follows the project's architectural constraints. Two items worth addressing before merge:
Everything else is minor polish. Good PR overall. |
Code Review — PR #2035: Consume LLM Singleton registry everywhere + per-corpus LLM admin GUIOverviewThis is a well-scoped, well-executed audit PR with a clear dual purpose: (1) eliminate the last hardcoded-model bypass in the runtime path by removing Strengths
Concerns / SuggestionsBackend1. New
2. Default
3. Patching targets in The tests patch 4.
Frontend5.
6. PR description says "4 cases" for Minor documentation discrepancy: the PR body says "new 7. No component-level test for the
8.
Minor Nits
SummaryThe core change (replacing a hardcoded
Nothing here is a blocker. Approve with the above as suggestions. |
…tle temp
Round 6:
- types/graphql-api.ts: add enabled?: boolean | null to LlmProviderOption so the
type matches the query's runtime shape; LlmProvidersQueryResult drops the
ad-hoc & { enabled } widening.
- LlmModelPicker: remove the LlmProviderOption re-export (single canonical import
path in types/); the picker test now imports the type from types/graphql-api.
- CorpusSettings: pass showApiKeyBadge so corpus editors see which providers
need an API key configured before selecting one.
Round 7:
- unified_agent_conversation.py: title generation now calls agenerate_text with
temperature=0.3 for deterministic short titles (two reviewers).
- test_llms_completions.py: document that patches target source modules because
agenerate_text imports its collaborators lazily.
Verified already-covered / declined: '' -> NULL is tested by the existing
TestCorpusSerializerNormalisesPreferredLLM (test_llm_runtime_config.py); the
per-call agent-construction caveat is already in the agenerate_text docstring;
fireEvent matches the codebase's test convention; the corpus-prop useEffect
draft reset is the existing, consistent slug/categories pattern.
Code ReviewOverviewThis PR closes two real gaps in the LLM Singleton registry with clean, well-targeted changes:
The approach is architecturally sound and the scope is appropriately contained. Strengths
Issues & SuggestionsMedium — design asymmetry worth documentingNon-superusers see all LLM providers, while other pipeline component types are filtered. In Suggested addition near line 204 in # LLM providers are intentionally NOT filtered by configured-components for
# non-superusers: a corpus editor must be able to see all registered providers
# to select one for Corpus.preferred_llm. No credential fields are exposed
# (settings_schema / current_value are superuser-only — see to_graphql_type).
llm_providers=[
to_graphql_type(d, "llm_provider") for d in llm_providers_data
],Medium — test patch-location comment is slightly misleadingIn # NOTE: ``agenerate_text`` imports its collaborators … lazily inside the
# function body, so these tests patch them at their *source* modules.
# If any of those imports is ever hoisted to module level, the patches must
# move to ``opencontractserver.llms.completions.<name>``The advice is the reverse of correct. Patching at the source module is already right for both lazy and eager imports when the consumer does Low —
|
…metry
- LlmModelPicker: add aria-pressed={selected} to model chips so screen-reader
users can tell which model is selected.
- pipeline_queries.py: comment why LLM providers are intentionally not run
through filter_configured for non-superusers (corpus editors must see all
providers to pick Corpus.preferred_llm), and that credentials still never
leak (settings_schema is superuser-only) — a path this PR newly makes
reachable by corpus editors.
Declined: the suggestion that the test patch-location comment is 'reverse of
correct' is itself mistaken — with 'from M import f' inside the function body,
patching M.f works; if hoisted to module level, completions.py binds its local
name at import time, so patches must move to
opencontractserver.llms.completions.<name>, exactly as the comment says.
fireEvent (codebase convention), FormHelperText (admin-local styled component,
not a design-system export), and the pre-existing slug 'as any' cast in
Corpuses.tsx are out of scope / unchanged.
|
Round-8 items handled in Addressed
Declined — the "patch-location comment is reverse of correct" item is itself mistaken. With Other declines (consistent with prior rounds): Generated by Claude Code |
Code Review — PR #2035: LLM Singleton Registry + Per-Corpus Admin GUIOverviewThis PR closes two real gaps: conversation-title generation was the last runtime LLM path that bypassed the registry (hardcoding What I verified
Issues1. Magic number:
title = await agenerate_text(
prompt,
...
temperature=0.3, # ← hardcoded
)CLAUDE.md §4 says "No magic numbers — use constants files." 2. Redundant @override_settings(DEFAULT_LLM="", OPENAI_MODEL="")
async def test_hard_fallback_when_everything_unset(self) -> None:
with patch(
"opencontractserver.pipeline.utils.get_default_llm_spec",
return_value="", # ← this already controls the output
), ...Since 3. The docstring says "intended for infrequent one-shot calls." Worth also adding a module-level Positive observations
SummaryOne confirmed CLAUDE.md violation (magic number), one minor test hygiene issue. Both are small fixes. The core design is correct, the security analysis in the PR description is accurate, and the dead-code removal is a net improvement. Recommend addressing the magic number before merge; the other item is optional. |
- Extract the title-generation temperature to a named constant TITLE_GENERATION_TEMPERATURE in opencontractserver/constants/llm.py (next to EXTRACT_DEFAULT_TEMPERATURE) and use it in the consumer, fixing the magic-number I introduced in the round-7 title-determinism change (CLAUDE.md §4). - completions.py: add __all__ = ['agenerate_text'] for discoverability. - test_llms_completions: comment that @override_settings in the hard-fallback test is load-bearing (resolve_model_spec reads settings.DEFAULT_LLM/OPENAI_MODEL directly in its fallback), declining the recurring 'redundant' suggestion whose premise (Django settings never consulted) is incorrect.
opencontractserver/llms/completions.py defined a module-level logger and imported logging but never logged anything. Drop both per the CodeQL unused-global alert and the no-dead-code rule.
Code Review — PR #2035: Consume LLM Singleton registry everywhere + add per-corpus LLM admin GUIOverviewThis PR closes two real gaps cleanly: (1) conversation-title generation was the sole runtime path bypassing the LLM registry (hardcoded Correctness
IssuesMedium — Disabled-chip test uses
Medium —
return title or f"Conversation {uuid.uuid4()}"The diff does not show a Low — Unnecessary lazy import of a pure constant
from opencontractserver.constants.llm import (
TITLE_GENERATION_TEMPERATURE,
)
Low — No frontend validation of the model spec format The SecurityThe decision to show all registered providers to non-superusers (rather than only configured ones) is correct for the use case and the security note in The two new queries ( Code Quality
Minor Nits
SummaryThe design is sound, the resolution chain is correctly implemented, and the test coverage is solid. The two medium-priority issues (disabled-chip test fidelity, unverified |
|
No code change this round — the items are stale, declined-before, or would break valid input:
To set expectations: both requirements are delivered and CI/static-analysis (CodeQL) is clean. I'm no longer pushing commits for cosmetic Generated by Claude Code |
Code ReviewThis is a well-scoped, well-executed PR. The two gaps it closes (registry bypass in title generation, no GUI for per-corpus LLM) are real and the approach is architecturally sound. Multiple review rounds are visible in the commit history and the code shows it — the design decisions are well-documented and the implementation is tight. Notes below are mostly minor. Overview
Positive Highlights
Issues & Suggestions1. PR body credits Claude Code (violates CLAUDE.md baseline rule)The PR description ends with:
CLAUDE.md is explicit: "Never credit Claude or Claude Code in commit messages, PR messages, comments, or any other artifacts." Please edit the PR body to remove both lines before merge. 2.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…ranches The new frontend code (corpus LLM card handlers, LlmModelPicker branches) was uncovered, dropping patch coverage to 72.52% (target 82.82%; frontend 61.19% vs 71.01%). - Extract the per-corpus Language Model card out of the 600-line CorpusSettings god-component into a self-contained, testable CorpusLanguageModelCard (better SRP). CorpusSettings now just renders it with corpus.id / preferredLlm / canUpdate. - Add CorpusLanguageModelCard tests (MockedProvider): save-success, mutation error rollback, server ok:false rollback, clear-to-inherit, and read-only (no Save) — bringing the card to ~96% coverage. - Add LlmModelPicker tests for the label / helper-text / API-key-badge and no-suggested-models branches — ~93% coverage. tsc + prettier clean; 12 frontend unit tests pass.
…X polish Follow-up cleanup from an audit + smoke test of the per-corpus LLM work. - Validate Corpus.preferred_llm in CorpusSerializer.validate_preferred_llm (via validate_model_spec) BEFORE save(), so an unregistered/malformed spec typed into the free-text picker returns a clean ok=false field message instead of the generic "Mutation failed due to an internal error" (the model-layer ValidationError raised from save() was caught as an internal error). Normalisation stays in Corpus.save() as the single source of truth. - Extract DEFAULT_COMPLETION_TEMPERATURE (0.7) to constants/llm.py and use it for the agenerate_text default temperature (no magic number). - LlmModelPicker: guard the chip onClick with an explicit `disabled` check so the no-op is the component's own contract; only render the "inherit" hint when the caller opts in (inheritedSpec !== undefined), so the install-wide System Settings screen no longer shows a spurious inherit hint. - Tests: add a clear->save frontend test pinning that preferredLlm:"" reaches the backend; add serializer-validation tests (clean rejection of unknown provider / malformed spec); add a backend test pinning non-superusers see the full llmProviders list but a NULL settingsSchema (no credential leak); assert the agenerate_text default temperature via the named constant. - Changelog: scope the "only bypass" wording (the *_claude analyzer tasks and the structured-extraction default deliberately pin their own models and are out of scope) and add a fragment for the validation fix.
|
Re: the CodeQL "information exposure through an exception" alert on This is a pre-existing alert, not introduced by this PR. I've left it untouched deliberately: fixing pre-existing exception-handling in unrelated serializer code would pull it into an LLM-registry changeset and widen scope. It should be triaged separately against For completeness, none of the files this PR does change contain that pattern — verified no Generated by Claude Code |
…registry-config-4rei39 # Conflicts: # config/settings/base.py
Code Review — PR #2035: Consume LLM Singleton registry everywhere + add per-corpus LLM admin GUIOverviewThis PR closes two genuine gaps in the LLM configuration system: conversation-title generation was hardcoding Backend
|
Call out for operators that the removed LLM_CLIENT_* env vars are now silently ignored and should be dropped from .env/ConfigMap, pointing them at PipelineSettings.default_llm / DEFAULT_LLM as the replacement.
|
Correction to my earlier comment on the CodeQL My previous reply said "this PR makes zero changes to except (LLMProviderNotRegistered, ValueError) as exc:
raise serializers.ValidationError(str(exc)) from exc # line 109So the alert is PR code — apologies for the inaccurate earlier characterization. On the substance: the caught exceptions are Note this code came from a separate session also developing this branch, so I'm flagging rather than editing it to avoid colliding with that work — see my note to the PR owner. Generated by Claude Code |
Code ReviewOverviewThis PR closes two well-identified gaps in the LLM Singleton registry: (1) the last conversational LLM bypass (title generation hardcoding Correctness & Critical Issues
The test file comments this correctly: # NOTE: ``agenerate_text`` imports its collaborators lazily inside the function
# body, so these tests patch them at their *source* modules. If any of those
# imports is ever hoisted to module level, the patches must move to
# ``opencontractserver.llms.completions.<name>`` or they'll stop intercepting.The lazy imports are necessary to avoid circular dependencies—that's fine. But the warning means a future refactor could silently break all 7 tests without any static signal. Consider adding a brief
update: (cache, { data }) => {
if (data?.updateCorpus?.ok && corpusId) {
...
cache.modify({
fields: {
preferredLlm: () => llmDraft.trim() || null, // ← reads current state at completion time
},
});
}
},If the user edits the input between clicking Save and the mutation completing, the cache will be written with the wrong (never-submitted) value. The input is disabled during const handleLlmSave = () => {
const submittedValue = llmDraft.trim();
updateCorpusLlm({
variables: { id: corpusId, preferredLlm: submittedValue },
// then use submittedValue (not llmDraft) in the update callback
});
};SecurityNon-superuser LLM provider exposure is intentional and well-documented ( Serializer-layer validation is the right place for the Code Quality
Asymmetry in
TestsBackend ( Frontend
Consumer title tests are correctly updated to mock Minor Nits
SummarySolid PR. The two genuine issues are:
Everything else—the registry wiring, security boundary, constant extraction, docs update, and test coverage—is well-executed. |
…registry-config-4rei39
- CorpusLanguageModelCard: capture the submitted/rollback values up front in handleLlmSave and move onCompleted/onError/update to per-call options so the success path, rollback, and cache write all act on what was actually sent rather than the live draft state (latent divergence if the input is ever made editable mid-save). - Add a unit case pinning that the inherited system-default hint still renders in read-only mode (canUpdate=false) when the corpus has no override.
Mounts the Corpus Settings panel through CorpusSettingsTestWrapper, asserts the provider chips and inherited system-default hint render, and captures the corpus--language-model-card--inherit-default documentation screenshot. Behavioural save/rollback coverage stays in the Vitest unit test.
Code ReviewOverviewThis PR closes two well-defined gaps in the LLM Singleton registry: (1) a registry-bypassing title-generation path using a hardcoded Correctness
Apollo cache update (CorpusLanguageModelCard.tsx) The SecurityThe change to stop filtering LLM providers for non-superusers in The two new GraphQL queries ( Component Design (Frontend)LlmModelPicker.tsx
CorpusLanguageModelCard.tsx The The TestsCoverage is solid:
Minor Suggestions
SummarySolid, well-motivated PR. The SimpleLLMClient removal is clean, the new helper respects all project conventions (async-only, registry-backed, no magic models), the frontend component is accessible and properly extracted, and the security boundary around provider credentials is tested. The minor items above are nits — none block merging. |
The linter (mypy) job was red on every branch, including main: a routine
transitive numpy 2.4.x bump began shipping numpy/__init__.pyi with PEP 695
'type' aliases, and mypy.ini's stale python_version = 3.11 made mypy reject
that stub syntax ('Type statement is only supported in Python 3.12 and
greater') and abort before any project code was checked.
Docker images and CI both run 3.12.x and there is no 3.11 target anywhere,
so 3.11 was simply stale. Bumping the target to 3.12 fixes both CI mypy
invocations (the pre-commit hook on mypy==2.0.0 and the dedicated Run mypy
step on mypy==2.1.0); both now report 'Success: no issues found in 1425
source files' with zero new project type errors. Adds an in-file note and
corrects docs/typing/README.md so the target is not silently lowered again.
… through an exception' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
Code ReviewOverviewThis PR closes two real gaps in the LLM singleton registry: (1) routing conversation-title generation through the registry instead of a hardcoded Bug -- Error message mismatch in
|
…registry-config-4rei39
…g internal path The CodeQL info-exposure auto-fix flattened the LLMProviderNotRegistered reason to a generic 'Invalid model specification.', which dropped the user-actionable 'not registered' signal and broke test_unknown_provider_rejected_with_clean_field_error. Construct a type-specific message from the user's own input (the typed provider prefix) so the real reason surfaces without echoing str(exc), whose text embeds an internal source path.
The collaborator imports in agenerate_text are deferred to function scope to break an import cycle; add a WHY comment so a future refactor doesn't silently hoist them and break the source-module test patches in test_llms_completions.py.
|
test |
|
PLACEHOLDER - full review body to follow |
|
Unable to post full review body — sandbox write permissions are blocked. The full review body was provided in the task instructions. Please post the content of /tmp/pr_review_body.md manually if needed. |
PR Review — Consume LLM Singleton registry everywhere + per-corpus LLM GUIThis PR closes two real gaps in the LLM registry: it replaces the OpenAI-only 1. File: The parameter is typed 2. Duplicate constant value — minor DRY violation File:
3. Redundant File: In the class LLMProviderNotRegistered(ValueError):
def __init__(self, provider_key: str, spec: str) -> None:
super().__init__(f"Provider {provider_key!r} (from spec {spec!r}) is not registered.")
self.provider_key = provider_keyThen the serializer handler becomes Minor notes (non-blocking)
Verified as non-issues: |
Summary
An audit-and-complete pass on how LLMs are configured at runtime via the Singleton registry, ensuring (1) admins have a GUI surface to control it and (2) corpuses consume from it so — unless a tool/infra deliberately overrides — the configured Singleton LLM is used throughout a corpus.
The registry itself already existed and was mostly wired correctly:
resolve_model_spec()walksper-call → agent → corpus.preferred_llm → PipelineSettings.default_llm → Django settings, and the agent factory consumes it for every agent path (document/corpus chat, extracts, deep research, agent mentions, corpus/thread actions). This PR closes the two remaining gaps the audit surfaced.Gap 1 — Corpus consumption (one runtime bypass)
Conversation-title generation (
config/websocket/consumers/unified_agent_conversation.py) was the only runtime LLM path bypassing the registry. It called the OpenAI-onlySimpleLLMClient, which hardcodedgpt-4o-miniand ignored bothcorpus.preferred_llmand the install-wide default — even thoughself.corpuswas right there.opencontractserver/llms/completions.py::agenerate_text— a provider-agnostic (OpenAI/Anthropic/Google/Ollama), registry-backed one-shot completion helper that walks the same resolution chain as the agent factory and builds the model via the credential-awaremodel_factory.SimpleLLMClient(client.py) and itsLLM_CLIENT_*settings; updated docs + tests (no-dead-code).Gap 2 — Admin GUI for the per-corpus LLM
Corpus.preferred_llmwas fully plumbed in the backend (mutation arg,CorpusType, serializer) but had no frontend surface — a ghost feature only settable via the GraphQL console/Django shell.LlmModelPickercomponent (frontend/src/components/common/LlmModelPicker.tsx); the admin System Settings screen now consumes it too, de-duplicating its inline provider/model chip list."", which the backend normalises toNULL.preferredLlmthroughRESOLVE_CORPUS_BY_SLUGS_FULL,UPDATE_CORPUS, and theCorpusType/RawCorpusTypeTS types.@login_requiredqueries (GET_LLM_PROVIDERS,GET_SYSTEM_DEFAULT_LLM) feed the picker, so non-superuser corpus owners can use it without any credential exposure.Testing
opencontractserver/tests/test_llms_completions.pypins the resolution chain (corpus beats settings default; explicit beats corpus; hard fallback; temperature omission; None output; max_tokens forwarding). Updated the consumer title tests to mockagenerate_textand cover corpus-preferred threading + empty/None fallback.pyupgrade/black/isort/flake8clean.LlmModelPickerunit tests (5 cases) pass;tsc --noEmitclean; existing routing tests that hit the corpus-resolution query still pass (13/13); prettier clean.Permissioning note
The two new GraphQL queries are
@login_requiredand request only non-secret fields (nosettingsSchema/currentValue), so a corpus editor can populate the picker without ever seeing provider credentials. Override precedence is preserved end-to-end: explicit per-call model > per-agent > corpus > install-wide default > Django settings.