Skip to content

feat(backends): support Ollama adapter functions - #1634

Open
planetf1 wants to merge 11 commits into
generative-computing:mainfrom
planetf1:feat/take-over-pr-1622
Open

planetf1 wants to merge 11 commits into
generative-computing:mainfrom
planetf1:feat/take-over-pr-1622

Conversation

@planetf1

@planetf1 planetf1 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Supersedes #1622. Related: #1633.

Description

This takes over the Ollama adapter-function work originally proposed in #1622. The original implementation commit remains in this branch history. The takeover rebases the feature onto the current composed-adapter API and adds a reproducible test model built from pinned official Granite artefacts instead of depending on a user-published model.

  • Route catalogue adapter functions through composed ServerMediatedBinding registration.
  • Support cold ALoraRequirement routing for configured catalogue adapter models.
  • Preserve the selected bundled model in generation metadata and logs.
  • Build the official uncertainty aLoRA as GGUF in the Ollama test harness; ordinary local pytest reuses or creates the generated model.
  • Document the supported catalogue and custom-adapter workflows, including the local build prerequisite.

For one adapter function, use its bundled aLoRA tag for both model_id and the adapter route, with adapter_base_model_name set to the matching Hugging Face base-model directory. Before its invocation tokens appear, the bundle behaves as the base model and retains one Ollama model identity. Applications that map several adapter functions to separate Ollama tags cannot share a KV cache across those tags; multi-adapter, single-model serving remains a Granite Switch use case.

The broader server-mediated lifecycle and telemetry design remains tracked in #1633.

Testing

  • Unit tests added for request rewriting, model selection, cold requirement routing, and the optional dependency error.
  • Live Ollama e2e uses the generated model from pinned official Granite artefacts.
  • Qualitative acceptance test confirms the aLoRA bundle materially changes the base model's certainty score for a fixed request.
  • ruff, mypy, Markdown lint, shell syntax, and whitespace checks pass.
  • Full local fast suite is blocked by unrelated PyTorch Inductor/OpenMP compilation failures on macOS; CI will provide the authoritative Linux result.

Attribution

  • AI coding assistants used

Adding a new component, requirement, sampling strategy, or tool?

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

NOTE: This PR supersedes an already acknowledged contribution in #1622.

aanokh and others added 2 commits September 7, 2026 16:01
@github-actions github-actions Bot added the enhancement New feature or request label Sep 8, 2026
@planetf1 planetf1 mentioned this pull request Sep 8, 2026
8 tasks
Assisted-by: Codex
Signed-off-by: Nigel Jones <[email protected]>
Assisted-by: Codex
Signed-off-by: Nigel Jones <[email protected]>
Assisted-by: Codex
Signed-off-by: Nigel Jones <[email protected]>
@planetf1 planetf1 self-assigned this Sep 8, 2026
@planetf1
planetf1 marked this pull request as ready for review September 8, 2026 09:52
@planetf1
planetf1 requested a review from a team as a code owner September 8, 2026 09:52
Assisted-by: Codex
Signed-off-by: Nigel Jones <[email protected]>

@psschwei psschwei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review of the Ollama bundled-adapter path. The uncertainty/aLoRA flow this PR tests looks correct, but the generalization to the other adapter functions has gaps, and three of them fail silently with a schema-valid score rather than raising. Details inline; openai.py already has the correct version of most of these.

Comment thread mellea/backends/ollama.py
Comment thread mellea/backends/ollama.py
Comment thread mellea/backends/ollama.py Outdated
Comment thread mellea/backends/ollama.py Outdated
Comment thread mellea/backends/ollama.py
Comment thread docs/examples/intrinsics/uncertainty_ollama.py Outdated
Comment thread .github/workflows/quality.yml
Comment thread test/scripts/run_tests_with_ollama_and_vllm.sh Outdated
Comment thread test/backends/test_ollama.py Outdated
…computing#1634)

Fixes 8 findings from psschwei's review plus AngeloDanducci's suggestion:

- Fold extra_body.documents into a message in _generate_from_intrinsic;
  Ollama's chat SDK has no extra_body passthrough, so answerability,
  citations, hallucination_detection, clarify_query, and
  find_context_attributions were sending zero documents.
