Skip to content

ADFA-5095 | Publish optional backend capabilities on the LLM inference contract - #1660

Open
jatezzz wants to merge 7 commits into
stagefrom
feat/ADFA-5095-llm-backend-capabilities
Open

ADFA-5095 | Publish optional backend capabilities on the LLM inference contract#1660
jatezzz wants to merge 7 commits into
stagefrom
feat/ADFA-5095-llm-backend-capabilities

Conversation

@jatezzz

@jatezzz jatezzz commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Extended the LlmInferenceService contract in the CodeOnTheGo plugin API so LLM backends can ship as independent plugins. This PR introduces opt-in interfaces for optional capabilities like cancellable streaming, multi-turn history, and tool calling, allowing backends to implement only what they support. It also enables backends to declare their own configuration fields (text, password, dropdown, etc.), removing the need for hardcoded provider settings in the consumer application.

Details

  • Added ConfigurableBackend and ConfigFieldSpec to allow backends to define their own settings UI.
  • Added CancellableBackend interface for opt-in streaming cancellation.
  • Introduced capability flags (supportsHistory(), supportsTools()) and default interface methods in LlmBackend to avoid forcing implementation of unsupported features.
  • Added getPreferredBackendId(), getDefaultTemperature(), and getSystemPrompt() for better backend-owned context management.
  • All changes are additive and binary-compatible with existing AI plugins.
document_5179206860728698887.mp4

Ticket

ADFA-5095

Observation

The additions to the LlmBackend interface use default methods that throw UnsupportedOperationException for history and tool generation. Consumers must check supportsHistory() and supportsTools() before invoking these methods, successfully removing the need for brittle, reflection-based discovery.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@jatezzz
jatezzz requested review from a team and Daniel-ADFA August 11, 2026 22:06
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e734a2b-25d1-4b13-b7f6-9d21c0248747

📥 Commits

Reviewing files that changed from the base of the PR and between fa0fa38 and a9119cf.

📒 Files selected for processing (1)
  • docs/PLUGIN_API_CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/PLUGIN_API_CHANGELOG.md

📝 Walkthrough
  • Extends LlmInferenceService with backend configuration, cancellation, history, tool calling, preferred backend lookup, default temperature, and system prompt support.
  • Adds tool metadata, correlated tool results, and the TOOL role to ChatMessage.
  • Adds validation, response factory methods, and tests for LLM value types.
  • Adds API compatibility guidance and a 26.33 plugin API changelog entry.
  • Reformats PluginContextTest and removes three service test classes.
  • Risks: Capability flags can disagree with supported methods. Consumers must check capability flags before calling optional methods.
  • Risks: ConfigurableBackend does not define configuration access or persistence. File-picker and password fields lack plugin identity, permission rules, and storage definitions.
  • Risks: Nullable public fields lack complete nullability annotations. Some collections and nested tool definitions remain mutable. Dropdown invariants are not enforced.
  • Risks: SystemPromptRequest requires toolCallSyntax for non-tool consumers.
  • Best-practice gaps: Tool calling lacks a complete return type and correlated tool-result message contract. Compatibility checklist validation and constructor coverage remain incomplete.

Walkthrough

The LLM inference plugin API adds backend lifecycle and capability contracts, tool-aware chat models, system-prompt support, tool-streaming callbacks, documentation, and validation tests. Plugin context tests are reformatted and their test double gains directory and preference APIs.

Changes

LLM backend API

Layer / File(s) Summary
Service lifecycle and chat messages
plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java, plugin-api/api/plugin-api.api, docs/plugin-api.md, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java
The service adds backend selection, cancellation, embeddings, availability, and registration operations. ChatMessage supports tool metadata and the TOOL role with validation. API compatibility guidance is updated.
Backend capabilities and configuration
plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java, plugin-api/api/plugin-api.api, docs/PLUGIN_API_CHANGELOG.md, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java
Backends add configuration, history, default temperature, and system-prompt contracts. SystemPromptRequest copies tools into an unmodifiable list.
Streaming and tool result contracts
plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java, plugin-api/api/plugin-api.api, docs/PLUGIN_API_CHANGELOG.md, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java
Tool streaming callbacks, response factories, mutable tool-call models, tool definitions, and validation tests are added.

Plugin test maintenance

