mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Honor desktop thinking controls for provider streams
Desktop sessions must treat the global Thinking toggle as the source of truth across active CLI sessions, title generation, and provider-compatible request bodies. DeepSeek-style streams can start with reasoning blocks before text, so the WebSocket bridge now keeps the UI in thinking state until text content actually starts. Constraint: DeepSeek and MiniMax use Anthropic-compatible surfaces but expose thinking behavior through provider-specific request and stream shapes Rejected: Keep provider-specific disabled-thinking env defaults | stale defaults kept overriding the user's global Thinking setting Rejected: Treat every non-tool stream block as text | reasoning-only startup blocks created a blank streaming gap before the visible thinking bubble Confidence: high Scope-risk: moderate Directive: Do not change thinking stream state transitions without replaying reasoning-before-text provider events Tested: bun test src/server/__tests__/ws-memory-events.test.ts Tested: bun run check:server Tested: bun run verify Not-tested: Live DeepSeek provider smoke with real quota
This commit is contained in:
parent
4e969475f2
commit
c866adc951
@ -220,6 +220,91 @@ describe('settingsStore desktop notification persistence', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('settingsStore thinking persistence', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('persists both enabled and disabled thinking states explicitly', async () => {
|
||||
const updateUser = vi.fn().mockResolvedValue({})
|
||||
|
||||
vi.doMock('../api/settings', () => ({
|
||||
settingsApi: {
|
||||
getUser: vi.fn(),
|
||||
updateUser,
|
||||
getPermissionMode: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
getCliLauncherStatus: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.doMock('../api/models', () => ({
|
||||
modelsApi: {
|
||||
list: vi.fn(),
|
||||
getCurrent: vi.fn(),
|
||||
setCurrent: vi.fn(),
|
||||
getEffort: vi.fn(),
|
||||
setEffort: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.doMock('../api/h5Access', () => ({
|
||||
h5AccessApi: {
|
||||
get: vi.fn(),
|
||||
enable: vi.fn(),
|
||||
disable: vi.fn(),
|
||||
regenerate: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const { useSettingsStore } = await import('./settingsStore')
|
||||
|
||||
await useSettingsStore.getState().setThinkingEnabled(false)
|
||||
await useSettingsStore.getState().setThinkingEnabled(true)
|
||||
|
||||
expect(updateUser).toHaveBeenNthCalledWith(1, { alwaysThinkingEnabled: false })
|
||||
expect(updateUser).toHaveBeenNthCalledWith(2, { alwaysThinkingEnabled: true })
|
||||
expect(useSettingsStore.getState().thinkingEnabled).toBe(true)
|
||||
})
|
||||
|
||||
it('rolls back the thinking toggle when persistence fails', async () => {
|
||||
vi.doMock('../api/settings', () => ({
|
||||
settingsApi: {
|
||||
getUser: vi.fn(),
|
||||
updateUser: vi.fn().mockRejectedValue(new Error('save failed')),
|
||||
getPermissionMode: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
getCliLauncherStatus: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.doMock('../api/models', () => ({
|
||||
modelsApi: {
|
||||
list: vi.fn(),
|
||||
getCurrent: vi.fn(),
|
||||
setCurrent: vi.fn(),
|
||||
getEffort: vi.fn(),
|
||||
setEffort: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.doMock('../api/h5Access', () => ({
|
||||
h5AccessApi: {
|
||||
get: vi.fn(),
|
||||
enable: vi.fn(),
|
||||
disable: vi.fn(),
|
||||
regenerate: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const { useSettingsStore } = await import('./settingsStore')
|
||||
|
||||
await useSettingsStore.getState().setThinkingEnabled(false)
|
||||
|
||||
expect(useSettingsStore.getState().thinkingEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('settingsStore theme persistence', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
@ -178,7 +178,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
const prev = get().thinkingEnabled
|
||||
set({ thinkingEnabled: enabled })
|
||||
try {
|
||||
await settingsApi.updateUser({ alwaysThinkingEnabled: enabled ? undefined : false })
|
||||
await settingsApi.updateUser({ alwaysThinkingEnabled: enabled })
|
||||
} catch {
|
||||
set({ thinkingEnabled: prev })
|
||||
}
|
||||
|
||||
@ -366,9 +366,9 @@
|
||||
--color-surface-hover: var(--color-surface-container-high);
|
||||
--color-surface-selected: var(--color-surface-container);
|
||||
--color-memory-accent: #3B7068;
|
||||
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 7%, var(--color-surface-container-lowest));
|
||||
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 26%, var(--color-border));
|
||||
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 10%, var(--color-surface-container-lowest));
|
||||
--color-memory-surface: #F6FAF8;
|
||||
--color-memory-border: #BCCDC8;
|
||||
--color-memory-icon-bg: #F3F8F7;
|
||||
--color-model-option-selected-bg: var(--color-primary-fixed);
|
||||
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
|
||||
--color-activity-heat-0: var(--color-surface-container);
|
||||
@ -567,9 +567,9 @@
|
||||
--color-surface-hover: #F2F5F8;
|
||||
--color-surface-selected: #E9EEF3;
|
||||
--color-memory-accent: #0F766E;
|
||||
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 5%, #FFFFFF);
|
||||
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 24%, var(--color-border));
|
||||
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 8%, #FFFFFF);
|
||||
--color-memory-surface: #F3F8F7;
|
||||
--color-memory-border: #AECBC9;
|
||||
--color-memory-icon-bg: #EDF7F6;
|
||||
--color-model-option-selected-bg: #FFF0EA;
|
||||
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
|
||||
--color-activity-heat-0: #EEF2F6;
|
||||
@ -745,9 +745,9 @@
|
||||
--color-surface-hover: var(--color-surface-container-highest);
|
||||
--color-surface-selected: var(--color-surface-container);
|
||||
--color-memory-accent: #7DD3C7;
|
||||
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 9%, var(--color-surface-container-lowest));
|
||||
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 26%, var(--color-border));
|
||||
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 12%, var(--color-surface-container-lowest));
|
||||
--color-memory-surface: #1D2423;
|
||||
--color-memory-border: #334743;
|
||||
--color-memory-icon-bg: #22302E;
|
||||
--color-model-option-selected-bg: rgba(255, 181, 159, 0.13);
|
||||
--color-model-option-selected-border: rgba(255, 181, 159, 0.34);
|
||||
--color-activity-heat-0: #242322;
|
||||
|
||||
@ -404,7 +404,7 @@ export function useReplBridge(messages: Message[], setMessages: (action: React.S
|
||||
});
|
||||
},
|
||||
onSetMaxThinkingTokens(maxTokens) {
|
||||
const enabled = maxTokens !== null;
|
||||
const enabled = maxTokens !== 0;
|
||||
setAppState(prev_11 => {
|
||||
if (prev_11.thinkingEnabled === enabled) return prev_11;
|
||||
return {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -242,6 +242,34 @@ describe('ConversationService', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('should send thinking token controls to active CLI sessions', () => {
|
||||
const svc = new ConversationService() as any
|
||||
const sent: string[] = []
|
||||
svc.sessions.set('session-thinking-control', {
|
||||
sdkSocket: { send: (data: string) => sent.push(data) },
|
||||
pendingOutbound: [],
|
||||
})
|
||||
|
||||
expect(svc.setMaxThinkingTokens('session-thinking-control', 0)).toBe(true)
|
||||
expect(svc.setMaxThinkingTokens('session-thinking-control', null)).toBe(true)
|
||||
expect(svc.setMaxThinkingTokensForActiveSessions(0)).toBe(1)
|
||||
|
||||
expect(sent.map((line) => JSON.parse(line).request)).toEqual([
|
||||
{
|
||||
subtype: 'set_max_thinking_tokens',
|
||||
max_thinking_tokens: 0,
|
||||
},
|
||||
{
|
||||
subtype: 'set_max_thinking_tokens',
|
||||
max_thinking_tokens: null,
|
||||
},
|
||||
{
|
||||
subtype: 'set_max_thinking_tokens',
|
||||
max_thinking_tokens: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should return false when sending interrupt to non-existent session', () => {
|
||||
const svc = new ConversationService()
|
||||
const result = svc.sendInterrupt('no-such-session')
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
* Unit tests for Settings, Models, and Status APIs
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'bun:test'
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach, spyOn } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { SettingsService } from '../services/settingsService.js'
|
||||
import { conversationService } from '../services/conversationService.js'
|
||||
import { handleSettingsApi } from '../api/settings.js'
|
||||
import { handleModelsApi } from '../api/models.js'
|
||||
import { handleStatusApi, resetUsage, addUsage } from '../api/status.js'
|
||||
@ -394,6 +395,28 @@ describe('Settings API', () => {
|
||||
expect(body2.model).toBe('claude-opus-4-7')
|
||||
})
|
||||
|
||||
it('PUT /api/settings/user should sync thinking changes to active CLI sessions', async () => {
|
||||
const syncSpy = spyOn(conversationService, 'setMaxThinkingTokensForActiveSessions')
|
||||
.mockImplementation(() => 0)
|
||||
|
||||
try {
|
||||
const disabled = makeRequest('PUT', '/api/settings/user', {
|
||||
alwaysThinkingEnabled: false,
|
||||
})
|
||||
expect((await handleSettingsApi(disabled.req, disabled.url, disabled.segments)).status).toBe(200)
|
||||
|
||||
const enabled = makeRequest('PUT', '/api/settings/user', {
|
||||
alwaysThinkingEnabled: true,
|
||||
})
|
||||
expect((await handleSettingsApi(enabled.req, enabled.url, enabled.segments)).status).toBe(200)
|
||||
|
||||
expect(syncSpy).toHaveBeenNthCalledWith(1, 0)
|
||||
expect(syncSpy).toHaveBeenNthCalledWith(2, null)
|
||||
} finally {
|
||||
syncSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('GET /api/settings/cli-launcher should expose bundled launcher status', async () => {
|
||||
if (process.platform === 'win32') return
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ describe('titleService', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('does not force disabled thinking for DeepSeek title generation', async () => {
|
||||
test('sends disabled thinking for DeepSeek title generation when desktop thinking is off', async () => {
|
||||
let requestBody: Record<string, unknown> | null = null
|
||||
const server = Bun.serve({
|
||||
hostname: '127.0.0.1',
|
||||
@ -114,7 +114,7 @@ describe('titleService', () => {
|
||||
)
|
||||
|
||||
await expect(generateTitle('请只回复 trace-ok')).resolves.toBe('Trace ok')
|
||||
expect(requestBody?.thinking).toBeUndefined()
|
||||
expect(requestBody?.thinking).toEqual({ type: 'disabled' })
|
||||
} finally {
|
||||
server.stop(true)
|
||||
}
|
||||
|
||||
@ -34,3 +34,54 @@ describe('WebSocket memory events', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('WebSocket stream event translation', () => {
|
||||
it('keeps DeepSeek-style thinking blocks in thinking state until text starts', () => {
|
||||
const sessionId = `deepseek-thinking-${crypto.randomUUID()}`
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'stream_event',
|
||||
event: { type: 'message_start' },
|
||||
}, sessionId)).toEqual([
|
||||
{ type: 'status', state: 'thinking' },
|
||||
])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'stream_event',
|
||||
event: {
|
||||
type: 'content_block_start',
|
||||
index: 0,
|
||||
content_block: { type: 'thinking', thinking: '' },
|
||||
},
|
||||
}, sessionId)).toEqual([
|
||||
{ type: 'status', state: 'thinking', verb: 'Thinking' },
|
||||
])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'stream_event',
|
||||
event: {
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'thinking_delta', thinking: 'Let me think' },
|
||||
},
|
||||
}, sessionId)).toEqual([
|
||||
{ type: 'thinking', text: 'Let me think' },
|
||||
])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'stream_event',
|
||||
event: { type: 'content_block_stop', index: 0 },
|
||||
}, sessionId)).toEqual([])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'stream_event',
|
||||
event: {
|
||||
type: 'content_block_start',
|
||||
index: 1,
|
||||
content_block: { type: 'text', text: '' },
|
||||
},
|
||||
}, sessionId)).toEqual([
|
||||
{ type: 'content_start', blockType: 'text' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
import { SettingsService } from '../services/settingsService.js'
|
||||
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
||||
import { ensureDesktopCliLauncherInstalled } from '../services/desktopCliLauncherService.js'
|
||||
import { conversationService } from '../services/conversationService.js'
|
||||
|
||||
const settingsService = new SettingsService()
|
||||
|
||||
@ -70,6 +71,7 @@ async function handleUserSettings(req: Request): Promise<Response> {
|
||||
if (req.method === 'PUT') {
|
||||
const body = await parseJsonBody(req)
|
||||
await settingsService.updateUserSettings(body)
|
||||
syncThinkingSettingToActiveSessions(body)
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
|
||||
@ -124,3 +126,16 @@ async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
||||
function methodNotAllowed(method: string): ApiError {
|
||||
return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED')
|
||||
}
|
||||
|
||||
function syncThinkingSettingToActiveSessions(settings: Record<string, unknown>): void {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(settings, 'alwaysThinkingEnabled') ||
|
||||
typeof settings.alwaysThinkingEnabled !== 'boolean'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
conversationService.setMaxThinkingTokensForActiveSessions(
|
||||
settings.alwaysThinkingEnabled ? null : 0,
|
||||
)
|
||||
}
|
||||
|
||||
@ -436,6 +436,27 @@ export class ConversationService {
|
||||
})
|
||||
}
|
||||
|
||||
setMaxThinkingTokens(sessionId: string, maxThinkingTokens: number | null): boolean {
|
||||
return this.sendSdkMessage(sessionId, {
|
||||
type: 'control_request',
|
||||
request_id: crypto.randomUUID(),
|
||||
request: {
|
||||
subtype: 'set_max_thinking_tokens',
|
||||
max_thinking_tokens: maxThinkingTokens,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setMaxThinkingTokensForActiveSessions(maxThinkingTokens: number | null): number {
|
||||
let sent = 0
|
||||
for (const sessionId of this.getActiveSessions()) {
|
||||
if (this.setMaxThinkingTokens(sessionId, maxThinkingTokens)) {
|
||||
sent += 1
|
||||
}
|
||||
}
|
||||
return sent
|
||||
}
|
||||
|
||||
sendInterrupt(sessionId: string): boolean {
|
||||
return this.sendSdkMessage(sessionId, {
|
||||
type: 'control_request',
|
||||
|
||||
@ -9,8 +9,6 @@
|
||||
import { ProviderService } from './providerService.js'
|
||||
import { SettingsService } from './settingsService.js'
|
||||
import { sessionService } from './sessionService.js'
|
||||
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { cleanSessionTitleSource, hasSessionTitleMarkup } from '../../utils/sessionTitleText.js'
|
||||
|
||||
const TITLE_MAX_LEN = 50
|
||||
@ -73,7 +71,7 @@ export async function generateTitle(
|
||||
|
||||
const model = resolvedProvider.models.haiku || resolvedProvider.models.main
|
||||
const url = `${resolvedProvider.baseUrl.replace(/\/+$/, '')}/v1/messages`
|
||||
const shouldDisableThinking = await shouldDisableThinkingForTitle(resolvedProvider.presetId)
|
||||
const shouldDisableThinking = await shouldDisableThinkingForTitle()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@ -177,12 +175,9 @@ function looksLikeStructuredTitleFragment(text: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
async function shouldDisableThinkingForTitle(presetId: string): Promise<boolean> {
|
||||
async function shouldDisableThinkingForTitle(): Promise<boolean> {
|
||||
const settings = await new SettingsService().getUserSettings()
|
||||
if (settings.alwaysThinkingEnabled !== false) return false
|
||||
|
||||
const presetEnv = PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv
|
||||
return isEnvTruthy(presetEnv?.CC_HAHA_SEND_DISABLED_THINKING)
|
||||
return settings.alwaysThinkingEnabled === false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -692,7 +692,7 @@ function triggerTitleGeneration(ws: ServerWebSocket<WebSocketData>, sessionId: s
|
||||
*/
|
||||
type SessionStreamState = {
|
||||
hasReceivedStreamEvents: boolean
|
||||
activeBlockTypes: Map<number, 'text' | 'tool_use'>
|
||||
activeBlockTypes: Map<number, 'text' | 'tool_use' | 'thinking'>
|
||||
activeToolBlocks: Map<number, { toolName: string; toolUseId: string; inputJson: string }>
|
||||
/** Tool blocks whose input JSON failed to parse in content_block_stop.
|
||||
* The assistant message carries the complete input — defer to that. */
|
||||
@ -990,7 +990,7 @@ export function translateCliMessage(cliMsg: any, sessionId: string): ServerMessa
|
||||
|
||||
switch (event.type) {
|
||||
case 'message_start': {
|
||||
return [{ type: 'status', state: 'streaming' }]
|
||||
return [{ type: 'status', state: 'thinking' }]
|
||||
}
|
||||
|
||||
case 'content_block_start': {
|
||||
@ -998,9 +998,9 @@ export function translateCliMessage(cliMsg: any, sessionId: string): ServerMessa
|
||||
if (!contentBlock) return []
|
||||
|
||||
const index = event.index ?? 0
|
||||
streamState.activeBlockTypes.set(index, contentBlock.type === 'tool_use' ? 'tool_use' : 'text')
|
||||
|
||||
if (contentBlock.type === 'tool_use') {
|
||||
streamState.activeBlockTypes.set(index, 'tool_use')
|
||||
// Track tool info so content_block_stop can emit complete data
|
||||
streamState.activeToolBlocks.set(index, {
|
||||
toolName: contentBlock.name || '',
|
||||
@ -1018,6 +1018,13 @@ export function translateCliMessage(cliMsg: any, sessionId: string): ServerMessa
|
||||
: undefined,
|
||||
}]
|
||||
}
|
||||
|
||||
if (contentBlock.type === 'thinking' || contentBlock.type === 'redacted_thinking') {
|
||||
streamState.activeBlockTypes.set(index, 'thinking')
|
||||
return [{ type: 'status', state: 'thinking', verb: 'Thinking' }]
|
||||
}
|
||||
|
||||
streamState.activeBlockTypes.set(index, 'text')
|
||||
return [{ type: 'content_start', blockType: 'text' }]
|
||||
}
|
||||
|
||||
|
||||
@ -1613,7 +1613,7 @@ async function* queryModel(
|
||||
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING)
|
||||
const modelCanThink = modelSupportsThinking(options.model)
|
||||
const sendsExplicitDisabledThinking =
|
||||
!(hasThinking && modelCanThink) && shouldSendExplicitDisabledThinking()
|
||||
!hasThinking && (modelCanThink || shouldSendExplicitDisabledThinking())
|
||||
|
||||
const outputConfig: BetaOutputConfig = {
|
||||
...((extraBodyParams.output_config as BetaOutputConfig) ?? {}),
|
||||
|
||||
@ -90,6 +90,16 @@ describe('provider-aware thinking support', () => {
|
||||
expect(shouldSendExplicitDisabledThinking()).toBe(false)
|
||||
})
|
||||
|
||||
test('MiniMax preset models declare thinking support without effort passthrough', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimaxi.com/anthropic'
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'MiniMax-M2.7'
|
||||
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
|
||||
clearCapabilityCache()
|
||||
|
||||
expect(modelSupportsThinking('MiniMax-M2.7')).toBe(true)
|
||||
expect(modelSupportsAdaptiveThinking('MiniMax-M2.7')).toBe(false)
|
||||
})
|
||||
|
||||
test('side queries inherit explicit disabled thinking for opted-in providers', () => {
|
||||
delete process.env.CC_HAHA_SEND_DISABLED_THINKING
|
||||
expect(resolveSideQueryThinkingConfig(undefined, 1024)).toBeUndefined()
|
||||
|
||||
@ -109,10 +109,18 @@ export function modelSupportsThinking(model: string): boolean {
|
||||
) {
|
||||
return !canonical.includes('claude-3-')
|
||||
}
|
||||
if (isMiniMaxAnthropicEndpoint() && canonical.includes('minimax')) {
|
||||
return true
|
||||
}
|
||||
// 3P (Bedrock/Vertex): only Opus 4+ and Sonnet 4+
|
||||
return canonical.includes('sonnet-4') || canonical.includes('opus-4')
|
||||
}
|
||||
|
||||
function isMiniMaxAnthropicEndpoint(): boolean {
|
||||
const baseUrl = process.env.ANTHROPIC_BASE_URL?.toLowerCase() ?? ''
|
||||
return baseUrl.includes('minimax') || baseUrl.includes('minimaxi')
|
||||
}
|
||||
|
||||
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports adaptive thinking.
|
||||
export function modelSupportsAdaptiveThinking(model: string): boolean {
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'adaptive_thinking')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user