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

+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