- resolve_adapter now raises if name has no adapter_models entry, instead
  of registering successfully and letting generation silently fall back
  to the base model.
- resolve_adapter picks LoRA vs aLoRA from the catalog's adapter_types
  instead of hardcoding aLoRA; restricted context-attribution, citations,
  and hallucination_detection to LoRA-only (verified against the Hub).
- reroute_to_alora now follows a new default_to_constraint_checking_alora
  flag (default True), matching OpenAIBackend/LocalHFBackend, instead of
  only firing for ALoraRequirement.
- _generate_from_context now awaits do_generate_walk before the
  Requirement/Intrinsic dispatch, matching the other backends.
- Fix Path(__file__).parents[2] -> parents[3] in the example script.
- Constrain granite4.1:3b's context in CI before the adapter build step;
  only granite4.2:3b was constrained, so the new bundled tag inherited
  the unconstrained default and reintroduced the CI-stall risk.
- Fix export VAR="$(cmd)" masking command-substitution failures under
  set -e in the test runner script, the build script's own usage
  comment, and docs/docs/advanced/intrinsics.md.
- test_ollama.py's adapter-build fixture now skips on build failure
  instead of erroring.

Added regression tests for the resolve_adapter and document-forwarding
fixes; updated two existing unit tests that had encoded the old
(buggy) unconditional-resolve behaviour.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <[email protected]>

@psschwei psschwei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a conflict, but otherwise LGTM

@planetf1

Copy link
Copy Markdown
Contributor Author

Will leave ready to merge so @jakelorocco can review on his return

Comment thread mellea/backends/ollama.py
"add one to `adapter_models` before resolving it."
)

metadata = fetch_intrinsic_metadata(name)

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.

Doesn't this mean that you can only add ollama adapters that are known to our internal catalog?

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.

Sort-of

  • if you use adapter_models={…} then the ollama support is the same as OpenAI and hugging face, and it does rely on an entry in the catalog
  • You can use add_adapter() on all backends as this doesn’t use the catalog, but you lose the simplity/consistency
  • There was an old CustomIntrinsicAdapter, and that function was lost in the refactor
  • We should probably open up an issue to create a proper custom adapter registration api
  • So I would suggest it’s not a gap with this pr in itself, rather something more we need to improve (and maybe patch up the CustomsIntrinsicAdapter removal regression?)

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

a few additional thoughts

