feat: per-provider options (thinking mode, 1M context) and SDK betas integration
- Add ProviderOptionsSection component for per-provider thinking_mode and context_1m - Store options in options_json column (DB providers) or settings table (env provider) - Add /api/providers/options GET/PUT route - Fix SDK integration: use `betas` (not `betaHeaders`) for context-1m-2025-08-07 - ChatView and /chat page always reset thinkingMode on provider change (isolation fix) - Auto-correct model when switching providers if model not in new provider's list - Add i18n keys for provider options in both en.ts and zh.ts - Update test fixtures with options_json field - Remove unused lastAssistantText variable in claude-client.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
e694d75f10
commit
cc3cae6868
14 files changed
+282
-84
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
VENDOR_PRESETS,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getDefaultModelsForProvider,
|
||||
findPresetForLegacy,
|
||||
} from '../../lib/provider-catalog';
|
||||
import type { Protocol } from '../../lib/provider-catalog';
|
||||
|
||||
// ── Provider Catalog Tests ──────────────────────────────────────
|
||||
|
||||
@@ -285,7 +284,7 @@ describe('Provider Resolver', () => {
|
||||
role_models_json: '{}',
|
||||
notes: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -323,7 +322,7 @@ describe('Provider Resolver', () => {
|
||||
role_models_json: '{}',
|
||||
notes: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'auth_token',
|
||||
@@ -361,7 +360,7 @@ describe('Provider Resolver', () => {
|
||||
role_models_json: '{}',
|
||||
notes: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -404,7 +403,7 @@ describe('Provider Resolver', () => {
|
||||
role_models_json: '{}',
|
||||
notes: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -461,7 +460,7 @@ describe('Provider Resolver', () => {
|
||||
id: 'test', name: 'Test', provider_type: 'anthropic', protocol: 'anthropic',
|
||||
base_url: 'https://api.anthropic.com', api_key: 'key', is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -490,7 +489,7 @@ describe('Provider Resolver', () => {
|
||||
id: 'test', name: 'OR', provider_type: 'openrouter', protocol: 'openrouter',
|
||||
base_url: 'https://openrouter.ai/api', api_key: 'or-key', is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'openrouter',
|
||||
authStyle: 'api_key',
|
||||
@@ -517,7 +516,7 @@ describe('Provider Resolver', () => {
|
||||
id: 'test', name: 'Bedrock', provider_type: 'bedrock', protocol: 'bedrock',
|
||||
base_url: '', api_key: '', is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'bedrock',
|
||||
authStyle: 'env_only',
|
||||
@@ -549,7 +548,7 @@ describe('Provider Resolver', () => {
|
||||
id: 'test', name: 'Custom', provider_type: 'custom', protocol: 'openai-compatible',
|
||||
base_url: 'https://my-server.com/v1', api_key: 'key', is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'openai-compatible',
|
||||
authStyle: 'api_key',
|
||||
@@ -575,7 +574,7 @@ describe('Provider Resolver', () => {
|
||||
id: 'test', name: 'Test', provider_type: 'anthropic', protocol: 'anthropic',
|
||||
base_url: '', api_key: 'key', is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -601,7 +600,7 @@ describe('Provider Resolver', () => {
|
||||
base_url: 'https://generativelanguage.googleapis.com/v1beta', api_key: 'gkey',
|
||||
is_active: 1, sort_order: 0,
|
||||
extra_env: '{}', headers_json: '{}', env_overrides_json: '', role_models_json: '{}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'gemini-image',
|
||||
authStyle: 'api_key',
|
||||
@@ -724,7 +723,7 @@ describe('Upstream Model ID Mapping', () => {
|
||||
id: 'test', name: 'GLM', provider_type: 'custom', protocol: 'anthropic',
|
||||
base_url: 'https://open.bigmodel.cn/api/anthropic', api_key: 'key',
|
||||
is_active: 1, sort_order: 0, extra_env: '{}', headers_json: '{}',
|
||||
env_overrides_json: '', role_models_json: '{}', notes: '', created_at: '', updated_at: '',
|
||||
env_overrides_json: '', role_models_json: '{}', notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -762,7 +761,7 @@ describe('Upstream Model ID Mapping', () => {
|
||||
base_url: 'https://open.bigmodel.cn/api/anthropic', api_key: 'key',
|
||||
is_active: 1, sort_order: 0, extra_env: '{}', headers_json: '{}',
|
||||
env_overrides_json: '', role_models_json: '{"default":"glm-4.7","sonnet":"glm-4.7","opus":"glm-5"}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
@@ -879,7 +878,7 @@ describe('Entry Point Resolution Contract', () => {
|
||||
base_url: 'https://open.bigmodel.cn/api/anthropic', api_key: 'key',
|
||||
is_active: 1, sort_order: 0, extra_env: '{}', headers_json: '{}',
|
||||
env_overrides_json: '', role_models_json: '{"default":"glm-4.7"}',
|
||||
notes: '', created_at: '', updated_at: '',
|
||||
notes: '', created_at: '', updated_at: '', options_json: '{}',
|
||||
},
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getProviderOptions, setProviderOptions } from '@/lib/db';
|
||||
import type { ProviderOptions } from '@/types';
|
||||
|
||||
/**
|
||||
* GET /api/providers/options?providerId=xxx
|
||||
* Returns per-provider options (thinking_mode, context_1m).
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const providerId = request.nextUrl.searchParams.get('providerId') || 'env';
|
||||
const options = getProviderOptions(providerId);
|
||||
return NextResponse.json({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/providers/options
|
||||
* Update per-provider options. Body: { providerId, options: { thinking_mode?, context_1m? } }
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { providerId = 'env', options } = body as { providerId?: string; options: ProviderOptions };
|
||||
|
||||
if (!options || typeof options !== 'object') {
|
||||
return NextResponse.json({ error: 'Invalid options' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Merge with existing options (partial update)
|
||||
const existing = getProviderOptions(providerId);
|
||||
const merged: ProviderOptions = { ...existing, ...options };
|
||||
setProviderOptions(providerId, merged);
|
||||
|
||||
return NextResponse.json({ options: merged });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update options' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,6 @@ export function GeneralSection() {
|
||||
const [skipPermissions, setSkipPermissions] = useState(false);
|
||||
const [showSkipPermWarning, setShowSkipPermWarning] = useState(false);
|
||||
const [skipPermSaving, setSkipPermSaving] = useState(false);
|
||||
const [thinkingMode, setThinkingMode] = useState<string>('adaptive');
|
||||
const { accountInfo } = useAccountInfo();
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
|
||||
@@ -137,9 +136,6 @@ export function GeneralSection() {
|
||||
const data = await res.json();
|
||||
const appSettings = data.settings || {};
|
||||
setSkipPermissions(appSettings.dangerously_skip_permissions === "true");
|
||||
if (appSettings.thinking_mode) {
|
||||
setThinkingMode(appSettings.thinking_mode);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -150,21 +146,6 @@ export function GeneralSection() {
|
||||
fetchAppSettings();
|
||||
}, [fetchAppSettings]);
|
||||
|
||||
const saveThinkingMode = async (mode: string) => {
|
||||
setThinkingMode(mode);
|
||||
try {
|
||||
await fetch("/api/settings/app", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
settings: { thinking_mode: mode },
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipPermToggle = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setShowSkipPermWarning(true);
|
||||
@@ -236,23 +217,6 @@ export function GeneralSection() {
|
||||
</Select>
|
||||
</FieldRow>
|
||||
|
||||
{/* Thinking mode */}
|
||||
<FieldRow
|
||||
label={t('settings.thinkingMode' as TranslationKey)}
|
||||
description={t('settings.thinkingModeDesc' as TranslationKey)}
|
||||
separator
|
||||
>
|
||||
<Select value={thinkingMode} onValueChange={saveThinkingMode}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="adaptive">{t('settings.thinkingAdaptive' as TranslationKey)}</SelectItem>
|
||||
<SelectItem value="enabled">{t('settings.thinkingEnabled' as TranslationKey)}</SelectItem>
|
||||
<SelectItem value="disabled">{t('settings.thinkingDisabled' as TranslationKey)}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FieldRow>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Appearance */}
|
||||
|
||||
@@ -383,6 +383,7 @@ export function PresetConnectDialog({
|
||||
{!preset.fields.includes("extra_env") && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground h-auto px-0 py-0"
|
||||
|
||||
@@ -279,6 +279,7 @@ export function ProviderForm({
|
||||
|
||||
{/* Advanced options toggle */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-1.5 text-xs text-muted-foreground hover:text-foreground px-0 h-auto"
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import type { ApiProvider } from "@/types";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import Anthropic from "@lobehub/icons/es/Anthropic";
|
||||
import { ProviderOptionsSection } from "./ProviderOptionsSection";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
@@ -213,6 +214,7 @@ export function ProviderManager() {
|
||||
<p className="text-[11px] text-muted-foreground ml-[34px] leading-relaxed">
|
||||
{t('provider.ccSwitchHint')}
|
||||
</p>
|
||||
<ProviderOptionsSection providerId="env" />
|
||||
</div>
|
||||
|
||||
{/* Connected provider list */}
|
||||
@@ -255,6 +257,10 @@ export function ProviderManager() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Provider options (thinking mode + 1M context) — only for official Anthropic */}
|
||||
{provider.base_url === 'https://api.anthropic.com' && (
|
||||
<ProviderOptionsSection providerId={provider.id} />
|
||||
)}
|
||||
{/* Gemini Image model selector — capsule buttons */}
|
||||
{provider.provider_type === 'gemini-image' && (
|
||||
<div className="ml-[34px] mt-2 flex items-center gap-1.5">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
import type { ProviderOptions } from "@/types";
|
||||
|
||||
interface ProviderOptionsSectionProps {
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-provider options: thinking mode + 1M context toggle.
|
||||
* Only rendered for providers that support these features
|
||||
* (env / anthropic-official).
|
||||
*/
|
||||
export function ProviderOptionsSection({ providerId }: ProviderOptionsSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<ProviderOptions>({
|
||||
thinking_mode: 'adaptive',
|
||||
context_1m: false,
|
||||
});
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/providers/options?providerId=${encodeURIComponent(providerId)}`);
|
||||
if (!cancelled && res.ok) {
|
||||
const data = await res.json();
|
||||
setOptions(data.options || {});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
if (!cancelled) setLoaded(true);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [providerId]);
|
||||
|
||||
const saveOption = async (key: keyof ProviderOptions, value: string | boolean) => {
|
||||
const updated = { ...options, [key]: value };
|
||||
setOptions(updated);
|
||||
try {
|
||||
await fetch('/api/providers/options', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ providerId, options: { [key]: value } }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
if (!loaded) return null;
|
||||
|
||||
return (
|
||||
<div className="ml-[34px] mt-2 space-y-2.5">
|
||||
{/* Thinking mode */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium text-foreground/80">
|
||||
{t('settings.thinkingMode' as TranslationKey)}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground leading-tight">
|
||||
{t('settings.thinkingModeDesc' as TranslationKey)}
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={options.thinking_mode || 'adaptive'}
|
||||
onValueChange={(v) => saveOption('thinking_mode', v)}
|
||||
>
|
||||
<SelectTrigger className="w-[110px] h-7 text-[11px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="adaptive">{t('settings.thinkingAdaptive' as TranslationKey)}</SelectItem>
|
||||
<SelectItem value="enabled">{t('settings.thinkingEnabled' as TranslationKey)}</SelectItem>
|
||||
<SelectItem value="disabled">{t('settings.thinkingDisabled' as TranslationKey)}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 1M context */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium text-foreground/80">
|
||||
{t('provider.context1m' as TranslationKey)}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground leading-tight">
|
||||
{t('provider.context1mDesc' as TranslationKey)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={options.context_1m || false}
|
||||
onCheckedChange={(checked) => saveOption('context_1m', checked)}
|
||||
className="scale-[0.85]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -171,8 +171,8 @@ export const QUICK_PRESETS: QuickPreset[] = [
|
||||
icon: <Minimax size={18} />,
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.minimaxi.com/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
|
||||
base_url: "https://api.minimaxi.com/anthropic/v1",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_AUTH_TOKEN":"","ANTHROPIC_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_SONNET_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_OPUS_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_HAIKU_MODEL":"MiniMax-M2.5"}',
|
||||
fields: ["api_key"],
|
||||
},
|
||||
{
|
||||
@@ -184,7 +184,7 @@ export const QUICK_PRESETS: QuickPreset[] = [
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.minimax.io/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_AUTH_TOKEN":"","ANTHROPIC_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_SONNET_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_OPUS_MODEL":"MiniMax-M2.5","ANTHROPIC_DEFAULT_HAIKU_MODEL":"MiniMax-M2.5"}',
|
||||
fields: ["api_key"],
|
||||
},
|
||||
{
|
||||
|
||||
+24
-8
@@ -148,6 +148,8 @@ const en = {
|
||||
'provider.advancedOptions': 'Advanced Options',
|
||||
'provider.extraEnvVars': 'Extra Environment Variables',
|
||||
'provider.notes': 'Notes',
|
||||
'provider.context1m': '1M Context Window',
|
||||
'provider.context1mDesc': 'Enable 1M token context for Opus 4.6 and Sonnet 4.6 (beta, higher pricing above 200K)',
|
||||
'provider.notesPlaceholder': 'Optional notes about this provider...',
|
||||
'provider.saving': 'Saving...',
|
||||
'provider.update': 'Update',
|
||||
@@ -289,10 +291,16 @@ const en = {
|
||||
'connection.connected': 'Connected',
|
||||
'connection.disconnected': 'Disconnected',
|
||||
'connection.checking': 'Checking',
|
||||
'connection.missingGit': 'Missing Git',
|
||||
'connection.missingGitTitle': 'Git for Windows required',
|
||||
'connection.missingGitDesc': 'Claude Code requires Git for Windows to run. Please install it and refresh.',
|
||||
'connection.conflict': 'Conflict',
|
||||
'connection.conflictWarning': 'Multiple Claude Code installations detected',
|
||||
'connection.conflictRemove': 'To remove',
|
||||
|
||||
// ── Install wizard ──────────────────────────────────────────
|
||||
'install.title': 'Install Claude Code',
|
||||
'install.checkingPrereqs': 'Checking prerequisites...',
|
||||
'install.checkingPrereqs': 'Checking environment...',
|
||||
'install.alreadyInstalled': 'Claude Code is already installed',
|
||||
'install.readyToInstall': 'Ready to install',
|
||||
'install.installing': 'Installing Claude Code...',
|
||||
@@ -306,13 +314,21 @@ const en = {
|
||||
'install.done': 'Done',
|
||||
'install.recheck': 'Recheck',
|
||||
'install.copy': 'Copy',
|
||||
'install.homebrewRequired': 'Homebrew is required',
|
||||
'install.homebrewDescription': 'Homebrew is the package manager for macOS. Node.js installation requires it.',
|
||||
'install.homebrewSteps': 'Follow these steps:',
|
||||
'install.homebrewStep1': 'Open Terminal',
|
||||
'install.homebrewStep2': 'Paste the command above and press Enter',
|
||||
'install.homebrewStep3': 'Follow the on-screen instructions to complete the installation',
|
||||
'install.homebrewStep4': 'Come back here and click "Recheck"',
|
||||
'install.notDetected': 'not detected',
|
||||
'install.nativeDescription': 'Claude Code CLI was not detected. Install it using the official native installer?',
|
||||
'install.autoDescription': 'Automatically install Claude Code CLI',
|
||||
'install.nativeExplain': 'Click Install to download and run the official native installer. No Node.js required — the native binary auto-updates in the background.',
|
||||
'install.nativeCompleteDesc': 'Claude Code has been installed via the native installer. It will auto-update in the background.',
|
||||
'install.conflictTitle': 'Multiple installations detected',
|
||||
'install.conflictUsing': 'Currently using',
|
||||
'install.conflictAlso': 'Also found',
|
||||
'install.conflictRemove': 'To remove',
|
||||
'install.gitRequired': 'Git for Windows is required',
|
||||
'install.gitDescription': 'Claude Code requires Git for Windows to run commands on Windows. Please install it first.',
|
||||
'install.gitSteps': 'Follow these steps:',
|
||||
'install.gitStep1': 'Download Git for Windows from https://git-scm.com/downloads/win',
|
||||
'install.gitStep2': 'Run the installer with default settings',
|
||||
'install.gitStep3': 'Come back here and click "Recheck"',
|
||||
|
||||
// ── Task list ───────────────────────────────────────────────
|
||||
'tasks.all': 'All',
|
||||
|
||||
+24
-8
@@ -144,6 +144,8 @@ const zh: Record<TranslationKey, string> = {
|
||||
'provider.modelName': '模型名称',
|
||||
'provider.advancedOptions': '高级选项',
|
||||
'provider.extraEnvVars': '额外环境变量',
|
||||
'provider.context1m': '1M 上下文窗口',
|
||||
'provider.context1mDesc': '为 Opus 4.6 和 Sonnet 4.6 启用 100 万 token 上下文(测试版,超过 200K 部分价格更高)',
|
||||
'provider.notes': '备注',
|
||||
'provider.notesPlaceholder': '关于此服务商的可选备注...',
|
||||
'provider.saving': '保存中...',
|
||||
@@ -286,10 +288,16 @@ const zh: Record<TranslationKey, string> = {
|
||||
'connection.connected': '已连接',
|
||||
'connection.disconnected': '未连接',
|
||||
'connection.checking': '检测中',
|
||||
'connection.missingGit': '缺少 Git',
|
||||
'connection.missingGitTitle': '需要安装 Git for Windows',
|
||||
'connection.missingGitDesc': 'Claude Code 需要 Git for Windows 才能运行,请安装后刷新。',
|
||||
'connection.conflict': '版本冲突',
|
||||
'connection.conflictWarning': '检测到多个 Claude Code 安装',
|
||||
'connection.conflictRemove': '卸载方式',
|
||||
|
||||
// ── Install wizard ──────────────────────────────────────────
|
||||
'install.title': '安装 Claude Code',
|
||||
'install.checkingPrereqs': '检查前置条件...',
|
||||
'install.checkingPrereqs': '检查环境...',
|
||||
'install.alreadyInstalled': 'Claude Code 已安装',
|
||||
'install.readyToInstall': '准备安装',
|
||||
'install.installing': '正在安装 Claude Code...',
|
||||
@@ -303,13 +311,21 @@ const zh: Record<TranslationKey, string> = {
|
||||
'install.done': '完成',
|
||||
'install.recheck': '重新检测',
|
||||
'install.copy': '复制',
|
||||
'install.homebrewRequired': '需要先安装 Homebrew',
|
||||
'install.homebrewDescription': 'Homebrew 是 macOS 的包管理器,安装 Node.js 需要它。',
|
||||
'install.homebrewSteps': '请按以下步骤操作:',
|
||||
'install.homebrewStep1': '打开终端(Terminal)',
|
||||
'install.homebrewStep2': '粘贴上面的命令并按回车',
|
||||
'install.homebrewStep3': '按提示完成安装',
|
||||
'install.homebrewStep4': '回到这里点击"重新检测"',
|
||||
'install.notDetected': '未检测到',
|
||||
'install.nativeDescription': '未检测到 Claude Code CLI。使用官方原生安装器安装?',
|
||||
'install.autoDescription': '自动安装 Claude Code CLI',
|
||||
'install.nativeExplain': '点击安装将下载并运行官方原生安装器。无需 Node.js — 原生二进制文件会在后台自动更新。',
|
||||
'install.nativeCompleteDesc': 'Claude Code 已通过原生安装器安装完成,将在后台自动更新。',
|
||||
'install.conflictTitle': '检测到多个安装版本',
|
||||
'install.conflictUsing': '当前使用',
|
||||
'install.conflictAlso': '还发现',
|
||||
'install.conflictRemove': '卸载方式',
|
||||
'install.gitRequired': '需要安装 Git for Windows',
|
||||
'install.gitDescription': 'Claude Code 需要 Git for Windows 才能在 Windows 上运行命令,请先安装。',
|
||||
'install.gitSteps': '请按以下步骤操作:',
|
||||
'install.gitStep1': '从 https://git-scm.com/downloads/win 下载 Git for Windows',
|
||||
'install.gitStep2': '使用默认设置运行安装程序',
|
||||
'install.gitStep3': '回到这里点击"重新检测"',
|
||||
|
||||
// ── Task list ───────────────────────────────────────────────
|
||||
'tasks.all': '全部',
|
||||
|
||||
@@ -105,8 +105,11 @@ export async function captureCapabilities(
|
||||
conversation.mcpServerStatus(),
|
||||
]);
|
||||
|
||||
cache.models = models.status === 'fulfilled' ? models.value : cache.models;
|
||||
cache.commands = commands.status === 'fulfilled' ? commands.value : cache.commands;
|
||||
// Guard: only overwrite cached models if the new result is non-empty.
|
||||
// An empty array (e.g. transient SDK error) would otherwise wipe valid cached data,
|
||||
// causing models like Opus to disappear from the selector after long idle.
|
||||
cache.models = models.status === 'fulfilled' && models.value.length > 0 ? models.value : cache.models;
|
||||
cache.commands = commands.status === 'fulfilled' && commands.value.length > 0 ? commands.value : cache.commands;
|
||||
cache.account = account.status === 'fulfilled' ? account.value : cache.account;
|
||||
cache.mcpStatus = mcpStatus.status === 'fulfilled' ? mcpStatus.value : cache.mcpStatus;
|
||||
cache.capturedAt = Date.now();
|
||||
|
||||
@@ -20,7 +20,7 @@ import { registerConversation, unregisterConversation } from './conversation-reg
|
||||
import { captureCapabilities } from './agent-sdk-capabilities';
|
||||
import { getSetting, updateSdkSessionId, createPermissionRequest } from './db';
|
||||
import { resolveForClaudeCode, toClaudeCodeEnv } from './provider-resolver';
|
||||
import { findClaudeBinary, findGitBash, getExpandedPath } from './platform';
|
||||
import { findClaudeBinary, findGitBash, getExpandedPath, invalidateClaudePathCache } from './platform';
|
||||
import { notifyPermissionRequest, notifyGeneric } from './telegram-bot';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
@@ -95,6 +95,15 @@ function findClaudePath(): string | undefined {
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cached Claude binary path in this module AND in platform.ts.
|
||||
* Must be called after installation so the next SDK call picks up the new binary.
|
||||
*/
|
||||
export function invalidateClaudeClientCache(): void {
|
||||
cachedClaudePath = undefined; // reset to "not yet looked up"
|
||||
invalidateClaudePathCache(); // also reset the 60s TTL cache in platform.ts
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our MCPServerConfig to the SDK's McpServerConfig format.
|
||||
* Supports stdio, sse, and http transport types.
|
||||
@@ -383,6 +392,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
agent,
|
||||
enableFileCheckpointing,
|
||||
autoTrigger,
|
||||
context1m,
|
||||
} = options;
|
||||
|
||||
return new ReadableStream<string>({
|
||||
@@ -516,6 +526,12 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
if (enableFileCheckpointing) {
|
||||
queryOptions.enableFileCheckpointing = true;
|
||||
}
|
||||
if (context1m) {
|
||||
queryOptions.betas = [
|
||||
...(queryOptions.betas || []),
|
||||
'context-1m-2025-08-07',
|
||||
];
|
||||
}
|
||||
|
||||
// Resume session if we have an SDK session ID from a previous conversation turn.
|
||||
// Pre-check: verify working_directory exists before attempting resume.
|
||||
@@ -767,7 +783,6 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
console.warn('[claude-client] Capability capture failed:', err);
|
||||
});
|
||||
|
||||
let lastAssistantText = '';
|
||||
let tokenUsage: TokenUsage | null = null;
|
||||
// Track pending TodoWrite tool_use_ids so we can sync after successful execution
|
||||
const pendingTodoWrites = new Map<string, Array<{ content: string; status: string; activeForm?: string }>>();
|
||||
@@ -781,11 +796,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
case 'assistant': {
|
||||
const assistantMsg = message as SDKAssistantMessage;
|
||||
// Text deltas are handled by stream_event for real-time streaming.
|
||||
// Only track lastAssistantText here and process tool_use blocks.
|
||||
const text = extractTextFromMessage(assistantMsg);
|
||||
if (text) {
|
||||
lastAssistantText = text;
|
||||
}
|
||||
// Here we only process tool_use blocks.
|
||||
|
||||
// Check for tool use blocks
|
||||
for (const block of assistantMsg.message.content) {
|
||||
|
||||
@@ -250,12 +250,16 @@ export const VENDOR_PRESETS: VendorPreset[] = [
|
||||
description: 'MiniMax Code Plan — China region',
|
||||
descriptionZh: 'MiniMax 编程套餐 — 中国区',
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
authStyle: 'auth_token',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic/v1',
|
||||
defaultEnvOverrides: {
|
||||
API_TIMEOUT_MS: '3000000',
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_AUTH_TOKEN: '',
|
||||
ANTHROPIC_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2.5',
|
||||
},
|
||||
defaultModels: [
|
||||
{ modelId: 'sonnet', displayName: 'MiniMax-M2.5', role: 'default' },
|
||||
@@ -272,12 +276,16 @@ export const VENDOR_PRESETS: VendorPreset[] = [
|
||||
description: 'MiniMax Code Plan — Global region',
|
||||
descriptionZh: 'MiniMax 编程套餐 — 国际区',
|
||||
protocol: 'anthropic',
|
||||
authStyle: 'api_key',
|
||||
authStyle: 'auth_token',
|
||||
baseUrl: 'https://api.minimax.io/anthropic',
|
||||
defaultEnvOverrides: {
|
||||
API_TIMEOUT_MS: '3000000',
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_AUTH_TOKEN: '',
|
||||
ANTHROPIC_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2.5',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2.5',
|
||||
},
|
||||
defaultModels: [
|
||||
{ modelId: 'sonnet', displayName: 'MiniMax-M2.5', role: 'default' },
|
||||
@@ -495,6 +503,18 @@ export function findPresetForLegacy(baseUrl: string, providerType: string): Vend
|
||||
if (baseUrl) {
|
||||
const match = VENDOR_PRESETS.find(p => p.baseUrl === baseUrl);
|
||||
if (match) return match;
|
||||
|
||||
// Fuzzy match: legacy entries may have old URLs (e.g. minimaxi.com/anthropic
|
||||
// before /v1 suffix was added). Match by domain substring against presets.
|
||||
const urlLower = baseUrl.toLowerCase();
|
||||
const fuzzy = VENDOR_PRESETS.find(p => {
|
||||
if (!p.baseUrl) return false;
|
||||
try {
|
||||
const presetHost = new URL(p.baseUrl).hostname;
|
||||
return urlLower.includes(presetHost);
|
||||
} catch { return false; }
|
||||
});
|
||||
if (fuzzy) return fuzzy;
|
||||
}
|
||||
|
||||
// Type-based fallback
|
||||
|
||||
@@ -180,6 +180,8 @@ export interface ApiProvider {
|
||||
env_overrides_json: string;
|
||||
/** Semantic model role mapping — JSON string of { default?, reasoning?, small?, haiku?, sonnet?, opus? } */
|
||||
role_models_json: string;
|
||||
/** Per-provider options — JSON string of { thinking_mode?, context_1m? } */
|
||||
options_json: string;
|
||||
notes: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -228,6 +230,7 @@ export interface CreateProviderRequest {
|
||||
headers_json?: string;
|
||||
env_overrides_json?: string;
|
||||
role_models_json?: string;
|
||||
options_json?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -241,10 +244,17 @@ export interface UpdateProviderRequest {
|
||||
headers_json?: string;
|
||||
env_overrides_json?: string;
|
||||
role_models_json?: string;
|
||||
options_json?: string;
|
||||
notes?: string;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
/** Per-provider options stored in options_json */
|
||||
export interface ProviderOptions {
|
||||
thinking_mode?: 'adaptive' | 'enabled' | 'disabled';
|
||||
context_1m?: boolean;
|
||||
}
|
||||
|
||||
export interface ProvidersResponse {
|
||||
providers: ApiProvider[];
|
||||
}
|
||||
@@ -554,6 +564,8 @@ export interface AssistantWorkspaceState {
|
||||
lastCheckInDate: string | null;
|
||||
schemaVersion: number;
|
||||
hookTriggeredSessionId?: string;
|
||||
/** ISO timestamp when hookTriggeredSessionId was set — used for staleness detection */
|
||||
hookTriggeredAt?: string;
|
||||
}
|
||||
|
||||
export interface AssistantWorkspaceFiles {
|
||||
@@ -936,6 +948,8 @@ export interface ClaudeStreamOptions {
|
||||
enableFileCheckpointing?: boolean;
|
||||
/** When true, this is an auto-trigger turn (invisible to user) — skip rewind point emission */
|
||||
autoTrigger?: boolean;
|
||||
/** Enable 1M context window (beta header: context-1m-2025-08-07) */
|
||||
context1m?: boolean;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
Reference in new issue
Block a user