From cc3cae68689dcbd25e66aa0328dbe76101785d54 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Thu, 12 Mar 2026 17:26:00 +0800 Subject: [PATCH] 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 --- src/__tests__/unit/provider-resolver.test.ts | 29 +++-- src/app/api/providers/options/route.ts | 40 +++++++ src/components/settings/GeneralSection.tsx | 36 ------ .../settings/PresetConnectDialog.tsx | 1 + src/components/settings/ProviderForm.tsx | 1 + src/components/settings/ProviderManager.tsx | 6 + .../settings/ProviderOptionsSection.tsx | 107 ++++++++++++++++++ src/components/settings/provider-presets.tsx | 6 +- src/i18n/en.ts | 32 ++++-- src/i18n/zh.ts | 32 ++++-- src/lib/agent-sdk-capabilities.ts | 7 +- src/lib/claude-client.ts | 25 ++-- src/lib/provider-catalog.ts | 30 ++++- src/types/index.ts | 14 +++ 14 files changed, 282 insertions(+), 84 deletions(-) create mode 100644 src/app/api/providers/options/route.ts create mode 100644 src/components/settings/ProviderOptionsSection.tsx diff --git a/src/__tests__/unit/provider-resolver.test.ts b/src/__tests__/unit/provider-resolver.test.ts index 63e8343..9365382 100644 --- a/src/__tests__/unit/provider-resolver.test.ts +++ b/src/__tests__/unit/provider-resolver.test.ts @@ -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', diff --git a/src/app/api/providers/options/route.ts b/src/app/api/providers/options/route.ts new file mode 100644 index 0000000..36cf067 --- /dev/null +++ b/src/app/api/providers/options/route.ts @@ -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 }, + ); + } +} diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx index f0fd2a2..ea01bdb 100644 --- a/src/components/settings/GeneralSection.tsx +++ b/src/components/settings/GeneralSection.tsx @@ -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('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() { - {/* Thinking mode */} - - - {/* Appearance */} diff --git a/src/components/settings/PresetConnectDialog.tsx b/src/components/settings/PresetConnectDialog.tsx index f2c6b0a..8f40f16 100644 --- a/src/components/settings/PresetConnectDialog.tsx +++ b/src/components/settings/PresetConnectDialog.tsx @@ -383,6 +383,7 @@ export function PresetConnectDialog({ {!preset.fields.includes("extra_env") && ( <> + {/* Provider options (thinking mode + 1M context) — only for official Anthropic */} + {provider.base_url === 'https://api.anthropic.com' && ( + + )} {/* Gemini Image model selector — capsule buttons */} {provider.provider_type === 'gemini-image' && (
diff --git a/src/components/settings/ProviderOptionsSection.tsx b/src/components/settings/ProviderOptionsSection.tsx new file mode 100644 index 0000000..474803b --- /dev/null +++ b/src/components/settings/ProviderOptionsSection.tsx @@ -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({ + 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 ( +
+ {/* Thinking mode */} +
+
+

+ {t('settings.thinkingMode' as TranslationKey)} +

+

+ {t('settings.thinkingModeDesc' as TranslationKey)} +

+
+ +
+ + {/* 1M context */} +
+
+

+ {t('provider.context1m' as TranslationKey)} +

+

+ {t('provider.context1mDesc' as TranslationKey)} +

+
+ saveOption('context_1m', checked)} + className="scale-[0.85]" + /> +
+
+ ); +} diff --git a/src/components/settings/provider-presets.tsx b/src/components/settings/provider-presets.tsx index 49905fe..646bda9 100644 --- a/src/components/settings/provider-presets.tsx +++ b/src/components/settings/provider-presets.tsx @@ -171,8 +171,8 @@ export const QUICK_PRESETS: QuickPreset[] = [ icon: , 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"], }, { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 55d8813..f519161 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -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', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 77e88a9..b7bcee4 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -144,6 +144,8 @@ const zh: Record = { '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 = { '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 = { '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': '全部', diff --git a/src/lib/agent-sdk-capabilities.ts b/src/lib/agent-sdk-capabilities.ts index f8ab781..e05beb1 100644 --- a/src/lib/agent-sdk-capabilities.ts +++ b/src/lib/agent-sdk-capabilities.ts @@ -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(); diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts index 6de134f..25bc41e 100644 --- a/src/lib/claude-client.ts +++ b/src/lib/claude-client.ts @@ -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({ @@ -516,6 +526,12 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream>(); @@ -781,11 +796,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream 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 diff --git a/src/types/index.ts b/src/types/index.ts index a0e878c..3a621ef 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; } // ==========================================