Skip to content
Open
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 @@ -25,6 +25,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`ai-core/`](ai-core/) | The **Agent** chat (tool-calling assistant) plus the shared LLM inference **router** other plugins consume. Ships no model — install at least one backend plugin below. Mandatory for every AI feature. |
| [`ai-agent-local/`](ai-agent-local/) | On-device `.gguf` inference backend for `ai-core` (bundled llama.cpp AAR). Registers as `local`; needs no network. |
| [`ai-agent-gemini/`](ai-agent-gemini/) | Google Gemini API inference backend for `ai-core`. Registers as `gemini`; needs an API key and network access. |
| [`ai-agent-openai/`](ai-agent-openai/) | OpenAI-compatible inference backend for `ai-core`. Registers as `openai`; talks to OpenAI by default, or to Ollama / LM Studio / OpenRouter / `llama-server` by changing one URL. |
| [`flutter-template/`](flutter-template/) | Adds Flutter starter project templates (Basic, BLoC, Provider, GetX, Riverpod) to the New Project screen. |
| [`code-suggestions-plugin/`](code-suggestions-plugin/) | Inline ghost-text code completions powered by AI. |
| [`speech-to-text-plugin/`](speech-to-text-plugin/) | Voice-to-code: converts speech to code with AI generation. |
Expand Down
3 changes: 3 additions & 0 deletions ai-agent-openai/.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 ai-agent-openai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# AI Agent OpenAI plugin for CodeOnTheGo

OpenAI-compatible inference for CodeOnTheGo's AI plugins. Registers itself as the
`openai` backend with [`ai-core`](../ai-core/)'s `LlmInferenceService`, which is
what `ai-core`'s Agent chat, `code-suggestions-plugin`, `speech-to-text-plugin`
and `vector-search-plugin` actually talk to.

**One backend, many servers.** It speaks `POST {baseUrl}/chat/completions`, and
the base URL is a setting. Across OpenAI, Ollama, LM Studio, OpenRouter and
llama.cpp's `llama-server` the auth header, request JSON, SSE framing and error
shape are identical — only the host changes. So this is one backend with a URL
field rather than one plugin per provider:

| Base URL | What it is |
|---|---|
| `https://api.openai.com/v1` | **Default.** OpenAI itself. |
| `http://localhost:11434/v1` | Ollama on the device (e.g. in the bundled Termux). |
| `http://192.168.1.50:11434/v1` | Ollama on the user's PC, over Wi-Fi. |
| `http://192.168.1.50:1234/v1` | LM Studio's server. |
| `http://localhost:8080/v1` | `llama-server` from llama.cpp. |
| `https://openrouter.ai/api/v1` | OpenRouter — many models behind one key, some free. |

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

## 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 ai-agent-openai
../gradlew assemblePlugin # release -> build/plugin/ai-agent-openai.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: server URL (with presets), API key, model, and one **Test Connection
& List Models** button. Nothing outside this plugin handles the key.

The pane adapts to the chosen server as it is picked, via
`BaseUrlPolicy.keyRequirement()`: `REQUIRED` for OpenAI's own host, `EXPECTED` for
another cloud provider, `NOT_NEEDED` for loopback or a private address — where the
whole key section is hidden rather than showing an empty, mandatory-looking field
for a server that wants no credential. Listing models and testing the connection
are the same `GET {baseUrl}/models`, so they are one control, and the model is a
single editable dropdown rather than a field beside a spinner.

Three rules that each break a real user if got wrong, and are covered by tests:

- **The API key is optional.** `isAvailable()` requires a key only when the base
URL is OpenAI's own host. For any other server a non-blank URL is enough —
local Ollama and LM Studio need no credential, and demanding one would leave
the backend permanently "not available" for exactly the users who wanted a
custom server.
- **The model is a field, not a constant.** Pointed at a local server the model
is whatever the user pulled (`qwen2.5-coder`, `llama3.2`), so free-text entry
always works and `GET /v1/models` is treated as optional — plenty of compatible
servers do not implement it, which is why a 404 there reports "check the URL"
rather than rejecting the key.
- **No auto-discovery.** There is no probing of `localhost:11434`; a background
port scan is not something the user asked for. The URL field already reaches
any server, on-device or on the LAN.

### Reasoning models

`gpt-5.x` and the `o` series reject `max_tokens` in favour of
`max_completion_tokens`, and several reject `temperature`. `RequestTuning` picks
the parameters from the model id and the server, and `UnsupportedParameter` reads
the offending name out of a 400 so the request is retried once without it —
compatible servers vary too much to hardcode a matrix.

### Cleartext URLs

`https` is required except for loopback and private ranges (RFC 1918, link-local,
IPv6 ULA, and bare LAN hostnames), where plain `http` is accepted and warned about
once on save. That is the "Ollama on my PC" case, and the host IDE's
`network_security_config` permits cleartext, so it works at runtime.

## API key handling

Stored encrypted (AES/GCM under a hardware-backed Android Keystore secret) and
sent as an `Authorization: Bearer` **header**, never in a URL query string. With
no key configured, no header is sent at all.