Layer / File(s) Summary
Plugin context test updates
plugin-api/src/test/kotlin/com/itsaky/androidide/plugins/PluginContextTest.kt, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeFileServiceTest.java, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeProjectServiceTest.java, plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeResourceServiceTest.java
TestPluginContext implements directory and preferences APIs. Existing tests are reformatted, and three service test classes are removed.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to a9119

The PR adds optional backend capability and configuration contracts without any supplied evidence of a current merge-blocking issue; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant LlmInferenceService
  participant ToolCallingBackend
  participant ToolStreamCallback
  LlmInferenceService->>ToolCallingBackend: generateStreamingWithTools(...)
  ToolCallingBackend->>ToolStreamCallback: onToken(...)
  ToolCallingBackend->>ToolStreamCallback: onToolCall(...)
  ToolCallingBackend->>ToolStreamCallback: onComplete(...)
  ToolCallingBackend->>ToolStreamCallback: onError(...)
Loading

Possibly related PRs

Poem

A rabbit checks each tool call with care,
Tokens and callbacks hop through the air.
Backends choose paths, prompts join the stream,
Tests guard each field in the API scheme.
New contracts thump in place.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: exposing optional backend capabilities in the LLM inference contract.
Description check ✅ Passed The description directly explains the LLM API additions, optional backend capabilities, configuration support, and compatibility goals.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ADFA-5095-llm-backend-capabilities

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🧹 Nitpick comments (3)
plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java (3)

196-201: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Document the storage requirement for PASSWORD fields.

ConfigFieldType.PASSWORD tells the consumer to mask the input, but the contract does not state how the value must be persisted. The consumer stores the value under key. State in the Javadoc that PASSWORD values must be stored with EncryptedSharedPreferences or the Android Keystore, and must never be logged or sent to analytics or crash reporting. A published contract is the place to fix this, because each backend author reads it instead of the app code.

As per coding guidelines: "Store credentials and signing secrets using EncryptedSharedPreferences or Android Keystore; never commit, log, or send secrets to analytics or crash reporting."

📝 Proposed doc addition
 	enum ConfigFieldType {
+		/**
+		 * Text, password, file path, dropdown selection and boolean field kinds. A {`@code` PASSWORD} value is a
+		 * credential: the consumer must persist it with EncryptedSharedPreferences or the Android Keystore, and must
+		 * never log it or send it to analytics or crash reporting.
+		 */
 		TEXT, PASSWORD, FILE_PICKER, DROPDOWN, BOOLEAN
 	}

Also applies to: 238-243

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`
around lines 196 - 201, Update the Javadoc for ConfigFieldType.PASSWORD and its
required field contract to state that password values stored under key must use
EncryptedSharedPreferences or the Android Keystore, and must never be logged or
sent to analytics or crash reporting.

Source: Coding guidelines


618-641: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add @param tags to the callback methods.

ToolStreamCallback documents each method with one sentence but omits @param tags. StreamCallback at lines 524-548 documents every parameter. Match that level for the published contract, in particular for onToolCall(ToolCallRequest), where the caller needs to know whether it must respond and on which thread the method is invoked.

As per coding guidelines: "Public classes, functions, and non-obvious logic must have KDoc or Javadoc documenting contracts, rationale, threading, nullability, side effects, or units."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`
around lines 618 - 641, Add Javadoc `@param` tags to each parameter of
ToolStreamCallback methods, matching the detail and contract style used by
StreamCallback. Document the token, error, response, and especially
ToolCallRequest parameters, including whether the caller must respond and the
invocation thread where applicable.

Source: Coding guidelines


310-341: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider reporting the unsupported case through the callback instead of throwing.

Both defaults throw UnsupportedOperationException from a void, callback-based method. Every other failure in this API reaches the caller through callback.onError(String). A caller that omits the supportsHistory() or supportsTools() check therefore gets an exception on the calling thread instead of an error event, and that exception reaches the global crash handler.

The documented rationale for not falling back to plain streaming is sound. Reporting the same refusal through callback.onError(...) keeps that rationale and keeps the failure inside the established error path.

As per coding guidelines: "Catch recoverable I/O, parsing, IPC, git, and plugin failures locally; convert them into explicit error states, never allow unexpected exceptions to reach the global GlitchTip crash handler."

♻️ Proposed change for the history default
 		default void generateStreamingWithHistory(
 				List<ChatMessage> history,
 				String prompt,
 				LlmConfig config,
 				StreamCallback callback) {
-			throw new UnsupportedOperationException("Multi-turn history is not supported by backend: " + getId());
+			callback.onError("Multi-turn history is not supported by backend: " + getId());
 		}

