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 { useTranslation } from 'react-i18next';
|
||||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||||
import { ipc } from '@/ipc/manager';
|
import { ipc } from '@/ipc/manager';
|
||||||
@@ -6,17 +6,42 @@ import { ipc } from '@/ipc/manager';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { StatCard } from '@/components/usage/StatCard';
|
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 = [
|
const IDE_CONFIGS = [
|
||||||
|
{ key: 'opencode', label: 'OpenCode', icon: Sparkles },
|
||||||
{ key: 'vscode', label: 'VS Code', icon: Code2 },
|
{ key: 'vscode', label: 'VS Code', icon: Code2 },
|
||||||
{ key: 'cursor', label: 'Cursor', icon: Terminal },
|
{ key: 'cursor', label: 'Cursor', icon: Terminal },
|
||||||
{ key: 'claude-code', label: 'Claude Code', icon: Braces },
|
{ key: 'claude-code', label: 'Claude Code', icon: Braces },
|
||||||
] as const;
|
] 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 { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [selectedModel, setSelectedModel] = useState<string>(availableModelIds[0]);
|
||||||
|
|
||||||
const { data: parityData, isLoading: parityLoading } = useQuery({
|
const { data: parityData, isLoading: parityLoading } = useQuery({
|
||||||
queryKey: ['proxyAdvanced', 'parityCounters'],
|
queryKey: ['proxyAdvanced', 'parityCounters'],
|
||||||
@@ -25,16 +50,11 @@ export const ToolsTab: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ideQueries = useQueries({
|
const ideQueries = useQueries({
|
||||||
queries: [
|
queries: IDE_CONFIGS.map(({ key }) => ({
|
||||||
{
|
queryKey: ['proxyAdvanced', 'ideConfig', key, selectedModel],
|
||||||
queryKey: ['proxyAdvanced', 'ideConfig', 'vscode'],
|
queryFn: () =>
|
||||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'vscode' }),
|
ipc.client.proxyAdvanced.generateIdeConfig({ ide: key, defaultModel: selectedModel }),
|
||||||
},
|
})),
|
||||||
{
|
|
||||||
queryKey: ['proxyAdvanced', 'ideConfig', 'cursor'],
|
|
||||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'cursor' }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parity = parityData?.success ? parityData.data : undefined;
|
const parity = parityData?.success ? parityData.data : undefined;
|
||||||
@@ -44,15 +64,21 @@ export const ToolsTab: React.FC = () => {
|
|||||||
toast({ title: t('proxy.advanced.tools.copyConfig') });
|
toast({ title: t('proxy.advanced.tools.copyConfig') });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIdeInstructions = (key: string): string => {
|
const getIdeInstructions = (key: string, index: number): string => {
|
||||||
if (key === 'claude-code') {
|
if (key === 'claude-code') {
|
||||||
return 'export ANTHROPIC_BASE_URL=http://localhost:8045/v1\nexport ANTHROPIC_API_KEY=YOUR_API_KEY';
|
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) {
|
if (result?.success && result.data) {
|
||||||
const d = result.data;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return `${d.instructions}\n${d.proxySetting}: http://localhost:8045\n${d.apiKeySetting}: YOUR_API_KEY`;
|
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 '';
|
return '';
|
||||||
};
|
};
|
||||||
@@ -87,24 +113,45 @@ export const ToolsTab: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* IDE Quick Setup */}
|
{/* IDE Quick Setup */}
|
||||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
<div className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
|
||||||
<div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
|
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
|
||||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-muted-foreground">Quick configuration for popular IDEs</p>
|
<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>
|
||||||
<div className="p-5 space-y-3">
|
<div className="space-y-3 p-5">
|
||||||
{isIdeLoading ? (
|
{isIdeLoading ? (
|
||||||
<div className="flex h-20 items-center justify-center">
|
<div className="flex h-20 items-center justify-center">
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
IDE_CONFIGS.map(({ key, label, icon: Icon }) => {
|
IDE_CONFIGS.map(({ key, label, icon: Icon }, index) => {
|
||||||
const instructions = getIdeInstructions(key);
|
const instructions = getIdeInstructions(key, index);
|
||||||
|
const resultData = ideQueries[index]?.data;
|
||||||
|
const hasError = resultData && !resultData.success;
|
||||||
|
const errorMsg = resultData?.error || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="overflow-hidden rounded-xl border border-white/[0.06]">
|
<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 justify-between p-3">
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="text-[13px] font-medium">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -112,13 +159,19 @@ export const ToolsTab: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
onClick={() => copyToClipboard(instructions)}
|
onClick={() => copyToClipboard(instructions)}
|
||||||
|
disabled={!instructions || hasError}
|
||||||
>
|
>
|
||||||
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
||||||
{t('proxy.advanced.tools.copyConfig')}
|
{t('proxy.advanced.tools.copyConfig')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{instructions && (
|
{hasError && (
|
||||||
<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">
|
<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}
|
{instructions}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -128,7 +181,6 @@ export const ToolsTab: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getServiceOrThrow } from './service-registry';
|
import { getServiceOrThrow, getServiceOptional } from './service-registry';
|
||||||
import type { IdeConfigResult } from '../../server/modules/proxy/proxy-ide-config.service';
|
import type {
|
||||||
|
IdeConfigResult,
|
||||||
|
SupportedIde,
|
||||||
|
} from '../../server/modules/proxy/proxy-ide-config.service';
|
||||||
import { TrafficLogsRepo } from '../database/proxyMetricsHandler';
|
import { TrafficLogsRepo } from '../database/proxyMetricsHandler';
|
||||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||||
|
import { ConfigManager } from '../config/manager';
|
||||||
|
import { networkInterfaces } from 'os';
|
||||||
|
|
||||||
export interface TrafficLogEntryParsed {
|
export interface TrafficLogEntryParsed {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
@@ -84,7 +89,15 @@ export async function exportTrafficLogs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CSV format
|
// 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) =>
|
const rowsCsv = rows.map((e) =>
|
||||||
[
|
[
|
||||||
new Date(e.timestamp).toISOString(),
|
new Date(e.timestamp).toISOString(),
|
||||||
@@ -108,11 +121,27 @@ export async function exportTrafficLogs(
|
|||||||
|
|
||||||
export async function getParityCounters(): Promise<{
|
export async function getParityCounters(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: { totalRequests: number; matchedRequests: number; parityViolations: number; lastUpdated: string };
|
data?: {
|
||||||
|
totalRequests: number;
|
||||||
|
matchedRequests: number;
|
||||||
|
parityViolations: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: tokenManager.getParityCounters(),
|
data: tokenManager.getParityCounters(),
|
||||||
@@ -124,14 +153,24 @@ export async function getParityCounters(): Promise<{
|
|||||||
|
|
||||||
export async function getCircuitBreakerStatus(): Promise<{
|
export async function getCircuitBreakerStatus(): Promise<{
|
||||||
success: boolean;
|
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;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const tokenManager = getServiceOrThrow('tokenManager');
|
const tokenManager = getServiceOptional('tokenManager');
|
||||||
|
if (!tokenManager) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { states: {}, globalEnabled: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const rawStatus = tokenManager.getCircuitBreakerStatus();
|
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)) {
|
for (const [accountId, entry] of Object.entries(rawStatus)) {
|
||||||
states[accountId] = {
|
states[accountId] = {
|
||||||
state: entry.state,
|
state: entry.state,
|
||||||
@@ -154,7 +193,7 @@ export async function getCircuitBreakerStatus(): Promise<{
|
|||||||
|
|
||||||
export async function getProxyMetrics(): Promise<{
|
export async function getProxyMetrics(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number;
|
||||||
totalRequests: number;
|
totalRequests: number;
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
@@ -166,7 +205,21 @@ export async function getProxyMetrics(): Promise<{
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
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();
|
const metrics = proxyMetrics.getMetrics();
|
||||||
return {
|
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 {
|
try {
|
||||||
const proxyReplay = getServiceOrThrow('proxyReplay');
|
const proxyReplay = getServiceOptional('proxyReplay');
|
||||||
|
if (!proxyReplay) return { success: true, data: [] };
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: proxyReplay.getRecentRequests(20),
|
data: proxyReplay.getRecentRequests(20),
|
||||||
@@ -201,7 +259,11 @@ export async function getRecentRequests(): Promise<{ success: boolean; data?: un
|
|||||||
|
|
||||||
export async function replayRequest(
|
export async function replayRequest(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
): Promise<{ success: boolean; data?: { original: unknown; newResponse: unknown }; error?: string }> {
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: { original: unknown; newResponse: unknown };
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const proxyReplay = getServiceOrThrow('proxyReplay');
|
const proxyReplay = getServiceOrThrow('proxyReplay');
|
||||||
const result = await proxyReplay.replayRequest(requestId);
|
const result = await proxyReplay.replayRequest(requestId);
|
||||||
@@ -278,14 +340,14 @@ export async function getModelCapabilities(): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapIdeConfigResult(
|
function mapIdeConfigResult(result: IdeConfigResult): {
|
||||||
result: IdeConfigResult,
|
|
||||||
): {
|
|
||||||
name: string;
|
name: string;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
proxySetting: string;
|
proxySetting?: string;
|
||||||
apiKeySetting: string;
|
apiKeySetting?: string;
|
||||||
instructions: string;
|
instructions?: string;
|
||||||
|
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||||
|
content: Record<string, unknown> | string;
|
||||||
} {
|
} {
|
||||||
switch (result.ide) {
|
switch (result.ide) {
|
||||||
case 'vscode':
|
case 'vscode':
|
||||||
@@ -295,6 +357,8 @@ function mapIdeConfigResult(
|
|||||||
proxySetting: 'openai.url',
|
proxySetting: 'openai.url',
|
||||||
apiKeySetting: 'openai.apiKey',
|
apiKeySetting: 'openai.apiKey',
|
||||||
instructions: 'Add these settings to your VS Code settings.json',
|
instructions: 'Add these settings to your VS Code settings.json',
|
||||||
|
format: result.format,
|
||||||
|
content: result.content as Record<string, unknown>,
|
||||||
};
|
};
|
||||||
case 'cursor':
|
case 'cursor':
|
||||||
return {
|
return {
|
||||||
@@ -303,6 +367,8 @@ function mapIdeConfigResult(
|
|||||||
proxySetting: 'OPENAI_BASE_URL',
|
proxySetting: 'OPENAI_BASE_URL',
|
||||||
apiKeySetting: 'OPENAI_API_KEY',
|
apiKeySetting: 'OPENAI_API_KEY',
|
||||||
instructions: 'Set these environment variables for Cursor',
|
instructions: 'Set these environment variables for Cursor',
|
||||||
|
format: result.format,
|
||||||
|
content: result.content as Record<string, unknown>,
|
||||||
};
|
};
|
||||||
case 'claude-code':
|
case 'claude-code':
|
||||||
return {
|
return {
|
||||||
@@ -311,6 +377,8 @@ function mapIdeConfigResult(
|
|||||||
proxySetting: 'ANTHROPIC_BASE_URL',
|
proxySetting: 'ANTHROPIC_BASE_URL',
|
||||||
apiKeySetting: 'ANTHROPIC_API_KEY',
|
apiKeySetting: 'ANTHROPIC_API_KEY',
|
||||||
instructions: 'Run these export commands before starting Claude Code',
|
instructions: 'Run these export commands before starting Claude Code',
|
||||||
|
format: result.format,
|
||||||
|
content: result.content as string,
|
||||||
};
|
};
|
||||||
case 'jetbrains':
|
case 'jetbrains':
|
||||||
return {
|
return {
|
||||||
@@ -319,6 +387,8 @@ function mapIdeConfigResult(
|
|||||||
proxySetting: 'Base URL',
|
proxySetting: 'Base URL',
|
||||||
apiKeySetting: 'API Key',
|
apiKeySetting: 'API Key',
|
||||||
instructions: String(result.content),
|
instructions: String(result.content),
|
||||||
|
format: result.format,
|
||||||
|
content: result.content as string,
|
||||||
};
|
};
|
||||||
case 'opencide':
|
case 'opencide':
|
||||||
return {
|
return {
|
||||||
@@ -327,9 +397,19 @@ function mapIdeConfigResult(
|
|||||||
proxySetting: 'OPENAI_BASE_URL',
|
proxySetting: 'OPENAI_BASE_URL',
|
||||||
apiKeySetting: 'OPENAI_API_KEY',
|
apiKeySetting: 'OPENAI_API_KEY',
|
||||||
instructions: 'Add these lines to your .env file',
|
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: {
|
default: {
|
||||||
const exhaustiveCheck: never = result.ide;
|
const exhaustiveCheck: never = result.ide as never;
|
||||||
throw new Error(`Unsupported IDE: ${String(exhaustiveCheck)}`);
|
throw new Error(`Unsupported IDE: ${String(exhaustiveCheck)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,29 +417,121 @@ function mapIdeConfigResult(
|
|||||||
|
|
||||||
export async function generateIdeConfig(
|
export async function generateIdeConfig(
|
||||||
ide: string,
|
ide: string,
|
||||||
|
defaultModel?: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
name: string;
|
name: string;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
proxySetting: string;
|
proxySetting?: string;
|
||||||
apiKeySetting: string;
|
apiKeySetting?: string;
|
||||||
instructions: string;
|
instructions?: string;
|
||||||
|
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||||
|
content: Record<string, unknown> | string;
|
||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const proxyIdeConfig = getServiceOrThrow('proxyIdeConfig');
|
const supportedIdes = [
|
||||||
|
'cursor',
|
||||||
const supportedIdes = ['cursor', 'vscode', 'claude-code', 'jetbrains', 'opencide'] as const;
|
'vscode',
|
||||||
const normalizedIde = ide.toLowerCase() as typeof supportedIdes[number];
|
'claude-code',
|
||||||
|
'jetbrains',
|
||||||
|
'opencide',
|
||||||
|
'opencode',
|
||||||
|
] as const;
|
||||||
|
const normalizedIde = ide.toLowerCase() as (typeof supportedIdes)[number];
|
||||||
if (!supportedIdes.includes(normalizedIde)) {
|
if (!supportedIdes.includes(normalizedIde)) {
|
||||||
return { success: false, error: `Unsupported IDE: ${ide}` };
|
return { success: false, error: `Unsupported IDE: ${ide}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = proxyIdeConfig.generateConfigForCurrentServer(normalizedIde);
|
// Attempt to use NestJS service if running, fallback to manual generation
|
||||||
if (!result) {
|
const proxyIdeConfig = getServiceOptional('proxyIdeConfig');
|
||||||
return { success: false, error: 'Failed to generate IDE config: server not configured' };
|
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 {
|
return {
|
||||||
|
|||||||
@@ -155,9 +155,11 @@ const IdeConfigResponseSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
settingsPath: z.string(),
|
settingsPath: z.string(),
|
||||||
proxySetting: z.string(),
|
proxySetting: z.string().optional(),
|
||||||
apiKeySetting: z.string(),
|
apiKeySetting: z.string().optional(),
|
||||||
instructions: z.string(),
|
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(),
|
.optional(),
|
||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
@@ -204,10 +206,10 @@ export const proxyAdvancedRouter = os.router({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
generateIdeConfig: os
|
generateIdeConfig: os
|
||||||
.input(z.object({ ide: z.string() }))
|
.input(z.object({ ide: z.string(), defaultModel: z.string().optional() }))
|
||||||
.output(IdeConfigResponseSchema)
|
.output(IdeConfigResponseSchema)
|
||||||
.handler(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
return generateIdeConfig(input.ide);
|
return generateIdeConfig(input.ide, input.defaultModel);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
replayRequest: os
|
replayRequest: os
|
||||||
|
|||||||
@@ -37,3 +37,9 @@ export function getServiceOrThrow<K extends keyof ProxyAdvancedRegistry>(
|
|||||||
if (!svc) throw new Error(`ProxyAdvanced service '${key}' is not registered`);
|
if (!svc) throw new Error(`ProxyAdvanced service '${key}' is not registered`);
|
||||||
return svc;
|
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 { getServerConfig } from '../../server-config';
|
||||||
import { registerProxyAdvancedService } from '../../../ipc/proxy-advanced/service-registry';
|
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 {
|
export interface IdeConfigResult {
|
||||||
ide: SupportedIde;
|
ide: SupportedIde;
|
||||||
format: 'json' | 'shell' | 'env' | 'instructions';
|
format: 'json' | 'shell' | 'env' | 'instructions';
|
||||||
content: Record<string, string> | string;
|
content: Record<string, any> | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -46,6 +52,7 @@ export class ProxyIdeConfigService {
|
|||||||
port: number,
|
port: number,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
localIp: string,
|
localIp: string,
|
||||||
|
defaultModel: string = 'gemini-3.1-pro-high',
|
||||||
): IdeConfigResult {
|
): IdeConfigResult {
|
||||||
const baseUrl = `http://${localIp}:${port}/v1`;
|
const baseUrl = `http://${localIp}:${port}/v1`;
|
||||||
|
|
||||||
@@ -99,6 +106,20 @@ export class ProxyIdeConfigService {
|
|||||||
content: `OPENAI_API_KEY=${apiKey}\nOPENAI_BASE_URL=${baseUrl}`,
|
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: {
|
default: {
|
||||||
// Exhaustive fallback to satisfy TypeScript
|
// Exhaustive fallback to satisfy TypeScript
|
||||||
const exhaustiveCheck: never = ide;
|
const exhaustiveCheck: never = ide;
|
||||||
@@ -112,7 +133,7 @@ export class ProxyIdeConfigService {
|
|||||||
* Convenience helper that reads the current server config and local IP,
|
* Convenience helper that reads the current server config and local IP,
|
||||||
* then delegates to generateIdeConfig.
|
* then delegates to generateIdeConfig.
|
||||||
*/
|
*/
|
||||||
generateConfigForCurrentServer(ide: SupportedIde): IdeConfigResult | null {
|
generateConfigForCurrentServer(ide: SupportedIde, defaultModel?: string): IdeConfigResult | null {
|
||||||
const config = getServerConfig();
|
const config = getServerConfig();
|
||||||
if (!config) {
|
if (!config) {
|
||||||
this.logger.warn('Server config not available; cannot generate IDE config');
|
this.logger.warn('Server config not available; cannot generate IDE config');
|
||||||
@@ -121,6 +142,6 @@ export class ProxyIdeConfigService {
|
|||||||
const port = config.port;
|
const port = config.port;
|
||||||
const apiKey = config.api_key;
|
const apiKey = config.api_key;
|
||||||
const localIp = this.getLocalIp();
|
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