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:
7418andClaude Opus 4.6 committed 2026-03-12 17:26:00 +08:00
1 parent e694d75f10
commit cc3cae6868
14 files changed
+282 -84

No files matched your search

+14 -15
View File
@@ -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',
+40
View File
@@ -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"
+1
View File
@@ -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>
);
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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': '全部',
+5 -2
View File
@@ -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();
+18 -7
View File
@@ -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) {
+25 -5
View File
@@ -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
+14
View File
@@ -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;
}
// ==========================================