Apply the same change to generateStreamingWithTools, and update the @throws tags to describe the error event.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`
around lines 310 - 341, Update the default implementations of
generateStreamingWithHistory and generateStreamingWithTools to report
unsupported operations through the respective callback’s onError(String) method
instead of throwing UnsupportedOperationException. Preserve the existing refusal
messages and update both Javadocs’ `@throws` documentation to describe the
callback error event rather than an exception.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`:
- Around line 359-369: Annotate the four nullable default methods in
LlmInferenceService and their parameters as requested: at
plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java:359-369,
add `@Nullable` to getDefaultTemperature() and document that callers must
null-check before assigning to primitive LlmConfig.temperature; at :385-397, add
`@Nullable` to getSystemPrompt(SystemPromptRequest) and `@NonNull` to request; at
:256-266, add `@Nullable` to getSettingsFragmentClassName() and `@NonNull` to
getConfigSpecs(); and at :111-121, add `@Nullable` to getPreferredBackendId().
- Around line 111-121: Annotate the default method getPreferredBackendId() with
the project’s existing `@Nullable` annotation, matching the nullability convention
used by getBackend and preserving its documented null return behavior.
- Around line 588-616: Update ToolCallRequest and ToolDefinition to use final
fields, add Javadoc for each public field, constructor, and constructor
parameter, and defensively copy args and parametersSchema into immutable or
unmodifiable map instances while preserving map order as required. Match the
defensive-copy conventions used by ConfigFieldSpec and SystemPromptRequest, add
any needed LinkedHashMap import, and regenerate the affected API dump.
- Around line 17-20: Update the newly added abstract methods in
LlmInferenceService, including cancelGeneration, to provide default
implementations so existing external implementations remain source- and
runtime-compatible. Preserve the current contract while avoiding mandatory
overrides; only treat this as a breaking change if the project’s compatibility
policy explicitly requires documenting and versioning it.

---

Nitpick comments:
In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`:
- Around line 196-201: Update the Javadoc for ConfigFieldType.PASSWORD and its
required field contract to state that password values stored under key must use
EncryptedSharedPreferences or the Android Keystore, and must never be logged or
sent to analytics or crash reporting.
- Around line 618-641: Add Javadoc `@param` tags to each parameter of
ToolStreamCallback methods, matching the detail and contract style used by
StreamCallback. Document the token, error, response, and especially
ToolCallRequest parameters, including whether the caller must respond and the
invocation thread where applicable.
- Around line 310-341: Update the default implementations of
generateStreamingWithHistory and generateStreamingWithTools to report
unsupported operations through the respective callback’s onError(String) method
instead of throwing UnsupportedOperationException. Preserve the existing refusal
messages and update both Javadocs’ `@throws` documentation to describe the
callback error event rather than an exception.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44a19cd7-2669-438a-bb29-b705bc2d101a

📥 Commits

Reviewing files that changed from the base of the PR and between 64d9222 and ce89dd1.

📒 Files selected for processing (2)
  • plugin-api/api/plugin-api.api
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java

@jatezzz
jatezzz force-pushed the feat/ADFA-5095-llm-backend-capabilities branch from ce89dd1 to 90d5364 Compare August 12, 2026 14:09

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/PLUGIN_API_CHANGELOG.md`:
- Around line 37-60: Clarify the compatibility wording in the “Optional LLM
backend capabilities” and “Backend-owned settings” entries: state that only
methods added to existing interfaces are default methods, while backends
adopting optional interfaces must implement abstract methods such as
CancellableBackend.cancelStreaming and ConfigurableBackend.getConfigSpecs. Keep
the existing API references and do not add caller gating changes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b32cc4c-089a-4271-b2c1-f42fe1e45b6d

📥 Commits

Reviewing files that changed from the base of the PR and between ce89dd1 and cbdf3b3.

📒 Files selected for processing (2)
  • docs/PLUGIN_API_CHANGELOG.md
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java

Comment thread docs/PLUGIN_API_CHANGELOG.md Outdated
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Review: eight findings still open

