Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a4455ecf | ||
|
|
aeaa658f77 | ||
|
|
d2a057c6f1 | ||
|
|
08cc6f3287 | ||
|
|
84fcc7f7e0 | ||
|
|
ad11414def | ||
|
|
9419e8a4a2 | ||
|
|
41a86d05fa | ||
|
|
fa4b6a96c0 | ||
|
|
d03d77b110 | ||
|
|
15de1d6190 | ||
|
|
812facf024 | ||
|
|
2e39d2607a |
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
@@ -70,6 +70,11 @@ jobs:
|
|||||||
- name: Dry-run package
|
- name: Dry-run package
|
||||||
run: npm pack --dry-run
|
run: npm pack --dry-run
|
||||||
|
|
||||||
|
- name: Clear token auth for trusted publishing
|
||||||
|
run: |
|
||||||
|
unset NODE_AUTH_TOKEN
|
||||||
|
echo "NODE_AUTH_TOKEN=" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: npm publish --access public --provenance
|
run: npm publish --access public --provenance
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "0.2.0"
|
".": "0.2.3"
|
||||||
}
|
}
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.2.3](https://github.com/Gitlawb/openclaude/compare/v0.2.2...v0.2.3) (2026-04-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* prevent infinite auto-compact loop for unknown 3P models ([#635](https://github.com/Gitlawb/openclaude/issues/635)) ([#636](https://github.com/Gitlawb/openclaude/issues/636)) ([aeaa658](https://github.com/Gitlawb/openclaude/commit/aeaa658f776fb8df95721e8b8962385f8b00f66a))
|
||||||
|
|
||||||
|
## [0.2.2](https://github.com/Gitlawb/openclaude/compare/v0.2.1...v0.2.2) (2026-04-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **read/edit:** make compact line prefix unambiguous for tab-indented files ([#613](https://github.com/Gitlawb/openclaude/issues/613)) ([08cc6f3](https://github.com/Gitlawb/openclaude/commit/08cc6f328711cd93ce9fa53351266c29a0b0a341))
|
||||||
|
|
||||||
|
## [0.2.1](https://github.com/Gitlawb/openclaude/compare/v0.2.0...v0.2.1) (2026-04-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **provider:** add recovery guidance for missing OpenAI API key ([#616](https://github.com/Gitlawb/openclaude/issues/616)) ([9419e8a](https://github.com/Gitlawb/openclaude/commit/9419e8a4a21b3771d9ddb10f7072e0a8c5b5b631))
|
||||||
|
|
||||||
## [0.2.0](https://github.com/Gitlawb/openclaude/compare/v0.1.8...v0.2.0) (2026-04-12)
|
## [0.2.0](https://github.com/Gitlawb/openclaude/compare/v0.1.8...v0.2.0) (2026-04-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@gitlawb/openclaude",
|
"name": "@gitlawb/openclaude",
|
||||||
"version": "0.2.0",
|
"version": "0.2.3",
|
||||||
"description": "Claude Code opened to any LLM — OpenAI, Gemini, DeepSeek, Ollama, and 200+ models",
|
"description": "Claude Code opened to any LLM — OpenAI, Gemini, DeepSeek, Ollama, and 200+ models",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude"
|
"url": "https://github.com/Gitlawb/openclaude.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
46
src/services/compact/autoCompact.test.ts
Normal file
46
src/services/compact/autoCompact.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
import {
|
||||||
|
getEffectiveContextWindowSize,
|
||||||
|
getAutoCompactThreshold,
|
||||||
|
} from './autoCompact.ts'
|
||||||
|
|
||||||
|
describe('getEffectiveContextWindowSize', () => {
|
||||||
|
test('returns positive value for known models with large context windows', () => {
|
||||||
|
// claude-sonnet-4 has 200k context
|
||||||
|
const effective = getEffectiveContextWindowSize('claude-sonnet-4')
|
||||||
|
expect(effective).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('never returns negative even for unknown 3P models (issue #635)', () => {
|
||||||
|
// Previously, unknown 3P models got 8k context → effective context was
|
||||||
|
// 8k minus 20k summary reservation = -12k, causing infinite auto-compact.
|
||||||
|
// Now the fallback is 128k and there's a floor, so effective is always
|
||||||
|
// at least reservedTokensForSummary + buffer.
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
try {
|
||||||
|
const effective = getEffectiveContextWindowSize('some-unknown-3p-model')
|
||||||
|
expect(effective).toBeGreaterThan(0)
|
||||||
|
// Must be at least summary reservation (20k) + buffer (13k) = 33k
|
||||||
|
expect(effective).toBeGreaterThanOrEqual(33_000)
|
||||||
|
} finally {
|
||||||
|
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getAutoCompactThreshold', () => {
|
||||||
|
test('returns positive threshold for known models', () => {
|
||||||
|
const threshold = getAutoCompactThreshold('claude-sonnet-4')
|
||||||
|
expect(threshold).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('never returns negative threshold even for unknown 3P models (issue #635)', () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
try {
|
||||||
|
const threshold = getAutoCompactThreshold('some-unknown-3p-model')
|
||||||
|
expect(threshold).toBeGreaterThan(0)
|
||||||
|
} finally {
|
||||||
|
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -45,7 +45,12 @@ export function getEffectiveContextWindowSize(model: string): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return contextWindow - reservedTokensForSummary
|
// Floor: effective context must be at least the summary reservation plus a
|
||||||
|
// usable buffer. If it goes lower, the auto-compact threshold becomes
|
||||||
|
// negative and fires on every message (issue #635).
|
||||||
|
const autocompactBuffer = 13_000 // must match AUTOCOMPACT_BUFFER_TOKENS
|
||||||
|
const effectiveContext = contextWindow - reservedTokensForSummary
|
||||||
|
return Math.max(effectiveContext, reservedTokensForSummary + autocompactBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AutoCompactTrackingState = {
|
export type AutoCompactTrackingState = {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function getEditToolDescription(): string {
|
|||||||
|
|
||||||
function getDefaultEditDescription(): string {
|
function getDefaultEditDescription(): string {
|
||||||
const prefixFormat = isCompactLinePrefixEnabled()
|
const prefixFormat = isCompactLinePrefixEnabled()
|
||||||
? 'line number + tab'
|
? 'line number + arrow'
|
||||||
: 'spaces + line number + arrow'
|
: 'spaces + line number + arrow'
|
||||||
const minimalUniquenessHint =
|
const minimalUniquenessHint =
|
||||||
process.env.USER_TYPE === 'ant'
|
process.env.USER_TYPE === 'ant'
|
||||||
|
|||||||
@@ -107,9 +107,23 @@ test('MiniMax-M2.7 uses explicit provider-specific context and output caps', ()
|
|||||||
expect(getMaxOutputTokensForModel('MiniMax-M2.7')).toBe(131_072)
|
expect(getMaxOutputTokensForModel('MiniMax-M2.7')).toBe(131_072)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unknown openai-compatible models still use the conservative fallback window', () => {
|
test('unknown openai-compatible models use the 128k fallback window (not 8k, see #635)', () => {
|
||||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
|
||||||
expect(getContextWindowForModel('some-unknown-3p-model')).toBe(8_000)
|
expect(getContextWindowForModel('some-unknown-3p-model')).toBe(128_000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('MiniMax-M2.5 and M2.1 use explicit provider-specific context and output caps', () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
|
||||||
|
expect(getContextWindowForModel('MiniMax-M2.5')).toBe(204_800)
|
||||||
|
expect(getContextWindowForModel('MiniMax-M2.5-highspeed')).toBe(204_800)
|
||||||
|
expect(getContextWindowForModel('MiniMax-M2.1')).toBe(204_800)
|
||||||
|
expect(getContextWindowForModel('MiniMax-M2.1-highspeed')).toBe(204_800)
|
||||||
|
expect(getModelMaxOutputTokens('MiniMax-M2.5')).toEqual({
|
||||||
|
default: 131_072,
|
||||||
|
upperLimit: 131_072,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import { getOpenAIContextWindow, getOpenAIMaxOutputTokens } from './model/openai
|
|||||||
// Model context window size (200k tokens for all models right now)
|
// Model context window size (200k tokens for all models right now)
|
||||||
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000
|
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000
|
||||||
|
|
||||||
|
// Fallback context window for unknown 3P models. Must be large enough that
|
||||||
|
// the effective context (this minus output token reservation) stays positive,
|
||||||
|
// otherwise auto-compact fires on every message (issue #635).
|
||||||
|
export const OPENAI_FALLBACK_CONTEXT_WINDOW = 128_000
|
||||||
|
|
||||||
// Maximum output tokens for compact operations
|
// Maximum output tokens for compact operations
|
||||||
export const COMPACT_MAX_OUTPUT_TOKENS = 20_000
|
export const COMPACT_MAX_OUTPUT_TOKENS = 20_000
|
||||||
|
|
||||||
@@ -73,8 +78,9 @@ export function getContextWindowForModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI-compatible provider — use known context windows for the model.
|
// OpenAI-compatible provider — use known context windows for the model.
|
||||||
// Unknown models get a conservative 8k default so auto-compact triggers
|
// Unknown models get a conservative 128k default. This was previously 8k,
|
||||||
// before hitting a hard context_window_exceeded error.
|
// but that caused auto-compact to fire on every turn because the effective
|
||||||
|
// context (8k minus output reservation) became negative (issue #635).
|
||||||
const isOpenAIProvider =
|
const isOpenAIProvider =
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
|
||||||
@@ -86,10 +92,10 @@ export function getContextWindowForModel(
|
|||||||
return openaiWindow
|
return openaiWindow
|
||||||
}
|
}
|
||||||
console.error(
|
console.error(
|
||||||
`[context] Warning: model "${model}" not in context window table — using conservative 8k default. ` +
|
`[context] Warning: model "${model}" not in context window table — using conservative 128k default. ` +
|
||||||
'Add it to src/utils/model/openaiContextWindows.ts for accurate compaction.',
|
'Add it to src/utils/model/openaiContextWindows.ts for accurate compaction.',
|
||||||
)
|
)
|
||||||
return 8_000
|
return OPENAI_FALLBACK_CONTEXT_WINDOW
|
||||||
}
|
}
|
||||||
|
|
||||||
const cap = getModelCapability(model)
|
const cap = getModelCapability(model)
|
||||||
|
|||||||
51
src/utils/file.test.ts
Normal file
51
src/utils/file.test.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||||
|
|
||||||
|
async function importFileModuleWithKillswitchEnabled(
|
||||||
|
killswitchEnabled: boolean,
|
||||||
|
) {
|
||||||
|
mock.module('../services/analytics/growthbook.js', () => ({
|
||||||
|
getFeatureValue_CACHED_MAY_BE_STALE: () => killswitchEnabled,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return import(`./file.js?ts=${Date.now()}-${Math.random()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addLineNumbers', () => {
|
||||||
|
test('uses unambiguous arrow compact prefix and preserves leading tabs', async () => {
|
||||||
|
const { addLineNumbers } = await importFileModuleWithKillswitchEnabled(false)
|
||||||
|
|
||||||
|
const result = addLineNumbers({
|
||||||
|
content: '\tfirst\n\t\tsecond',
|
||||||
|
startLine: 41,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBe('41→\tfirst\n42→\t\tsecond')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps padded arrow format when compact mode is disabled', async () => {
|
||||||
|
const { addLineNumbers } = await importFileModuleWithKillswitchEnabled(true)
|
||||||
|
|
||||||
|
const result = addLineNumbers({
|
||||||
|
content: 'alpha\nbeta',
|
||||||
|
startLine: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBe(' 1→alpha\n 2→beta')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('stripLineNumberPrefix', () => {
|
||||||
|
test('strips compact arrow, padded arrow, and legacy tab prefixes', async () => {
|
||||||
|
const { stripLineNumberPrefix } = await importFileModuleWithKillswitchEnabled(
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stripLineNumberPrefix('41→\tfirst')).toBe('\tfirst')
|
||||||
|
expect(stripLineNumberPrefix(' 2→beta')).toBe('beta')
|
||||||
|
expect(stripLineNumberPrefix('7\t\tlegacy-tab')).toBe('\tlegacy-tab')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -267,7 +267,7 @@ export async function suggestPathUnderCwd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to use the compact line-number prefix format (`N\t` instead of
|
* Whether to use the compact line-number prefix format (`N→` instead of
|
||||||
* ` N→`). The padded-arrow format costs 9 bytes/line overhead; at
|
* ` N→`). The padded-arrow format costs 9 bytes/line overhead; at
|
||||||
* 1.35B Read calls × 132 lines avg this is 2.18% of fleet uncached input
|
* 1.35B Read calls × 132 lines avg this is 2.18% of fleet uncached input
|
||||||
* (bq-queries/read_line_prefix_overhead_verify.sql).
|
* (bq-queries/read_line_prefix_overhead_verify.sql).
|
||||||
@@ -303,7 +303,7 @@ export function addLineNumbers({
|
|||||||
|
|
||||||
if (isCompactLinePrefixEnabled()) {
|
if (isCompactLinePrefixEnabled()) {
|
||||||
return lines
|
return lines
|
||||||
.map((line, index) => `${index + startLine}\t${line}`)
|
.map((line, index) => `${index + startLine}→${line}`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,9 +104,19 @@ const OPENAI_CONTEXT_WINDOWS: Record<string, number> = {
|
|||||||
'devstral-latest': 256_000,
|
'devstral-latest': 256_000,
|
||||||
'ministral-3b-latest': 256_000,
|
'ministral-3b-latest': 256_000,
|
||||||
|
|
||||||
// MiniMax
|
// MiniMax (all M2.x variants share 204,800 context, 131,072 max output)
|
||||||
'MiniMax-M2.7': 204_800,
|
'MiniMax-M2.7': 204_800,
|
||||||
|
'MiniMax-M2.7-highspeed': 204_800,
|
||||||
|
'MiniMax-M2.5': 204_800,
|
||||||
|
'MiniMax-M2.5-highspeed': 204_800,
|
||||||
|
'MiniMax-M2.1': 204_800,
|
||||||
|
'MiniMax-M2.1-highspeed': 204_800,
|
||||||
'minimax-m2.7': 204_800,
|
'minimax-m2.7': 204_800,
|
||||||
|
'minimax-m2.7-highspeed': 204_800,
|
||||||
|
'minimax-m2.5': 204_800,
|
||||||
|
'minimax-m2.5-highspeed': 204_800,
|
||||||
|
'minimax-m2.1': 204_800,
|
||||||
|
'minimax-m2.1-highspeed': 204_800,
|
||||||
|
|
||||||
// Google (via OpenRouter)
|
// Google (via OpenRouter)
|
||||||
'google/gemini-2.0-flash':1_048_576,
|
'google/gemini-2.0-flash':1_048_576,
|
||||||
@@ -223,9 +233,19 @@ const OPENAI_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
|||||||
'mistral-large-latest': 32_768,
|
'mistral-large-latest': 32_768,
|
||||||
'mistral-small-latest': 32_768,
|
'mistral-small-latest': 32_768,
|
||||||
|
|
||||||
// MiniMax
|
// MiniMax (all M2.x variants share 131,072 max output)
|
||||||
'MiniMax-M2.7': 131_072,
|
'MiniMax-M2.7': 131_072,
|
||||||
|
'MiniMax-M2.7-highspeed': 131_072,
|
||||||
|
'MiniMax-M2.5': 131_072,
|
||||||
|
'MiniMax-M2.5-highspeed': 131_072,
|
||||||
|
'MiniMax-M2.1': 131_072,
|
||||||
|
'MiniMax-M2.1-highspeed': 131_072,
|
||||||
'minimax-m2.7': 131_072,
|
'minimax-m2.7': 131_072,
|
||||||
|
'minimax-m2.7-highspeed': 131_072,
|
||||||
|
'minimax-m2.5': 131_072,
|
||||||
|
'minimax-m2.5-highspeed': 131_072,
|
||||||
|
'minimax-m2.1': 131_072,
|
||||||
|
'minimax-m2.1-highspeed': 131_072,
|
||||||
|
|
||||||
// Google (via OpenRouter)
|
// Google (via OpenRouter)
|
||||||
'google/gemini-2.0-flash': 8_192,
|
'google/gemini-2.0-flash': 8_192,
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { afterEach, expect, test } from 'bun:test'
|
|||||||
import { getProviderValidationError } from './providerValidation.ts'
|
import { getProviderValidationError } from './providerValidation.ts'
|
||||||
|
|
||||||
const originalEnv = {
|
const originalEnv = {
|
||||||
|
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
|
||||||
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
|
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
||||||
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
|
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
|
||||||
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
|
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
|
||||||
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
||||||
@@ -20,6 +23,9 @@ function restoreEnv(key: string, value: string | undefined): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||||
|
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
||||||
|
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
|
||||||
restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI)
|
restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI)
|
||||||
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
|
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
|
||||||
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
|
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
|
||||||
@@ -71,3 +77,19 @@ test('still errors when no Gemini credential source is available', async () => {
|
|||||||
'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_USE_GEMINI=1.',
|
'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_USE_GEMINI=1.',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('openai missing key error includes recovery guidance and config locations', async () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1'
|
||||||
|
delete process.env.OPENAI_API_KEY
|
||||||
|
|
||||||
|
const message = await getProviderValidationError(process.env)
|
||||||
|
expect(message).toContain(
|
||||||
|
'OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1 and OPENAI_BASE_URL is not local.',
|
||||||
|
)
|
||||||
|
expect(message).toContain(
|
||||||
|
'set CLAUDE_CODE_USE_OPENAI=0 in your shell environment',
|
||||||
|
)
|
||||||
|
expect(message).toContain('Saved startup settings can come from')
|
||||||
|
expect(message).toContain('.openclaude-profile.json')
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import { resolve } from 'node:path'
|
||||||
import {
|
import {
|
||||||
getGithubEndpointType,
|
getGithubEndpointType,
|
||||||
isLocalProviderUrl,
|
isLocalProviderUrl,
|
||||||
resolveCodexApiCredentials,
|
resolveCodexApiCredentials,
|
||||||
resolveProviderRequest,
|
resolveProviderRequest,
|
||||||
} from '../services/api/providerConfig.js'
|
} from '../services/api/providerConfig.js'
|
||||||
|
import { getGlobalClaudeFile } from './env.js'
|
||||||
import {
|
import {
|
||||||
type GeminiResolvedCredential,
|
type GeminiResolvedCredential,
|
||||||
resolveGeminiCredential,
|
resolveGeminiCredential,
|
||||||
} from './geminiAuth.js'
|
} from './geminiAuth.js'
|
||||||
import { redactSecretValueForDisplay } from './providerProfile.js'
|
import { PROFILE_FILE_NAME, redactSecretValueForDisplay } from './providerProfile.js'
|
||||||
|
|
||||||
function isEnvTruthy(value: string | undefined): boolean {
|
function isEnvTruthy(value: string | undefined): boolean {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
@@ -61,6 +63,17 @@ function checkGithubTokenStatus(
|
|||||||
return 'valid'
|
return 'valid'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOpenAIMissingKeyMessage(): string {
|
||||||
|
const globalConfigPath = getGlobalClaudeFile()
|
||||||
|
const profilePath = resolve(process.cwd(), PROFILE_FILE_NAME)
|
||||||
|
|
||||||
|
return [
|
||||||
|
'OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1 and OPENAI_BASE_URL is not local.',
|
||||||
|
`To recover, run /provider and switch provider, or set CLAUDE_CODE_USE_OPENAI=0 in your shell environment.`,
|
||||||
|
`Saved startup settings can come from ${globalConfigPath} or ${profilePath}.`,
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
export async function getProviderValidationError(
|
export async function getProviderValidationError(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -137,7 +150,7 @@ export async function getProviderValidationError(
|
|||||||
if (useGithub && hasGithubToken) {
|
if (useGithub && hasGithubToken) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return 'OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1 and OPENAI_BASE_URL is not local.'
|
return getOpenAIMissingKeyMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user