feat(app): activate plugin registry with Claude provider adapter

Create plugins/index.ts bootstrap and claude-provider.ts implementing
the AIProviderAdapter interface from @aiui/core. The Claude provider
wraps the existing proxy streaming logic as an async generator. Plugin
initialization is called in main.ts before app mount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 20:37:00 +00:00
co-authored by Claude Opus 4.6
parent ebaee4c2b9
commit ad4612a5c2
3 changed files with 193 additions and 0 deletions
+6
View File
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import './styles/main.css'
import { initializePlugins } from './plugins'
const router = createRouter({
history: createWebHistory(),
@@ -23,4 +24,9 @@ const router = createRouter({
const app = createApp(App)
app.use(createPinia())
app.use(router)
initializePlugins().catch((err) => {
console.error('[AIUI] Failed to initialize plugins:', err)
})
app.mount('#app')
+161
View File
@@ -0,0 +1,161 @@
import type {
AIProviderAdapter,
AIModel,
ChatMessage,
ChatOptions,
ChatChunk,
PluginContext,
} from '@aiui/core'
const CLAUDE_PATH = '/api/claude/v1/messages'
async function* streamChat(
messages: ChatMessage[],
options: ChatOptions,
systemPrompt?: string,
): AsyncGenerator<ChatChunk> {
const body: Record<string, unknown> = {
model: options.model,
messages: messages.filter(m => m.role !== 'system').map(m => ({
role: m.role,
content: typeof m.content === 'string' ? m.content : m.content.map(p => p.text ?? '').join(''),
})),
stream: options.stream ?? true,
}
// Extract system message from messages or use provided systemPrompt
const systemMsg = messages.find(m => m.role === 'system')
if (systemPrompt) {
body.system = systemPrompt
} else if (systemMsg) {
body.system = typeof systemMsg.content === 'string'
? systemMsg.content
: systemMsg.content.map(p => p.text ?? '').join('')
}
if (options.maxTokens) body.max_tokens = options.maxTokens
if (options.temperature !== undefined) body.temperature = options.temperature
const res = await fetch(CLAUDE_PATH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) {
const errBody = await res.text().catch(() => 'Could not read error body')
yield { type: 'error', error: `Claude proxy error ${res.status}: ${errBody}` }
return
}
const reader = res.body?.getReader()
if (!reader) {
yield { type: 'error', error: 'No response body' }
return
}
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || !trimmed.startsWith('data: ')) continue
const payload = trimmed.slice(6)
if (payload === '[DONE]') {
yield { type: 'done' }
return
}
try {
const parsed = JSON.parse(payload)
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
yield { type: 'text', text: parsed.delta.text }
} else if (parsed.type === 'message_stop') {
yield { type: 'done', usage: parsed.usage ? {
promptTokens: parsed.usage.input_tokens ?? 0,
completionTokens: parsed.usage.output_tokens ?? 0,
} : undefined }
} else if (parsed.type === 'error') {
yield { type: 'error', error: parsed.error?.message ?? 'Claude stream error' }
}
} catch {
// skip malformed chunks
}
}
}
} finally {
reader.cancel().catch(() => {})
}
yield { type: 'done' }
}
export const claudeProvider: AIProviderAdapter = {
id: 'claude',
name: 'Claude (Anthropic)',
version: '1.0.0',
type: 'ai-provider',
description: 'Anthropic Claude AI via proxy server',
supportsStreaming: true,
supportsVision: true,
supportsTools: true,
async init(_context: PluginContext): Promise<void> {
// No initialization needed — uses proxy
},
async destroy(): Promise<void> {
// No cleanup needed
},
async isAvailable(): Promise<boolean> {
try {
const res = await fetch(CLAUDE_PATH, { method: 'OPTIONS' })
return res.ok || res.status === 405 // OPTIONS may not be supported but proxy is up
} catch {
return false
}
},
chat(messages: ChatMessage[], options: ChatOptions): AsyncIterable<ChatChunk> {
return streamChat(messages, options)
},
async models(): Promise<AIModel[]> {
return [
{
id: 'claude-haiku-4.5',
name: 'Claude 4.5 Haiku',
provider: 'claude',
supportsVision: true,
supportsTools: true,
contextWindow: 200000,
},
{
id: 'claude-sonnet-4',
name: 'Claude Sonnet 4',
provider: 'claude',
supportsVision: true,
supportsTools: true,
contextWindow: 200000,
},
{
id: 'claude-opus-4',
name: 'Claude Opus 4',
provider: 'claude',
supportsVision: true,
supportsTools: true,
contextWindow: 200000,
},
]
},
}
+26
View File
@@ -0,0 +1,26 @@
import { registerPlugin } from '@aiui/core'
export async function initializePlugins(): Promise<void> {
// Register built-in AI provider
const { claudeProvider } = await import('./claude-provider')
registerPlugin(claudeProvider)
await claudeProvider.init({
settings: {
get: () => undefined,
set: () => {},
},
events: {
emit: () => {},
on: () => () => {},
},
logger: {
info: console.info.bind(console, '[AIUI plugin]'),
warn: console.warn.bind(console, '[AIUI plugin]'),
error: console.error.bind(console, '[AIUI plugin]'),
},
})
if (import.meta.env.DEV) {
console.log('[AIUI] Plugins initialized:', claudeProvider.id)
}
}