Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,13 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# Turn on whichever audience you're debugging; both can run together.
# OPENCLAUDE_LOG_TOKEN_USAGE=verbose

# Set the effort level for this OpenClaude process.
# Accepted values: low, medium, high, max, auto, unset.
# This environment variable overrides the persisted global /effort setting while
# it is present. Use /effort inside OpenClaude when you want to save a global
# default for future sessions; use auto/unset here to ignore saved effort.
# CLAUDE_CODE_EFFORT_LEVEL=medium

# Custom timeout for API requests in milliseconds (default: varies)
# API_TIMEOUT_MS=60000

Expand Down
7 changes: 7 additions & 0 deletions docs/advanced-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,18 @@ export OPENAI_MODEL=gpt-4o
| `CODEX_HOME` | Codex only | Alternative Codex home directory |
| `OPENCLAUDE_DISABLE_CO_AUTHORED_BY` | No | Suppress the default `Co-Authored-By` trailer in generated git commits |
| `OPENCLAUDE_LOG_TOKEN_USAGE` | No | When truthy (e.g. `verbose`), emits one JSON line on stderr per API request with input/output/cache tokens and the resolved provider. **User-facing debug output** — complements the REPL display controlled by `/config showCacheStats`. Distinct from `CLAUDE_CODE_ENABLE_TOKEN_USAGE_ATTACHMENT`, which is **model-facing** (injects context usage info into the prompt itself). Both can run together. |
| `CLAUDE_CODE_EFFORT_LEVEL` | No | Override model effort for the current process. Accepted values: `low`, `medium`, `high`, `max`, `auto`, or `unset`. This takes precedence over the global `/effort` setting while present. |

Model env vars are provider-scoped: Anthropic-native sessions read
`ANTHROPIC_MODEL`, OpenAI-compatible sessions read `OPENAI_MODEL`, Gemini reads
`GEMINI_MODEL`, and Mistral reads `MISTRAL_MODEL`.

`/effort` saves a global default in user settings for future OpenClaude
sessions. `CLAUDE_CODE_EFFORT_LEVEL` is a launch/session override: when it is
set, it wins over the saved `/effort` value without changing that saved value.
Use `CLAUDE_CODE_EFFORT_LEVEL=auto` or `unset` to ignore saved effort for the
current process.

## Runtime Hardening

Use these commands to validate your setup and catch mistakes early:
Expand Down
26 changes: 8 additions & 18 deletions src/commands/effort/effort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { useAppState, useSetAppState } from '../../state/AppState.js';
import type { LocalJSXCommandOnDone } from '../../types/command.js';
import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, isOpenAIEffortLevel, modelUsesOpenAIEffort, openAIEffortToStandard, toPersistableEffort } from '../../utils/effort.js';
import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, isOpenAIEffortLevel, openAIEffortToStandard, toPersistableEffort } from '../../utils/effort.js';
import { EffortPicker } from '../../components/EffortPicker.js';
import { updateSettingsForSource } from '../../utils/settings/settings.js';
const COMMON_HELP_ARGS = ['help', '-h', '--help'];
Expand Down Expand Up @@ -191,26 +191,16 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg

