Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego

| Plugin | Purpose |
| -------------------------------------------------- | ----------------------------------------------------------------- |
| [`plugins/AI-Agent-Claude/`](plugins/AI-Agent-Claude/) | Adds Anthropic's Claude as an inference backend for AI Core. |
| [`plugins/AI-Agent-Gemini/`](plugins/AI-Agent-Gemini/) | Adds Google Gemini as an inference backend for AI Core. |
| [`plugins/AI-Agent-Local/`](plugins/AI-Agent-Local/) | Runs GGUF models on the device itself, with no network access. |
| [`plugins/AI-Agent-MCP/`](plugins/AI-Agent-MCP/) | Gives the AI Core agent the tools of Model Context Protocol servers. |
Expand Down
3 changes: 3 additions & 0 deletions plugins/AI-Agent-Claude/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/.cxx/
build-output.log
**/.kotlin/
143 changes: 143 additions & 0 deletions plugins/AI-Agent-Claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# AI Agent Claude plugin for CodeOnTheGo

Claude inference for CodeOnTheGo's AI plugins. Registers itself as the `claude`
backend with [`ai-core`](../AI-Core/)'s `LlmInferenceService`, which is what
`ai-core`'s Agent chat, `Code-Suggestions` and `Speech-to-Text` talk to.

It speaks Anthropic's Messages API, `POST https://api.anthropic.com/v1/messages`,
with the agent's tools declared natively (`tools[]` out, `tool_use` blocks in).
The endpoint is fixed: unlike the OpenAI backend there is no family of
compatible servers to point it at.

Calls the API directly over `HttpURLConnection` rather than Anthropic's Java
SDK: plugins run in the host IDE's classloader, which resolves `okhttp3` to the
host's older OkHttp first, and an SDK bundling its own copy crashes generation
with a `NoSuchMethodError`.

**No embeddings.** Anthropic has no embeddings endpoint, so this backend does not
implement `EmbeddingBackend` and `Vector-Search` needs `ai-agent-openai` or
`ai-agent-gemini` for indexing.

## Building

Prerequisites: Android SDK (API 33+), JDK 17. Create `local.properties` with
`sdk.dir=...`. This plugin uses the shared wrapper at the repo root:

```bash
cd plugins/AI-Agent-Claude
../../gradlew assemblePlugin # release -> build/plugin/ai-agent-claude.cgp
../../gradlew assemblePluginDebug # debug variant
../../gradlew testDebugUnitTest # the JVM unit tests
```

## Configuration

Everything is configured in **AI Core → Agent settings**, on the pane this plugin
contributes: API key, model, and one **Test Connection & List Models** button.
Nothing outside this plugin handles the key. Listing models and testing the key
are the same `GET /v1/models`, so they are one control, and the model is a
single editable dropdown: type any id, or pick one the key can use.

## Request shape

`ClaudeRequestBuilder` owns it. Most rules below exist because the alternative is
a 400:

- **History is text.** AI Core hands tool results back as user text and never the
assistant `tool_use` block they answer, so they are sent as user text rather
than `tool_result` blocks, which the API rejects without a matching call.
Consecutive same-role turns are merged and blank turns dropped; `SYSTEM` turns
join the top-level `system`. Because no thinking block is ever replayed, the
preserved-thinking history check never applies.
- **Two kinds of turn.** A streamed turn is the agent's: it asks for
`thinking: {type: "adaptive"}` where the model takes it (on Opus 4.x and
Sonnet 4.6 an omitted `thinking` means none), effort `high`, and a 64K
budget, since thinking counts against `max_tokens`. A turn that is not
streamed is a small job (a chat title, an inline suggestion): no `thinking`,
effort `low`, and the caller's own budget, raised to 4K only on the 5.x models,
which think whether asked to or not.
- **Never `temperature`:** it is a 400 on current Opus and Sonnet models.
- **Per-model fields** come from `ClaudeModelTraits`, which reads what the live
catalog said each model accepts: output cap, adaptive thinking, effort. The
settings pane stores that with the model list. For a model the catalog has not
described, allow-lists decide, so an unknown model gets a bare request rather
than a field it may reject. `fallbacks: "default"` with the
`server-side-fallback-2026-07-01` beta goes only to the models whose safety
classifiers can decline a request (Fable 5.1, Opus 5.5, Opus 5, Sonnet 5.5).
- **Aliases and dated ids are the same model.** The catalog lists some models
only by dated id (`claude-haiku-4-5-20251001`), so a saved alias is matched to
it rather than retired.
- **`required_tool` is ignored.** Forced `tool_choice` is a 400 on current
models, and the contract lets a backend that cannot force a call ignore it.
- **Tools are not `strict`.** Strict mode needs closed schemas, which contributed
MCP schemas rarely are.
- **Top-level `cache_control`** caches the system prompt and tool list an agent
run re-sends every turn.

## Failures

