feat(opencode): integrate OpenCode SDK for enhanced AI interactions
- Add support for OpenCode as a new AI provider, allowing users to connect to a local OpenCode server. - Implement new API endpoints for generating and streaming chat responses via OpenCode. - Update UI components to include OpenCode in provider selection and display. - Introduce OpenCode logo and enhance the agent settings dialog to manage OpenCode connections. - Refactor existing chat and generation logic to accommodate provider-specific routing. - Include type definitions for OpenCode SDK to ensure type safety and improve developer experience.
This commit is contained in:
parent
61441d4014
commit
05b4cd6e49
3
bun.lock
3
bun.lock
|
|
@ -7,6 +7,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.47",
|
||||
"@anthropic-ai/sdk": "^0.77.0",
|
||||
"@opencode-ai/sdk": "1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
|
|
@ -237,6 +238,8 @@
|
|||
|
||||
"@oozcitak/util": ["@oozcitak/util@10.0.0", "https://registry.npmmirror.com/@oozcitak/util/-/util-10.0.0.tgz", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.6", "https://registry.npmmirror.com/@opencode-ai/sdk/-/sdk-1.2.6.tgz", {}, "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.113.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.113.0.tgz", {}, "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.47",
|
||||
"@anthropic-ai/sdk": "^0.77.0",
|
||||
"@opencode-ai/sdk": "1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface ChatBody {
|
|||
system: string
|
||||
messages: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||
model?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,6 +26,12 @@ export default defineEventHandler(async (event) => {
|
|||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
// Explicit provider routing
|
||||
if (body.provider === 'opencode') {
|
||||
return streamViaOpenCode(body, body.model)
|
||||
}
|
||||
|
||||
// Default: existing behavior (backward-compatible)
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||
if (apiKey) {
|
||||
try {
|
||||
|
|
@ -172,3 +179,89 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
|
|||
|
||||
return new Response(stream)
|
||||
}
|
||||
|
||||
/** Parse an OpenCode model string ("providerID/modelID") into its parts */
|
||||
function parseOpenCodeModel(model?: string): { providerID: string; modelID: string } | undefined {
|
||||
if (!model || !model.includes('/')) return undefined
|
||||
const idx = model.indexOf('/')
|
||||
return { providerID: model.slice(0, idx), modelID: model.slice(idx + 1) }
|
||||
}
|
||||
|
||||
/** Stream via OpenCode SDK (connects to a running OpenCode server) */
|
||||
function streamViaOpenCode(body: ChatBody, model?: string) {
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
const pingTimer = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'ping', content: '' })}\n\n`))
|
||||
} catch { /* stream already closed */ }
|
||||
}, KEEPALIVE_INTERVAL_MS)
|
||||
|
||||
let ocServer: { close(): void } | undefined
|
||||
try {
|
||||
const { createOpencode } = await import('@opencode-ai/sdk/v2')
|
||||
const oc = await createOpencode()
|
||||
ocServer = oc.server
|
||||
|
||||
// Create a session for this conversation
|
||||
const { data: session, error: sessionError } = await oc.client.session.create({
|
||||
title: 'OpenPencil Chat',
|
||||
})
|
||||
if (sessionError || !session) {
|
||||
throw new Error('Failed to create OpenCode session')
|
||||
}
|
||||
|
||||
// Inject system prompt as context (no AI reply)
|
||||
await oc.client.session.prompt({
|
||||
sessionID: session.id,
|
||||
noReply: true,
|
||||
parts: [{ type: 'text', text: body.system }],
|
||||
})
|
||||
|
||||
// Build prompt from the last user message
|
||||
const lastUserMsg = [...body.messages].reverse().find((m) => m.role === 'user')
|
||||
const prompt = lastUserMsg?.content ?? ''
|
||||
|
||||
const parsed = parseOpenCodeModel(model)
|
||||
|
||||
// Send prompt and await full response
|
||||
const { data: result, error: promptError } = await oc.client.session.prompt({
|
||||
sessionID: session.id,
|
||||
...(parsed ? { model: parsed } : {}),
|
||||
parts: [{ type: 'text', text: prompt }],
|
||||
})
|
||||
|
||||
if (promptError) {
|
||||
throw new Error('OpenCode prompt failed')
|
||||
}
|
||||
|
||||
// Extract text from response parts
|
||||
clearInterval(pingTimer)
|
||||
if (result?.parts) {
|
||||
for (const part of result.parts) {
|
||||
if (part.type === 'text' && 'text' in part) {
|
||||
const data = JSON.stringify({ type: 'text', content: part.text })
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: 'done', content: '' })}\n\n`),
|
||||
)
|
||||
} catch (error) {
|
||||
const content = error instanceof Error ? error.message : 'Unknown error'
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: 'error', content })}\n\n`),
|
||||
)
|
||||
} finally {
|
||||
ocServer?.close()
|
||||
clearInterval(pingTimer)
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
|
|||
import type { GroupedModel } from '../../../src/types/agent-settings'
|
||||
|
||||
interface ConnectBody {
|
||||
agent: 'claude-code' | 'codex-cli'
|
||||
agent: 'claude-code' | 'codex-cli' | 'opencode'
|
||||
}
|
||||
|
||||
interface ConnectResult {
|
||||
|
|
@ -31,6 +31,10 @@ export default defineEventHandler(async (event) => {
|
|||
return connectCodexCli()
|
||||
}
|
||||
|
||||
if (body.agent === 'opencode') {
|
||||
return connectOpenCode()
|
||||
}
|
||||
|
||||
return { connected: false, models: [], error: `Unknown agent: ${body.agent}` } satisfies ConnectResult
|
||||
})
|
||||
|
||||
|
|
@ -151,3 +155,54 @@ async function connectCodexCli(): Promise<ConnectResult> {
|
|||
return { connected: false, models: [], error: msg }
|
||||
}
|
||||
}
|
||||
|
||||
/** Connect to OpenCode via createOpencode() and fetch its configured providers/models */
|
||||
async function connectOpenCode(): Promise<ConnectResult> {
|
||||
try {
|
||||
const { createOpencode } = await import('@opencode-ai/sdk/v2')
|
||||
const oc = await createOpencode()
|
||||
|
||||
const { data, error } = await oc.client.config.providers()
|
||||
oc.server.close()
|
||||
|
||||
if (error) {
|
||||
return { connected: false, models: [], error: 'Failed to fetch providers from OpenCode server.' }
|
||||
}
|
||||
|
||||
const models: GroupedModel[] = []
|
||||
for (const provider of data?.providers ?? []) {
|
||||
if (!provider.models) continue
|
||||
for (const [, model] of Object.entries(provider.models)) {
|
||||
models.push({
|
||||
value: `${provider.id}/${model.id}`,
|
||||
displayName: model.name || model.id,
|
||||
description: `via ${provider.name || provider.id}`,
|
||||
provider: 'opencode' as const,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
return { connected: false, models: [], error: 'No models configured in OpenCode. Run "opencode" to set up providers.' }
|
||||
}
|
||||
|
||||
return { connected: true, models }
|
||||
} catch (error) {
|
||||
const raw = error instanceof Error ? error.message : 'Failed to connect'
|
||||
return { connected: false, models: [], error: friendlyOpenCodeError(raw) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Map OpenCode connection errors to user-friendly messages */
|
||||
function friendlyOpenCodeError(raw: string): string {
|
||||
if (/ECONNREFUSED/i.test(raw)) {
|
||||
return 'OpenCode server not running. Start it with "opencode" in your terminal first.'
|
||||
}
|
||||
if (/not found|ENOENT/i.test(raw)) {
|
||||
return 'OpenCode CLI not found. Please install it first.'
|
||||
}
|
||||
if (/timed?\s*out/i.test(raw)) {
|
||||
return 'Connection timed out. Please try again.'
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface GenerateBody {
|
|||
system: string
|
||||
message: string
|
||||
model?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +20,12 @@ export default defineEventHandler(async (event) => {
|
|||
return { error: 'Missing required fields: system, message' }
|
||||
}
|
||||
|
||||
// Explicit provider routing
|
||||
if (body.provider === 'opencode') {
|
||||
return generateViaOpenCode(body, body.model)
|
||||
}
|
||||
|
||||
// Default: existing behavior (backward-compatible)
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||
if (apiKey) {
|
||||
try {
|
||||
|
|
@ -88,3 +95,62 @@ async function generateViaAgentSDK(body: GenerateBody, model?: string): Promise<
|
|||
return { error: message }
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate via OpenCode SDK (connects to a running OpenCode server) */
|
||||
async function generateViaOpenCode(body: GenerateBody, model?: string): Promise<{ text?: string; error?: string }> {
|
||||
let ocServer: { close(): void } | undefined
|
||||
try {
|
||||
const { createOpencode } = await import('@opencode-ai/sdk/v2')
|
||||
const oc = await createOpencode()
|
||||
ocServer = oc.server
|
||||
|
||||
const { data: session, error: sessionError } = await oc.client.session.create({
|
||||
title: 'OpenPencil Generate',
|
||||
})
|
||||
if (sessionError || !session) {
|
||||
return { error: 'Failed to create OpenCode session' }
|
||||
}
|
||||
|
||||
// Inject system prompt as context (no AI reply)
|
||||
await oc.client.session.prompt({
|
||||
sessionID: session.id,
|
||||
noReply: true,
|
||||
parts: [{ type: 'text', text: body.system }],
|
||||
})
|
||||
|
||||
// Parse model string ("providerID/modelID")
|
||||
let modelOption: { providerID: string; modelID: string } | undefined
|
||||
if (model && model.includes('/')) {
|
||||
const idx = model.indexOf('/')
|
||||
modelOption = { providerID: model.slice(0, idx), modelID: model.slice(idx + 1) }
|
||||
}
|
||||
|
||||
// Send main prompt and await full response
|
||||
const { data: result, error: promptError } = await oc.client.session.prompt({
|
||||
sessionID: session.id,
|
||||
...(modelOption ? { model: modelOption } : {}),
|
||||
parts: [{ type: 'text', text: body.message }],
|
||||
})
|
||||
|
||||
if (promptError) {
|
||||
return { error: 'OpenCode generation failed' }
|
||||
}
|
||||
|
||||
// Extract text from response parts
|
||||
const texts: string[] = []
|
||||
if (result?.parts) {
|
||||
for (const part of result.parts) {
|
||||
if (part.type === 'text' && part.text) {
|
||||
texts.push(part.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { text: texts.join('') }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return { error: message }
|
||||
} finally {
|
||||
ocServer?.close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
import ClaudeLogo from '@/components/icons/claude-logo'
|
||||
import OpenAILogo from '@/components/icons/openai-logo'
|
||||
import OpenCodeLogo from '@/components/icons/opencode-logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -209,6 +210,7 @@ export default function TopBar() {
|
|||
>
|
||||
<ClaudeLogo className="w-4 h-4" />
|
||||
<OpenAILogo className="w-4 h-4 -ml-1" />
|
||||
<OpenCodeLogo className="w-4 h-4 -ml-1" />
|
||||
<span className="hidden sm:inline">Agents & MCP</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
|
|||
63
src/components/icons/opencode-logo.tsx
Normal file
63
src/components/icons/opencode-logo.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { SVGProps } from 'react'
|
||||
|
||||
export default function OpenCodeLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#oc-clip)">
|
||||
{/* Terminal window frame */}
|
||||
<rect
|
||||
x="12"
|
||||
y="14"
|
||||
width="40"
|
||||
height="36"
|
||||
rx="4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
fill="none"
|
||||
/>
|
||||
{/* Title bar line */}
|
||||
<line
|
||||
x1="12"
|
||||
y1="22"
|
||||
x2="52"
|
||||
y2="22"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
{/* Title bar dots */}
|
||||
<circle cx="18" cy="18" r="1.5" fill="currentColor" />
|
||||
<circle cx="23" cy="18" r="1.5" fill="currentColor" />
|
||||
<circle cx="28" cy="18" r="1.5" fill="currentColor" />
|
||||
{/* Terminal prompt chevron > */}
|
||||
<path
|
||||
d="M18 29L26 35L18 41"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
{/* Cursor line */}
|
||||
<line
|
||||
x1="30"
|
||||
y1="40"
|
||||
x2="44"
|
||||
y2="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="oc-clip">
|
||||
<rect width="40" height="40" fill="white" transform="translate(12 12)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -23,11 +23,13 @@ import type { ChatMessage as ChatMessageType } from '@/services/ai/ai-types'
|
|||
import type { AIProviderType } from '@/types/agent-settings'
|
||||
import ClaudeLogo from '@/components/icons/claude-logo'
|
||||
import OpenAILogo from '@/components/icons/openai-logo'
|
||||
import OpenCodeLogo from '@/components/icons/opencode-logo'
|
||||
import ChatMessage from './chat-message'
|
||||
|
||||
const PROVIDER_ICON: Record<AIProviderType, typeof ClaudeLogo> = {
|
||||
anthropic: ClaudeLogo,
|
||||
openai: OpenAILogo,
|
||||
opencode: OpenCodeLogo,
|
||||
}
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
|
|
@ -221,8 +223,12 @@ function useChatHandlers() {
|
|||
} else {
|
||||
// --- CHAT MODE ---
|
||||
chatHistory.push({ role: 'user', content: fullUserMessage })
|
||||
// Resolve which provider the currently selected model belongs to
|
||||
const currentProvider = useAIStore.getState().modelGroups.find((g) =>
|
||||
g.models.some((m) => m.value === model),
|
||||
)?.provider
|
||||
let chatThinking = false
|
||||
for await (const chunk of streamChat(CHAT_SYSTEM_PROMPT, chatHistory, model)) {
|
||||
for await (const chunk of streamChat(CHAT_SYSTEM_PROMPT, chatHistory, model, undefined, currentProvider)) {
|
||||
if (chunk.type === 'thinking') {
|
||||
// Show a brief thinking indicator so the UI isn't stuck on empty
|
||||
if (!chatThinking && !accumulated) {
|
||||
|
|
@ -343,6 +349,7 @@ export default function AIChatPanel() {
|
|||
const providerNames: Record<AIProviderType, string> = {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
opencode: 'OpenCode',
|
||||
}
|
||||
const groups = connectedProviders.map((p) => ({
|
||||
provider: p,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import { useAgentSettingsStore } from '@/stores/agent-settings-store'
|
|||
import type { AIProviderType, GroupedModel } from '@/types/agent-settings'
|
||||
import ClaudeLogo from '@/components/icons/claude-logo'
|
||||
import OpenAILogo from '@/components/icons/openai-logo'
|
||||
import OpenCodeLogo from '@/components/icons/opencode-logo'
|
||||
|
||||
/** Provider display metadata */
|
||||
const PROVIDER_META: Record<
|
||||
AIProviderType,
|
||||
{ label: string; description: string; agent: 'claude-code' | 'codex-cli'; Icon: ComponentType<SVGProps<SVGSVGElement>> }
|
||||
{ label: string; description: string; agent: 'claude-code' | 'codex-cli' | 'opencode'; Icon: ComponentType<SVGProps<SVGSVGElement>> }
|
||||
> = {
|
||||
anthropic: {
|
||||
label: 'Claude Code',
|
||||
|
|
@ -26,10 +27,16 @@ const PROVIDER_META: Record<
|
|||
agent: 'codex-cli',
|
||||
Icon: OpenAILogo,
|
||||
},
|
||||
opencode: {
|
||||
label: 'OpenCode',
|
||||
description: 'Connect to local OpenCode server to use 75+ LLM providers',
|
||||
agent: 'opencode',
|
||||
Icon: OpenCodeLogo,
|
||||
},
|
||||
}
|
||||
|
||||
async function connectAgent(
|
||||
agent: 'claude-code' | 'codex-cli',
|
||||
agent: 'claude-code' | 'codex-cli' | 'opencode',
|
||||
): Promise<{ connected: boolean; models: GroupedModel[]; error?: string }> {
|
||||
try {
|
||||
const res = await fetch('/api/ai/connect-agent', {
|
||||
|
|
@ -194,6 +201,7 @@ export default function AgentSettingsDialog() {
|
|||
<div className="space-y-2">
|
||||
<ProviderCard type="anthropic" />
|
||||
<ProviderCard type="openai" />
|
||||
<ProviderCard type="opencode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export async function* streamChat(
|
|||
messages: Array<{ role: 'user' | 'assistant'; content: string }>,
|
||||
model?: string,
|
||||
options?: StreamChatOptions,
|
||||
provider?: string,
|
||||
): AsyncGenerator<AIStreamChunk> {
|
||||
const hardTimeoutMs = Math.max(10_000, options?.hardTimeoutMs ?? DEFAULT_STREAM_HARD_TIMEOUT_MS)
|
||||
const noTextTimeoutMs = Math.max(10_000, options?.noTextTimeoutMs ?? DEFAULT_STREAM_NO_TEXT_TIMEOUT_MS)
|
||||
|
|
@ -50,7 +51,7 @@ export async function* streamChat(
|
|||
const response = await fetch('/api/ai/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ system: systemPrompt, messages, model }),
|
||||
body: JSON.stringify({ system: systemPrompt, messages, model, provider }),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
|
|
@ -213,6 +214,7 @@ export async function generateCompletion(
|
|||
systemPrompt: string,
|
||||
userMessage: string,
|
||||
model?: string,
|
||||
provider?: string,
|
||||
): Promise<string> {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), DEFAULT_GENERATE_TIMEOUT_MS)
|
||||
|
|
@ -222,7 +224,7 @@ export async function generateCompletion(
|
|||
response = await fetch('/api/ai/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ system: systemPrompt, message: userMessage, model }),
|
||||
body: JSON.stringify({ system: systemPrompt, message: userMessage, model, provider }),
|
||||
signal: controller.signal,
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ const DEFAULT_PROVIDERS: Record<AIProviderType, AIProviderConfig> = {
|
|||
connectionMethod: null,
|
||||
models: [],
|
||||
},
|
||||
opencode: {
|
||||
type: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
isConnected: false,
|
||||
connectionMethod: null,
|
||||
models: [],
|
||||
},
|
||||
}
|
||||
|
||||
const DEFAULT_MCP_INTEGRATIONS: MCPCliIntegration[] = [
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
export type AIProviderType = 'anthropic' | 'openai'
|
||||
export type AIProviderType = 'anthropic' | 'openai' | 'opencode'
|
||||
|
||||
export interface AIProviderConfig {
|
||||
type: AIProviderType
|
||||
displayName: string
|
||||
isConnected: boolean
|
||||
connectionMethod: 'claude-code' | 'codex-cli' | null
|
||||
connectionMethod: 'claude-code' | 'codex-cli' | 'opencode' | null
|
||||
/** Models fetched when the user connects this provider */
|
||||
models: GroupedModel[]
|
||||
}
|
||||
|
|
|
|||
82
src/types/opencode-sdk.d.ts
vendored
Normal file
82
src/types/opencode-sdk.d.ts
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Minimal type declarations for @opencode-ai/sdk.
|
||||
* The package's export map points to non-existent paths (dist/ vs dist/src/).
|
||||
* A postinstall script patches this, but we keep declarations as fallback.
|
||||
*/
|
||||
|
||||
interface OpencodeClient {
|
||||
config: {
|
||||
providers(options?: unknown): Promise<{
|
||||
data: {
|
||||
providers: OpencodeProvider[]
|
||||
default: Record<string, string>
|
||||
}
|
||||
error: unknown
|
||||
}>
|
||||
}
|
||||
session: {
|
||||
create(options?: {
|
||||
body?: { parentID?: string; title?: string }
|
||||
}): Promise<{
|
||||
data: OpencodeSession | undefined
|
||||
error: unknown
|
||||
}>
|
||||
prompt(options: {
|
||||
path: { id: string }
|
||||
body: {
|
||||
model?: { providerID: string; modelID: string }
|
||||
noReply?: boolean
|
||||
parts: Array<{ type: string; text: string }>
|
||||
}
|
||||
}): Promise<{
|
||||
data:
|
||||
| {
|
||||
info: Record<string, unknown>
|
||||
parts: Array<{ type: string; text?: string } & Record<string, unknown>>
|
||||
}
|
||||
| undefined
|
||||
error: unknown
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
interface OpencodeProvider {
|
||||
id: string
|
||||
name: string
|
||||
models: Record<string, OpencodeModel>
|
||||
}
|
||||
|
||||
interface OpencodeModel {
|
||||
id: string
|
||||
name: string
|
||||
providerID: string
|
||||
}
|
||||
|
||||
interface OpencodeSession {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
declare module '@opencode-ai/sdk' {
|
||||
export function createOpencode(options?: {
|
||||
hostname?: string
|
||||
port?: number
|
||||
signal?: AbortSignal
|
||||
timeout?: number
|
||||
}): Promise<{
|
||||
client: OpencodeClient
|
||||
server: { url: string; close(): void }
|
||||
}>
|
||||
|
||||
export function createOpencodeClient(config?: {
|
||||
baseUrl?: string
|
||||
directory?: string
|
||||
}): OpencodeClient
|
||||
}
|
||||
|
||||
declare module '@opencode-ai/sdk/client' {
|
||||
export function createOpencodeClient(config?: {
|
||||
baseUrl?: string
|
||||
directory?: string
|
||||
}): OpencodeClient
|
||||
}
|
||||
|
|
@ -13,10 +13,13 @@ const config = defineConfig({
|
|||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
external: ['@opencode-ai/sdk'],
|
||||
},
|
||||
plugins: [
|
||||
devtools(),
|
||||
nitro({
|
||||
rollupConfig: { external: [/^@sentry\//] },
|
||||
rollupConfig: { external: [/^@sentry\//, /^@opencode-ai\//] },
|
||||
serverDir: './server',
|
||||
}),
|
||||
// this is the plugin that enables path aliases
|
||||
|
|
|
|||
Loading…
Reference in a new issue