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:

  1. defaults to config.COLIBRI_ANTHROPIC_MODEL when options.model is omitted — assert the request body’s model matches the singleton.
  2. injected options.model takes precedence over config default — assert the override wins (regression guard for the precedence rule).
  3. 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 Config type or src/config.ts schema.
  • No change to the public surface (function signatures, exported types, exported errors).
  • No change to the apiKey resolution path — that stays on process.env per 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.

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.