- **`stop_reason: "refusal"`** is checked before anything else: the turn is
reported as declined, and a tool call inside it is not run.
- **A server-side fallback** drops any tool call the declined model began, since
the model that takes over never saw it. A tool call cut off by `max_tokens`
is dropped too, even when its input happens to parse.
- **Stop** cancels the turn and closes its socket at once, so the API stops
generating rather than running on until its next line.
- **A stream that goes silent** past the 120 s read timeout is reported as
Claude stopping mid-reply, not as a lost connection.
- **Overload and rate limits** (408, 409, 429, 500, 502, 503, 504, 529) are
retried twice with backoff,
honouring `retry-after` up to 10 s, but only before a stream has delivered
anything, so a retry never repeats text on screen (`TransientRetry`).
- An `error` event inside a 200 stream is raised as the HTTP status its type
stands for, so it classifies and retries like the same failure on the status
line.
- `ClaudeErrorFormatter` turns every failure into one translated sentence; a raw
JSON body never reaches the transcript. An empty credit balance arrives as a
400 whose message names it, and is reported as billing, not as a bad request.

## API key handling

Stored encrypted with the IDE's `KeystoreSecretStore` and sent as an `x-api-key`
**header**, never in a URL. `security/SecureApiKeyStore.kt` holds only this
plugin's Keystore alias (`cotg_ai_claude_key_v1`), unique so that another plugin's
invalidated-key recovery cannot delete this one's key. A key is checked against
`/v1/models` before it is saved; one that cannot be checked because the device is
offline can be saved anyway and is marked unverified.

**Keys outside a workspace.** Such a key is refused with a 400 on every request
until it carries an `anthropic-workspace-id` header. The plugin cannot look the
id up (listing workspaces needs an admin key), so when the key check reports
this, the pane shows a **Workspace ID** field and says where to find the id
(the Claude Console's **Workspaces** page; ids start with `wrkspc_`); the id is saved with the key,
sent on every request, and removed with the key. A key in a workspace never sees
the field. Typed ids are checked by `WorkspaceIds` before they become a header:
one printable token, no whitespace or line breaks.

## Installation

Install **`ai-core` as well**; without the router this plugin has nothing to
register with. Order does not matter: this plugin re-registers when it sees
ai-core activate. Copy `build/plugin/ai-agent-claude.cgp` to the device, install
via CodeOnTheGo's Plugin Manager, then restart the IDE.

## Key classes

- `plugin/ClaudePlugin.kt` — entry point; registers the backend with ai-core
- `backend/ClaudeBackend.kt` — the conversation: streaming, retries, empty-reply diagnosis
- `backend/ClaudeRequestBuilder.kt` — `messages[]` mapping and request JSON (pure)
- `backend/ClaudeModelTraits.kt` — which optional fields each model accepts (pure)
- `backend/ClaudeStreamEvent.kt` — one line of the event stream (pure)
- `backend/TurnAssembler.kt` — folds a stream into a turn and decides how it ends (pure)
- `backend/ClaudeToolProtocol.kt` — `tools[]` declaration and `tool_use` accumulation (pure)
- `backend/TransientRetry.kt` — the retry schedule (pure)
- `backend/ClaudeModelCatalog.kt` — reads `GET /v1/models` (pure)
- `backend/ClaudeHttpClient.kt` — sockets, headers and timeouts
- `errors/ClaudeErrorFormatter.kt` — turns a failure into one translated sentence
- `prompt/ClaudeSystemPrompt.kt` — the system prompt this cloud model is given
- `settings/` — the pane this backend contributes to the selector
- `logging/` — `LOG_PREFIX` (`AiAgentClaude`), prefixing every logcat tag

## License

GPL-3.0 — same as AndroidIDE / CodeOnTheGo.
15 changes: 15 additions & 0 deletions plugins/AI-Agent-Claude/addon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"summary": "Use Anthropic's Claude as the AI backend for the Agent. Requires the AI Core addon and a Claude API key.",
"description": "Connects the AI Core Agent to Anthropic's Claude API with your own API key, billed to your Claude Console account. The Agent's tools are declared to Claude directly, so it reads and edits your project through structured calls. Prompts and any file contents the Agent sends go to Anthropic.",
"tags": [
"ai",
"agent",
"claude"
],
"origin": "appdevforall",
"license": "AGPL-3.0-or-later",
"author": {
"name": "App Dev For All",
"url": "https://www.appdevforall.org"
}
}
91 changes: 91 additions & 0 deletions plugins/AI-Agent-Claude/ai-agent-claude.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Agent Claude Plugin</title>
<style>
body {
background: #ffffff;
color: #000000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 1.25rem;
max-width: 820px;
}
h1 { font-size: 1.6rem; margin: 0 0 0.75rem; }
h2 { font-size: 1.2rem; margin: 1.75rem 0 0.5rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.2rem; }
code {
background: #f4f4f4;
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 0.9em;
}
ul { padding-left: 1.25rem; }
li { margin: 0.3rem 0; }
table { border-collapse: collapse; margin: 0.5rem 0; width: 100%; }
th, td { border: 1px solid #ddd; padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; }
th { background: #f4f4f4; }
.note {
border-left: 3px solid #999;
padding: 0.25rem 0.75rem;
margin: 1rem 0;
background: #fafafa;
}
</style>
</head>
<body>
<h1>AI Agent Claude Plugin</h1>

<h2>Executive overview</h2>
<p><b>AI Agent Claude</b> adds Anthropic's Claude as an inference backend for
<b>AI Core</b>. With a Claude API key, the Agent chat, Code Suggestions and
Speech to Text can run on Claude Opus, Sonnet or Haiku, with the Agent's tools
declared natively so it edits projects through structured calls.</p>

<h2>Core functionality</h2>
<ul>
<li>Streaming replies over Anthropic's Messages API.</li>
<li>Multi-turn conversations and native tool calling for the Agent.</li>
<li>Stop: an in-flight reply is cancelled and its connection closed.</li>
<li>A settings pane with key entry, Keystore-encrypted storage, a model
picker filled from the API, and a connection test.</li>
<li>Automatic retry when Claude is briefly overloaded or rate-limited.</li>
<li>Server-side fallback when a safety check declines a request on the
models that support it.</li>
</ul>

<h2>Technical architecture</h2>
<ul>
<li>Raw <code>HttpURLConnection</code> and <code>org.json</code>, no SDK: the
host loads plugin classes parent-first, and a bundled OkHttp would collide
with the host's.</li>
<li>Pure, unit-tested units for the request shape, the event stream, tool
calls, per-model parameters, retries and error classification.</li>
<li>The API key is encrypted with the IDE's <code>KeystoreSecretStore</code>
under this plugin's own alias.</li>
</ul>

<h2>Usage</h2>
<ol>
<li>Install <b>AI Core</b> and this plugin, then restart the IDE.</li>
<li>Open <b>AI Core → Agent settings</b> and select <b>Claude</b>.</li>
<li>Tap <b>Get API Key</b>, create a key in the Claude Console, paste it and
tap <b>Save Key</b>.</li>
<li>Optionally pick a model, then start a chat.</li>
</ol>

<h2>Cost</h2>
<p>The Claude API is billed to prepaid credit, separate from a Claude.ai
subscription. The free alternatives are <b>AI Agent Local</b> and <b>AI Agent
Gemini</b>.</p>

<h2>Limitations</h2>
<ul>
<li>No embeddings, so it cannot power Vector Search.</li>
<li>No web search yet.</li>
</ul>
</body>
</html>
80 changes: 80 additions & 0 deletions plugins/AI-Agent-Claude/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
plugins {
id("com.android.application")
id("com.itsaky.androidide.plugins.build")
}

pluginBuilder {
pluginName = "ai-agent-claude"
}

android {
namespace = "com.itsaky.androidide.plugins.aiagentclaude"
compileSdk = 36

defaultConfig {
applicationId = "com.itsaky.androidide.plugins.aiagentclaude"
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
}

buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("debug")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/INDEX.LIST"
)
}
}
}

