osborn 0.9.205 → 0.9.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/claude-llm.js
CHANGED
|
@@ -845,9 +845,10 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
845
845
|
// The [1m] suffix opts into Opus 4.8's 1M context window (Claude Code's
|
|
846
846
|
// context-1m-2025-08-07 beta). WITHOUT it the SDK runs opus at its 200k
|
|
847
847
|
// base, so auto-compaction fired at ~153k every ~10 min (confirmed in the
|
|
848
|
-
// live agent log: compact_boundary pre_tokens≈153k).
|
|
849
|
-
//
|
|
850
|
-
|
|
848
|
+
// live agent log: compact_boundary pre_tokens≈153k). Opus 5.5 has 1M context
|
|
849
|
+
// by default — no [1m] beta suffix needed. Overridable via opts.model.
|
|
850
|
+
// prev: 'claude-opus-4-8[1m]' → 'claude-opus-5-5' (2026-09-24, cheaper + matches Fable 5.1)
|
|
851
|
+
return this.#opts.model || 'claude-opus-5-5';
|
|
851
852
|
}
|
|
852
853
|
get sessionId() {
|
|
853
854
|
return this.#sessionId;
|
|
@@ -869,7 +870,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
869
870
|
*/
|
|
870
871
|
setTurbo(on) {
|
|
871
872
|
this.#turbo = on;
|
|
872
|
-
console.log(`⚡ Turbo mode ${on ? 'ON' : 'OFF'} — main model → ${on ? FAST_MODEL : (this.#opts.model || 'claude-opus-
|
|
873
|
+
console.log(`⚡ Turbo mode ${on ? 'ON' : 'OFF'} — main model → ${on ? FAST_MODEL : (this.#opts.model || 'claude-opus-5-5')}; applies at next query cold start`);
|
|
873
874
|
}
|
|
874
875
|
/** Read-only accessor used by ClaudeLLMStream (private fields are class-scoped). */
|
|
875
876
|
get turbo() { return this.#turbo; }
|
|
@@ -1708,7 +1709,8 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1708
1709
|
allowedTools,
|
|
1709
1710
|
// model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
|
|
1710
1711
|
// Turbo: when on, override main model to FAST_MODEL regardless of config.
|
|
1711
|
-
model: this.#llmRef.turbo ? FAST_MODEL : (this.#opts.model || 'claude-opus-4-8[1m]'), //
|
|
1712
|
+
// model: this.#llmRef.turbo ? FAST_MODEL : (this.#opts.model || 'claude-opus-4-8[1m]'), // prev
|
|
1713
|
+
model: this.#llmRef.turbo ? FAST_MODEL : (this.#opts.model || 'claude-opus-5-5'), // Opus 5.5 default — 1M context built-in, no beta suffix needed
|
|
1712
1714
|
enableFileCheckpointing: true,
|
|
1713
1715
|
settingSources: ['project', 'user'],
|
|
1714
1716
|
extraArgs: { 'replay-user-messages': null },
|
package/package.json
CHANGED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for CLAUDE_AUTOCOMPACT_PCT_OVERRIDE in claude-llm.ts
|
|
3
|
-
*
|
|
4
|
-
* Scope: verifies that the compaction threshold is set to a value that
|
|
5
|
-
* leaves meaningful context headroom before compaction fires.
|
|
6
|
-
*
|
|
7
|
-
* Derived from: requirements documented in CLAUDE.md (ENABLE_1M_CONTEXT,
|
|
8
|
-
* compaction rationale comments in claude-llm.ts) and the 0.9.177 → 0.9.178
|
|
9
|
-
* change that lowered the threshold from 92 → 60.
|
|
10
|
-
* NOT derived from the implementation diff.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { strict as assert } from 'node:assert'
|
|
14
|
-
import { readFileSync } from 'node:fs'
|
|
15
|
-
import { dirname, join } from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
|
|
18
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
|
-
const src = readFileSync(join(here, '../src/claude-llm.ts'), 'utf8')
|
|
20
|
-
|
|
21
|
-
let passed = 0
|
|
22
|
-
let failed = 0
|
|
23
|
-
|
|
24
|
-
function test(name: string, fn: () => void) {
|
|
25
|
-
try {
|
|
26
|
-
fn()
|
|
27
|
-
console.log(` ✅ ${name}`)
|
|
28
|
-
passed++
|
|
29
|
-
} catch (err: any) {
|
|
30
|
-
console.error(` ❌ ${name}`)
|
|
31
|
-
console.error(` ${err.message}`)
|
|
32
|
-
failed++
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Extract the numeric value of CLAUDE_AUTOCOMPACT_PCT_OVERRIDE from source
|
|
37
|
-
const match = src.match(/process\.env\.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE\s*=\s*'(\d+)'/)
|
|
38
|
-
const pctValue = match ? parseInt(match[1], 10) : NaN
|
|
39
|
-
|
|
40
|
-
console.log('\n=== autocompact PCT override regression tests ===\n')
|
|
41
|
-
console.log(` (detected value: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '${pctValue}')`)
|
|
42
|
-
console.log()
|
|
43
|
-
|
|
44
|
-
test('CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is set in claude-llm.ts', () => {
|
|
45
|
-
assert.ok(
|
|
46
|
-
!isNaN(pctValue),
|
|
47
|
-
'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE must be explicitly set in claude-llm.ts'
|
|
48
|
-
)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is a valid percentage (1–99)', () => {
|
|
52
|
-
assert.ok(
|
|
53
|
-
pctValue >= 1 && pctValue <= 99,
|
|
54
|
-
`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE must be between 1 and 99, got ${pctValue}`
|
|
55
|
-
)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is ≤ 92 (prevents compaction at extreme tail)', () => {
|
|
59
|
-
// Values above 92 leave almost no headroom before hitting the token limit.
|
|
60
|
-
// The long-standing default was 92; anything higher than that risks the
|
|
61
|
-
// "compacting every message" bug described in memory/osborn-1m-context-and-worker-restart.md
|
|
62
|
-
assert.ok(
|
|
63
|
-
pctValue <= 92,
|
|
64
|
-
`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is ${pctValue} which is above 92 — ` +
|
|
65
|
-
'values this high risk compaction firing on every message at the context ceiling'
|
|
66
|
-
)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('ENABLE_1M_CONTEXT is also set in claude-llm.ts', () => {
|
|
70
|
-
assert.ok(
|
|
71
|
-
src.includes("process.env.ENABLE_1M_CONTEXT = '1'"),
|
|
72
|
-
"ENABLE_1M_CONTEXT = '1' must be set alongside CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"
|
|
73
|
-
)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
console.log(`\n--- ${passed + failed} tests: ${passed} passed, ${failed} failed ---\n`)
|
|
77
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression test for fastBrainModel config (agent/src/config.ts)
|
|
3
|
-
*
|
|
4
|
-
* Scope: verifies the DEFAULT_CONFIG.inference.fastBrainModel is set to
|
|
5
|
-
* 'deepseek/deepseek-chat' and that the env-override path (OSBORN_FAST_BRAIN_MODEL)
|
|
6
|
-
* still works.
|
|
7
|
-
*
|
|
8
|
-
* History:
|
|
9
|
-
* 0.9.202 switched deepseek → gpt-4o-mini (deepseek credits depleted).
|
|
10
|
-
* 0.9.203 reverts back to deepseek/deepseek-chat (credits restored).
|
|
11
|
-
*
|
|
12
|
-
* Derived from: config.ts interface definitions and documented inference
|
|
13
|
-
* alternatives block. NOT derived from the implementation diff.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { strict as assert } from 'node:assert'
|
|
17
|
-
import { readFileSync } from 'node:fs'
|
|
18
|
-
import { dirname, join } from 'node:path'
|
|
19
|
-
import { fileURLToPath } from 'node:url'
|
|
20
|
-
|
|
21
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
22
|
-
const src = readFileSync(join(here, '../src/config.ts'), 'utf8')
|
|
23
|
-
|
|
24
|
-
let passed = 0
|
|
25
|
-
let failed = 0
|
|
26
|
-
|
|
27
|
-
function test(name: string, fn: () => void) {
|
|
28
|
-
try {
|
|
29
|
-
fn()
|
|
30
|
-
console.log(` ✅ ${name}`)
|
|
31
|
-
passed++
|
|
32
|
-
} catch (err: any) {
|
|
33
|
-
console.error(` ❌ ${name}`)
|
|
34
|
-
console.error(` ${err.message}`)
|
|
35
|
-
failed++
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── Extract the active fastBrainModel value from the source (non-commented line) ───
|
|
40
|
-
// We look for the uncommented fastBrainModel: '<value>' line (no leading //)
|
|
41
|
-
const modelMatch = src.match(/^\s+fastBrainModel:\s*'([^']+)'/m)
|
|
42
|
-
const activeModel = modelMatch?.[1]
|
|
43
|
-
|
|
44
|
-
// ── Extract the active fastBrainProvider value ────────────────────────────────────
|
|
45
|
-
const providerMatch = src.match(/^\s+fastBrainProvider:\s*'([^']+)'/m)
|
|
46
|
-
const activeProvider = providerMatch?.[1]
|
|
47
|
-
|
|
48
|
-
console.log('\n=== fastBrainModel config regression tests (0.9.203) ===\n')
|
|
49
|
-
console.log(` (detected fastBrainModel: '${activeModel}')`)
|
|
50
|
-
console.log(` (detected fastBrainProvider: '${activeProvider}')`)
|
|
51
|
-
console.log()
|
|
52
|
-
|
|
53
|
-
// ── 1. Active model is deepseek/deepseek-chat ─────────────────────────────────────
|
|
54
|
-
test('DEFAULT_CONFIG.inference.fastBrainModel is "deepseek/deepseek-chat"', () => {
|
|
55
|
-
assert.equal(
|
|
56
|
-
activeModel,
|
|
57
|
-
'deepseek/deepseek-chat',
|
|
58
|
-
`Expected 'deepseek/deepseek-chat' but got '${activeModel}'. ` +
|
|
59
|
-
'0.9.203 reverts to deepseek-chat after credits were restored.'
|
|
60
|
-
)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// ── 2. Active model is NOT the previous interim model ────────────────────────────
|
|
64
|
-
test('DEFAULT_CONFIG.inference.fastBrainModel is NOT "openai/gpt-4o-mini"', () => {
|
|
65
|
-
assert.notEqual(
|
|
66
|
-
activeModel,
|
|
67
|
-
'openai/gpt-4o-mini',
|
|
68
|
-
'gpt-4o-mini was the interim model set in 0.9.202 — it should no longer be the default.'
|
|
69
|
-
)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
// ── 3. Provider is openrouter ─────────────────────────────────────────────────────
|
|
73
|
-
test('DEFAULT_CONFIG.inference.fastBrainProvider is "openrouter"', () => {
|
|
74
|
-
assert.equal(
|
|
75
|
-
activeProvider,
|
|
76
|
-
'openrouter',
|
|
77
|
-
`Expected provider 'openrouter' but got '${activeProvider}'.`
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// ── 4. Env-override path still present ───────────────────────────────────────────
|
|
82
|
-
test('config.ts still has OSBORN_FAST_BRAIN_MODEL env override', () => {
|
|
83
|
-
assert.ok(
|
|
84
|
-
src.includes('OSBORN_FAST_BRAIN_MODEL'),
|
|
85
|
-
'OSBORN_FAST_BRAIN_MODEL env-override must remain in config.ts for ops flexibility'
|
|
86
|
-
)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// ── 5. deepseek-chat is listed in alternatives block (not removed) ───────────────
|
|
90
|
-
test('gpt-4o-mini is still listed in the Alternatives block (not erased)', () => {
|
|
91
|
-
// gpt-4o-mini must remain as a commented alternative — removing it could
|
|
92
|
-
// surprise operators who relied on it when deepseek credits lapsed.
|
|
93
|
-
assert.ok(
|
|
94
|
-
src.includes("// fastBrainModel: 'openai/gpt-4o-mini'"),
|
|
95
|
-
"openai/gpt-4o-mini must remain as a commented alternative in config.ts"
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// ── 6. fastBrainModel field type allows arbitrary string ─────────────────────────
|
|
100
|
-
test('fastBrainModel interface field is typed as string (accepts any OpenRouter model ID)', () => {
|
|
101
|
-
assert.ok(
|
|
102
|
-
src.includes('fastBrainModel?: string'),
|
|
103
|
-
'fastBrainModel must remain typed as string (not an enum) — allows future model swaps without type changes'
|
|
104
|
-
)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// ── Summary ───────────────────────────────────────────────────────────────────────
|
|
108
|
-
console.log(`\n--- ${passed + failed} tests: ${passed} passed, ${failed} failed ---\n`)
|
|
109
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the OpenRouter fallback onboarding tier (0.9.204)
|
|
3
|
-
*
|
|
4
|
-
* Scope: verifies FALLBACK_MODEL, applyAuthFallback(), and the upgrade guard
|
|
5
|
-
* (clearAuthFallbackIfActive). Derived from requirements in CLAUDE.md,
|
|
6
|
-
* the feature docstring, and documented behavior — NOT from the diff.
|
|
7
|
-
*
|
|
8
|
-
* Strategy: static source analysis (same pattern as existing tests in this
|
|
9
|
-
* directory) to avoid live credential / LiveKit plugin dependencies.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { strict as assert } from 'node:assert'
|
|
13
|
-
import { readFileSync } from 'node:fs'
|
|
14
|
-
import { dirname, join } from 'node:path'
|
|
15
|
-
import { fileURLToPath } from 'node:url'
|
|
16
|
-
|
|
17
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
18
|
-
const authSrc = readFileSync(join(here, '../src/claude-auth.ts'), 'utf8')
|
|
19
|
-
const llmSrc = readFileSync(join(here, '../src/claude-llm.ts'), 'utf8')
|
|
20
|
-
const indexSrc = readFileSync(join(here, '../src/index.ts'), 'utf8')
|
|
21
|
-
|
|
22
|
-
let passed = 0
|
|
23
|
-
let failed = 0
|
|
24
|
-
|
|
25
|
-
function test(name: string, fn: () => void) {
|
|
26
|
-
try {
|
|
27
|
-
fn()
|
|
28
|
-
console.log(` ✅ ${name}`)
|
|
29
|
-
passed++
|
|
30
|
-
} catch (err: any) {
|
|
31
|
-
console.error(` ❌ ${name}`)
|
|
32
|
-
console.error(` ${err.message}`)
|
|
33
|
-
failed++
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log('\n=== OpenRouter fallback tier regression tests (0.9.204) ===\n')
|
|
38
|
-
|
|
39
|
-
// ── 1. FALLBACK_MODEL is exported from claude-llm.ts ─────────────────────────
|
|
40
|
-
const fallbackModelMatch = llmSrc.match(/export const FALLBACK_MODEL\s*=\s*'([^']+)'/)
|
|
41
|
-
const fallbackModel = fallbackModelMatch?.[1]
|
|
42
|
-
console.log(` (detected FALLBACK_MODEL: '${fallbackModel}')`)
|
|
43
|
-
console.log()
|
|
44
|
-
|
|
45
|
-
test('FALLBACK_MODEL is exported from claude-llm.ts', () => {
|
|
46
|
-
assert.ok(
|
|
47
|
-
llmSrc.includes('export const FALLBACK_MODEL'),
|
|
48
|
-
'FALLBACK_MODEL must be exported from claude-llm.ts for use by applyAuthFallback()'
|
|
49
|
-
)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('FALLBACK_MODEL is "minimax/minimax-m3"', () => {
|
|
53
|
-
assert.equal(
|
|
54
|
-
fallbackModel,
|
|
55
|
-
'minimax/minimax-m3',
|
|
56
|
-
`Expected 'minimax/minimax-m3' but got '${fallbackModel}'. ` +
|
|
57
|
-
'0.9.204 sets MiniMax M3 as the OpenRouter tier-3 default.'
|
|
58
|
-
)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('FALLBACK_MODEL is a non-empty string', () => {
|
|
62
|
-
assert.ok(
|
|
63
|
-
fallbackModel && fallbackModel.trim() !== '',
|
|
64
|
-
'FALLBACK_MODEL must be a non-empty string — an empty fallback would 404 every call'
|
|
65
|
-
)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// ── 2. applyAuthFallback is exported from claude-auth.ts ─────────────────────
|
|
69
|
-
|
|
70
|
-
test('applyAuthFallback is exported from claude-auth.ts', () => {
|
|
71
|
-
assert.ok(
|
|
72
|
-
authSrc.includes('export function applyAuthFallback()'),
|
|
73
|
-
'applyAuthFallback must be exported so index.ts can call it as the tier-resolution entry point'
|
|
74
|
-
)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// ── 3. Tier 3 sets ALL required env vars ─────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
test('Tier 3 sets ANTHROPIC_BASE_URL to openrouter.ai endpoint', () => {
|
|
80
|
-
assert.ok(
|
|
81
|
-
authSrc.includes("process.env.ANTHROPIC_BASE_URL = 'https://openrouter.ai/api'"),
|
|
82
|
-
'Tier 3 must redirect the SDK to the OpenRouter Anthropic-Messages endpoint'
|
|
83
|
-
)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('Tier 3 sets ANTHROPIC_AUTH_TOKEN to the OpenRouter key', () => {
|
|
87
|
-
assert.ok(
|
|
88
|
-
authSrc.includes('process.env.ANTHROPIC_AUTH_TOKEN = openRouterKey'),
|
|
89
|
-
'ANTHROPIC_AUTH_TOKEN must be set to openRouterKey — the SDK injects it as Authorization: Bearer'
|
|
90
|
-
)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('Tier 3 sets ANTHROPIC_API_KEY to empty string (disables SDK key-validation bypass)', () => {
|
|
94
|
-
assert.ok(
|
|
95
|
-
authSrc.includes("process.env.ANTHROPIC_API_KEY = ''"),
|
|
96
|
-
'ANTHROPIC_API_KEY must be set to empty string to prevent the SDK key-validation path ' +
|
|
97
|
-
'from overriding ANTHROPIC_AUTH_TOKEN'
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('Tier 3 sets ANTHROPIC_MODEL to FALLBACK_MODEL', () => {
|
|
102
|
-
assert.ok(
|
|
103
|
-
authSrc.includes('process.env.ANTHROPIC_MODEL = FALLBACK_MODEL'),
|
|
104
|
-
'ANTHROPIC_MODEL must be pinned to FALLBACK_MODEL — an unmapped slot 404s on OpenRouter'
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('Tier 3 sets ANTHROPIC_SMALL_FAST_MODEL to FALLBACK_MODEL', () => {
|
|
109
|
-
assert.ok(
|
|
110
|
-
authSrc.includes('process.env.ANTHROPIC_SMALL_FAST_MODEL = FALLBACK_MODEL'),
|
|
111
|
-
'ANTHROPIC_SMALL_FAST_MODEL must be pinned to FALLBACK_MODEL for background/small-fast calls'
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('Tier 3 sets CLAUDE_CODE_SUBAGENT_MODEL to FALLBACK_MODEL', () => {
|
|
116
|
-
assert.ok(
|
|
117
|
-
authSrc.includes('process.env.CLAUDE_CODE_SUBAGENT_MODEL = FALLBACK_MODEL'),
|
|
118
|
-
'CLAUDE_CODE_SUBAGENT_MODEL must be pinned to FALLBACK_MODEL — sub-agent calls use this slot'
|
|
119
|
-
)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test('Tier 3 sets CLAUDE_CODE_MAX_CONTEXT_TOKENS to 131072', () => {
|
|
123
|
-
assert.ok(
|
|
124
|
-
authSrc.includes("process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = '131072'"),
|
|
125
|
-
"CLAUDE_CODE_MAX_CONTEXT_TOKENS must be set to '131072' to silence unrecognized-model context cap warning"
|
|
126
|
-
)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
// ── 4. Tier 3 guards: fallbackTierActive flag ────────────────────────────────
|
|
130
|
-
|
|
131
|
-
test('fallbackTierActive flag is set to true in tier-3 path', () => {
|
|
132
|
-
assert.ok(
|
|
133
|
-
authSrc.includes('fallbackTierActive = true'),
|
|
134
|
-
'fallbackTierActive must be set to true in tier 3 so the upgrade guard knows to clear the redirect'
|
|
135
|
-
)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('clearAuthFallbackIfActive resets fallbackTierActive to false', () => {
|
|
139
|
-
assert.ok(
|
|
140
|
-
authSrc.includes('fallbackTierActive = false'),
|
|
141
|
-
'clearAuthFallbackIfActive must reset fallbackTierActive so subsequent calls are no-ops'
|
|
142
|
-
)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
// ── 5. Upgrade guard: clear is called from onComplete ─────────────────────────
|
|
146
|
-
|
|
147
|
-
test('clearAuthFallbackIfActive is called from onComplete callback in ensureClaudeAuth', () => {
|
|
148
|
-
assert.ok(
|
|
149
|
-
authSrc.includes('clearAuthFallbackIfActive()'),
|
|
150
|
-
'clearAuthFallbackIfActive must be called inside onComplete so real Claude login clears the OpenRouter redirect'
|
|
151
|
-
)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// ── 6. clearAuthFallbackIfActive: guarded by flag (no-op when inactive) ──────
|
|
155
|
-
|
|
156
|
-
test('clearAuthFallbackIfActive checks fallbackTierActive before clearing env vars', () => {
|
|
157
|
-
assert.ok(
|
|
158
|
-
authSrc.includes('if (!fallbackTierActive) return'),
|
|
159
|
-
'clearAuthFallbackIfActive must be a no-op when fallbackTierActive is false to protect user-supplied ANTHROPIC_BASE_URL'
|
|
160
|
-
)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
// ── 7. clearAuthFallbackIfActive deletes the three vars it set ────────────────
|
|
164
|
-
|
|
165
|
-
test('clearAuthFallbackIfActive deletes ANTHROPIC_BASE_URL', () => {
|
|
166
|
-
assert.ok(
|
|
167
|
-
authSrc.includes('delete process.env.ANTHROPIC_BASE_URL'),
|
|
168
|
-
'clearAuthFallbackIfActive must delete ANTHROPIC_BASE_URL to stop routing to OpenRouter after real login'
|
|
169
|
-
)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
test('clearAuthFallbackIfActive deletes ANTHROPIC_AUTH_TOKEN', () => {
|
|
173
|
-
assert.ok(
|
|
174
|
-
authSrc.includes('delete process.env.ANTHROPIC_AUTH_TOKEN'),
|
|
175
|
-
'clearAuthFallbackIfActive must delete ANTHROPIC_AUTH_TOKEN to avoid leaking the OpenRouter key'
|
|
176
|
-
)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
test('clearAuthFallbackIfActive deletes ANTHROPIC_API_KEY (was forced to empty string)', () => {
|
|
180
|
-
assert.ok(
|
|
181
|
-
authSrc.includes('delete process.env.ANTHROPIC_API_KEY'),
|
|
182
|
-
'clearAuthFallbackIfActive must delete the empty ANTHROPIC_API_KEY so the real OAuth path works cleanly'
|
|
183
|
-
)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// ── 8. Tier guards: tier-1 and tier-2 short-circuit ─────────────────────────
|
|
187
|
-
|
|
188
|
-
test('Tier 1: isClaudeAuthenticated() check exists before any env mutation', () => {
|
|
189
|
-
// The isClaudeAuthenticated call must come before ANTHROPIC_BASE_URL assignment
|
|
190
|
-
const authCheckIdx = authSrc.indexOf('if (isClaudeAuthenticated())')
|
|
191
|
-
const envSetIdx = authSrc.indexOf("process.env.ANTHROPIC_BASE_URL = 'https://openrouter.ai/api'")
|
|
192
|
-
assert.ok(authCheckIdx !== -1, 'isClaudeAuthenticated() check must exist in applyAuthFallback')
|
|
193
|
-
assert.ok(envSetIdx !== -1, 'ANTHROPIC_BASE_URL assignment must exist')
|
|
194
|
-
assert.ok(
|
|
195
|
-
authCheckIdx < envSetIdx,
|
|
196
|
-
`Tier 1 guard (pos ${authCheckIdx}) must precede env mutation (pos ${envSetIdx}) — ` +
|
|
197
|
-
'otherwise an authenticated user could have their env vars overwritten'
|
|
198
|
-
)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test('Tier 2: ANTHROPIC_API_KEY non-empty check exists before any env mutation', () => {
|
|
202
|
-
const keyCheckIdx = authSrc.indexOf("process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim() !== ''")
|
|
203
|
-
const envSetIdx = authSrc.indexOf("process.env.ANTHROPIC_BASE_URL = 'https://openrouter.ai/api'")
|
|
204
|
-
assert.ok(keyCheckIdx !== -1, 'ANTHROPIC_API_KEY non-empty guard must exist in applyAuthFallback')
|
|
205
|
-
assert.ok(
|
|
206
|
-
keyCheckIdx < envSetIdx,
|
|
207
|
-
`Tier 2 guard (pos ${keyCheckIdx}) must precede env mutation (pos ${envSetIdx}) — ` +
|
|
208
|
-
'a user with their own ANTHROPIC_API_KEY must not have it clobbered'
|
|
209
|
-
)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// ── 9. Tier 3 fail: no throw ─────────────────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
test('Tier 3 fail path uses console.warn, NOT throw', () => {
|
|
215
|
-
// The no-creds warn must appear in the source; a throw would be wrong per spec.
|
|
216
|
-
assert.ok(
|
|
217
|
-
authSrc.includes("console.warn('[auth-fallback] no Claude login, no ANTHROPIC_API_KEY, and no OPENROUTER_API_KEY"),
|
|
218
|
-
'Tier 3 fail must warn rather than throw — the SDK may still find credentials we did not detect'
|
|
219
|
-
)
|
|
220
|
-
// There must be no throw inside applyAuthFallback
|
|
221
|
-
// Extract just the applyAuthFallback function body
|
|
222
|
-
const fnStart = authSrc.indexOf('export function applyAuthFallback()')
|
|
223
|
-
const fnEnd = authSrc.indexOf('\nexport function ', fnStart + 1)
|
|
224
|
-
const fnBody = fnEnd !== -1 ? authSrc.slice(fnStart, fnEnd) : authSrc.slice(fnStart, fnStart + 2000)
|
|
225
|
-
assert.ok(
|
|
226
|
-
!fnBody.includes('throw '),
|
|
227
|
-
'applyAuthFallback must NOT throw — it is a no-op fallback when no OpenRouter key is present'
|
|
228
|
-
)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
// ── 10. FALLBACK_MODEL is imported by claude-auth.ts ─────────────────────────
|
|
232
|
-
|
|
233
|
-
test('claude-auth.ts imports FALLBACK_MODEL from claude-llm.ts', () => {
|
|
234
|
-
assert.ok(
|
|
235
|
-
authSrc.includes("import { FALLBACK_MODEL } from './claude-llm.js'"),
|
|
236
|
-
"claude-auth.ts must import FALLBACK_MODEL from './claude-llm.js' — circular imports or wrong path cause runtime failures"
|
|
237
|
-
)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
// ── 11. index.ts integration ─────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
test('index.ts imports applyAuthFallback from claude-auth', () => {
|
|
243
|
-
assert.ok(
|
|
244
|
-
indexSrc.includes('applyAuthFallback') && indexSrc.includes("from './claude-auth.js'"),
|
|
245
|
-
'index.ts must import applyAuthFallback from ./claude-auth.js'
|
|
246
|
-
)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
test('index.ts calls applyAuthFallback() after ensureClaudeAuth', () => {
|
|
250
|
-
const ensureIdx = indexSrc.indexOf('ensureClaudeAuth(')
|
|
251
|
-
const applyIdx = indexSrc.indexOf('applyAuthFallback()')
|
|
252
|
-
assert.ok(applyIdx !== -1, 'applyAuthFallback() must be called in index.ts')
|
|
253
|
-
assert.ok(
|
|
254
|
-
applyIdx > ensureIdx,
|
|
255
|
-
`applyAuthFallback call (pos ${applyIdx}) must come AFTER ensureClaudeAuth (pos ${ensureIdx}) — ` +
|
|
256
|
-
'auth must be attempted first so tier-1 detection is accurate'
|
|
257
|
-
)
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// ── 12. Backward-compat: existing FAST_MODEL export still present ─────────────
|
|
261
|
-
|
|
262
|
-
test('FAST_MODEL constant still exported from claude-llm.ts (backward compat)', () => {
|
|
263
|
-
assert.ok(
|
|
264
|
-
llmSrc.includes("export const FAST_MODEL = 'haiku'"),
|
|
265
|
-
"FAST_MODEL = 'haiku' must remain — removing it breaks existing turbo-mode callers"
|
|
266
|
-
)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test('FAST_MODEL and FALLBACK_MODEL are distinct constants', () => {
|
|
270
|
-
assert.notEqual(
|
|
271
|
-
fallbackModel,
|
|
272
|
-
'haiku',
|
|
273
|
-
'FALLBACK_MODEL must not equal FAST_MODEL — they serve different roles: ' +
|
|
274
|
-
'FAST_MODEL is the turbo override, FALLBACK_MODEL is the OpenRouter onboarding tier'
|
|
275
|
-
)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
// ── 13. OpenRouter URL is the native Anthropic-Messages endpoint ──────────────
|
|
279
|
-
|
|
280
|
-
test('ANTHROPIC_BASE_URL uses the /api path (not /api/v1 or proxy path)', () => {
|
|
281
|
-
assert.ok(
|
|
282
|
-
authSrc.includes("'https://openrouter.ai/api'"),
|
|
283
|
-
"The URL must be 'https://openrouter.ai/api' (native Anthropic-Messages endpoint) — " +
|
|
284
|
-
"a /v1 or proxy path would require a translation layer"
|
|
285
|
-
)
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
289
|
-
console.log(`\n--- ${passed + failed} tests: ${passed} passed, ${failed} failed ---\n`)
|
|
290
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for voice-io.ts STT configuration
|
|
3
|
-
*
|
|
4
|
-
* Scope: covers the 0.9.175 → 0.9.177 change that switches DIRECT_MODE_STT
|
|
5
|
-
* from deepgram-flux (ML turn detection) back to deepgram (silence-based
|
|
6
|
-
* endpointing), due to Flux V2 keepalive timeout bug (~30s of silence kills
|
|
7
|
-
* the connection).
|
|
8
|
-
*
|
|
9
|
-
* Derived from: requirements in CLAUDE.md (Three Voice Modes), voice-io.ts
|
|
10
|
-
* interface definitions, and the documented bug. NOT derived from the diff.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { strict as assert } from 'node:assert'
|
|
14
|
-
|
|
15
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
16
|
-
// Inline the minimal types — avoid importing from the module which requires
|
|
17
|
-
// live LK plugins (can't instantiate in a unit test without credentials).
|
|
18
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
interface STTConfig {
|
|
20
|
-
provider: 'deepgram' | 'deepgram-flux' | 'groq-whisper' | 'openai-whisper'
|
|
21
|
-
model?: string
|
|
22
|
-
language?: string
|
|
23
|
-
eotThreshold?: number
|
|
24
|
-
eotTimeoutMs?: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
// Read the compiled output so we get the real runtime values, not just TS types
|
|
29
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
30
|
-
// We import from the transpiled dist to avoid plugin side-effects at import time.
|
|
31
|
-
// If dist is missing, fall back to a direct static-analysis of the source file.
|
|
32
|
-
|
|
33
|
-
let DIRECT_MODE_STT_PROVIDER: string | undefined
|
|
34
|
-
let DIRECT_MODE_STT_MODEL: string | undefined
|
|
35
|
-
let DIRECT_MODE_STT_LANG: string | undefined
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
// dist/voice-io.js is the compiled output of the build step
|
|
39
|
-
const mod = await import('../dist/voice-io.js')
|
|
40
|
-
const cfg: STTConfig = mod.DIRECT_MODE_STT
|
|
41
|
-
DIRECT_MODE_STT_PROVIDER = cfg.provider
|
|
42
|
-
DIRECT_MODE_STT_MODEL = cfg.model
|
|
43
|
-
DIRECT_MODE_STT_LANG = cfg.language
|
|
44
|
-
} catch (e) {
|
|
45
|
-
// Fallback: parse source file statically to extract the active provider line
|
|
46
|
-
const { readFileSync } = await import('node:fs')
|
|
47
|
-
const { fileURLToPath } = await import('node:url')
|
|
48
|
-
const { dirname, join } = await import('node:path')
|
|
49
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
50
|
-
const src = readFileSync(join(here, '../src/voice-io.ts'), 'utf8')
|
|
51
|
-
|
|
52
|
-
// Find the non-commented active provider line in DIRECT_MODE_STT
|
|
53
|
-
const providerMatch = src.match(
|
|
54
|
-
/export const DIRECT_MODE_STT[\s\S]*?^\s+provider:\s*'([^']+)'/m
|
|
55
|
-
)
|
|
56
|
-
const modelMatch = src.match(
|
|
57
|
-
/export const DIRECT_MODE_STT[\s\S]*?model:\s*'([^']+)'/m
|
|
58
|
-
)
|
|
59
|
-
const langMatch = src.match(
|
|
60
|
-
/export const DIRECT_MODE_STT[\s\S]*?language:\s*'([^']+)'/m
|
|
61
|
-
)
|
|
62
|
-
DIRECT_MODE_STT_PROVIDER = providerMatch?.[1]
|
|
63
|
-
DIRECT_MODE_STT_MODEL = modelMatch?.[1]
|
|
64
|
-
DIRECT_MODE_STT_LANG = langMatch?.[1]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
68
|
-
// Tests
|
|
69
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
let passed = 0
|
|
72
|
-
let failed = 0
|
|
73
|
-
|
|
74
|
-
function test(name: string, fn: () => void) {
|
|
75
|
-
try {
|
|
76
|
-
fn()
|
|
77
|
-
console.log(` ✅ ${name}`)
|
|
78
|
-
passed++
|
|
79
|
-
} catch (err: any) {
|
|
80
|
-
console.error(` ❌ ${name}`)
|
|
81
|
-
console.error(` ${err.message}`)
|
|
82
|
-
failed++
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log('\n=== voice-io STT config regression tests ===\n')
|
|
87
|
-
|
|
88
|
-
// ── 1. Provider must be deepgram (not deepgram-flux) ──────────────────────────
|
|
89
|
-
test('DIRECT_MODE_STT.provider is "deepgram" (not deepgram-flux)', () => {
|
|
90
|
-
assert.equal(
|
|
91
|
-
DIRECT_MODE_STT_PROVIDER,
|
|
92
|
-
'deepgram',
|
|
93
|
-
`Expected provider "deepgram" but got "${DIRECT_MODE_STT_PROVIDER}". ` +
|
|
94
|
-
'deepgram-flux has a known keepalive timeout bug (silent ~30s kills the connection).'
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('DIRECT_MODE_STT.provider is NOT "deepgram-flux"', () => {
|
|
99
|
-
assert.notEqual(
|
|
100
|
-
DIRECT_MODE_STT_PROVIDER,
|
|
101
|
-
'deepgram-flux',
|
|
102
|
-
'deepgram-flux must not be the active provider — ' +
|
|
103
|
-
'Flux V2 has silent keepalive timeout bug (~30s silence kills connection).'
|
|
104
|
-
)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// ── 2. Model and language ──────────────────────────────────────────────────────
|
|
108
|
-
test('DIRECT_MODE_STT.model is "nova-3"', () => {
|
|
109
|
-
assert.equal(
|
|
110
|
-
DIRECT_MODE_STT_MODEL,
|
|
111
|
-
'nova-3',
|
|
112
|
-
`Expected model "nova-3" but got "${DIRECT_MODE_STT_MODEL}".`
|
|
113
|
-
)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('DIRECT_MODE_STT.language is "en"', () => {
|
|
117
|
-
assert.equal(
|
|
118
|
-
DIRECT_MODE_STT_LANG,
|
|
119
|
-
'en',
|
|
120
|
-
`Expected language "en" but got "${DIRECT_MODE_STT_LANG}".`
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// ── 3. STTConfig type-level: provider values are the expected set ─────────────
|
|
125
|
-
// (These are compile-time guarantees verified at build time, but we assert
|
|
126
|
-
// them at runtime too so future changes surface here.)
|
|
127
|
-
test('DIRECT_MODE_STT provider is one of the valid union members', () => {
|
|
128
|
-
const validProviders = ['deepgram', 'deepgram-flux', 'groq-whisper', 'openai-whisper']
|
|
129
|
-
assert.ok(
|
|
130
|
-
validProviders.includes(DIRECT_MODE_STT_PROVIDER!),
|
|
131
|
-
`provider "${DIRECT_MODE_STT_PROVIDER}" is not in valid set: ${validProviders.join(', ')}`
|
|
132
|
-
)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// ── 4. Backward-compat: createSTT deepgram path shape ────────────────────────
|
|
136
|
-
// We verify the source still has the deepgram case in createSTT so the switch
|
|
137
|
-
// won't hit the `default: throw` path at runtime.
|
|
138
|
-
test('createSTT source still contains deepgram case (backward compat)', async () => {
|
|
139
|
-
const { readFileSync } = await import('node:fs')
|
|
140
|
-
const { fileURLToPath } = await import('node:url')
|
|
141
|
-
const { dirname, join } = await import('node:path')
|
|
142
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
143
|
-
const src = readFileSync(join(here, '../src/voice-io.ts'), 'utf8')
|
|
144
|
-
assert.ok(
|
|
145
|
-
src.includes("case 'deepgram':"),
|
|
146
|
-
"createSTT source must still contain case 'deepgram' — removing it would throw at runtime"
|
|
147
|
-
)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
test('createSTT source still contains deepgram-flux case (backward compat)', async () => {
|
|
151
|
-
const { readFileSync } = await import('node:fs')
|
|
152
|
-
const { fileURLToPath } = await import('node:url')
|
|
153
|
-
const { dirname, join } = await import('node:path')
|
|
154
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
155
|
-
const src = readFileSync(join(here, '../src/voice-io.ts'), 'utf8')
|
|
156
|
-
assert.ok(
|
|
157
|
-
src.includes("case 'deepgram-flux':"),
|
|
158
|
-
"createSTT source must still contain case 'deepgram-flux' — it must remain selectable via config"
|
|
159
|
-
)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// ── 5. deepgram endpointing value in createSTT ────────────────────────────────
|
|
163
|
-
test('createSTT deepgram case uses 550ms endpointing (mid-sentence fragment prevention)', async () => {
|
|
164
|
-
const { readFileSync } = await import('node:fs')
|
|
165
|
-
const { fileURLToPath } = await import('node:url')
|
|
166
|
-
const { dirname, join } = await import('node:path')
|
|
167
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
168
|
-
const src = readFileSync(join(here, '../src/voice-io.ts'), 'utf8')
|
|
169
|
-
assert.ok(
|
|
170
|
-
src.includes('endpointing: 550'),
|
|
171
|
-
'createSTT deepgram case must use endpointing: 550ms to prevent mid-sentence transcript fragments'
|
|
172
|
-
)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
176
|
-
console.log(`\n--- ${passed + failed} tests: ${passed} passed, ${failed} failed ---\n`)
|
|
177
|
-
if (failed > 0) process.exit(1)
|