Reviewed at b9de41a and cross-checked against the consumer that adopts this contract, appdevforall/plugin-examples#65 (head 78316c7). Two compile-breaking findings from the first pass — the non-override generateStreamingWith* methods on the shipped backends, and the CancellableBackend simple-name collision — are resolved on that branch and need nothing here. Two more, getPreferredBackendId() never being implemented and getSettingsFragmentClassName() having no consumer, are answered there too (LlmInferenceServiceImpl.kt:167, BackendRegistry.classLoaderFor()).

These eight are not.

Contract design

1. supportsHistory() (419) / supportsTools() (428) are unbound to the overrides they describe. Nothing ties the probe to the method, so both mismatched states are representable: probe false with the method overridden, and probe true with it not. PR 65 lands in the first one deliberately — LocalLlmBackend.kt:520 and GeminiBackend.kt:558 both return supportsTools() = false while overriding generateStreamingWithTools, because the override exists only to preserve history under a GBNF grammar. Its router then works by ignoring the probes: LlmInferenceServiceImpl.kt:19 says routing goes "by the supportsTools/supportsHistory predicates — rather than by casting", but :107-133 calls generateStreamingWithTools unconditionally. Any consumer that honours the probe loses tool-driven chat on both backends. Modelling these as HistoryCapableBackend/ToolCallingBackend extending LlmBackend — the marker-plus-instanceof mechanism this same file already uses at 152 and 249 — makes both bad states unrepresentable and removes four members from LlmBackend.

2. Tool calling has no return path. ToolCallRequest.callId (604) is documented as correlating a call with the result the consumer sends back, but there is no ToolResult type and ChatMessage.Role (183) is still USER, ASSISTANT, SYSTEM. List<ChatMessage> is the only conduit into the next turn, so a tool result cannot be represented in it. PR 65 shows the consequence unchanged from before this PR: AgentLoop.kt:148 feeds results back as ChatMessage(Role.USER, formatToolResults(...)), correlating multiple calls in a turn positionally, and callId appears nowhere in the repo. A provider-native tool API keyed by tool_call_id cannot be driven from this contract at all.

3. ConfigurableBackend (249) is describe-only. getConfigSpecs() declares fields for the app to draw, but there is no setter, no getter and no named store, so a value the user types can never reach the backend and the screen cannot be prefilled. ConfigFieldSpec.key (191) names a storage key without naming a store. PR 65 confirms this by routing around it: both backends implement getConfigSpecs(), but real configuration flows through their own fragments and their own SharedPreferences, and the spec list is now documented as a fallback "for consumers too simple to mount a fragment" — a consumer that cannot exist, because there is nothing to write through. Either give the interface a read/write pair with a defined owner, or drop the specs and keep the fragment.

4. ConfigFieldType.FILE_PICKER / PASSWORD (243) have no identity or permission to enforce against. PluginPathAllowlist.isAllowed(path, permissions, pluginId) needs both a pluginId and a Set<PluginPermission>; registerBackend records neither, LlmBackend.getId() is a backend id (local, gemini) not a plugin id, and ConfigFieldSpec has no identity field. PR 65's LocalLlmBackend.getConfigSpecs() ships exactly this shape — a FILE_PICKER for local_llm_model_path — so a plugin declaring no filesystem.read can have the host open a chooser and hand back any absolute path the user picks. PASSWORD has the mirror problem: the host is made responsible for collecting a live credential with no storage contract and no permission string that makes it visible at install time.

Kotlin-consumer correctness

5. Nullable public fields are unannotated. The nullability commit annotated constructor parameters and left the fields bare: ConfigFieldSpec.defaultValue (205), SystemPromptRequest.exampleFilePath (574), ToolCallRequest.args (610), ToolDefinition.parametersSchema (643), LlmResponse.text (508) and LlmResponse.error (511) — each documented or constructed as nullable, each reaching Kotlin as a non-null platform type. Dereferences compile with zero diagnostics and throw at runtime; adding @Nullable to the field flips kotlinc to a hard error. Every consumer of this jar is an out-of-tree Kotlin plugin, so this is the whole audience.

6. "Never null, never mutable" overstates what the constructors deliver. ConfigFieldSpec.options (208) and SystemPromptRequest.tools (566) surface to Kotlin as (Mutable)List<...>!, so .add/.clear compile and throw. Worse for tools: the defensive copy is spine-only, while ToolDefinition has public non-final fields and stores parametersSchema by reference (659). A backend can rename a tool and clear the consumer's live schema map, after which the prompt advertises one tool and onToolCall dispatches against another. The ToolDefinition javadoc (633) asks callers to treat it as read-only and explains the fields are non-final for compatibility — worth revisiting, since read-only-by-request is the weakest form this could take. Same constructor: the cross-field invariant at 207 (options empty unless DROPDOWN) is unenforced in both directions, so a required DROPDOWN with no options constructs fine and yields a field the user can never satisfy.

