Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions src/app/api/claude-status/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { NextResponse } from 'next/server';
import { findClaudeBinary, getClaudeVersion } from '@/lib/platform';
import { getActiveProvider } from '@/lib/db';

export async function GET() {
try {
// If a non-anthropic provider is active, the Claude CLI subprocess is
// not used at all. Return connected=true immediately with provider info
// so the UI reflects the real connection state.
const activeProvider = getActiveProvider();
if (activeProvider && activeProvider.provider_type !== 'anthropic') {
return NextResponse.json({
connected: true,
version: null,
provider_name: activeProvider.name,
provider_type: activeProvider.provider_type,
});
}

const claudePath = findClaudeBinary();
if (!claudePath) {
return NextResponse.json({ connected: false, version: null });
Expand Down
50 changes: 28 additions & 22 deletions src/app/api/providers/models/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import { getAllProviders, getDefaultProviderId } from '@/lib/db';
import { PROVIDER_MODEL_RESOLUTION } from '@/lib/provider-models';
import type { ErrorResponse, ProviderModelGroup } from '@/types';

// Default Claude model options
Expand All @@ -9,43 +10,44 @@ const DEFAULT_MODELS = [
{ value: 'haiku', label: 'Haiku 4.5' },
];

// Provider-specific model label mappings (base_url -> alias -> display name)
// Provider-specific model label mappings (base_url -> actual_api_model_name -> display name).
// IMPORTANT: for non-Anthropic providers that use the direct API path (streamDirectFromProvider),
// the `value` field is sent verbatim to the provider's /v1/messages endpoint as the "model"
// parameter. It must be the actual API model name, NOT a CodePilot internal alias
// (sonnet / opus / haiku). Those aliases only work when the Claude Code CLI resolves them.
//
// Build PROVIDER_MODEL_LABELS from the shared PROVIDER_MODEL_RESOLUTION map.
// This ensures consistency with claude-client.ts model alias resolution.
const PROVIDER_MODEL_LABELS: Record<string, { value: string; label: string }[]> = {
// ── BigModel / GLM (智谱 AI) ────────────────────────────────────────────────
'https://api.z.ai/api/anthropic': [
{ value: 'sonnet', label: 'GLM-4.7' },
{ value: 'opus', label: 'GLM-5' },
{ value: 'haiku', label: 'GLM-4.5-Air' },
{ value: 'glm-4.7', label: 'GLM-4.7' },
{ value: 'glm-5', label: 'GLM-5' },
{ value: 'glm-4.5-air', label: 'GLM-4.5-Air' },
],
'https://open.bigmodel.cn/api/anthropic': [
{ value: 'sonnet', label: 'GLM-4.7' },
{ value: 'opus', label: 'GLM-5' },
{ value: 'haiku', label: 'GLM-4.5-Air' },
{ value: 'glm-4.7', label: 'GLM-4.7' },
{ value: 'glm-5', label: 'GLM-5' },
{ value: 'glm-4.5-air', label: 'GLM-4.5-Air' },
],
// ── Kimi / Moonshot ─────────────────────────────────────────────────────────
'https://api.kimi.com/coding/': [
{ value: 'sonnet', label: 'Kimi K2.5' },
{ value: 'opus', label: 'Kimi K2.5' },
{ value: 'haiku', label: 'Kimi K2.5' },
{ value: 'kimi-k2.5', label: 'Kimi K2.5' },
],
'https://api.moonshot.ai/anthropic': [
{ value: 'sonnet', label: 'Kimi K2.5' },
{ value: 'opus', label: 'Kimi K2.5' },
{ value: 'haiku', label: 'Kimi K2.5' },
{ value: 'kimi-k2.5', label: 'Kimi K2.5' },
],
'https://api.moonshot.cn/anthropic': [
{ value: 'sonnet', label: 'Kimi K2.5' },
{ value: 'opus', label: 'Kimi K2.5' },
{ value: 'haiku', label: 'Kimi K2.5' },
{ value: 'kimi-k2.5', label: 'Kimi K2.5' },
],
// ── MiniMax ─────────────────────────────────────────────────────────────────
'https://api.minimaxi.com/anthropic': [
{ value: 'sonnet', label: 'MiniMax-M2.5' },
{ value: 'opus', label: 'MiniMax-M2.5' },
{ value: 'haiku', label: 'MiniMax-M2.5' },
{ value: 'MiniMax-M2.5', label: 'MiniMax-M2.5' },
],
'https://api.minimax.io/anthropic': [
{ value: 'sonnet', label: 'MiniMax-M2.5' },
{ value: 'opus', label: 'MiniMax-M2.5' },
{ value: 'haiku', label: 'MiniMax-M2.5' },
{ value: 'MiniMax-M2.5', label: 'MiniMax-M2.5' },
],
// ── OpenRouter ──────────────────────────────────────────────────────────────
'https://openrouter.ai/api': [
{ value: 'sonnet', label: 'Sonnet 4.6' },
{ value: 'opus', label: 'Opus 4.6' },
Expand All @@ -62,6 +64,10 @@ const PROVIDER_MODEL_LABELS: Record<string, { value: string; label: string }[]>
],
};

// Note: PROVIDER_MODEL_RESOLUTION is now imported from @/lib/provider-models
// to maintain a single source of truth for model alias resolution.
// If you need to add a new provider, update provider-models.ts.

/**
* Deduplicate models: if multiple aliases map to the same label, keep only the first one.
*/
Expand Down
13 changes: 11 additions & 2 deletions src/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ export default function NewChatPage() {
const [statusText, setStatusText] = useState<string | undefined>();
const [workingDir, setWorkingDir] = useState('');
const [mode, setMode] = useState('code');
const [currentModel, setCurrentModel] = useState('sonnet');
const [currentProviderId, setCurrentProviderId] = useState('');
// Restore last-used model/provider from localStorage so the new chat page
// behaves consistently with ChatView (where the same pattern is used).
// Without this, every new chat page visit reset the provider to '' (empty),
// causing the API to fall back to env-var Claude even when the user had
// previously selected a third-party provider like Kimi. (Issue #85)
const [currentModel, setCurrentModel] = useState(
(typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') : null) || 'sonnet'
);
const [currentProviderId, setCurrentProviderId] = useState(
(typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') : null) || ''
);
const [pendingPermission, setPendingPermission] = useState<PermissionRequestEvent | null>(null);
const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null);
const [streamingToolOutput, setStreamingToolOutput] = useState('');
Expand Down
45 changes: 35 additions & 10 deletions src/components/layout/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { InstallWizard } from "@/components/layout/InstallWizard";
interface ClaudeStatus {
connected: boolean;
version: string | null;
/** Present when a non-anthropic provider is active (Claude CLI not needed) */
provider_name?: string;
provider_type?: string;
}

const BASE_INTERVAL = 30_000; // 30s
Expand Down Expand Up @@ -143,29 +146,51 @@ export function ConnectionStatus() {
{status === null
? t('connection.checking')
: connected
? t('connection.connected')
? status.provider_name
? t('connection.connectedToProvider', { provider: status.provider_name })
: t('connection.connected')
: t('connection.disconnected')}
</button>

<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{connected ? t('connection.installed') : t('connection.notInstalled')}
{status?.provider_name
? t('connection.connectedToProviderTitle', { provider: status.provider_name })
: connected ? t('connection.installed') : t('connection.notInstalled')}
</DialogTitle>
<DialogDescription>
{connected
? `Claude Code CLI v${status?.version} is running and ready.`
: "Claude Code CLI is required to use this application."}
{status?.provider_name
? t('connection.usingCustomProvider')
: connected
? t('connection.cliRunning', { version: status?.version ?? '' })
: t('connection.cliRequired')}
</DialogDescription>
</DialogHeader>

{connected ? (
{status?.provider_name ? (
// Non-anthropic custom provider: no Claude CLI needed
<div className="space-y-3 text-sm">
<div className="flex items-center gap-3 rounded-lg bg-emerald-500/10 px-4 py-3">
<span className="block h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-500" />
<div>
<p className="font-medium text-emerald-700 dark:text-emerald-400">Active</p>
<p className="font-medium text-emerald-700 dark:text-emerald-400">
{status.provider_name}
</p>
<p className="text-xs text-muted-foreground">
{t('connection.customProviderHint')}
</p>
</div>
</div>
</div>
) : connected ? (
// Claude CLI is installed and running
<div className="space-y-3 text-sm">
<div className="flex items-center gap-3 rounded-lg bg-emerald-500/10 px-4 py-3">
<span className="block h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-500" />
<div>
<p className="font-medium text-emerald-700 dark:text-emerald-400">{t('connection.active')}</p>
<p className="text-xs text-muted-foreground">{t('connection.version', { version: status?.version ?? '' })}</p>
</div>
</div>
Expand All @@ -174,7 +199,7 @@ export function ConnectionStatus() {
<div className="space-y-4 text-sm">
<div className="flex items-center gap-3 rounded-lg bg-red-500/10 px-4 py-3">
<span className="block h-2.5 w-2.5 shrink-0 rounded-full bg-red-500" />
<p className="font-medium text-red-700 dark:text-red-400">Not detected</p>
<p className="font-medium text-red-700 dark:text-red-400">{t('connection.notDetected')}</p>
</div>

<div>
Expand All @@ -185,14 +210,14 @@ export function ConnectionStatus() {
</div>

<div>
<h4 className="font-medium mb-1.5">2. Authenticate</h4>
<h4 className="font-medium mb-1.5">2. {t('connection.authenticate')}</h4>
<code className="block rounded-md bg-muted px-3 py-2 text-xs">
claude login
</code>
</div>

<div>
<h4 className="font-medium mb-1.5">3. Verify Installation</h4>
<h4 className="font-medium mb-1.5">3. {t('connection.verifyInstallation')}</h4>
<code className="block rounded-md bg-muted px-3 py-2 text-xs">
claude --version
</code>
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ const en = {
'connection.connected': 'Connected',
'connection.disconnected': 'Disconnected',
'connection.checking': 'Checking',
'connection.connectedToProvider': 'Connected · {provider}',
'connection.connectedToProviderTitle': 'Connected to {provider}',
'connection.usingCustomProvider': 'Using custom API provider — Claude Code CLI is not required.',
'connection.cliRunning': 'Claude Code CLI v{version} is running and ready.',
'connection.cliRequired': 'Claude Code CLI is required to use this application.',
'connection.customProviderHint': 'Custom API provider · Claude CLI not required',
'connection.active': 'Active',
'connection.notDetected': 'Not detected',
'connection.authenticate': 'Authenticate',
'connection.verifyInstallation': 'Verify Installation',

// ── Install wizard ──────────────────────────────────────────
'install.title': 'Install Claude Code',
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,16 @@ const zh: Record<TranslationKey, string> = {
'connection.connected': '已连接',
'connection.disconnected': '未连接',
'connection.checking': '检测中',
'connection.connectedToProvider': '已连接 · {provider}',
'connection.connectedToProviderTitle': '已连接到 {provider}',
'connection.usingCustomProvider': '使用自定义 API 服务商 — 无需 Claude Code CLI。',
'connection.cliRunning': 'Claude Code CLI v{version} 正在运行。',
'connection.cliRequired': '需要安装 Claude Code CLI 才能使用本应用。',
'connection.customProviderHint': '自定义 API 服务商 · 无需 Claude CLI',
'connection.active': '活跃',
'connection.notDetected': '未检测到',
'connection.authenticate': '身份验证',
'connection.verifyInstallation': '验证安装',

// ── Install wizard ──────────────────────────────────────────
'install.title': '安装 Claude Code',
Expand Down
34 changes: 33 additions & 1 deletion src/lib/bridge/adapters/telegram-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ export class TelegramAdapter extends BaseChannelAdapter {
}

private enqueue(msg: InboundMessage): void {
// ── [DIAG-4] Message enqueued ────────────────────────────────────────
console.log(`[telegram-adapter][DIAG] 📥 Enqueued messageId=${msg.messageId} chatId=${msg.address.chatId} text="${msg.text.slice(0, 50)}" | waiters=${this.waiters.length} queue=${this.queue.length}`);
// ─────────────────────────────────────────────────────────────────────
const waiter = this.waiters.shift();
if (waiter) {
waiter(msg);
Expand Down Expand Up @@ -494,6 +497,13 @@ export class TelegramAdapter extends BaseChannelAdapter {
continue;
}
const updates: TelegramUpdate[] = data.result;

// ── [DIAG-1] Log every getUpdates batch (even empty) ──────────────
if (updates.length > 0) {
console.log(`[telegram-adapter][DIAG] ✅ Received ${updates.length} update(s) from Telegram. update_ids: [${updates.map(u => u.update_id).join(', ')}]`);
}
// ──────────────────────────────────────────────────────────────────

for (const update of updates) {
// Advance fetchOffset so the next getUpdates call skips this update
if (update.update_id >= fetchOffset) {
Expand All @@ -502,6 +512,7 @@ export class TelegramAdapter extends BaseChannelAdapter {

// Idempotency: skip updates already processed (dedup on restart)
if (this.recentUpdateIds.has(update.update_id)) {
console.log(`[telegram-adapter][DIAG] ⏭️ update_id=${update.update_id} already processed (dedup), skipping`);
this.markUpdateProcessed(update.update_id);
continue;
}
Expand All @@ -511,8 +522,13 @@ export class TelegramAdapter extends BaseChannelAdapter {
const chatId = cb.message?.chat.id ? String(cb.message.chat.id) : '';
const userId = String(cb.from.id);

// ── [DIAG-2] Callback auth check ─────────────────────────────
console.log(`[telegram-adapter][DIAG] 🔘 Callback from userId=${userId} chatId=${chatId} | authorized=${this.isAuthorized(userId, chatId)}`);
// ─────────────────────────────────────────────────────────────

if (!this.isAuthorized(userId, chatId)) {
console.warn('[telegram-adapter] Unauthorized callback from userId:', userId, 'chatId:', chatId);
// Silently skip unauthorized callbacks — no need to reply to button presses from unknown users
this.markUpdateProcessed(update.update_id);
continue;
}
Expand Down Expand Up @@ -543,8 +559,24 @@ export class TelegramAdapter extends BaseChannelAdapter {
const userId = m.from ? String(m.from.id) : chatId;
const displayName = m.from?.username || m.from?.first_name || chatId;

if (!this.isAuthorized(userId, chatId)) {
// ── [DIAG-3] Message auth check ──────────────────────────────
const authorized = this.isAuthorized(userId, chatId);
console.log(`[telegram-adapter][DIAG] 📨 Message from userId=${userId} chatId=${chatId} text="${(m.text ?? m.caption ?? '').slice(0, 50)}" | authorized=${authorized}`);
// ─────────────────────────────────────────────────────────────

if (!authorized) {
console.warn('[telegram-adapter] Unauthorized message from userId:', userId, 'chatId:', chatId);
// Send explicit rejection notice instead of silent drop.
// This prevents the "message black hole" symptom where the user
// sends a message and receives zero feedback.
// Note: `token` is already declared at the top of this try-block — no re-declaration needed.
if (token) {
callTelegramApi(token, 'sendMessage', {
chat_id: chatId,
text: '⛔ Unauthorized: your user ID is not in the Bridge allowed list.\n\nPlease add your Telegram user ID to the "Allowed Users" field in CodePilot → Bridge → Telegram settings.',
disable_web_page_preview: true,
}).catch(() => {});
}
this.markUpdateProcessed(update.update_id);
continue;
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/bridge/bridge-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ async function handleMessage(
adapter: BaseChannelAdapter,
msg: InboundMessage,
): Promise<void> {
// ── [DIAG-5] handleMessage entry ────────────────────────────────────────
console.log(`[bridge-manager][DIAG] 🚀 handleMessage started | channel=${adapter.channelType} chatId=${msg.address.chatId} messageId=${msg.messageId} isCallback=${!!msg.callbackData} text="${msg.text.slice(0, 60)}"`);
// ─────────────────────────────────────────────────────────────────────────

// Update lastMessageAt for this adapter
const adapterState = getState();
const meta = adapterState.adapterMeta.get(adapter.channelType) || { lastMessageAt: null, lastError: null };
Expand Down
Loading