fix provider switch not presistingin session (#903)
* fix provider switch not presistingin session * fix broken tests
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
buildCodexOAuthProfileEnv,
|
buildCodexOAuthProfileEnv,
|
||||||
buildCurrentProviderSummary,
|
buildCurrentProviderSummary,
|
||||||
buildProfileSaveMessage,
|
buildProfileSaveMessage,
|
||||||
|
buildProviderManagerCompletion,
|
||||||
getProviderWizardDefaults,
|
getProviderWizardDefaults,
|
||||||
ProviderWizard,
|
ProviderWizard,
|
||||||
TextEntryDialog,
|
TextEntryDialog,
|
||||||
@@ -264,6 +265,32 @@ test('wizard step remount prevents a typed API key from leaking into the next fi
|
|||||||
expect(output).not.toContain('sk-secret-12345678')
|
expect(output).not.toContain('sk-secret-12345678')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('buildProviderManagerCompletion records provider switch event and model-visible reminder', () => {
|
||||||
|
const completion = buildProviderManagerCompletion({
|
||||||
|
action: 'activated',
|
||||||
|
activeProviderName: 'Sadaf Provider',
|
||||||
|
activeProviderModel: 'sadaf-model',
|
||||||
|
message: 'Provider switched to Sadaf Provider (sadaf-model)',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completion.message).toBe(
|
||||||
|
'Provider switched to Sadaf Provider (sadaf-model)',
|
||||||
|
)
|
||||||
|
expect(completion.metaMessages).toEqual([
|
||||||
|
'<system-reminder>Provider switched mid-session to Sadaf Provider using model sadaf-model. Use this provider/model for subsequent requests unless the user switches again.</system-reminder>',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildProviderManagerCompletion skips provider reminder when manager is cancelled', () => {
|
||||||
|
const completion = buildProviderManagerCompletion({
|
||||||
|
action: 'cancelled',
|
||||||
|
message: 'Provider manager closed',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completion.message).toBe('Provider manager closed')
|
||||||
|
expect(completion.metaMessages).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test('buildProfileSaveMessage maps provider fields without echoing secrets', () => {
|
test('buildProfileSaveMessage maps provider fields without echoing secrets', () => {
|
||||||
const message = buildProfileSaveMessage(
|
const message = buildProfileSaveMessage(
|
||||||
'openai',
|
'openai',
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import * as React from 'react'
|
|||||||
|
|
||||||
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js'
|
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
||||||
import { ProviderManager } from '../../components/ProviderManager.js'
|
import {
|
||||||
|
ProviderManager,
|
||||||
|
type ProviderManagerResult,
|
||||||
|
} from '../../components/ProviderManager.js'
|
||||||
import TextInput from '../../components/TextInput.js'
|
import TextInput from '../../components/TextInput.js'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -70,6 +73,29 @@ import {
|
|||||||
type OllamaGenerationReadiness,
|
type OllamaGenerationReadiness,
|
||||||
} from '../../utils/providerDiscovery.js'
|
} from '../../utils/providerDiscovery.js'
|
||||||
|
|
||||||
|
export function buildProviderManagerCompletion(result?: ProviderManagerResult): {
|
||||||
|
message: string
|
||||||
|
metaMessages?: string[]
|
||||||
|
} {
|
||||||
|
const message =
|
||||||
|
result?.message ??
|
||||||
|
(result?.action === 'saved'
|
||||||
|
? 'Provider profile updated'
|
||||||
|
: 'Provider manager closed')
|
||||||
|
const metaMessages =
|
||||||
|
result?.action === 'activated' && result.activeProviderName
|
||||||
|
? [
|
||||||
|
`<system-reminder>Provider switched mid-session to ${result.activeProviderName}${
|
||||||
|
result.activeProviderModel
|
||||||
|
? ` using model ${result.activeProviderModel}`
|
||||||
|
: ''
|
||||||
|
}. Use this provider/model for subsequent requests unless the user switches again.</system-reminder>`,
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return { message, metaMessages }
|
||||||
|
}
|
||||||
|
|
||||||
function describeOllamaReadinessIssue(
|
function describeOllamaReadinessIssue(
|
||||||
readiness: OllamaGenerationReadiness,
|
readiness: OllamaGenerationReadiness,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -1703,13 +1729,8 @@ export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
|
|||||||
<ProviderManager
|
<ProviderManager
|
||||||
mode="manage"
|
mode="manage"
|
||||||
onDone={result => {
|
onDone={result => {
|
||||||
const message =
|
const { message, metaMessages } = buildProviderManagerCompletion(result)
|
||||||
result?.message ??
|
onDone(message, { display: 'system', metaMessages })
|
||||||
(result?.action === 'saved'
|
|
||||||
? 'Provider profile updated'
|
|
||||||
: 'Provider manager closed')
|
|
||||||
|
|
||||||
onDone(message, { display: 'system' })
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,8 +58,10 @@ import TextInput from './TextInput.js'
|
|||||||
import { useCodexOAuthFlow } from './useCodexOAuthFlow.js'
|
import { useCodexOAuthFlow } from './useCodexOAuthFlow.js'
|
||||||
|
|
||||||
export type ProviderManagerResult = {
|
export type ProviderManagerResult = {
|
||||||
action: 'saved' | 'cancelled'
|
action: 'saved' | 'cancelled' | 'activated'
|
||||||
activeProfileId?: string
|
activeProfileId?: string
|
||||||
|
activeProviderName?: string
|
||||||
|
activeProviderModel?: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,12 +761,14 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
|||||||
mainLoopModelForSession: null,
|
mainLoopModelForSession: null,
|
||||||
}))
|
}))
|
||||||
refreshProfiles()
|
refreshProfiles()
|
||||||
setAppState(prev => ({
|
|
||||||
...prev,
|
|
||||||
mainLoopModel: GITHUB_PROVIDER_DEFAULT_MODEL,
|
|
||||||
}))
|
|
||||||
setStatusMessage(`Active provider: ${GITHUB_PROVIDER_LABEL}`)
|
setStatusMessage(`Active provider: ${GITHUB_PROVIDER_LABEL}`)
|
||||||
setIsActivating(false)
|
setIsActivating(false)
|
||||||
|
onDone({
|
||||||
|
action: 'activated',
|
||||||
|
activeProviderName: GITHUB_PROVIDER_LABEL,
|
||||||
|
activeProviderModel: GITHUB_PROVIDER_DEFAULT_MODEL,
|
||||||
|
message: `Provider switched to ${GITHUB_PROVIDER_LABEL} (${GITHUB_PROVIDER_DEFAULT_MODEL})`,
|
||||||
|
})
|
||||||
returnToMenu()
|
returnToMenu()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -799,23 +803,29 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
|||||||
: null
|
: null
|
||||||
|
|
||||||
refreshProfiles()
|
refreshProfiles()
|
||||||
setStatusMessage(
|
const activationMessage = isActiveCodexOAuth
|
||||||
isActiveCodexOAuth
|
? buildCodexOAuthActivationMessage({
|
||||||
? buildCodexOAuthActivationMessage({
|
prefix: `Active provider: ${active.name}`,
|
||||||
prefix: `Active provider: ${active.name}`,
|
activationWarning,
|
||||||
|
warnings: [
|
||||||
activationWarning,
|
activationWarning,
|
||||||
warnings: [
|
settingsOverrideError
|
||||||
activationWarning,
|
? `could not clear startup provider override (${settingsOverrideError})`
|
||||||
settingsOverrideError
|
: null,
|
||||||
? `could not clear startup provider override (${settingsOverrideError})`
|
].filter((warning): warning is string => Boolean(warning)),
|
||||||
: null,
|
})
|
||||||
].filter((warning): warning is string => Boolean(warning)),
|
: settingsOverrideError
|
||||||
})
|
? `Active provider: ${active.name}. Warning: could not clear startup provider override (${settingsOverrideError}).`
|
||||||
: settingsOverrideError
|
: `Active provider: ${active.name}`
|
||||||
? `Active provider: ${active.name}. Warning: could not clear startup provider override (${settingsOverrideError}).`
|
setStatusMessage(activationMessage)
|
||||||
: `Active provider: ${active.name}`,
|
|
||||||
)
|
|
||||||
setIsActivating(false)
|
setIsActivating(false)
|
||||||
|
onDone({
|
||||||
|
action: 'activated',
|
||||||
|
activeProfileId: active.id,
|
||||||
|
activeProviderName: active.name,
|
||||||
|
activeProviderModel: newModel,
|
||||||
|
message: `Provider switched to ${active.name} (${newModel})`,
|
||||||
|
})
|
||||||
returnToMenu()
|
returnToMenu()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
refreshProfiles()
|
refreshProfiles()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { afterEach, expect, test } from 'bun:test'
|
|||||||
NATIVE_PACKAGE_URL: undefined,
|
NATIVE_PACKAGE_URL: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { clearSystemPromptSections } from './systemPromptSections.js'
|
||||||
import { getSystemPrompt, DEFAULT_AGENT_PROMPT } from './prompts.js'
|
import { getSystemPrompt, DEFAULT_AGENT_PROMPT } from './prompts.js'
|
||||||
import { CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } from './system.js'
|
import { CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } from './system.js'
|
||||||
import { CLAUDE_CODE_GUIDE_AGENT } from '../tools/AgentTool/built-in/claudeCodeGuideAgent.js'
|
import { CLAUDE_CODE_GUIDE_AGENT } from '../tools/AgentTool/built-in/claudeCodeGuideAgent.js'
|
||||||
@@ -23,6 +24,7 @@ const originalSimpleEnv = process.env.CLAUDE_CODE_SIMPLE
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.CLAUDE_CODE_SIMPLE = originalSimpleEnv
|
process.env.CLAUDE_CODE_SIMPLE = originalSimpleEnv
|
||||||
|
clearSystemPromptSections()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('CLI identity prefixes describe OpenClaude instead of Claude Code', () => {
|
test('CLI identity prefixes describe OpenClaude instead of Claude Code', () => {
|
||||||
@@ -47,6 +49,21 @@ test('simple mode identity describes OpenClaude instead of Claude Code', async (
|
|||||||
expect(prompt[0]).not.toContain("Anthropic's official CLI for Claude")
|
expect(prompt[0]).not.toContain("Anthropic's official CLI for Claude")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('system prompt model identity updates when model changes mid-session', async () => {
|
||||||
|
delete process.env.CLAUDE_CODE_SIMPLE
|
||||||
|
clearSystemPromptSections()
|
||||||
|
|
||||||
|
const firstPrompt = await getSystemPrompt([], 'old-test-model')
|
||||||
|
const secondPrompt = await getSystemPrompt([], 'new-test-model')
|
||||||
|
|
||||||
|
const firstText = firstPrompt.join('\n')
|
||||||
|
const secondText = secondPrompt.join('\n')
|
||||||
|
|
||||||
|
expect(firstText).toContain('You are powered by the model old-test-model.')
|
||||||
|
expect(secondText).toContain('You are powered by the model new-test-model.')
|
||||||
|
expect(secondText).not.toContain('You are powered by the model old-test-model.')
|
||||||
|
})
|
||||||
|
|
||||||
test('built-in agent prompts describe OpenClaude instead of Claude Code', () => {
|
test('built-in agent prompts describe OpenClaude instead of Claude Code', () => {
|
||||||
expect(DEFAULT_AGENT_PROMPT).toContain('OpenClaude')
|
expect(DEFAULT_AGENT_PROMPT).toContain('OpenClaude')
|
||||||
expect(DEFAULT_AGENT_PROMPT).not.toContain('Claude Code')
|
expect(DEFAULT_AGENT_PROMPT).not.toContain('Claude Code')
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ ${CYBER_RISK_INSTRUCTION}`,
|
|||||||
systemPromptSection('ant_model_override', () =>
|
systemPromptSection('ant_model_override', () =>
|
||||||
getAntModelOverrideSection(),
|
getAntModelOverrideSection(),
|
||||||
),
|
),
|
||||||
systemPromptSection('env_info_simple', () =>
|
systemPromptSection(`env_info_simple:${model}`, () =>
|
||||||
computeSimpleEnvInfo(model, additionalWorkingDirectories),
|
computeSimpleEnvInfo(model, additionalWorkingDirectories),
|
||||||
),
|
),
|
||||||
systemPromptSection('language', () =>
|
systemPromptSection('language', () =>
|
||||||
@@ -519,7 +519,7 @@ ${CYBER_RISK_INSTRUCTION}`,
|
|||||||
'MCP servers connect/disconnect between turns',
|
'MCP servers connect/disconnect between turns',
|
||||||
),
|
),
|
||||||
systemPromptSection('scratchpad', () => getScratchpadInstructions()),
|
systemPromptSection('scratchpad', () => getScratchpadInstructions()),
|
||||||
systemPromptSection('frc', () => getFunctionResultClearingSection(model)),
|
systemPromptSection(`frc:${model}`, () => getFunctionResultClearingSection(model)),
|
||||||
systemPromptSection(
|
systemPromptSection(
|
||||||
'summarize_tool_results',
|
'summarize_tool_results',
|
||||||
() => SUMMARIZE_TOOL_RESULTS_SECTION,
|
() => SUMMARIZE_TOOL_RESULTS_SECTION,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
import { notifyCommandLifecycle } from './utils/commandLifecycle.js'
|
import { notifyCommandLifecycle } from './utils/commandLifecycle.js'
|
||||||
import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js'
|
import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js'
|
||||||
import {
|
import {
|
||||||
|
getDefaultMainLoopModelSetting,
|
||||||
getRuntimeMainLoopModel,
|
getRuntimeMainLoopModel,
|
||||||
renderModelName,
|
renderModelName,
|
||||||
} from './utils/model/model.js'
|
} from './utils/model/model.js'
|
||||||
@@ -604,9 +605,13 @@ async function* queryLoop(
|
|||||||
|
|
||||||
const appState = toolUseContext.getAppState()
|
const appState = toolUseContext.getAppState()
|
||||||
const permissionMode = appState.toolPermissionContext.mode
|
const permissionMode = appState.toolPermissionContext.mode
|
||||||
|
const appStateMainLoopModel =
|
||||||
|
appState.mainLoopModelForSession ??
|
||||||
|
appState.mainLoopModel ??
|
||||||
|
getDefaultMainLoopModelSetting()
|
||||||
let currentModel = getRuntimeMainLoopModel({
|
let currentModel = getRuntimeMainLoopModel({
|
||||||
permissionMode,
|
permissionMode,
|
||||||
mainLoopModel: toolUseContext.options.mainLoopModel,
|
mainLoopModel: appStateMainLoopModel,
|
||||||
exceeds200kTokens:
|
exceeds200kTokens:
|
||||||
permissionMode === 'plan' &&
|
permissionMode === 'plan' &&
|
||||||
doesMostRecentAssistantMessageExceed200k(messagesForQuery),
|
doesMostRecentAssistantMessageExceed200k(messagesForQuery),
|
||||||
|
|||||||
@@ -130,10 +130,18 @@ export function isAnthropicAuthEnabled(): boolean {
|
|||||||
apiKeyHelper ||
|
apiKeyHelper ||
|
||||||
process.env.CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR
|
process.env.CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR
|
||||||
|
|
||||||
// Check if API key is from an external source (not managed by /login)
|
// Check if API key is from an external source (not managed by /login).
|
||||||
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
|
// Predicate must not throw: getAnthropicApiKeyWithSource throws under
|
||||||
skipRetrievingKeyFromApiKeyHelper: true,
|
// CI/NODE_ENV=test when no key is configured, but here we just want to
|
||||||
})
|
// know the source — "no key" is a valid answer.
|
||||||
|
let apiKeySource: ApiKeySource
|
||||||
|
try {
|
||||||
|
;({ source: apiKeySource } = getAnthropicApiKeyWithSource({
|
||||||
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
apiKeySource = 'none'
|
||||||
|
}
|
||||||
const hasExternalApiKey =
|
const hasExternalApiKey =
|
||||||
apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper'
|
apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper'
|
||||||
|
|
||||||
@@ -221,10 +229,17 @@ export function getAnthropicApiKey(): null | string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasAnthropicApiKeyAuth(): boolean {
|
export function hasAnthropicApiKeyAuth(): boolean {
|
||||||
const { key, source } = getAnthropicApiKeyWithSource({
|
// Predicate: never throw. getAnthropicApiKeyWithSource throws under
|
||||||
skipRetrievingKeyFromApiKeyHelper: true,
|
// CI/NODE_ENV=test when no key is configured — but "do we have auth?" is
|
||||||
})
|
// exactly the question that has to answer cleanly in that state.
|
||||||
return key !== null && source !== 'none'
|
try {
|
||||||
|
const { key, source } = getAnthropicApiKeyWithSource({
|
||||||
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
|
})
|
||||||
|
return key !== null && source !== 'none'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnthropicApiKeyWithSource(
|
export function getAnthropicApiKeyWithSource(
|
||||||
|
|||||||
Reference in New Issue
Block a user