dependencies {
compileOnly(files("../../libs/plugin-api.jar"))

// 'implementation' (not 'compileOnly') for the androidx/Material libraries: AAPT2 needs them
// at compile time to process the settings pane's layout, as in every CoGo plugin with XML.
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("com.google.android.material:material:1.10.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

testImplementation(files("../../libs/plugin-api.jar"))
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.json:json:20231013")
}

// No SecureApiKeyStore parity check any more: the AES/GCM core is the host's KeystoreSecretStore
// (plugin-api), so there is one implementation in the process rather than copies to keep in step.

// AAR metadata checks are disabled by convention for these application-as-library plugins.
tasks.matching {
it.name.contains("checkDebugAarMetadata") ||
it.name.contains("checkReleaseAarMetadata")
}.configureEach { enabled = false }
10 changes: 10 additions & 0 deletions plugins/AI-Agent-Claude/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
android.enableJetifier=false
android.jetifier.ignorelist=common-30.2.2.jar
android.nonTransitiveRClass=false
android.useAndroidX=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096M -Dkotlin.daemon.jvm.options\="-Xmx4096M"
org.gradle.parallel=true

kotlin.code.style=official
21 changes: 21 additions & 0 deletions plugins/AI-Agent-Claude/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# AI Agent Claude Plugin ProGuard Rules

# Keep plugin entry point
-keep public class com.itsaky.androidide.plugins.aiagentclaude.plugin.ClaudePlugin {
public <methods>;
}

# Keep the backend: its settings pane resolves it through ClaudePlugin.getBackend() to list
# models and test a connection, and AI Core reaches it across the plugin classloader boundary.
-keep public class com.itsaky.androidide.plugins.aiagentclaude.backend.ClaudeBackend {
public <methods>;
}

# Keep the settings pane: it is named to the host as a string by
# ClaudeBackend.getSettingsFragmentClassName() and instantiated reflectively.
-keep public class com.itsaky.androidide.plugins.aiagentclaude.settings.ClaudeSettingsFragment {
public <methods>;
}

# Keep plugin-api interfaces
-keep interface com.itsaky.androidide.plugins.** { *; }
Loading