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:
co-authored by
Claude Opus 4.6
parent
ebaee4c2b9
commit
ad4612a5c2
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user