function EffortPickerWrapper({ onDone }: { onDone: LocalJSXCommandOnDone }) {
const setAppState = useSetAppState();
const model = useMainLoopModel();
const usesOpenAIEffort = modelUsesOpenAIEffort(model);

function handleSelect(effort: EffortValue | undefined) {
const persistable = toPersistableEffort(effort);
if (persistable !== undefined) {
updateSettingsForSource('userSettings', {
effortLevel: persistable
});
const result = effort === undefined ? unsetEffortLevel() : setEffortValue(effort);
if (result.effortUpdate) {
setAppState(prev => ({
...prev,
effortValue: result.effortUpdate?.value
}));
}
logEvent('tengu_effort_command', {
effort: (effort ?? 'auto') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
setAppState(prev => ({
...prev,
effortValue: effort
}));
const description = effort ? getEffortValueDescription(effort) : 'Use default effort level for your model';
const suffix = persistable !== undefined ? '' : ' (this session only)';
onDone(`Set effort level to ${effort ?? 'auto'}${suffix}: ${description}`);
onDone(result.message);
}

function handleCancel() {
Expand Down
11 changes: 1 addition & 10 deletions src/components/EffortPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import { Box, Text } from '../ink.js'
import { useMainLoopModel } from '../hooks/useMainLoopModel.js'
import { useAppState, useSetAppState } from '../state/AppState.js'
import { useAppState } from '../state/AppState.js'
import type { EffortLevel } from '../utils/effort.js'
import {
getAvailableEffortLevels,
Expand Down Expand Up @@ -35,7 +35,6 @@ type Props = {
export function EffortPicker({ onSelect, onCancel }: Props) {
const model = useMainLoopModel()
const appStateEffort = useAppState((s: any) => s.effortValue)
const setAppState = useSetAppState()
const provider = getAPIProvider()
const usesOpenAIEffort = modelUsesOpenAIEffort(model)
const availableLevels = getAvailableEffortLevels(model)
Expand Down Expand Up @@ -72,10 +71,6 @@ export function EffortPicker({ onSelect, onCancel }: Props) {

function handleSelect(value: string) {
if (value === 'auto') {
setAppState(prev => ({
...prev,
effortValue: undefined,
}))
onSelect(undefined)
} else {
// Normalize OpenAI-shaped 'xhigh' to the standard EffortLevel ('max')
Expand All @@ -84,10 +79,6 @@ export function EffortPicker({ onSelect, onCancel }: Props) {
const effortLevel = isOpenAIEffortLevel(value)
? openAIEffortToStandard(value)
: (value as EffortLevel)
setAppState(prev => ({
...prev,
effortValue: effortLevel,
}))
onSelect(effortLevel)
}
}
Expand Down
32 changes: 31 additions & 1 deletion src/components/StartupScreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ const ENV_KEYS = [
'OPENAI_MODEL',
'GEMINI_MODEL',
'MISTRAL_MODEL',
'ANTHROPIC_MODEL',
'CLAUDE_MODEL',
'CLAUDE_CODE_EFFORT_LEVEL',
'NVIDIA_NIM',
'MINIMAX_API_KEY',
'XAI_API_KEY',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
Expand Down Expand Up @@ -267,6 +268,35 @@ describe('detectProvider — explicit dedicated-provider env flags', () => {
})
})

describe('detectProvider — startup effort display', () => {
test('OpenAI startup banner uses provider alias default when no saved or env effort is set', () => {
setupOpenAIMode('https://api.openai.com/v1', 'gpt-5.4')

const result = detectProvider()

expect(result.model).toBe('gpt-5.4 (high)')
})

test('OpenAI startup banner uses saved /effort over provider alias default', () => {
setupOpenAIMode('https://api.openai.com/v1', 'gpt-5.4')
setSessionSettingsCache({ settings: { effortLevel: 'medium' }, errors: [] })

const result = detectProvider()

expect(result.model).toBe('gpt-5.4 (medium)')
})

test('OpenAI startup banner uses CLAUDE_CODE_EFFORT_LEVEL over saved /effort', () => {
setupOpenAIMode('https://api.openai.com/v1', 'gpt-5.4')
setSessionSettingsCache({ settings: { effortLevel: 'medium' }, errors: [] })
process.env.CLAUDE_CODE_EFFORT_LEVEL = 'low'

const result = detectProvider()

expect(result.model).toBe('gpt-5.4 (low)')
})
})

// --- modelOverride from --model flag ---

describe('detectProvider — modelOverride from --model flag', () => {
Expand Down
30 changes: 23 additions & 7 deletions src/components/StartupScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ import {
resolveRouteIdFromBaseUrl,
} from '../integrations/routeMetadata.js'
import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js'
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
import { getInitialSettings } from '../utils/settings/settings.js'
import { parseUserSpecifiedModel } from '../utils/model/model.js'
import { DEFAULT_GEMINI_MODEL } from '../utils/providerProfile.js'
import { getGlobalConfig } from '../utils/config.js'
import {
getDisplayedEffortLevel,
getEffortEnvOverride,
getInitialEffortSetting,
modelSupportsEffort,
openAIEffortToStandard,
} from '../utils/effort.js'
import { ANSI_DIM, ANSI_RESET, ansiRgb } from '../utils/terminalAnsi.js'
import {
resolveLogoPalette,
Expand Down Expand Up @@ -145,18 +152,27 @@ export function detectProvider(modelOverride?: string): { name: string; model: s
else if (/bankr/i.test(baseUrl)) name = 'Bankr'
else if (/bankr/i.test(rawModel)) name = 'Bankr'
else if (isLocal) name = getLocalOpenAICompatibleProviderLabel(baseUrl)

// Resolve model alias to actual model name + reasoning effort

let displayModel = resolvedRequest.resolvedModel
if (resolvedRequest.reasoning?.effort) {
displayModel = `${displayModel} (${resolvedRequest.reasoning.effort})`
// Display the same effective effort as the in-app header/status UI. The
// provider alias may carry a default reasoning effort, but saved /effort
// and CLAUDE_CODE_EFFORT_LEVEL must take precedence in the startup banner.
if (modelSupportsEffort(displayModel)) {
const savedEffort = getInitialEffortSetting()
const aliasDefaultEffort =
getEffortEnvOverride() === undefined &&
savedEffort === undefined &&
resolvedRequest.reasoning?.effort
? openAIEffortToStandard(resolvedRequest.reasoning.effort)
: undefined
displayModel = `${displayModel} (${getDisplayedEffortLevel(displayModel, savedEffort ?? aliasDefaultEffort)})`
}

return { name, model: displayModel, baseUrl, isLocal }
}

// Default: Anthropic - check settings.model first, then env vars
const settings = getSettings_DEPRECATED() || {}
const settings = getInitialSettings() || {}
const modelSetting = modelOverride || process.env.ANTHROPIC_MODEL || process.env.CLAUDE_MODEL || settings.model || 'claude-sonnet-4-6'
const resolvedModel = parseUserSpecifiedModel(modelSetting)
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'
Expand Down
Loading