`security/SecureApiKeyStore.kt` is this plugin's **own copy**, under its own
Keystore alias (`cotg_ai_openai_key_v1`). It is deliberately not shared with
ai-agent-gemini's copy: every plugin runs in the host app's process and UID and
therefore shares one Keystore, so a shared alias would let one plugin's
invalidated-key recovery (`deleteEntry`) destroy the other backend's stored key.
The two never read each other's ciphertext, so they have no reason to share an
alias — and there is therefore nothing to keep in parity. Extracting the shared
*source* is tracked separately.

## 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-openai.cgp` to the device, install
via CodeOnTheGo's Plugin Manager, then restart the IDE.

## Native function calling

Not implemented, deliberately. This backend declares `HistoryCapableBackend` but
not `ToolCallingBackend`, so ai-core streams it the whole conversation and the
agent loop drives tools through a text envelope in the system prompt, which is
provider-agnostic. Declaring `ToolCallingBackend` without native function calling
would leave the caller waiting on a call this backend never makes. The system
prompt also tells the model not to use its own function-calling channel, since
nothing reads it.

## Key classes

Every source file sits in a package named for its layer; nothing is loose at the
root of `com/itsaky/androidide/plugins/aiagentopenai/`.

- `plugin/OpenAiPlugin.kt` — plugin entry point; registers the backend with ai-core
- `backend/OpenAiBackend.kt` — the HTTP transport, SSE streaming and model catalog
- `backend/OpenAiRequestBuilder.kt` — `messages[]` mapping and request JSON (pure)
- `backend/RequestTuning.kt` — reasoning-model parameters and the 400-retry rule (pure)
- `backend/SseChunk.kt` — one line of the token stream (pure)
- `backend/ChatModelFilter.kt` — keeps non-chat models out of the picker (pure)
- `errors/OpenAiErrorFormatter.kt` — turns a failure into one translated sentence
- `security/SecureApiKeyStore.kt` — AES/GCM at rest
- `preferences/OpenAiPreferences.kt` — this plugin's settings store
- `prompt/OpenAiSystemPrompt.kt` — the system prompt this cloud model is given
- `settings/BaseUrlPolicy.kt` — URL normalization and the cleartext rule (pure)
- `settings/ServerPreset.kt` — the one-tap server list
- `settings/ConnectionVerification.kt` — what a live check established (pure)
- `settings/` — the pane this backend contributes to the selector
- `logging/` — `LOG_PREFIX` (`AiAgentOpenAi`), prefixing every logcat tag

The pure units carry the logic that would otherwise only fail on a device; they
are covered by 118 JVM tests.

## License

GPL-3.0 — same as AndroidIDE / CodeOnTheGo.
170 changes: 170 additions & 0 deletions ai-agent-openai/ai-agent-openai.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Agent OpenAI 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 OpenAI Plugin</h1>

<h2>Executive overview</h2>
<p><b>AI Agent OpenAI</b> adds <b>OpenAI</b>'s models to CodeOnTheGo's AI
features — and, through one editable server URL, any other server that speaks
the same protocol. It is a <b>headless</b> plugin with no screens of its own
beyond a settings pane: it registers itself as the <code>openai</code> inference
backend with <b>AI Core</b>, which routes requests from the Agent chat,
<b>Code Suggestions</b>, <b>Speech to Text</b> and <b>Vector Search</b>.</p>
<p>Install <b>AI Core</b> alongside it — without the router this plugin has
nothing to register with. Install order does not matter.</p>
<p>Using this backend sends prompts, and any file contents a plugin includes in
them, to whichever server is configured. Pointed at OpenAI that is a third
party; pointed at a machine on your own network it never leaves that network.</p>

<h2>One backend, many servers</h2>
<p>Across every server below the auth header, request JSON, streaming format
and error shape are identical — only the host changes. So this is one backend
with a URL field rather than one plugin per provider.</p>
<table>
<tr><th>Server URL</th><th>What it is</th></tr>
<tr><td><code>https://api.openai.com/v1</code></td><td><b>Default.</b> OpenAI itself.</td></tr>
<tr><td><code>http://localhost:11434/v1</code></td><td>Ollama running on the device.</td></tr>
<tr><td><code>http://192.168.1.50:11434/v1</code></td><td>Ollama on your own PC, over Wi-Fi.</td></tr>
<tr><td><code>http://192.168.1.50:1234/v1</code></td><td>LM Studio's server.</td></tr>
<tr><td><code>http://localhost:8080/v1</code></td><td><code>llama-server</code> from llama.cpp.</td></tr>
<tr><td><code>https://openrouter.ai/api/v1</code></td><td>OpenRouter — many models behind one key, some free.</td></tr>
</table>
<p>Presets in the settings pane fill these in. The local ones use
<code>localhost</code>; to reach another machine, pick the preset and edit the
host.</p>

<h2>Core functionality</h2>
<ul>
<li><b>Chat completions</b> over <code>POST {baseUrl}/chat/completions</code>,
with text completion, real multi-turn history as a
<code>messages[]</code> array, and server-sent-events streaming.</li>
<li><b>Server-aware key field</b> — the API key section adapts to the chosen
server as you pick it: mandatory for OpenAI, optional for another cloud
provider, and hidden entirely for a local or LAN address that needs no
credential.</li>
<li><b>Free-text or discovered models</b> — one field that is also a list:
type any model name, or pick one the server reported. Non-chat models
(embeddings, audio, images) are filtered out.</li>
<li><b>Test Connection &amp; List Models</b> — one request that both checks the
URL and key without saving either and fills the model list. Distinguishes a
refused key, a wrong path, an unreachable server and a server with no models
loaded.</li>
<li><b>Reasoning-model handling</b> — <code>gpt-5.x</code> and the
<code>o</code> series take <code>max_completion_tokens</code> and may reject
<code>temperature</code>; the right parameters are chosen from the model
name, and a refused parameter is retried once without it.</li>
<li><b>Encrypted at rest</b> — the API key is stored as AES/GCM ciphertext
under a hardware-backed Android Keystore secret.</li>
<li><b>Translated, safe error messages</b> — a failure becomes one
user-facing sentence; the raw HTTP error body stays in the log and never
reaches the chat transcript.</li>
</ul>

<h2>Technical architecture</h2>
<table>
<tr><th>Component</th><th>Role</th></tr>
<tr><td><code>OpenAiPlugin</code></td><td>Plugin entry point. Registers the
backend with AI Core on activation, re-registering if AI Core activates
later; cancels in-flight requests and drops the decrypted key on
dispose.</td></tr>
<tr><td><code>OpenAiBackend</code></td><td>The transport. Calls
<code>chat/completions</code> over <code>HttpURLConnection</code>, parses the
streaming response, and fetches the model catalog.</td></tr>
<tr><td><code>BaseUrlPolicy</code></td><td>Normalizes the server URL — trims a
pasted <code>/chat/completions</code> path, lowercases the host — and
enforces the cleartext rule.</td></tr>
<tr><td><code>RequestTuning</code></td><td>Decides which optional parameters a
request carries, and which one to stop sending after a server refuses
it.</td></tr>
<tr><td><code>OpenAiErrorFormatter</code></td><td>Classifies a failure
(unknown model, rate limit, spent balance, refused key, outage, server not
running) so it can be reported as one translated sentence.</td></tr>
<tr><td><code>SecureApiKeyStore</code></td><td>AES/GCM encryption of the API
key, under this plugin's own Keystore alias.</td></tr>
</table>
<p><b>No third-party HTTP SDK.</b> Plugins run in the host IDE's classloader
where <code>okhttp3</code> resolves to the host's older OkHttp — a mismatch that
crashed generation with a <code>NoSuchMethodError</code> when an SDK bundled its
own copy. <code>HttpURLConnection</code> has no third-party dependency and works
regardless of the host's OkHttp version.</p>

<h2>Usage</h2>
<ol>
<li>Install <b>AI Core</b> and <b>AI Agent OpenAI</b> via the Plugin Manager,
then restart the IDE.</li>
<li>Open <b>Preferences &rarr; Configuration &rarr; Agent</b> and select
<b>OpenAI</b> as the backend.</li>
<li>Set the <b>Server</b>: keep the OpenAI default, or pick a preset and edit
the host to reach your own machine.</li>
<li>Enter an <b>API key</b> if the server needs one — it is checked before it
is saved. For a local server the section is hidden; there is nothing to
enter.</li>
<li>Tap <b>Test Connection &amp; List Models</b>, then set the <b>Model</b> by
picking from that list or typing a name.</li>
</ol>
<div class="note">
The key is sent as an <code>Authorization: Bearer</code> request header, never
in a URL query string — query strings leak into logs, proxies and crash
reports. With no key configured, no header is sent at all.
</div>

<h2>Cost</h2>
<p>OpenAI has <b>no free tier</b>: an API key draws on a prepaid balance and is
separate from a ChatGPT subscription. The free paths are to run a model on your
own computer and point the server URL at it, to use <b>AI Agent Local</b> for
on-device inference, to use <b>AI Agent Gemini</b>, which has a free tier, or to
point this backend at one of OpenRouter's free models.</p>

<h2>Key benefits</h2>
<ul>
<li><b>Frontier models</b> on a device that could not run them locally.</li>
<li><b>Or your own hardware</b> — the same plugin reaches a large model on
your PC over Wi-Fi, at no cost and without your code leaving the
network.</li>
<li><b>Small footprint</b> — no bundled model or native library.</li>
<li><b>Credential hygiene</b> — encrypted at rest, header-only in transit,
dropped from memory when the plugin unloads.</li>
<li><b>Legible failures</b> — a wrong path, a stopped server and a refused key
each say so specifically instead of producing one generic error.</li>
<li><b>Coexists with the other backends</b> — install several and switch in
Agent settings.</li>
</ul>
</body>
</html>
Loading
Loading