Add Aider provider with OpenRouter support and per-provider sidecar routing
- Add aider-runner.ts sidecar that spawns aider CLI in non-interactive mode - Add Aider provider metadata with OpenRouter model presets - Add aider-messages.ts adapter for Aider event format - Refactor SidecarManager from single-process to per-provider process management with lazy startup on first query and session→provider routing - Add openrouter_api_key to secrets system (keyring storage) - Inject OPENROUTER_API_KEY from secrets into Aider agent environment - Register Aider in provider registry, build pipeline, and resource bundle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
35963be686
commit
5b7ad30573
12 changed files with 549 additions and 84 deletions
94
v2/src/lib/adapters/aider-messages.ts
Normal file
94
v2/src/lib/adapters/aider-messages.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Aider Message Adapter — transforms Aider runner events to internal AgentMessage format
|
||||
// Aider runner emits: system/init, assistant (text lines), result, error
|
||||
|
||||
import type {
|
||||
AgentMessage,
|
||||
InitContent,
|
||||
TextContent,
|
||||
CostContent,
|
||||
ErrorContent,
|
||||
} from './claude-messages';
|
||||
|
||||
import { str, num } from '../utils/type-guards';
|
||||
|
||||
/**
|
||||
* Adapt a raw Aider runner event to AgentMessage[].
|
||||
*
|
||||
* The Aider runner emits events in this format:
|
||||
* - {type:'system', subtype:'init', model, session_id, cwd}
|
||||
* - {type:'assistant', message:{role:'assistant', content:'...'}}
|
||||
* - {type:'result', subtype:'result', result:'...', cost_usd, duration_ms, is_error}
|
||||
* - {type:'error', message:'...'}
|
||||
*/
|
||||
export function adaptAiderMessage(raw: Record<string, unknown>): AgentMessage[] {
|
||||
const timestamp = Date.now();
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
switch (raw.type) {
|
||||
case 'system':
|
||||
if (str(raw.subtype) === 'init') {
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'init',
|
||||
content: {
|
||||
sessionId: str(raw.session_id),
|
||||
model: str(raw.model),
|
||||
cwd: str(raw.cwd),
|
||||
tools: [],
|
||||
} satisfies InitContent,
|
||||
timestamp,
|
||||
}];
|
||||
}
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'unknown',
|
||||
content: raw,
|
||||
timestamp,
|
||||
}];
|
||||
|
||||
case 'assistant': {
|
||||
const msg = typeof raw.message === 'object' && raw.message !== null
|
||||
? raw.message as Record<string, unknown>
|
||||
: {};
|
||||
const text = str(msg.content);
|
||||
if (!text) return [];
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'text',
|
||||
content: { text } satisfies TextContent,
|
||||
timestamp,
|
||||
}];
|
||||
}
|
||||
|
||||
case 'result':
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'cost',
|
||||
content: {
|
||||
totalCostUsd: num(raw.cost_usd),
|
||||
durationMs: num(raw.duration_ms),
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
numTurns: num(raw.num_turns) || 1,
|
||||
isError: raw.is_error === true,
|
||||
} satisfies CostContent,
|
||||
timestamp,
|
||||
}];
|
||||
|
||||
case 'error':
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'error',
|
||||
content: { message: str(raw.message, 'Aider error') } satisfies ErrorContent,
|
||||
timestamp,
|
||||
}];
|
||||
|
||||
default:
|
||||
return [{
|
||||
id: uuid,
|
||||
type: 'unknown',
|
||||
content: raw,
|
||||
timestamp,
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import type { ProviderId } from '../providers/types';
|
|||
import { adaptSDKMessage } from './claude-messages';
|
||||
import { adaptCodexMessage } from './codex-messages';
|
||||
import { adaptOllamaMessage } from './ollama-messages';
|
||||
import { adaptAiderMessage } from './aider-messages';
|
||||
|
||||
/** Function signature for a provider message adapter */
|
||||
export type MessageAdapter = (raw: Record<string, unknown>) => AgentMessage[];
|
||||
|
|
@ -31,3 +32,4 @@ export function adaptMessage(providerId: ProviderId, raw: Record<string, unknown
|
|||
registerMessageAdapter('claude', adaptSDKMessage);
|
||||
registerMessageAdapter('codex', adaptCodexMessage);
|
||||
registerMessageAdapter('ollama', adaptOllamaMessage);
|
||||
registerMessageAdapter('aider', adaptAiderMessage);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export async function knownSecretKeys(): Promise<string[]> {
|
|||
export const SECRET_KEY_LABELS: Record<string, string> = {
|
||||
anthropic_api_key: 'Anthropic API Key',
|
||||
openai_api_key: 'OpenAI API Key',
|
||||
openrouter_api_key: 'OpenRouter API Key',
|
||||
github_token: 'GitHub Token',
|
||||
relay_token: 'Relay Token',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
import type { AgentMessage } from '../../adapters/claude-messages';
|
||||
import { getProvider, getDefaultProviderId } from '../../providers/registry.svelte';
|
||||
import { loadAnchorsForProject } from '../../stores/anchors.svelte';
|
||||
import { getSecret } from '../../adapters/secrets-bridge';
|
||||
import { getWakeEvent, consumeWakeEvent, updateManagerSession } from '../../stores/wake-scheduler.svelte';
|
||||
import { SessionId, ProjectId } from '../../types/ids';
|
||||
import AgentPane from '../Agent/AgentPane.svelte';
|
||||
|
|
@ -58,15 +59,34 @@
|
|||
return project.systemPrompt || undefined;
|
||||
});
|
||||
|
||||
// Provider-specific API keys loaded from system keyring
|
||||
let openrouterKey = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (providerId === 'aider') {
|
||||
getSecret('openrouter_api_key').then(key => {
|
||||
openrouterKey = key;
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
openrouterKey = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Inject BTMSG_AGENT_ID for agent projects so they can use btmsg/bttask CLIs
|
||||
// Manager agents also get CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS to enable subagent delegation
|
||||
// Provider-specific API keys are injected from the system keyring
|
||||
let agentEnv = $derived.by(() => {
|
||||
if (!project.isAgent) return undefined;
|
||||
const env: Record<string, string> = { BTMSG_AGENT_ID: project.id };
|
||||
if (project.agentRole === 'manager') {
|
||||
env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
||||
const env: Record<string, string> = {};
|
||||
if (project.isAgent) {
|
||||
env.BTMSG_AGENT_ID = project.id;
|
||||
if (project.agentRole === 'manager') {
|
||||
env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
||||
}
|
||||
}
|
||||
return env;
|
||||
if (openrouterKey) {
|
||||
env.OPENROUTER_API_KEY = openrouterKey;
|
||||
}
|
||||
return Object.keys(env).length > 0 ? env : undefined;
|
||||
});
|
||||
|
||||
// Periodic context re-injection timer
|
||||
|
|
|
|||
32
v2/src/lib/providers/aider.ts
Normal file
32
v2/src/lib/providers/aider.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Aider Provider — metadata and capabilities for Aider (OpenRouter / multi-model agent)
|
||||
|
||||
import type { ProviderMeta } from './types';
|
||||
|
||||
export const AIDER_PROVIDER: ProviderMeta = {
|
||||
id: 'aider',
|
||||
name: 'Aider',
|
||||
description: 'Aider AI coding agent — supports OpenRouter, OpenAI, Anthropic and local models',
|
||||
capabilities: {
|
||||
hasProfiles: false,
|
||||
hasSkills: false,
|
||||
hasModelSelection: true,
|
||||
hasSandbox: false,
|
||||
supportsSubagents: false,
|
||||
supportsCost: false,
|
||||
supportsResume: false,
|
||||
},
|
||||
sidecarRunner: 'aider-runner.mjs',
|
||||
defaultModel: 'openrouter/anthropic/claude-sonnet-4',
|
||||
models: [
|
||||
{ id: 'openrouter/anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (OpenRouter)' },
|
||||
{ id: 'openrouter/anthropic/claude-haiku-4', label: 'Claude Haiku 4 (OpenRouter)' },
|
||||
{ id: 'openrouter/openai/gpt-4.1', label: 'GPT-4.1 (OpenRouter)' },
|
||||
{ id: 'openrouter/openai/o3', label: 'o3 (OpenRouter)' },
|
||||
{ id: 'openrouter/google/gemini-2.5-pro', label: 'Gemini 2.5 Pro (OpenRouter)' },
|
||||
{ id: 'openrouter/deepseek/deepseek-r1', label: 'DeepSeek R1 (OpenRouter)' },
|
||||
{ id: 'openrouter/meta-llama/llama-4-maverick', label: 'Llama 4 Maverick (OpenRouter)' },
|
||||
{ id: 'anthropic/claude-sonnet-4-5-20250514', label: 'Claude Sonnet 4.5 (direct)' },
|
||||
{ id: 'o3', label: 'o3 (OpenAI direct)' },
|
||||
{ id: 'ollama/qwen3:8b', label: 'Qwen3 8B (Ollama)' },
|
||||
],
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Provider abstraction types — defines the interface for multi-provider agent support
|
||||
|
||||
export type ProviderId = 'claude' | 'codex' | 'ollama';
|
||||
export type ProviderId = 'claude' | 'codex' | 'ollama' | 'aider';
|
||||
|
||||
/** What a provider can do — UI gates features on these flags */
|
||||
export interface ProviderCapabilities {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue