feat(proxy): add OpenCode IDE config and fix service availability
- Add opencode to SupportedIde with JSON providers.json output - Add defaultModel parameter for IDE config generation - Add model selector dropdown in ToolsTab for IDE configurations - Use getServiceOptional for graceful degradation when proxy is stopped - Add fallback IDE config generation using ConfigManager when NestJS is offline - Update IPC router schema to accept defaultModel parameter
This commit is contained in:
1 parent
641c739b64
commit
713c4497c6
5 files changed
+321
-68
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
import { ipc } from '@/ipc/manager';
|
||||
@@ -6,17 +6,42 @@ import { ipc } from '@/ipc/manager';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { StatCard } from '@/components/usage/StatCard';
|
||||
import { Loader2, Copy, CheckCircle, ShieldAlert, XCircle, Terminal, Code2, Braces } from 'lucide-react';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
ShieldAlert,
|
||||
XCircle,
|
||||
Terminal,
|
||||
Code2,
|
||||
Braces,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
const IDE_CONFIGS = [
|
||||
{ key: 'opencode', label: 'OpenCode', icon: Sparkles },
|
||||
{ key: 'vscode', label: 'VS Code', icon: Code2 },
|
||||
{ key: 'cursor', label: 'Cursor', icon: Terminal },
|
||||
{ key: 'claude-code', label: 'Claude Code', icon: Braces },
|
||||
] as const;
|
||||
|
||||
export const ToolsTab: React.FC = () => {
|
||||
interface ToolsTabProps {
|
||||
availableModelIds?: string[];
|
||||
}
|
||||
|
||||
export const ToolsTab: React.FC<ToolsTabProps> = ({
|
||||
availableModelIds = ['gemini-3.1-pro-high'],
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [selectedModel, setSelectedModel] = useState<string>(availableModelIds[0]);
|
||||
|
||||
const { data: parityData, isLoading: parityLoading } = useQuery({
|
||||
queryKey: ['proxyAdvanced', 'parityCounters'],
|
||||
@@ -25,16 +50,11 @@ export const ToolsTab: React.FC = () => {
|
||||
});
|
||||
|
||||
const ideQueries = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'vscode'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'vscode' }),
|
||||
},
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'cursor'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'cursor' }),
|
||||
},
|
||||
],
|
||||
queries: IDE_CONFIGS.map(({ key }) => ({
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', key, selectedModel],
|
||||
queryFn: () =>
|
||||
ipc.client.proxyAdvanced.generateIdeConfig({ ide: key, defaultModel: selectedModel }),
|
||||
})),
|
||||
});
|
||||
|
||||
const parity = parityData?.success ? parityData.data : undefined;
|
||||
@@ -44,15 +64,21 @@ export const ToolsTab: React.FC = () => {
|
||||
toast({ title: t('proxy.advanced.tools.copyConfig') });
|
||||
};
|
||||
|
||||
const getIdeInstructions = (key: string): string => {
|
||||
const getIdeInstructions = (key: string, index: number): string => {
|
||||
if (key === 'claude-code') {
|
||||
return 'export ANTHROPIC_BASE_URL=http://localhost:8045/v1\nexport ANTHROPIC_API_KEY=YOUR_API_KEY';
|
||||
}
|
||||
const index = key === 'vscode' ? 0 : 1;
|
||||
const result = ideQueries[index].data;
|
||||
const result = ideQueries[index]?.data;
|
||||
if (result?.success && result.data) {
|
||||
const d = result.data;
|
||||
return `${d.instructions}\n${d.proxySetting}: http://localhost:8045\n${d.apiKeySetting}: YOUR_API_KEY`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = result.data as any;
|
||||
if (d.format === 'json') {
|
||||
return JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
if (d.format === 'env' || d.format === 'shell') {
|
||||
return typeof d.content === 'string' ? d.content : JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
return `${d.instructions || ''}\n${d.proxySetting || 'URL'}: http://localhost:8045\n${d.apiKeySetting || 'Key'}: YOUR_API_KEY`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -87,24 +113,45 @@ export const ToolsTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* IDE Quick Setup */}
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
||||
<div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-xs text-muted-foreground">Quick configuration for popular IDEs</p>
|
||||
<div className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-muted-foreground text-xs">Quick configuration for popular IDEs</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">Default Model:</span>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger className="h-8 w-[180px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableModelIds.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="space-y-3 p-5">
|
||||
{isIdeLoading ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }) => {
|
||||
const instructions = getIdeInstructions(key);
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }, index) => {
|
||||
const instructions = getIdeInstructions(key, index);
|
||||
const resultData = ideQueries[index]?.data;
|
||||
const hasError = resultData && !resultData.success;
|
||||
const errorMsg = resultData?.error || '';
|
||||
|
||||
return (
|
||||
<div key={key} className="overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
<Icon className="text-muted-foreground h-4 w-4" />
|
||||
<span className="text-[13px] font-medium">{label}</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -112,13 +159,19 @@ export const ToolsTab: React.FC = () => {
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => copyToClipboard(instructions)}
|
||||
disabled={!instructions || hasError}
|
||||
>
|
||||
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t('proxy.advanced.tools.copyConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
{instructions && (
|
||||
<pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
|
||||
{hasError && (
|
||||
<div className="bg-card border-t border-white/[0.06] p-3 font-mono text-xs text-amber-500/80">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{instructions && !hasError && (
|
||||
<pre className="bg-card text-foreground/90 overflow-x-auto border-t border-white/[0.06] p-3 font-mono text-xs whitespace-pre-wrap">
|
||||
{instructions}
|
||||
</pre>
|
||||
)}
|
||||
@@ -128,7 +181,6 @@ export const ToolsTab: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,13 @@
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getServiceOrThrow } from './service-registry';
|
||||
import type { IdeConfigResult } from '../../server/modules/proxy/proxy-ide-config.service';
|
||||
import { getServiceOrThrow, getServiceOptional } from './service-registry';
|
||||
import type {
|
||||
IdeConfigResult,
|
||||
SupportedIde,
|
||||
} from '../../server/modules/proxy/proxy-ide-config.service';
|
||||
import { TrafficLogsRepo } from '../database/proxyMetricsHandler';
|
||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||
import { ConfigManager } from '../config/manager';
|
||||
import { networkInterfaces } from 'os';
|
||||
|
||||
export interface TrafficLogEntryParsed {
|
||||
timestamp: string;
|
||||
@@ -84,7 +89,15 @@ export async function exportTrafficLogs(
|
||||
}
|
||||
|
||||
// CSV format
|
||||
const headers = ['timestamp', 'direction', 'requestId', 'endpoint', 'method', 'status', 'durationMs'];
|
||||
const headers = [
|
||||
'timestamp',
|
||||
'direction',
|
||||
'requestId',
|
||||
'endpoint',
|
||||
'method',
|
||||
'status',
|
||||
'durationMs',
|
||||
];
|
||||
const rowsCsv = rows.map((e) =>
|
||||
[
|
||||
new Date(e.timestamp).toISOString(),
|
||||
@@ -108,11 +121,27 @@ export async function exportTrafficLogs(
|
||||
|
||||
export async function getParityCounters(): Promise<{
|
||||
success: boolean;
|
||||
data?: { totalRequests: number; matchedRequests: number; parityViolations: number; lastUpdated: string };
|
||||
data?: {
|
||||
totalRequests: number;
|
||||
matchedRequests: number;
|
||||
parityViolations: number;
|
||||
lastUpdated: string;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const tokenManager = getServiceOrThrow('tokenManager');
|
||||
const tokenManager = getServiceOptional('tokenManager');
|
||||
if (!tokenManager) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalRequests: 0,
|
||||
matchedRequests: 0,
|
||||
parityViolations: 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: tokenManager.getParityCounters(),
|
||||
@@ -124,14 +153,24 @@ export async function getParityCounters(): Promise<{
|
||||
|
||||
export async function getCircuitBreakerStatus(): Promise<{
|
||||
success: boolean;
|
||||
data?: { states: Record<string, { state: string; failures: number; lastFailure: string | null }>; globalEnabled: boolean };
|
||||
data?: {
|
||||
states: Record<string, { state: string; failures: number; lastFailure: string | null }>;
|
||||
globalEnabled: boolean;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const tokenManager = getServiceOrThrow('tokenManager');
|
||||
const tokenManager = getServiceOptional('tokenManager');
|
||||
if (!tokenManager) {
|
||||
return {
|
||||
success: true,
|
||||
data: { states: {}, globalEnabled: true },
|
||||
};
|
||||
}
|
||||
|
||||
const rawStatus = tokenManager.getCircuitBreakerStatus();
|
||||
const states: Record<string, { state: string; failures: number; lastFailure: string | null }> = {};
|
||||
const states: Record<string, { state: string; failures: number; lastFailure: string | null }> =
|
||||
{};
|
||||
for (const [accountId, entry] of Object.entries(rawStatus)) {
|
||||
states[accountId] = {
|
||||
state: entry.state,
|
||||
@@ -154,7 +193,7 @@ export async function getCircuitBreakerStatus(): Promise<{
|
||||
|
||||
export async function getProxyMetrics(): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
data?: {
|
||||
uptimeSeconds: number;
|
||||
totalRequests: number;
|
||||
activeConnections: number;
|
||||
@@ -166,7 +205,21 @@ export async function getProxyMetrics(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const proxyMetrics = getServiceOrThrow('proxyMetrics');
|
||||
const proxyMetrics = getServiceOptional('proxyMetrics');
|
||||
if (!proxyMetrics) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
uptimeSeconds: 0,
|
||||
totalRequests: 0,
|
||||
activeConnections: 0,
|
||||
avgLatency: 0,
|
||||
errorRate: 0,
|
||||
requestsPerMinute: 0,
|
||||
cacheHitRate: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const metrics = proxyMetrics.getMetrics();
|
||||
return {
|
||||
@@ -187,9 +240,14 @@ export async function getProxyMetrics(): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRecentRequests(): Promise<{ success: boolean; data?: unknown[]; error?: string }> {
|
||||
export async function getRecentRequests(): Promise<{
|
||||
success: boolean;
|
||||
data?: unknown[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const proxyReplay = getServiceOrThrow('proxyReplay');
|
||||
const proxyReplay = getServiceOptional('proxyReplay');
|
||||
if (!proxyReplay) return { success: true, data: [] };
|
||||
return {
|
||||
success: true,
|
||||
data: proxyReplay.getRecentRequests(20),
|
||||
@@ -201,7 +259,11 @@ export async function getRecentRequests(): Promise<{ success: boolean; data?: un
|
||||
|
||||
export async function replayRequest(
|
||||
requestId: string,
|
||||
): Promise<{ success: boolean; data?: { original: unknown; newResponse: unknown }; error?: string }> {
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: { original: unknown; newResponse: unknown };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const proxyReplay = getServiceOrThrow('proxyReplay');
|
||||
const result = await proxyReplay.replayRequest(requestId);
|
||||
@@ -278,14 +340,14 @@ export async function getModelCapabilities(): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
function mapIdeConfigResult(
|
||||
result: IdeConfigResult,
|
||||
): {
|
||||
function mapIdeConfigResult(result: IdeConfigResult): {
|
||||
name: string;
|
||||
settingsPath: string;
|
||||
proxySetting: string;
|
||||
apiKeySetting: string;
|
||||
instructions: string;
|
||||
proxySetting?: string;
|
||||
apiKeySetting?: string;
|
||||
instructions?: string;
|
||||
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||
content: Record<string, unknown> | string;
|
||||
} {
|
||||
switch (result.ide) {
|
||||
case 'vscode':
|
||||
@@ -295,6 +357,8 @@ function mapIdeConfigResult(
|
||||
proxySetting: 'openai.url',
|
||||
apiKeySetting: 'openai.apiKey',
|
||||
instructions: 'Add these settings to your VS Code settings.json',
|
||||
format: result.format,
|
||||
content: result.content as Record<string, unknown>,
|
||||
};
|
||||
case 'cursor':
|
||||
return {
|
||||
@@ -303,6 +367,8 @@ function mapIdeConfigResult(
|
||||
proxySetting: 'OPENAI_BASE_URL',
|
||||
apiKeySetting: 'OPENAI_API_KEY',
|
||||
instructions: 'Set these environment variables for Cursor',
|
||||
format: result.format,
|
||||
content: result.content as Record<string, unknown>,
|
||||
};
|
||||
case 'claude-code':
|
||||
return {
|
||||
@@ -311,6 +377,8 @@ function mapIdeConfigResult(
|
||||
proxySetting: 'ANTHROPIC_BASE_URL',
|
||||
apiKeySetting: 'ANTHROPIC_API_KEY',
|
||||
instructions: 'Run these export commands before starting Claude Code',
|
||||
format: result.format,
|
||||
content: result.content as string,
|
||||
};
|
||||
case 'jetbrains':
|
||||
return {
|
||||
@@ -319,6 +387,8 @@ function mapIdeConfigResult(
|
||||
proxySetting: 'Base URL',
|
||||
apiKeySetting: 'API Key',
|
||||
instructions: String(result.content),
|
||||
format: result.format,
|
||||
content: result.content as string,
|
||||
};
|
||||
case 'opencide':
|
||||
return {
|
||||
@@ -327,9 +397,19 @@ function mapIdeConfigResult(
|
||||
proxySetting: 'OPENAI_BASE_URL',
|
||||
apiKeySetting: 'OPENAI_API_KEY',
|
||||
instructions: 'Add these lines to your .env file',
|
||||
format: result.format,
|
||||
content: result.content as string,
|
||||
};
|
||||
case 'opencode':
|
||||
return {
|
||||
name: 'OpenCode',
|
||||
settingsPath: '.config/opencode/providers.json',
|
||||
instructions: 'Add this snippet to your providers.json',
|
||||
format: result.format,
|
||||
content: result.content as Record<string, unknown>,
|
||||
};
|
||||
default: {
|
||||
const exhaustiveCheck: never = result.ide;
|
||||
const exhaustiveCheck: never = result.ide as never;
|
||||
throw new Error(`Unsupported IDE: ${String(exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
@@ -337,29 +417,121 @@ function mapIdeConfigResult(
|
||||
|
||||
export async function generateIdeConfig(
|
||||
ide: string,
|
||||
defaultModel?: string,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
name: string;
|
||||
settingsPath: string;
|
||||
proxySetting: string;
|
||||
apiKeySetting: string;
|
||||
instructions: string;
|
||||
proxySetting?: string;
|
||||
apiKeySetting?: string;
|
||||
instructions?: string;
|
||||
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||
content: Record<string, unknown> | string;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const proxyIdeConfig = getServiceOrThrow('proxyIdeConfig');
|
||||
|
||||
const supportedIdes = ['cursor', 'vscode', 'claude-code', 'jetbrains', 'opencide'] as const;
|
||||
const normalizedIde = ide.toLowerCase() as typeof supportedIdes[number];
|
||||
const supportedIdes = [
|
||||
'cursor',
|
||||
'vscode',
|
||||
'claude-code',
|
||||
'jetbrains',
|
||||
'opencide',
|
||||
'opencode',
|
||||
] as const;
|
||||
const normalizedIde = ide.toLowerCase() as (typeof supportedIdes)[number];
|
||||
if (!supportedIdes.includes(normalizedIde)) {
|
||||
return { success: false, error: `Unsupported IDE: ${ide}` };
|
||||
}
|
||||
|
||||
const result = proxyIdeConfig.generateConfigForCurrentServer(normalizedIde);
|
||||
if (!result) {
|
||||
return { success: false, error: 'Failed to generate IDE config: server not configured' };
|
||||
// Attempt to use NestJS service if running, fallback to manual generation
|
||||
const proxyIdeConfig = getServiceOptional('proxyIdeConfig');
|
||||
if (proxyIdeConfig) {
|
||||
const result = proxyIdeConfig.generateConfigForCurrentServer(normalizedIde, defaultModel);
|
||||
if (result) return { success: true, data: mapIdeConfigResult(result) };
|
||||
}
|
||||
|
||||
// Fallback logic when proxy is stopped
|
||||
const config = ConfigManager.getCachedConfig()?.proxy;
|
||||
const port = config?.port || 8045;
|
||||
const apiKey = config?.api_key || 'YOUR_API_KEY';
|
||||
|
||||
let localIp = '127.0.0.1';
|
||||
const interfaces = networkInterfaces();
|
||||
for (const iface of Object.values(interfaces)) {
|
||||
if (iface) {
|
||||
for (const entry of iface) {
|
||||
if (entry.family === 'IPv4' && !entry.internal) {
|
||||
localIp = entry.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback generator mimicking the proxyIdeConfig.service
|
||||
const baseUrl = `http://${localIp}:${port}/v1`;
|
||||
let result: IdeConfigResult;
|
||||
|
||||
switch (normalizedIde) {
|
||||
case 'cursor':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'json',
|
||||
content: { OPENAI_API_KEY: apiKey, OPENAI_BASE_URL: baseUrl },
|
||||
};
|
||||
break;
|
||||
case 'vscode':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'json',
|
||||
content: { 'openai.url': baseUrl, 'openai.apiKey': apiKey },
|
||||
};
|
||||
break;
|
||||
case 'claude-code':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'shell',
|
||||
content: `export OPENAI_API_KEY=${apiKey} && export OPENAI_BASE_URL=${baseUrl}`,
|
||||
};
|
||||
break;
|
||||
case 'jetbrains':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'instructions',
|
||||
content: [
|
||||
'JetBrains AI Assistant Setup:',
|
||||
'1. Open Settings (File > Settings or Ctrl+Alt+S).',
|
||||
'2. Navigate to Languages & Frameworks > AI Assistant.',
|
||||
'3. Under "Custom OpenAI Endpoint", set:',
|
||||
` - Base URL: ${baseUrl}`,
|
||||
` - API Key: ${apiKey}`,
|
||||
'4. Click "Apply" and restart the IDE if prompted.',
|
||||
].join('\n'),
|
||||
};
|
||||
break;
|
||||
case 'opencide':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'env',
|
||||
content: `OPENAI_API_KEY=${apiKey}\nOPENAI_BASE_URL=${baseUrl}`,
|
||||
};
|
||||
break;
|
||||
case 'opencode':
|
||||
result = {
|
||||
ide: normalizedIde,
|
||||
format: 'json',
|
||||
content: {
|
||||
apiType: 'openai',
|
||||
apiKey: apiKey,
|
||||
baseURL: baseUrl,
|
||||
models: { chat: [defaultModel || 'gemini-3.1-pro-high'] },
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -155,9 +155,11 @@ const IdeConfigResponseSchema = z.object({
|
||||
.object({
|
||||
name: z.string(),
|
||||
settingsPath: z.string(),
|
||||
proxySetting: z.string(),
|
||||
apiKeySetting: z.string(),
|
||||
instructions: z.string(),
|
||||
proxySetting: z.string().optional(),
|
||||
apiKeySetting: z.string().optional(),
|
||||
instructions: z.string().optional(),
|
||||
format: z.enum(['json', 'shell', 'env', 'instructions']).optional(),
|
||||
content: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
error: z.string().optional(),
|
||||
@@ -204,10 +206,10 @@ export const proxyAdvancedRouter = os.router({
|
||||
}),
|
||||
|
||||
generateIdeConfig: os
|
||||
.input(z.object({ ide: z.string() }))
|
||||
.input(z.object({ ide: z.string(), defaultModel: z.string().optional() }))
|
||||
.output(IdeConfigResponseSchema)
|
||||
.handler(async ({ input }) => {
|
||||
return generateIdeConfig(input.ide);
|
||||
return generateIdeConfig(input.ide, input.defaultModel);
|
||||
}),
|
||||
|
||||
replayRequest: os
|
||||
|
||||
@@ -37,3 +37,9 @@ export function getServiceOrThrow<K extends keyof ProxyAdvancedRegistry>(
|
||||
if (!svc) throw new Error(`ProxyAdvanced service '${key}' is not registered`);
|
||||
return svc;
|
||||
}
|
||||
|
||||
export function getServiceOptional<K extends keyof ProxyAdvancedRegistry>(
|
||||
key: K,
|
||||
): ProxyAdvancedRegistry[K] {
|
||||
return proxyAdvancedRegistry[key];
|
||||
}
|
||||
@@ -3,12 +3,18 @@ import { networkInterfaces } from 'os';
|
||||
import { getServerConfig } from '../../server-config';
|
||||
import { registerProxyAdvancedService } from '../../../ipc/proxy-advanced/service-registry';
|
||||
|
||||
export type SupportedIde = 'cursor' | 'vscode' | 'claude-code' | 'jetbrains' | 'opencide';
|
||||
export type SupportedIde =
|
||||
| 'cursor'
|
||||
| 'vscode'
|
||||
| 'claude-code'
|
||||
| 'jetbrains'
|
||||
| 'opencide'
|
||||
| 'opencode';
|
||||
|
||||
export interface IdeConfigResult {
|
||||
ide: SupportedIde;
|
||||
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||
content: Record<string, string> | string;
|
||||
content: Record<string, any> | string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -46,6 +52,7 @@ export class ProxyIdeConfigService {
|
||||
port: number,
|
||||
apiKey: string,
|
||||
localIp: string,
|
||||
defaultModel: string = 'gemini-3.1-pro-high',
|
||||
): IdeConfigResult {
|
||||
const baseUrl = `http://${localIp}:${port}/v1`;
|
||||
|
||||
@@ -99,6 +106,20 @@ export class ProxyIdeConfigService {
|
||||
content: `OPENAI_API_KEY=${apiKey}\nOPENAI_BASE_URL=${baseUrl}`,
|
||||
};
|
||||
}
|
||||
case 'opencode': {
|
||||
return {
|
||||
ide,
|
||||
format: 'json',
|
||||
content: {
|
||||
apiType: 'openai',
|
||||
apiKey: apiKey,
|
||||
baseURL: baseUrl,
|
||||
models: {
|
||||
chat: [defaultModel],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
// Exhaustive fallback to satisfy TypeScript
|
||||
const exhaustiveCheck: never = ide;
|
||||
@@ -112,7 +133,7 @@ export class ProxyIdeConfigService {
|
||||
* Convenience helper that reads the current server config and local IP,
|
||||
* then delegates to generateIdeConfig.
|
||||
*/
|
||||
generateConfigForCurrentServer(ide: SupportedIde): IdeConfigResult | null {
|
||||
generateConfigForCurrentServer(ide: SupportedIde, defaultModel?: string): IdeConfigResult | null {
|
||||
const config = getServerConfig();
|
||||
if (!config) {
|
||||
this.logger.warn('Server config not available; cannot generate IDE config');
|
||||
@@ -121,6 +142,6 @@ export class ProxyIdeConfigService {
|
||||
const port = config.port;
|
||||
const apiKey = config.api_key;
|
||||
const localIp = this.getLocalIp();
|
||||
return this.generateIdeConfig(ide, port, apiKey, localIp);
|
||||
return this.generateIdeConfig(ide, port, apiKey, localIp, defaultModel);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user