7. SystemPromptRequest hard-requires toolCallSyntax (591). It is the only accessor for a backend's model-specific prompt, yet a plain-chat consumer, or one using native function calling, has no syntax to pass and gets an NPE. Passing "" is worse: getSystemPrompt's javadoc tells the backend to reproduce the syntax verbatim, so the model is instructed to emit calls in an empty envelope during a conversation that parses none — the silent prose-instead-of-tool-call failure this type exists to prevent. Nullable with a documented meaning, matching tools and exampleFilePath, is the consistent shape.

Process

8. Eleven new public symbols are frozen into the ABI with none of the checklist run. The diff is two files. docs/PLUGIN_API_CHANGELOG.md has no entry (newest heading is still ### 26.31), so an author adopting ConfigurableBackend has no plugin.min_ide_version to floor at and their .cgp installs on an IDE lacking the types, dying with NoSuchMethodError at first use. docs/plugin-api.md never lists LlmInferenceService in its surface inventory, so its "adding a method is safe" rule for Ide*Service is being applied to an interface plugins implement — which is how the two override breaks got through; the doc's own impact-check step against plugin-examples is the one that would have caught them. And there are no tests: the ConfigFieldSpec (226) and SystemPromptRequest (586) constructors have real requireNonNull/null-coalesce/defensive-copy branches at 0% coverage, next to existing IdeFileServiceTest/IdeProjectServiceTest covering exactly this shape.

Findings 1-4 are shape changes worth settling before this ABI is frozen; 5-7 are small and mechanical; 8 is the checklist.

jatezzz added a commit that referenced this pull request Aug 12, 2026
Review on #1660: capability interfaces replace the flag/method pairs that could disagree, tool results get a return path, describe-only config specs drop ToolDefinition and ToolCallRequest turn immutable, and :plugin-api's tests compile for the first time.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
docs/plugin-api.md (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the two streaming callback interfaces to this list.

A backend receives StreamCallback and ToolStreamCallback and must call them, so both are part of the contract a backend binds to. The bullet lists the backend interfaces and the value types but omits the callbacks.

📝 Proposed wording change
-  - Cross-plugin service interfaces, where **one plugin implements what another calls** (via `SharedServices`): `LlmInferenceService` — implemented by ai-core, called by every AI plugin — together with the types nested in it that a *backend* plugin implements (`LlmBackend`, `HistoryCapableBackend`, `ToolCallingBackend`, `CancellableBackend`, `ConfigurableBackend`) and the value types either side constructs (`ChatMessage`, `LlmConfig`, `LlmResponse`, `SystemPromptRequest`, `ToolDefinition`, `ToolCallRequest`).
+  - Cross-plugin service interfaces, where **one plugin implements what another calls** (via `SharedServices`): `LlmInferenceService` — implemented by ai-core, called by every AI plugin — together with the types nested in it that a *backend* plugin implements (`LlmBackend`, `HistoryCapableBackend`, `ToolCallingBackend`, `CancellableBackend`, `ConfigurableBackend`), the callbacks a backend invokes (`StreamCallback`, `ToolStreamCallback`), and the value types either side constructs (`ChatMessage`, `LlmConfig`, `LlmResponse`, `SystemPromptRequest`, `ToolDefinition`, `ToolCallRequest`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plugin-api.md` at line 15, Update the cross-plugin service interfaces
list in the documentation to include both StreamCallback and ToolStreamCallback
alongside the existing backend interfaces and value types, reflecting that
backends receive and invoke these callbacks.
plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java (2)

31-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the try/fail/catch blocks with assertThrows.

Use JUnit 4.13.2’s org.junit.Assert.assertThrows; capture its returned exception where the test checks the message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`
around lines 31 - 59, Update the three tests—chatMessageRejectsANullRole,
chatMessageRejectsAToolRoleWithoutCorrelators, and
llmConfigRejectsAMissingBackendId—to use JUnit 4.13.2’s
org.junit.Assert.assertThrows instead of try/fail/catch blocks. Capture the
returned IllegalArgumentException in the tests that verify error messages,
preserving the existing exception types and message assertions.

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Truth assertions, but retain JUnit 4.

Because :plugin-api uses JUnit 4.13.2 and has no JUnit Platform setup, do not migrate this file to Jupiter. Replace org.junit.Assert assertions with Truth and add libs.tests.google.truth if required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`
around lines 1 - 19, Update LlmInferenceServiceTest to retain JUnit 4 while
replacing all org.junit.Assert usages with Google Truth assertions, including
adapting assertion forms as needed for Truth’s API. Add the
libs.tests.google.truth dependency to the plugin-api test configuration if it is
not already available.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugin-api/api/plugin-api.api`:
- Around line 1586-1588: Revert the public field declarations for
ToolCallRequest and ToolDefinition in LlmInferenceService so args, callId, name,
description, and parametersSchema remain non-final and mutable. Regenerate the
plugin-api API dump to match these declarations; defer any immutability change
to a separately scoped breaking change with a breaking changelog entry.

In
`@plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java`:
- Around line 585-660: Restore binary-compatible mutability in ToolCallRequest
and ToolDefinition by removing final from all six public fields, removing
defensive map copies and the Objects.requireNonNull checks for callId, name, and
description, and preserving the ownership/mutation warnings in their Javadocs;
update plugin-api/api/plugin-api.api entries at 1586-1599 to public field,
remove only the immutability and null-rejection tests in
plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java:127-200
while retaining SystemPromptRequest and ChatMessage coverage, and track any
future immutability change separately with a breaking row in
docs/PLUGIN_API_CHANGELOG.md.
- Around line 242-253: Update PluginFragmentFactory to validate each
ConfigurableBackend settings fragment before instantiation: require a Fragment
subclass, load it with the selected plugin classloader, and require a public
no-argument constructor. Reject invalid classes before creation while preserving
the host’s existing instantiation-failure handling.

---

Nitpick comments:
In `@docs/plugin-api.md`:
- Line 15: Update the cross-plugin service interfaces list in the documentation
to include both StreamCallback and ToolStreamCallback alongside the existing
backend interfaces and value types, reflecting that backends receive and invoke
these callbacks.

In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`:
- Around line 31-59: Update the three tests—chatMessageRejectsANullRole,
chatMessageRejectsAToolRoleWithoutCorrelators, and
llmConfigRejectsAMissingBackendId—to use JUnit 4.13.2’s
org.junit.Assert.assertThrows instead of try/fail/catch blocks. Capture the
returned IllegalArgumentException in the tests that verify error messages,
preserving the existing exception types and message assertions.
- Around line 1-19: Update LlmInferenceServiceTest to retain JUnit 4 while
replacing all org.junit.Assert usages with Google Truth assertions, including
adapting assertion forms as needed for Truth’s API. Add the
libs.tests.google.truth dependency to the plugin-api test configuration if it is
not already available.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 31a50219-7927-4fcb-836b-23af599d562e

📥 Commits

Reviewing files that changed from the base of the PR and between cbdf3b3 and e724de2.

📒 Files selected for processing (8)
  • docs/plugin-api.md
  • plugin-api/api/plugin-api.api
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeFileServiceTest.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeProjectServiceTest.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeResourceServiceTest.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java
  • plugin-api/src/test/kotlin/com/itsaky/androidide/plugins/PluginContextTest.kt
💤 Files with no reviewable changes (3)
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeResourceServiceTest.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeFileServiceTest.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/IdeProjectServiceTest.java

Comment thread plugin-api/api/plugin-api.api Outdated
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Please update the PLUGIN_API_CHANGELOG.md file

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java (2)

141-159: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test every null precondition of ChatMessage.toolResult.

The test covers a missing toolCallId only. The production contract also rejects null toolName and null content in plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java, Lines 165-236. Add separate assertions for both inputs.

As per coding guidelines: "Use unit tests for non-UI logic, cover error and edge paths, and target at least 50% line and branch coverage for new or changed non-UI code."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`
around lines 141 - 159, Extend the ChatMessage.toolResult null-precondition
tests by adding separate assertions that null toolName and null content each
throw NullPointerException, alongside the existing toolCallId test. Keep the
valid toolResult assertions in toolResultCarriesBothCorrelators unchanged.

Source: Coding guidelines


1-20: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Configure plugin-api for JUnit Jupiter and Truth.

plugin-api currently declares only JUnit 4.13.2 and does not enable the Jupiter platform. Add the existing Jupiter, platform launcher, and Truth dependencies, configure the test task, and migrate LlmInferenceServiceTest.java.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`
around lines 1 - 20, Configure the plugin-api test setup to use JUnit Jupiter
and Truth by adding the existing Jupiter, platform launcher, and Truth
dependencies, enabling the Jupiter platform on the test task, and migrating
LlmInferenceServiceTest from JUnit 4 assertions and annotations to the
corresponding Jupiter and Truth APIs.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/PLUGIN_API_CHANGELOG.md`:
- Around line 55-62: Update the “Tool results correlated by call id” changelog
entry to describe correlation using both toolCallId and toolName, reflecting
that providers may key results by either value. Revise the heading and
explanatory prose while preserving the existing API symbols and Kotlin Role.TOOL
guidance.

---

Outside diff comments:
In
`@plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java`:
- Around line 141-159: Extend the ChatMessage.toolResult null-precondition tests
by adding separate assertions that null toolName and null content each throw
NullPointerException, alongside the existing toolCallId test. Keep the valid
toolResult assertions in toolResultCarriesBothCorrelators unchanged.
- Around line 1-20: Configure the plugin-api test setup to use JUnit Jupiter and
Truth by adding the existing Jupiter, platform launcher, and Truth dependencies,
enabling the Jupiter platform on the test task, and migrating
LlmInferenceServiceTest from JUnit 4 assertions and annotations to the
corresponding Jupiter and Truth APIs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cfeaa78e-728b-497f-8a0f-315d30a2bf94

📥 Commits

Reviewing files that changed from the base of the PR and between e724de2 and fa0fa38.

📒 Files selected for processing (4)
  • docs/PLUGIN_API_CHANGELOG.md
  • plugin-api/api/plugin-api.api
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java
  • plugin-api/src/test/java/com/itsaky/androidide/plugins/services/LlmInferenceServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin-api/api/plugin-api.api
  • plugin-api/src/main/java/com/itsaky/androidide/plugins/services/LlmInferenceService.java

Comment thread docs/PLUGIN_API_CHANGELOG.md Outdated
…alling

An LlmBackend could only stream a single prompt, so anything model-specific --
prompt wording, temperature, settings UI -- had to be guessed by the consumer or
read out of another plugin's preferences. Add, all as defaults so existing
backends are untouched:

- generateStreamingWithHistory / generateStreamingWithTools, degrading to plain
  streaming rather than failing
- getSystemPrompt(SystemPromptRequest) and getDefaultTemperature: the consumer
  supplies the tool contract, the backend supplies the wording
- getConfigSpecs (ConfigFieldSpec / ConfigFieldType) or getSettingsFragmentClassName
  for a backend that owns its own settings screen
- CancellableBackend for stopping an in-flight stream
- LlmInferenceService.getPreferredBackendId, so a costly backend can tell whether
  it is the one about to be used
…dels

getDefaultTemperature() returns a boxed Float where LlmConfig.temperature is primitive, so the obvious assignment unboxes null and throws, now annotated and stated in the Javadoc, along with the four other unannotated members.
Review on #1660: capability interfaces replace the flag/method pairs that could disagree, tool results get a return path, describe-only config specs drop ToolDefinition and ToolCallRequest turn immutable, and :plugin-api's tests compile for the first time.
The last commit tightened ToolCallRequest and ToolDefinition to final fields
with defensive copies. Both shipped non-final in 26.28, so that turned an
additive PR into an ABI break: an already-built .cgp assigning one of those
fields throws IllegalAccessError on putfield, and the null-checks and
unmodifiable maps change behaviour for callers that were within contract
before. Reverted to the published shape; the hazard the tightening was aimed
at is now an ownership rule in the Javadoc instead -- who owns an instance,
and that SystemPromptRequest copies only the list spine.

Also adds the 26.33 changelog entry the additions were missing, so a plugin
author has a min_ide_version to floor at, including the note that Role.TOOL
can break an exhaustive Kotlin `when`.
@jatezzz
jatezzz force-pushed the feat/ADFA-5095-llm-backend-capabilities branch from a9119cf to eadaabb Compare August 13, 2026 13:31
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Two compatibility breaks that CI can't see

Flagging these because the PR description says "All changes are additive and binary-compatible with existing AI plugins", and both of these falsify it against the plugins we actually ship. :plugin-api:apiCheck passes with a zero-byte ABI diff in both cases, so green CI isn't evidence here.

1. Role.TOOL crashes already-installed backends

Adding a fourth constant to Role (LlmInferenceService.java:234) breaks exhaustive when expressions in backends compiled against the 3-constant enum. Both shipped backends have one, with no else:

  • ai-core/.../LocalLlmBackend.kt:312-315 - val role = when (msg.role) { ... }
  • ai-core/.../GeminiBackend.kt:289-292

Credit where due - the changelog (line 63) already flags this: "Adding Role.TOOL can break an exhaustive Kotlin when over Role - add an else branch or handle it." Two things undercut that mitigation, though:

The advice only helps someone who recompiles. Plugins ship as prebuilt .cgp and are never recompiled when the IDE updates, so the compile error ('when' expression must be exhaustive) is not the failure mode users hit. The runtime one is: a when compiled against the 3-constant enum and run against the 4-constant one throws kotlin.NoWhenBranchMatchedException with a null message. The crash report never mentions Role or TOOL, so it lands as an unattributable crash inside a third-party plugin.

The follow-on sentence is false across the plugin boundary. "Nothing produces a TOOL message unless the plugin calls toolResult itself" holds within one plugin, but cross-plugin is the whole point of this API. The real path: consumer ai-assistant calls toolResult, appends it to history, hands that history to backend ai-core, which crashes. AgentLoop.kt:148 packs tool output into a Role.USER message today, so it's latent - ADFA-5096 is the change that flips it to Role.TOOL, and this goes live the moment that lands.

Given both, the row being filed as added under a legend reading "safe to adopt" seems like the wrong classification.

2. The nullability sweep source-breaks callers, not just implementors

@Nullable on LlmResponse.text / .error, ToolCallRequest.args and ToolDefinition.parametersSchema (LlmInferenceService.java:457) converts them from Kotlin platform types (String!) to nullable types (String?). That breaks every Kotlin caller, not only implementors - a much wider blast radius than the @NonNull-on-LlmBackend change.

Concretely, in ai-assistant, inside override fun onComplete(response: LlmInferenceService.LlmResponse) (ChatViewModel.kt:869):

// ChatViewModel.kt:879
"chars=${response.text.length} ..."

now fails with only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'. Same at :880, :882, :906, and at :872 / :901 / :929 depending on the deferred's type argument. request.args["path"] breaks identically.

Worth noting docs/plugin-api.md:44 - added by this PR - already lists "Types & nullability. Flipping nullable<->non-null" as breaking. The changelog files all four rows as added ("safe to adopt") and keeps the standing "Every change so far is additive - no capability has been removed or had its signature broken since the plugin system shipped" claim at lines 29-31.

Why neither shows up in CI

binary-compatibility-validator doesn't record nullability, so apiCheck (debug.yml:198) can't catch #2, and no CI job builds any example plugin - so there's no downstream compile to catch either one. :plugin-api is also excluded from the v7/v8 flavors (AndroidModuleConf.kt:232), so it has no testV8DebugUnitTest, which is the only unit-test task name any CI path reaches. That's the same gap that let both plugin-api test source sets sit uncompilable on stage for 5+ weeks before this PR repaired them.

Suggested resolution

  • Role.TOOL: either coordinate the ai-core update with this landing and bump min_ide_version on both, or avoid widening the enum - carry tool results in a separate type so existing whens stay exhaustive.
  • Nullability: keeping the annotations is the right call long-term, but it should ship as a breaking row with the ChatViewModel call sites fixed in the same release, not as added.
  • Either way, the "additive and binary-compatible" line in the PR description and the blanket claim at changelog lines 29-31 need to come out.

From a code review pass over the branch; findings verified by compiling and running against the shipped example plugins rather than read off the diff.

Both need a source change in plugin repos, so drop the blanket "every change is additive" claim, define the `breaking` legend entry, and document the previously unlisted nullability change.
@jatezzz

jatezzz commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch on both.

The changelog now files Role.TOOL and the nullability sweep as breaking, drops the blanket "every change is additive" claim, and adds a row for the nullability change that was undocumented. Worth noting the ai-core coordination you suggested already landed in plugin-examples (37c4398, 1a3b533), and the paths shifted since then, the backends now live in ai-agent-local/ai-agent-gemini, with a third one in ai-agent-openai. Still open: the PR description line, the min_ide_version bumps, and the CI gap you describe.

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.

3 participants