Contract — fix-claude-config-cache
Behavioral contract
Public API (unchanged — invariant)
export async function createCompletion(
prompt: string,
options?: CompletionOptions,
): Promise<CompletionResult>;
export async function createCompletionWithTools(
prompt: string,
tools: AnthropicTool[],
options?: CompletionOptions,
): Promise<CompletionResult>;
export class AnthropicConfigError extends Error { /* unchanged */ }
export class AnthropicApiError extends Error { /* unchanged */ }
export interface CompletionOptions { /* unchanged */ }
export interface CompletionResult { /* unchanged */ }
export interface AnthropicTool { /* unchanged */ }
Resolved-model precedence — invariant before/after
For both entry points, the model that is actually sent in the API request body is resolved by the precedence:
options.model > config.COLIBRI_ANTHROPIC_MODEL (> schema default 'claude-sonnet-4-5')
The arrow > means “if defined, wins”. The schema default
'claude-sonnet-4-5' is invisible in code now — it lives only in
src/config.ts schema declaration. The hardcoded fallback literal in
claude.ts is removed.
ANTHROPIC_API_KEY precedence — invariant before/after
options.apiKey > process.env['ANTHROPIC_API_KEY']
Note: this stays as direct process.env lookup, not config.ANTHROPIC_API_KEY.
Reason: the design invariant 5 in claude.ts header comment states the key is
“optional at schema level, required at call-time”. Reading process.env
directly preserves the call-time lookup semantics and the existing test seam
that omits apiKey from options to trigger AnthropicConfigError. The
singleton’s config.ANTHROPIC_API_KEY is also string | undefined and would
produce identical behavior — but the existing direct-env lookup is the
explicit contract today, and it is not the subject of this fix.
Injection seams — preserved
| Seam | Type | Default after fix | Test obligation |
|---|---|---|---|
options.apiKey |
string? |
process.env['ANTHROPIC_API_KEY'] |
Unchanged |
options.model |
string? |
config.COLIBRI_ANTHROPIC_MODEL |
Verify default + override precedence |
options.maxTokens |
number? |
1024 (DEFAULT_MAX_TOKENS) |
Unchanged |
options.systemPrompt |
string? |
undefined |
Unchanged |
options.fetchFn |
typeof fetch? |
global fetch |
Unchanged |
options.logger |
? |
console.error |
Unchanged |
options.delayFn |
? |
local sleep |
Unchanged |
Error semantics — preserved
| Pre-condition | Post-condition |
|---|---|
No apiKey in options AND no process.env.ANTHROPIC_API_KEY |
throw new AnthropicConfigError(...) |
| 4xx (excluding 429) from API | throw new AnthropicApiError(..., status) with code: 'ANTHROPIC_API_ERROR' |
| 4 consecutive 429/5xx | throw new AnthropicApiError(..., status, true) with code: 'ANTHROPIC_RETRIES_EXHAUSTED' |
| Network error (fetch throws) | throw new AnthropicApiError(...) with code: 'ANTHROPIC_API_ERROR' |
| Successful 2xx | resolves to CompletionResult |
Diff contract
Source change in src/domains/integrations/claude.ts
Removed (line 33):
import { loadConfig } from '../../config.js';
Added (line 33):
import { config } from '../../config.js';
Removed in createCompletion (lines 344–353, 10 lines):
// Resolve model: options → env → config default
let defaultModel = 'claude-sonnet-4-5';
try {
const cfg = loadConfig(process.env);
defaultModel = cfg.COLIBRI_ANTHROPIC_MODEL;
} catch {
// config may fail in test environments with minimal env — use hardcoded default
}
const model = options.model ?? defaultModel;
Added in createCompletion (1 line):
const model = options.model ?? config.COLIBRI_ANTHROPIC_MODEL;
Removed in createCompletionWithTools (lines 385–393, 9 lines): identical pattern.
Added in createCompletionWithTools (1 line):
const model = options.model ?? config.COLIBRI_ANTHROPIC_MODEL;
Net LOC delta: -17 source lines (claude.ts goes from 405 → 388). Imports swap one symbol for another — net zero on import lines.
Test additions in src/__tests__/domains/integrations/claude.test.ts
A new describe('default-model resolution from config') block adds:
defaults to config.COLIBRI_ANTHROPIC_MODEL when options.model is omitted— assert the request body’smodelmatches the singleton.injected options.model takes precedence over config default— assert the override wins (regression guard for the precedence rule).createCompletionWithTools defaults to config.COLIBRI_ANTHROPIC_MODEL— parity for the tool-use entry point.
Net LOC delta: +50 to +70 test lines (3 new tests). No existing tests are modified or deleted; the silent-fallback path is gone, and no current test depends on it (verified in audit).
Non-goals
- No refactor of retry logic, header construction, response parsing, or token accounting.
- No change to error classes or their public properties.
- No change to
Configtype orsrc/config.tsschema. - No change to the public surface (function signatures, exported types, exported errors).
- No change to the
apiKeyresolution path — that stays onprocess.envper the existing call-time validation invariant.
Acceptance gates
npm run build— TypeScript compiles cleanly.npm run lint— ESLint clean (no new warnings).npm test— entire suite green; new tests added pass; existing claude.test.ts tests untouched and green.git diff src/domains/integrations/claude.ts— net deletion of the try/catch blocks and the hardcoded fallback literal.