Comment thread mellea/backends/ollama.py Outdated
Comment on lines +715 to +718
if rewritten.extra_body is not None and rewritten.extra_body.documents:
rewritten = move_documents_to_message( # type: ignore[assignment]
rewritten, "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.

Doesn't the rewriter handle this internally? The io.yaml should specify this. If it doesn't, then it's incompatible with ollama and we should throw an error / handle this in the io.yaml conversion.

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.

Not what you suggested — found a different, real bug instead.

Before this fix — silent failure:

  • find_citations, flag_hallucinated_content, etc. ran without error and returned a result.
  • That result was always empty. No citations, no flags, ever, on Ollama.
  • Nothing indicated anything was wrong — it looked like the documents just had nothing to say.

If we'd thrown an error instead (your suggestion) — loud failure:

  • Same functions would raise instead of returning an empty result.
  • Trades "silently useless" for "doesn't work at all" — worse for the user, since these are meant to work on Ollama.

After this fix — working:

  • Same functions now return actual citations / actual hallucination flags, correctly.

Root cause: two parts of the code disagreed about where the documents ended up in the request. One part moved them into the message; the other part still looked for them in the old spot, found nothing, and returned an empty (but valid-looking) result. Fixed by making both parts agree.

Side note: io.yaml is the right long-term owner of this setting, but that's upstream/published config, not ours to change here.

@jakelorocco jakelorocco Sep 15, 2026

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.

actually, is the io.yaml not handling this correctly right now? I do see for citations (and hallucination detection), the docs get added using roles which appears to be the correct way to send docs to ollama: https://huggingface.co/ibm-granite/granitelib-rag-r1.0/blob/main/citations/granite4_micro/lora/io.yaml#L92.

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.

  • We pin citations to a fixed revision + the granite-4.1-3b variant. That file has no docs_as_message key.
  • Your link is main + granite4_micro — different variant, different (newer) content.
  • Checked main's granite-4.1-3b too — still missing the key there as well. Not just a stale pin.
  • So roles is the right fix, just not present yet where we actually pull from.
  • Our code-side default handles it either way — becomes a no-op once upstream adds the key.
  • Worth asking upstream to add it to granite-4.1-3b too.

Comment thread mellea/backends/ollama.py Outdated
Comment on lines +876 to +877
await asyncio.to_thread(self.resolve_adapter, adapter_name)
alora_req_adapter = self._find_adapter(adapter_name, search_types)

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.

Does this need to be guarded with a try-catch so that a failure when trying to reroute doesn't cause LLMaJ to fail?

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.

Done as suggested. Resolve is now wrapped in try/except — a failure falls back to regular generation instead of killing the whole call.

Comment thread mellea/backends/ollama.py Outdated
top_logprobs = model_opts.pop("top_logprobs", None)

# each adapter function is served by its own ollama model tag
model = self._adapter_models.get(action.intrinsic_name, self._model_id)

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.

Does the above find_adapter call ensure that the adapter exists and that we don't just fall back to the base model? If so, can we add a comment here?

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.

Done as suggested. Raises instead of silently falling back to the base model — same guard resolve_adapter() already has, extended to generation time.

Comment thread mellea/backends/ollama.py Outdated
alora_req_adapter is None
and reroute_to_alora
and adapter_name in self._adapter_models
and not explicit_types

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.

Should we check that if there is an explicit type here, that it's alora?

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.

Done as suggested. An explicit override that excludes aLoRA now skips the resolve — resolve_adapter() has no way to request a specific type, so resolving there could register the wrong one. An override that includes aLoRA still resolves, since thats exactly what a cold resolve would produce anyway. Added tests for both cases.

Fix three issues from jakelorocco's review of the Ollama adapter-function
support:

- _generate_from_intrinsic() looked up the adapter's model tag via
  self._adapter_models.get(name, self._model_id), silently falling back to
  the plain base model for an adapter registered directly via add_adapter()
  (bypassing adapter_models). The rewriter still built the adapter's
  activation prompt and enforced its response schema, so the base model
  returned a schema-valid but meaningless answer with no adapter weights
  behind it. Now raises instead, matching the guard resolve_adapter() already
  has for the same failure class.

- The opportunistic resolve_adapter() call in the automatic
  requirement-check reroute path was unguarded; a network or config error
  during resolution killed the whole generate call instead of falling back
  to regular generation as intended. Wrapped in try/except.

- That same reroute path skipped the opportunistic resolve whenever any
  explicit adapter_types override was given, even one that included aLoRA
  and would have been satisfied by the resolve. Now only skips when the
  override excludes aLoRA.

- Investigating the docs_as_message question surfaced an unrelated bug: the
  result processor was constructed from the adapter's unmodified io.yaml
  config while the rewriter separately folded documents into the message,
  so the two disagreed about where documents lived. Citations and
  hallucination flags decoded as empty on every call, silently. Fixed by
  forcing docs_as_message onto the config before constructing both the
  rewriter and the result processor, and removed the now-redundant manual
  fold.

Updated test/backends/test_ollama_intrinsics_unit.py to match: replaced
test_adapter_model_tag_defaults_to_model_id (asserted the old silent
fallback) with test_generation_without_configured_tag_raises, defaulted the
adapter fixture to a configured tag so unrelated tests aren't affected, and
added coverage for the explicit-adapter-types resolve behaviour.

Assisted-by: Claude Code
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Signed-off-by: Nigel Jones <[email protected]>
@planetf1

Copy link
Copy Markdown
Contributor Author

@jakelorocco this is another candidate for our release.

planetf1 and others added 2 commits September 16, 2026 07:32
…er path

Upstream's send_to_queue() (generative-computing#1631) now takes the ModelOutputThunk directly
instead of a bare queue, to stamp TTFB at provider receipt. The adapter
function code path added by this PR still passed output._gen.queue,
which broke type-checking after merging upstream/main.

Assisted-by: Claude Code
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Signed-off-by: Nigel Jones <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants