titan-agent 6.4.4 → 6.5.0

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.
Files changed (55) hide show
  1. package/dist/agent/agentLessons.js.map +1 -1
  2. package/dist/agent/agentLoop.js +5 -1
  3. package/dist/agent/agentLoop.js.map +1 -1
  4. package/dist/agent/orchestrator.js +5 -14
  5. package/dist/agent/orchestrator.js.map +1 -1
  6. package/dist/agent/somaInitiative.js.map +1 -1
  7. package/dist/agent/toolRunner.js +35 -0
  8. package/dist/agent/toolRunner.js.map +1 -1
  9. package/dist/channels/discord.js +10 -6
  10. package/dist/channels/discord.js.map +1 -1
  11. package/dist/channels/slack.js +7 -2
  12. package/dist/channels/slack.js.map +1 -1
  13. package/dist/channels/telegram.js +1 -1
  14. package/dist/channels/telegram.js.map +1 -1
  15. package/dist/config/schema.js +6 -0
  16. package/dist/config/schema.js.map +1 -1
  17. package/dist/gateway/openai-compat.js +69 -10
  18. package/dist/gateway/openai-compat.js.map +1 -1
  19. package/dist/gateway/server.js +79 -0
  20. package/dist/gateway/server.js.map +1 -1
  21. package/dist/migrations/runner.js.map +1 -1
  22. package/dist/migrations/safeRun.js.map +1 -1
  23. package/dist/providers/anthropic.js +7 -4
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js +8 -1
  26. package/dist/providers/base.js.map +1 -1
  27. package/dist/providers/google.js +7 -4
  28. package/dist/providers/google.js.map +1 -1
  29. package/dist/providers/ollama.js +8 -7
  30. package/dist/providers/ollama.js.map +1 -1
  31. package/dist/providers/openai.js +7 -4
  32. package/dist/providers/openai.js.map +1 -1
  33. package/dist/providers/openai_compat.js +7 -4
  34. package/dist/providers/openai_compat.js.map +1 -1
  35. package/dist/providers/router.js +7 -3
  36. package/dist/providers/router.js.map +1 -1
  37. package/dist/skills/builtin/backup.js.map +1 -1
  38. package/dist/skills/builtin/browser.js +11 -21
  39. package/dist/skills/builtin/browser.js.map +1 -1
  40. package/dist/skills/builtin/calendar.js +6 -2
  41. package/dist/skills/builtin/calendar.js.map +1 -1
  42. package/dist/skills/builtin/canvas_spaces.js.map +1 -1
  43. package/dist/skills/builtin/data_analysis.js +1 -1
  44. package/dist/skills/builtin/data_analysis.js.map +1 -1
  45. package/dist/skills/builtin/jira_linear.js +6 -4
  46. package/dist/skills/builtin/jira_linear.js.map +1 -1
  47. package/dist/skills/builtin/web_browser.js +6 -0
  48. package/dist/skills/builtin/web_browser.js.map +1 -1
  49. package/dist/skills/builtin/web_fetch.js +1 -0
  50. package/dist/skills/builtin/web_fetch.js.map +1 -1
  51. package/dist/storage/starterSpaces.js.map +1 -1
  52. package/dist/utils/constants.js +1 -1
  53. package/dist/utils/constants.js.map +1 -1
  54. package/package.json +1 -1
  55. package/ui/dist/sw.js +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/runner.ts"],"sourcesContent":["/**\n * TITAN — Migration Runner (U2, v5.7.2+ upgrade-safety phase)\n *\n * Why this exists\n * ---------------\n * Tony's standing rule for v6.0: \"make sure that everyone on older versions\n * of TITAN are able to update properly without breaking their old TITAN\n * settings and functions.\" That means every v6.0 step that changes data\n * shape needs a versioned, idempotent, reversible migration.\n *\n * The runner:\n * 1. Loads `~/.titan/MIGRATION_STATE.json` (which migrations have already\n * run on this install).\n * 2. Asks every registered migration whether it still needs to run.\n * 3. Runs the pending ones in order (sorted by id ascending).\n * 4. Optionally creates a pre-flight backup via the U1 backup primitives\n * so a failed migration can be auto-rolled-back (U3 wires this).\n * 5. Persists state after each successful migration so a crash mid-run\n * doesn't redo work.\n *\n * Each migration is a plain object: `{id, version, description, up, rollback?}`.\n * The id is a stable, zero-padded numeric prefix + slug (e.g.\n * `001-localstorage-spaces-to-server`). Sorting by id alphabetically gives\n * deterministic execution order — that's load-bearing.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (U1–U6 in detail)\n * - docs/MIGRATIONS.md (lands as U6)\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MigrationRunner';\n\n/** Lazy-resolved path so tests can point HOME at a temp dir. Mirrors\n * src/storage/backup.ts. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction statePath(): string { return join(titanHome(), 'MIGRATION_STATE.json'); }\n\n/** Migration state persisted to disk between runs. */\nexport interface MigrationState {\n /** Version-2 schema for this file. Bump only on breaking shape changes. */\n schema: 1;\n /** Applied migration ids, in the order they ran. */\n applied: Array<{\n id: string;\n version: string;\n appliedAt: string; // ISO timestamp\n durationMs: number;\n }>;\n /** Last successful run id — convenience for \"where did we leave off?\". */\n lastAppliedId?: string;\n /** Last save timestamp. */\n updatedAt: string;\n}\n\n/** Context passed to migration `up()` / `rollback()` functions. */\nexport interface MigrationContext {\n /** TITAN data root (~/.titan/). */\n titanHome: string;\n /** Resolved at runtime so migrations don't accidentally cache paths. */\n titanVersion: string;\n /** Helper to write a file inside titanHome. Idempotent — writes only\n * when content differs, so re-runs are no-ops. */\n writeIfChanged: (relPath: string, content: string) => boolean;\n /** Helper to read a JSON file. Returns null when missing or malformed. */\n readJson: <T = unknown>(relPath: string) => T | null;\n /** Helper to write a JSON file (pretty-printed). Idempotent. */\n writeJson: (relPath: string, value: unknown) => void;\n /** Structured log channel — output appears in the migration runner's\n * consolidated log, not as scattered logger.info calls. */\n log: (msg: string) => void;\n}\n\n/** A single migration's contract. */\nexport interface Migration {\n /** Sortable id — zero-padded numeric prefix + kebab slug. */\n id: string;\n /** TITAN version this migration targets (informational). */\n version: string;\n /** One-sentence description of what this migration does. */\n description: string;\n /** Run the migration. MUST be idempotent — same input twice = same output. */\n up: (ctx: MigrationContext) => Promise<void> | void;\n /** Optional reverse. Called by the auto-rollback path in U3/U5 when a\n * migration's smoke check fails. */\n rollback?: (ctx: MigrationContext) => Promise<void> | void;\n}\n\n/** Result of a runner invocation. */\nexport interface MigrationRunResult {\n /** True only if every pending migration applied without throwing. */\n success: boolean;\n /** Migrations actually run this invocation (in order). */\n ran: string[];\n /** Migrations skipped because they were already applied. */\n skipped: string[];\n /** First failing migration id, if any. */\n failed?: string;\n /** Error message from the failing migration. */\n error?: string;\n /** Per-migration timing for telemetry. */\n timings: Array<{ id: string; durationMs: number; status: 'ok' | 'fail' | 'skip' }>;\n}\n\n// ─── State load / save ─────────────────────────────────────────────\n\nfunction loadState(): MigrationState {\n try {\n const p = statePath();\n if (existsSync(p)) {\n const raw = JSON.parse(readFileSync(p, 'utf-8'));\n if (raw && typeof raw === 'object' && raw.schema === 1 && Array.isArray(raw.applied)) {\n return raw as MigrationState;\n }\n }\n } catch (err) {\n logger.warn(COMPONENT, `MIGRATION_STATE.json malformed — starting fresh: ${(err as Error).message}`);\n }\n return { schema: 1, applied: [], updatedAt: new Date().toISOString() };\n}\n\nfunction saveState(state: MigrationState): void {\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n state.updatedAt = new Date().toISOString();\n writeFileSync(statePath(), JSON.stringify(state, null, 2));\n}\n\n// ─── Context construction ─────────────────────────────────────────\n\nfunction makeContext(titanVersion: string, logBuffer: string[]): MigrationContext {\n const home = titanHome();\n return {\n titanHome: home,\n titanVersion,\n log: (msg: string) => logBuffer.push(msg),\n readJson<T = unknown>(relPath: string): T | null {\n try {\n const full = join(home, relPath);\n if (!existsSync(full)) return null;\n return JSON.parse(readFileSync(full, 'utf-8')) as T;\n } catch { return null; }\n },\n writeJson(relPath: string, value: unknown): void {\n const full = join(home, relPath);\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, JSON.stringify(value, null, 2));\n },\n writeIfChanged(relPath: string, content: string): boolean {\n const full = join(home, relPath);\n try {\n if (existsSync(full)) {\n const prev = readFileSync(full, 'utf-8');\n if (prev === content) return false;\n }\n } catch { /* fall through to write */ }\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, content);\n return true;\n },\n };\n}\n\n// ─── Runner ────────────────────────────────────────────────────────\n\n/**\n * Plan-only: returns the ids of pending migrations without running them.\n * Useful for `titan migrate --dry-run` and the U3 pre-flight check.\n */\nexport function planMigrations(allMigrations: Migration[]): { pending: string[]; alreadyApplied: string[] } {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n const pending: string[] = [];\n const alreadyApplied: string[] = [];\n for (const m of sorted) {\n if (appliedIds.has(m.id)) alreadyApplied.push(m.id);\n else pending.push(m.id);\n }\n return { pending, alreadyApplied };\n}\n\n/**\n * Run all pending migrations in id-ascending order. Stops on the first failure\n * — does NOT continue past a failed migration (otherwise we'd compound errors).\n *\n * The caller (U3 wiring) is responsible for the pre-flight backup and the\n * auto-rollback on failure.\n */\nexport async function runMigrations(allMigrations: Migration[], titanVersion: string): Promise<MigrationRunResult> {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n\n const result: MigrationRunResult = {\n success: true,\n ran: [],\n skipped: [],\n timings: [],\n };\n\n for (const m of sorted) {\n if (appliedIds.has(m.id)) {\n result.skipped.push(m.id);\n result.timings.push({ id: m.id, durationMs: 0, status: 'skip' });\n continue;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n const t0 = Date.now();\n try {\n logger.info(COMPONENT, `→ ${m.id} (${m.version}) ${m.description}`);\n await m.up(ctx);\n const dur = Date.now() - t0;\n state.applied.push({ id: m.id, version: m.version, appliedAt: new Date().toISOString(), durationMs: dur });\n state.lastAppliedId = m.id;\n // Persist after each successful migration so a crash mid-run\n // doesn't redo finished migrations.\n saveState(state);\n result.ran.push(m.id);\n result.timings.push({ id: m.id, durationMs: dur, status: 'ok' });\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} log:\\n ${logBuffer.join('\\n ')}`);\n }\n logger.info(COMPONENT, `✓ ${m.id} applied in ${dur}ms`);\n } catch (err) {\n const dur = Date.now() - t0;\n result.success = false;\n result.failed = m.id;\n result.error = (err as Error).message;\n result.timings.push({ id: m.id, durationMs: dur, status: 'fail' });\n logger.error(COMPONENT, `✗ ${m.id} FAILED after ${dur}ms: ${result.error}`);\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} partial log:\\n ${logBuffer.join('\\n ')}`);\n }\n return result;\n }\n }\n return result;\n}\n\n/**\n * Run a single migration's `rollback()` if defined. Used by the auto-rollback\n * path in U3/U5 when post-migration smoke checks fail. Failures here are\n * logged but never thrown — at this point we're already in error-recovery\n * and the caller has more authoritative fallback (restore from backup).\n */\nexport async function rollbackMigration(migration: Migration, titanVersion: string): Promise<void> {\n if (!migration.rollback) {\n logger.warn(COMPONENT, `Migration ${migration.id} has no rollback() — relying on backup restore.`);\n return;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n try {\n await migration.rollback(ctx);\n logger.info(COMPONENT, `↩ ${migration.id} rolled back`);\n } catch (err) {\n logger.error(COMPONENT, `↩ ${migration.id} rollback FAILED: ${(err as Error).message}`);\n }\n}\n\n// ─── Test surface ─────────────────────────────────────────────────\n\n/** Test-only: reset the state file. Production code never calls this. */\nexport function __resetStateForTests(): void {\n try { unlinkSync(statePath()); } catch { /* fine */ }\n}\n\n/** Test-only: read state directly. */\nexport function __readStateForTests(): MigrationState {\n return loadState();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/runner.ts"],"sourcesContent":["/**\n * TITAN — Migration Runner (U2, v5.7.2+ upgrade-safety phase)\n *\n * Why this exists\n * ---------------\n * Tony's standing rule for v6.0: \"make sure that everyone on older versions\n * of TITAN are able to update properly without breaking their old TITAN\n * settings and functions.\" That means every v6.0 step that changes data\n * shape needs a versioned, idempotent, reversible migration.\n *\n * The runner:\n * 1. Loads `~/.titan/MIGRATION_STATE.json` (which migrations have already\n * run on this install).\n * 2. Asks every registered migration whether it still needs to run.\n * 3. Runs the pending ones in order (sorted by id ascending).\n * 4. Optionally creates a pre-flight backup via the U1 backup primitives\n * so a failed migration can be auto-rolled-back (U3 wires this).\n * 5. Persists state after each successful migration so a crash mid-run\n * doesn't redo work.\n *\n * Each migration is a plain object: `{id, version, description, up, rollback?}`.\n * The id is a stable, zero-padded numeric prefix + slug (e.g.\n * `001-localstorage-spaces-to-server`). Sorting by id alphabetically gives\n * deterministic execution order — that's load-bearing.\n *\n * Reference:\n * - ~/.claude/projects/-Users-localuser/memory/titan-v6-living-canvas.md\n * (U1–U6 in detail)\n * - docs/MIGRATIONS.md (lands as U6)\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MigrationRunner';\n\n/** Lazy-resolved path so tests can point HOME at a temp dir. Mirrors\n * src/storage/backup.ts. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction statePath(): string { return join(titanHome(), 'MIGRATION_STATE.json'); }\n\n/** Migration state persisted to disk between runs. */\nexport interface MigrationState {\n /** Version-2 schema for this file. Bump only on breaking shape changes. */\n schema: 1;\n /** Applied migration ids, in the order they ran. */\n applied: Array<{\n id: string;\n version: string;\n appliedAt: string; // ISO timestamp\n durationMs: number;\n }>;\n /** Last successful run id — convenience for \"where did we leave off?\". */\n lastAppliedId?: string;\n /** Last save timestamp. */\n updatedAt: string;\n}\n\n/** Context passed to migration `up()` / `rollback()` functions. */\nexport interface MigrationContext {\n /** TITAN data root (~/.titan/). */\n titanHome: string;\n /** Resolved at runtime so migrations don't accidentally cache paths. */\n titanVersion: string;\n /** Helper to write a file inside titanHome. Idempotent — writes only\n * when content differs, so re-runs are no-ops. */\n writeIfChanged: (relPath: string, content: string) => boolean;\n /** Helper to read a JSON file. Returns null when missing or malformed. */\n readJson: <T = unknown>(relPath: string) => T | null;\n /** Helper to write a JSON file (pretty-printed). Idempotent. */\n writeJson: (relPath: string, value: unknown) => void;\n /** Structured log channel — output appears in the migration runner's\n * consolidated log, not as scattered logger.info calls. */\n log: (msg: string) => void;\n}\n\n/** A single migration's contract. */\nexport interface Migration {\n /** Sortable id — zero-padded numeric prefix + kebab slug. */\n id: string;\n /** TITAN version this migration targets (informational). */\n version: string;\n /** One-sentence description of what this migration does. */\n description: string;\n /** Run the migration. MUST be idempotent — same input twice = same output. */\n up: (ctx: MigrationContext) => Promise<void> | void;\n /** Optional reverse. Called by the auto-rollback path in U3/U5 when a\n * migration's smoke check fails. */\n rollback?: (ctx: MigrationContext) => Promise<void> | void;\n}\n\n/** Result of a runner invocation. */\nexport interface MigrationRunResult {\n /** True only if every pending migration applied without throwing. */\n success: boolean;\n /** Migrations actually run this invocation (in order). */\n ran: string[];\n /** Migrations skipped because they were already applied. */\n skipped: string[];\n /** First failing migration id, if any. */\n failed?: string;\n /** Error message from the failing migration. */\n error?: string;\n /** Per-migration timing for telemetry. */\n timings: Array<{ id: string; durationMs: number; status: 'ok' | 'fail' | 'skip' }>;\n}\n\n// ─── State load / save ─────────────────────────────────────────────\n\nfunction loadState(): MigrationState {\n try {\n const p = statePath();\n if (existsSync(p)) {\n const raw = JSON.parse(readFileSync(p, 'utf-8'));\n if (raw && typeof raw === 'object' && raw.schema === 1 && Array.isArray(raw.applied)) {\n return raw as MigrationState;\n }\n }\n } catch (err) {\n logger.warn(COMPONENT, `MIGRATION_STATE.json malformed — starting fresh: ${(err as Error).message}`);\n }\n return { schema: 1, applied: [], updatedAt: new Date().toISOString() };\n}\n\nfunction saveState(state: MigrationState): void {\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n state.updatedAt = new Date().toISOString();\n writeFileSync(statePath(), JSON.stringify(state, null, 2));\n}\n\n// ─── Context construction ─────────────────────────────────────────\n\nfunction makeContext(titanVersion: string, logBuffer: string[]): MigrationContext {\n const home = titanHome();\n return {\n titanHome: home,\n titanVersion,\n log: (msg: string) => logBuffer.push(msg),\n readJson<T = unknown>(relPath: string): T | null {\n try {\n const full = join(home, relPath);\n if (!existsSync(full)) return null;\n return JSON.parse(readFileSync(full, 'utf-8')) as T;\n } catch { return null; }\n },\n writeJson(relPath: string, value: unknown): void {\n const full = join(home, relPath);\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, JSON.stringify(value, null, 2));\n },\n writeIfChanged(relPath: string, content: string): boolean {\n const full = join(home, relPath);\n try {\n if (existsSync(full)) {\n const prev = readFileSync(full, 'utf-8');\n if (prev === content) return false;\n }\n } catch { /* fall through to write */ }\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, content);\n return true;\n },\n };\n}\n\n// ─── Runner ────────────────────────────────────────────────────────\n\n/**\n * Plan-only: returns the ids of pending migrations without running them.\n * Useful for `titan migrate --dry-run` and the U3 pre-flight check.\n */\nexport function planMigrations(allMigrations: Migration[]): { pending: string[]; alreadyApplied: string[] } {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n const pending: string[] = [];\n const alreadyApplied: string[] = [];\n for (const m of sorted) {\n if (appliedIds.has(m.id)) alreadyApplied.push(m.id);\n else pending.push(m.id);\n }\n return { pending, alreadyApplied };\n}\n\n/**\n * Run all pending migrations in id-ascending order. Stops on the first failure\n * — does NOT continue past a failed migration (otherwise we'd compound errors).\n *\n * The caller (U3 wiring) is responsible for the pre-flight backup and the\n * auto-rollback on failure.\n */\nexport async function runMigrations(allMigrations: Migration[], titanVersion: string): Promise<MigrationRunResult> {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n\n const result: MigrationRunResult = {\n success: true,\n ran: [],\n skipped: [],\n timings: [],\n };\n\n for (const m of sorted) {\n if (appliedIds.has(m.id)) {\n result.skipped.push(m.id);\n result.timings.push({ id: m.id, durationMs: 0, status: 'skip' });\n continue;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n const t0 = Date.now();\n try {\n logger.info(COMPONENT, `→ ${m.id} (${m.version}) ${m.description}`);\n await m.up(ctx);\n const dur = Date.now() - t0;\n state.applied.push({ id: m.id, version: m.version, appliedAt: new Date().toISOString(), durationMs: dur });\n state.lastAppliedId = m.id;\n // Persist after each successful migration so a crash mid-run\n // doesn't redo finished migrations.\n saveState(state);\n result.ran.push(m.id);\n result.timings.push({ id: m.id, durationMs: dur, status: 'ok' });\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} log:\\n ${logBuffer.join('\\n ')}`);\n }\n logger.info(COMPONENT, `✓ ${m.id} applied in ${dur}ms`);\n } catch (err) {\n const dur = Date.now() - t0;\n result.success = false;\n result.failed = m.id;\n result.error = (err as Error).message;\n result.timings.push({ id: m.id, durationMs: dur, status: 'fail' });\n logger.error(COMPONENT, `✗ ${m.id} FAILED after ${dur}ms: ${result.error}`);\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} partial log:\\n ${logBuffer.join('\\n ')}`);\n }\n return result;\n }\n }\n return result;\n}\n\n/**\n * Run a single migration's `rollback()` if defined. Used by the auto-rollback\n * path in U3/U5 when post-migration smoke checks fail. Failures here are\n * logged but never thrown — at this point we're already in error-recovery\n * and the caller has more authoritative fallback (restore from backup).\n */\nexport async function rollbackMigration(migration: Migration, titanVersion: string): Promise<void> {\n if (!migration.rollback) {\n logger.warn(COMPONENT, `Migration ${migration.id} has no rollback() — relying on backup restore.`);\n return;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n try {\n await migration.rollback(ctx);\n logger.info(COMPONENT, `↩ ${migration.id} rolled back`);\n } catch (err) {\n logger.error(COMPONENT, `↩ ${migration.id} rollback FAILED: ${(err as Error).message}`);\n }\n}\n\n// ─── Test surface ─────────────────────────────────────────────────\n\n/** Test-only: reset the state file. Production code never calls this. */\nexport function __resetStateForTests(): void {\n try { unlinkSync(statePath()); } catch { /* fine */ }\n}\n\n/** Test-only: read state directly. */\nexport function __readStateForTests(): MigrationState {\n return loadState();\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/safeRun.ts"],"sourcesContent":["/**\n * U3 + U5 — Safe migration runner with pre-flight backup + post-migration\n * smoke check + auto-rollback.\n *\n * Why a wrapper around `runMigrations`?\n * -------------------------------------\n * `runMigrations` is the pure execution engine — it doesn't know about\n * backups or smoke tests. `safeRunMigrations` adds the safety perimeter:\n *\n * 1. **Pre-flight backup (U3).** If any migration is pending, create a\n * labelled backup (`pre-migration-<timestamp>`) BEFORE the runner\n * touches anything. Failure to create the backup blocks the run —\n * we never migrate without a safety net.\n *\n * 2. **Run migrations** via `runMigrations` (the U2 engine).\n *\n * 3. **Post-migration smoke check (U5).** Verify the data we just touched\n * is still parseable + the gateway can still load config + the\n * registry can still find the new tools. Any failure flips the run\n * to \"needs rollback.\"\n *\n * 4. **Auto-rollback on failure.** First try each migration's own\n * `rollback()` in reverse order. If that doesn't restore the smoke\n * check, fall back to the U1 `restoreBackup()` from the pre-flight\n * backup we made in step 1.\n *\n * This is the ONE entry point production code should call. The CLI\n * (`titan migrate`) and the gateway-boot migration hook both use it.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (U3 + U5 sections)\n */\nimport { runMigrations, rollbackMigration, planMigrations, type Migration, type MigrationRunResult } from './runner.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'SafeMigrationRunner';\n\nexport interface SmokeCheck {\n /** Display name for logs. */\n name: string;\n /** Throw on failure. Return value ignored. */\n check: () => Promise<void> | void;\n}\n\nexport interface SafeRunResult {\n success: boolean;\n /** Migration-level outcome. */\n migrationResult: MigrationRunResult;\n /** Path of the pre-flight backup, if we made one. */\n preflightBackupPath?: string;\n /** Smoke checks that ran + their pass/fail. */\n smokeChecks: Array<{ name: string; passed: boolean; error?: string }>;\n /** True if we restored from backup. */\n rolledBack: boolean;\n /** Human-friendly summary suitable for stdout / API responses. */\n summary: string;\n}\n\nexport interface SafeRunOptions {\n /** Skip the pre-flight backup. ONLY for tests; production must keep this true. */\n skipBackup?: boolean;\n /** Custom smoke checks to run AFTER migrations apply. Default checks below. */\n smokeChecks?: SmokeCheck[];\n /** Tag used in the pre-flight backup label (default \"pre-migration\"). */\n backupLabel?: string;\n /** TITAN_VERSION to pass into migration contexts. */\n titanVersion: string;\n}\n\n/** Default smoke checks — minimal \"did we break the basics?\" coverage. */\nasync function defaultSmokeChecks(): Promise<SmokeCheck[]> {\n return [\n {\n name: 'config loads',\n async check() {\n const { loadConfig } = await import('../config/config.js');\n loadConfig(); // throws if config is malformed\n },\n },\n {\n name: 'spaces.json is parseable',\n async check() {\n const { existsSync, readFileSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const envHome = process.env.TITAN_HOME;\n const home = envHome\n ? (envHome.startsWith('~/') ? join(homedir(), envHome.slice(2)) : envHome)\n : join(homedir(), '.titan');\n const path = join(home, 'spaces.json');\n if (!existsSync(path)) return; // first install — fine\n JSON.parse(readFileSync(path, 'utf-8'));\n },\n },\n ];\n}\n\n/**\n * Production entry point. Runs migrations with full safety net.\n */\nexport async function safeRunMigrations(\n migrations: Migration[],\n opts: SafeRunOptions,\n): Promise<SafeRunResult> {\n const titanVersion = opts.titanVersion;\n const result: SafeRunResult = {\n success: false,\n migrationResult: { success: false, ran: [], skipped: [], timings: [] },\n smokeChecks: [],\n rolledBack: false,\n summary: '',\n };\n\n // Step 1: plan. If nothing pending, exit happy without a backup.\n const plan = planMigrations(migrations);\n if (plan.pending.length === 0) {\n result.success = true;\n result.migrationResult = { success: true, ran: [], skipped: plan.alreadyApplied, timings: [] };\n result.summary = `No pending migrations. ${plan.alreadyApplied.length} already applied.`;\n return result;\n }\n logger.info(COMPONENT, `${plan.pending.length} pending migration(s): ${plan.pending.join(', ')}`);\n\n // Step 2: pre-flight backup (U3) — block if it fails.\n if (!opts.skipBackup) {\n try {\n const { createBackup } = await import('../storage/backup.js');\n const label = opts.backupLabel ?? 'pre-migration';\n const info = await createBackup({ includeWorkspace: true });\n // Rename to embed the label for findability — same trick as the\n // backup skill. Fail-soft: a rename failure doesn't block the run.\n try {\n const { renameSync } = await import('fs');\n const { join } = await import('path');\n const labelled = info.filename.replace(/\\.tar\\.gz$/, `-${label}.tar.gz`);\n const labelledPath = join(info.path, '..', labelled);\n renameSync(info.path, labelledPath);\n result.preflightBackupPath = labelledPath;\n } catch {\n result.preflightBackupPath = info.path;\n }\n logger.info(COMPONENT, `Pre-flight backup: ${result.preflightBackupPath}`);\n } catch (err) {\n // No backup = no run. Tony's rule: \"we do this properly.\"\n const msg = (err as Error).message;\n logger.error(COMPONENT, `Pre-flight backup FAILED — aborting migration: ${msg}`);\n result.summary = `Pre-flight backup failed: ${msg}. Migration aborted (no changes applied).`;\n return result;\n }\n }\n\n // Step 3: run migrations.\n result.migrationResult = await runMigrations(migrations, titanVersion);\n\n if (!result.migrationResult.success) {\n logger.error(COMPONENT, `Migration ${result.migrationResult.failed} failed — entering rollback path.`);\n await rollbackAndRestore(migrations, result, titanVersion);\n return result;\n }\n\n // Step 4: smoke checks (U5).\n const checks = opts.smokeChecks ?? await defaultSmokeChecks();\n for (const c of checks) {\n try {\n await c.check();\n result.smokeChecks.push({ name: c.name, passed: true });\n } catch (err) {\n result.smokeChecks.push({ name: c.name, passed: false, error: (err as Error).message });\n logger.error(COMPONENT, `Smoke check \"${c.name}\" FAILED: ${(err as Error).message}`);\n }\n }\n const smokeFailed = result.smokeChecks.some(s => !s.passed);\n if (smokeFailed) {\n await rollbackAndRestore(migrations, result, titanVersion);\n return result;\n }\n\n // All good.\n result.success = true;\n const ranCount = result.migrationResult.ran.length;\n const skippedCount = result.migrationResult.skipped.length;\n const checksOk = result.smokeChecks.length;\n result.summary = `✓ ${ranCount} migration(s) applied, ${skippedCount} already-applied, ${checksOk} smoke checks passed.`;\n if (result.preflightBackupPath) {\n result.summary += ` Pre-flight backup at ${result.preflightBackupPath}.`;\n }\n return result;\n}\n\n/**\n * Roll back: first try each ran migration's own `rollback()` in reverse,\n * then restore from the pre-flight backup as the safety net.\n */\nasync function rollbackAndRestore(\n migrations: Migration[],\n result: SafeRunResult,\n titanVersion: string,\n): Promise<void> {\n const ranIds = new Set(result.migrationResult.ran);\n const ranInReverse = [...migrations]\n .filter(m => ranIds.has(m.id))\n .sort((a, b) => b.id.localeCompare(a.id));\n\n for (const m of ranInReverse) {\n await rollbackMigration(m, titanVersion);\n }\n\n // Then restore from pre-flight backup if we have one.\n if (result.preflightBackupPath) {\n try {\n const { restoreBackup } = await import('../storage/backup.js');\n await restoreBackup(result.preflightBackupPath);\n result.rolledBack = true;\n logger.info(COMPONENT, `Restored from pre-flight backup: ${result.preflightBackupPath}`);\n result.summary = `✗ Migration failed; rolled back via pre-flight backup. State restored to pre-migration snapshot.`;\n } catch (err) {\n logger.error(COMPONENT, `Pre-flight backup restore FAILED: ${(err as Error).message}`);\n result.summary = `✗ Migration FAILED and rollback ALSO FAILED. Manual recovery required. Backup at ${result.preflightBackupPath}.`;\n }\n } else {\n result.summary = `✗ Migration failed; per-migration rollback() called but no pre-flight backup to restore from. Manual review needed.`;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/safeRun.ts"],"sourcesContent":["/**\n * U3 + U5 — Safe migration runner with pre-flight backup + post-migration\n * smoke check + auto-rollback.\n *\n * Why a wrapper around `runMigrations`?\n * -------------------------------------\n * `runMigrations` is the pure execution engine — it doesn't know about\n * backups or smoke tests. `safeRunMigrations` adds the safety perimeter:\n *\n * 1. **Pre-flight backup (U3).** If any migration is pending, create a\n * labelled backup (`pre-migration-<timestamp>`) BEFORE the runner\n * touches anything. Failure to create the backup blocks the run —\n * we never migrate without a safety net.\n *\n * 2. **Run migrations** via `runMigrations` (the U2 engine).\n *\n * 3. **Post-migration smoke check (U5).** Verify the data we just touched\n * is still parseable + the gateway can still load config + the\n * registry can still find the new tools. Any failure flips the run\n * to \"needs rollback.\"\n *\n * 4. **Auto-rollback on failure.** First try each migration's own\n * `rollback()` in reverse order. If that doesn't restore the smoke\n * check, fall back to the U1 `restoreBackup()` from the pre-flight\n * backup we made in step 1.\n *\n * This is the ONE entry point production code should call. The CLI\n * (`titan migrate`) and the gateway-boot migration hook both use it.\n *\n * Reference:\n * - ~/.claude/projects/-Users-localuser/memory/titan-v6-living-canvas.md\n * (U3 + U5 sections)\n */\nimport { runMigrations, rollbackMigration, planMigrations, type Migration, type MigrationRunResult } from './runner.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'SafeMigrationRunner';\n\nexport interface SmokeCheck {\n /** Display name for logs. */\n name: string;\n /** Throw on failure. Return value ignored. */\n check: () => Promise<void> | void;\n}\n\nexport interface SafeRunResult {\n success: boolean;\n /** Migration-level outcome. */\n migrationResult: MigrationRunResult;\n /** Path of the pre-flight backup, if we made one. */\n preflightBackupPath?: string;\n /** Smoke checks that ran + their pass/fail. */\n smokeChecks: Array<{ name: string; passed: boolean; error?: string }>;\n /** True if we restored from backup. */\n rolledBack: boolean;\n /** Human-friendly summary suitable for stdout / API responses. */\n summary: string;\n}\n\nexport interface SafeRunOptions {\n /** Skip the pre-flight backup. ONLY for tests; production must keep this true. */\n skipBackup?: boolean;\n /** Custom smoke checks to run AFTER migrations apply. Default checks below. */\n smokeChecks?: SmokeCheck[];\n /** Tag used in the pre-flight backup label (default \"pre-migration\"). */\n backupLabel?: string;\n /** TITAN_VERSION to pass into migration contexts. */\n titanVersion: string;\n}\n\n/** Default smoke checks — minimal \"did we break the basics?\" coverage. */\nasync function defaultSmokeChecks(): Promise<SmokeCheck[]> {\n return [\n {\n name: 'config loads',\n async check() {\n const { loadConfig } = await import('../config/config.js');\n loadConfig(); // throws if config is malformed\n },\n },\n {\n name: 'spaces.json is parseable',\n async check() {\n const { existsSync, readFileSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const envHome = process.env.TITAN_HOME;\n const home = envHome\n ? (envHome.startsWith('~/') ? join(homedir(), envHome.slice(2)) : envHome)\n : join(homedir(), '.titan');\n const path = join(home, 'spaces.json');\n if (!existsSync(path)) return; // first install — fine\n JSON.parse(readFileSync(path, 'utf-8'));\n },\n },\n ];\n}\n\n/**\n * Production entry point. Runs migrations with full safety net.\n */\nexport async function safeRunMigrations(\n migrations: Migration[],\n opts: SafeRunOptions,\n): Promise<SafeRunResult> {\n const titanVersion = opts.titanVersion;\n const result: SafeRunResult = {\n success: false,\n migrationResult: { success: false, ran: [], skipped: [], timings: [] },\n smokeChecks: [],\n rolledBack: false,\n summary: '',\n };\n\n // Step 1: plan. If nothing pending, exit happy without a backup.\n const plan = planMigrations(migrations);\n if (plan.pending.length === 0) {\n result.success = true;\n result.migrationResult = { success: true, ran: [], skipped: plan.alreadyApplied, timings: [] };\n result.summary = `No pending migrations. ${plan.alreadyApplied.length} already applied.`;\n return result;\n }\n logger.info(COMPONENT, `${plan.pending.length} pending migration(s): ${plan.pending.join(', ')}`);\n\n // Step 2: pre-flight backup (U3) — block if it fails.\n if (!opts.skipBackup) {\n try {\n const { createBackup } = await import('../storage/backup.js');\n const label = opts.backupLabel ?? 'pre-migration';\n const info = await createBackup({ includeWorkspace: true });\n // Rename to embed the label for findability — same trick as the\n // backup skill. Fail-soft: a rename failure doesn't block the run.\n try {\n const { renameSync } = await import('fs');\n const { join } = await import('path');\n const labelled = info.filename.replace(/\\.tar\\.gz$/, `-${label}.tar.gz`);\n const labelledPath = join(info.path, '..', labelled);\n renameSync(info.path, labelledPath);\n result.preflightBackupPath = labelledPath;\n } catch {\n result.preflightBackupPath = info.path;\n }\n logger.info(COMPONENT, `Pre-flight backup: ${result.preflightBackupPath}`);\n } catch (err) {\n // No backup = no run. Tony's rule: \"we do this properly.\"\n const msg = (err as Error).message;\n logger.error(COMPONENT, `Pre-flight backup FAILED — aborting migration: ${msg}`);\n result.summary = `Pre-flight backup failed: ${msg}. Migration aborted (no changes applied).`;\n return result;\n }\n }\n\n // Step 3: run migrations.\n result.migrationResult = await runMigrations(migrations, titanVersion);\n\n if (!result.migrationResult.success) {\n logger.error(COMPONENT, `Migration ${result.migrationResult.failed} failed — entering rollback path.`);\n await rollbackAndRestore(migrations, result, titanVersion);\n return result;\n }\n\n // Step 4: smoke checks (U5).\n const checks = opts.smokeChecks ?? await defaultSmokeChecks();\n for (const c of checks) {\n try {\n await c.check();\n result.smokeChecks.push({ name: c.name, passed: true });\n } catch (err) {\n result.smokeChecks.push({ name: c.name, passed: false, error: (err as Error).message });\n logger.error(COMPONENT, `Smoke check \"${c.name}\" FAILED: ${(err as Error).message}`);\n }\n }\n const smokeFailed = result.smokeChecks.some(s => !s.passed);\n if (smokeFailed) {\n await rollbackAndRestore(migrations, result, titanVersion);\n return result;\n }\n\n // All good.\n result.success = true;\n const ranCount = result.migrationResult.ran.length;\n const skippedCount = result.migrationResult.skipped.length;\n const checksOk = result.smokeChecks.length;\n result.summary = `✓ ${ranCount} migration(s) applied, ${skippedCount} already-applied, ${checksOk} smoke checks passed.`;\n if (result.preflightBackupPath) {\n result.summary += ` Pre-flight backup at ${result.preflightBackupPath}.`;\n }\n return result;\n}\n\n/**\n * Roll back: first try each ran migration's own `rollback()` in reverse,\n * then restore from the pre-flight backup as the safety net.\n */\nasync function rollbackAndRestore(\n migrations: Migration[],\n result: SafeRunResult,\n titanVersion: string,\n): Promise<void> {\n const ranIds = new Set(result.migrationResult.ran);\n const ranInReverse = [...migrations]\n .filter(m => ranIds.has(m.id))\n .sort((a, b) => b.id.localeCompare(a.id));\n\n for (const m of ranInReverse) {\n await rollbackMigration(m, titanVersion);\n }\n\n // Then restore from pre-flight backup if we have one.\n if (result.preflightBackupPath) {\n try {\n const { restoreBackup } = await import('../storage/backup.js');\n await restoreBackup(result.preflightBackupPath);\n result.rolledBack = true;\n logger.info(COMPONENT, `Restored from pre-flight backup: ${result.preflightBackupPath}`);\n result.summary = `✗ Migration failed; rolled back via pre-flight backup. State restored to pre-migration snapshot.`;\n } catch (err) {\n logger.error(COMPONENT, `Pre-flight backup restore FAILED: ${(err as Error).message}`);\n result.summary = `✗ Migration FAILED and rollback ALSO FAILED. Manual recovery required. Backup at ${result.preflightBackupPath}.`;\n }\n } else {\n result.summary = `✗ Migration failed; per-migration rollback() called but no pre-flight backup to restore from. Manual review needed.`;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- LLMProvider
3
+ LLMProvider,
4
+ withTimeoutSignal,
5
+ STREAM_FETCH_TIMEOUT_MS
4
6
  } from "./base.js";
5
7
  import { loadConfig } from "../config/config.js";
6
8
  import logger from "../utils/logger.js";
@@ -168,12 +170,13 @@ class AnthropicProvider extends LLMProvider {
168
170
  "anthropic-version": "2023-06-01",
169
171
  "anthropic-beta": "prompt-caching-2024-07-31"
170
172
  },
171
- body: JSON.stringify(body)
173
+ body: JSON.stringify(body),
174
+ signal: withTimeoutSignal(options.signal, STREAM_FETCH_TIMEOUT_MS)
172
175
  });
173
176
  if (!response.ok || !response.body) {
174
177
  const errorText = await response.text();
175
- yield { type: "error", error: `Anthropic API error (${response.status}): ${errorText}` };
176
- return;
178
+ const { createProviderError } = await import("./errorTaxonomy.js");
179
+ throw createProviderError("Anthropic API", response, errorText, { provider: "anthropic", model });
177
180
  }
178
181
  const reader = response.body.getReader();
179
182
  const decoder = new TextDecoder();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n if (p.enabled === false) return '';\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `Anthropic API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or the live discovery call fails. Covers every Claude API model\n // ID currently in production as of 2026-05. Keeps the picker\n // useful even on a fresh-clone TITAN with no key — the user can\n // browse and choose, then configure the key. Live discovery (when\n // a key IS configured) will replace this with whatever the upstream\n // /v1/models returns, which may include models the key doesn't\n // have access to.\n const FALLBACK = [\n // Claude 4.x family (2025-2026)\n 'claude-opus-4-7',\n 'claude-opus-4-6',\n 'claude-opus-4-5',\n 'claude-opus-4-0',\n 'claude-sonnet-4-6',\n 'claude-sonnet-4-5',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-6',\n 'claude-haiku-4-5-20251001',\n 'claude-haiku-4-20250414',\n // Claude 3.7 / 3.5 / 3.0 (still production-supported)\n 'claude-3-7-sonnet-20250219',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-sonnet-20240620',\n 'claude-3-5-haiku-20241022',\n 'claude-3-opus-20240229',\n 'claude-3-sonnet-20240229',\n 'claude-3-haiku-20240307',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery — Anthropic's /v1/models is paginated (page size 1000)\n // and returns objects shaped like `{ id, type: 'model', display_name, created_at }`.\n try {\n const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {\n headers: {\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(Boolean);\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n withTimeoutSignal,\n STREAM_FETCH_TIMEOUT_MS,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n if (p.enabled === false) return '';\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n signal: withTimeoutSignal(options.signal, STREAM_FETCH_TIMEOUT_MS),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n // v6.5 — THROW (not yield) so the router routes HTTP errors through\n // retry / fallback chain / circuit breaker, like non-streaming chat().\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or the live discovery call fails. Covers every Claude API model\n // ID currently in production as of 2026-05. Keeps the picker\n // useful even on a fresh-clone TITAN with no key — the user can\n // browse and choose, then configure the key. Live discovery (when\n // a key IS configured) will replace this with whatever the upstream\n // /v1/models returns, which may include models the key doesn't\n // have access to.\n const FALLBACK = [\n // Claude 4.x family (2025-2026)\n 'claude-opus-4-7',\n 'claude-opus-4-6',\n 'claude-opus-4-5',\n 'claude-opus-4-0',\n 'claude-sonnet-4-6',\n 'claude-sonnet-4-5',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-6',\n 'claude-haiku-4-5-20251001',\n 'claude-haiku-4-20250414',\n // Claude 3.7 / 3.5 / 3.0 (still production-supported)\n 'claude-3-7-sonnet-20250219',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-sonnet-20240620',\n 'claude-3-5-haiku-20241022',\n 'claude-3-opus-20240229',\n 'claude-3-sonnet-20240229',\n 'claude-3-haiku-20240307',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery — Anthropic's /v1/models is paginated (page size 1000)\n // and returns objects shaped like `{ id, type: 'model', display_name, created_at }`.\n try {\n const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {\n headers: {\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(Boolean);\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -4,6 +4,11 @@ function isClaudeCodeAllowed(options) {
4
4
  if (po && typeof po === "object" && po.allowClaudeCode === true) return true;
5
5
  return options.allowClaudeCode === true;
6
6
  }
7
+ const STREAM_FETCH_TIMEOUT_MS = 6e5;
8
+ function withTimeoutSignal(signal, timeoutMs) {
9
+ const timeout = AbortSignal.timeout(timeoutMs);
10
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
11
+ }
7
12
  class LLMProvider {
8
13
  /**
9
14
  * Whether this provider has the credentials it needs to make a real
@@ -36,6 +41,8 @@ class LLMProvider {
36
41
  }
37
42
  export {
38
43
  LLMProvider,
39
- isClaudeCodeAllowed
44
+ STREAM_FETCH_TIMEOUT_MS,
45
+ isClaudeCodeAllowed,
46
+ withTimeoutSignal
40
47
  };
41
48
  //# sourceMappingURL=base.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n /**\n * Out-of-band retry status — emitted while the router is retrying a\n * transient failure on the same provider. Stream consumers MUST NOT\n * forward `content` to user-visible output; it's a signal for UI\n * status indicators (spinners, toasts) and structured logs only.\n *\n * Pre-fix (v5.4.x): the router yielded a `text` chunk like\n * \"[Retrying request (1/4) due to rate_limit...]\" which leaked into\n * model responses. The dedicated chunk type isolates that signal.\n */\n type: 'retry';\n /** Which retry this is (1-based). */\n attempt: number;\n /** Configured maximum number of retries before failover. */\n maxRetries: number;\n /** Classified failure reason (e.g. \"rate_limit\", \"server_error\"). */\n reason: string;\n /** Provider being retried. */\n provider: string;\n /** Model being retried. */\n model: string;\n /** Backoff delay in ms before the next attempt. */\n delayMs: number;\n }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /**\n * Whether this provider has the credentials it needs to make a real\n * request. Returning `false` here causes the router's chat()/chatStream()\n * to fail FAST with a clear \"configure key for {provider}\" error,\n * BEFORE it would otherwise burn circuit-breaker budget on a request\n * that has zero chance of succeeding.\n *\n * Default implementation: assume the provider doesn't need credentials.\n * Override in providers that do (the cloud ones).\n *\n * Note: this is intentionally synchronous and cheap. It must NOT make\n * network calls. If the provider's credential store is async (very\n * unusual), keep a cached snapshot.\n */\n isConfigured(): boolean {\n return true;\n }\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AA2FO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAkEO,MAAe,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8B9B,eAAwB;AACpB,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n /** v6.5 — abort signal for end-to-end cancellation. Wired from the user's\n * Stop button and OpenAI-compat client disconnects, through the router, to\n * each provider's fetch(), so in-flight generation is actually cancelled\n * (not just ignored between rounds). Each provider combines it with a\n * streaming idle timeout so a stalled stream can't hang forever. */\n signal?: AbortSignal;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** v6.5 — generous total-time backstop for a streaming fetch so a stalled\n * upstream socket can't hang a request forever. Long legitimate generations\n * finish well under this; it only catches truly-stuck connections. The real\n * per-request cancellation comes from the caller's AbortSignal (Stop button /\n * client disconnect), combined in via withTimeoutSignal below. */\nexport const STREAM_FETCH_TIMEOUT_MS = 600_000;\n\n/** v6.5 — combine an optional caller AbortSignal with a fresh timeout so a\n * streaming fetch both honors user/disconnect cancellation AND can't hang\n * forever on a stalled upstream. Node >= 22 provides AbortSignal.any/timeout. */\nexport function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {\n const timeout = AbortSignal.timeout(timeoutMs);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n /**\n * Out-of-band retry status — emitted while the router is retrying a\n * transient failure on the same provider. Stream consumers MUST NOT\n * forward `content` to user-visible output; it's a signal for UI\n * status indicators (spinners, toasts) and structured logs only.\n *\n * Pre-fix (v5.4.x): the router yielded a `text` chunk like\n * \"[Retrying request (1/4) due to rate_limit...]\" which leaked into\n * model responses. The dedicated chunk type isolates that signal.\n */\n type: 'retry';\n /** Which retry this is (1-based). */\n attempt: number;\n /** Configured maximum number of retries before failover. */\n maxRetries: number;\n /** Classified failure reason (e.g. \"rate_limit\", \"server_error\"). */\n reason: string;\n /** Provider being retried. */\n provider: string;\n /** Model being retried. */\n model: string;\n /** Backoff delay in ms before the next attempt. */\n delayMs: number;\n }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /**\n * Whether this provider has the credentials it needs to make a real\n * request. Returning `false` here causes the router's chat()/chatStream()\n * to fail FAST with a clear \"configure key for {provider}\" error,\n * BEFORE it would otherwise burn circuit-breaker budget on a request\n * that has zero chance of succeeding.\n *\n * Default implementation: assume the provider doesn't need credentials.\n * Override in providers that do (the cloud ones).\n *\n * Note: this is intentionally synchronous and cheap. It must NOT make\n * network calls. If the provider's credential store is async (very\n * unusual), keep a cached snapshot.\n */\n isConfigured(): boolean {\n return true;\n }\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AAiGO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAOO,MAAM,0BAA0B;AAKhC,SAAS,kBAAkB,QAAiC,WAAgC;AAC/F,QAAM,UAAU,YAAY,QAAQ,SAAS;AAC7C,SAAO,SAAS,YAAY,IAAI,CAAC,QAAQ,OAAO,CAAC,IAAI;AACzD;AAkEO,MAAe,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8B9B,eAAwB;AACpB,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- LLMProvider
3
+ LLMProvider,
4
+ withTimeoutSignal,
5
+ STREAM_FETCH_TIMEOUT_MS
4
6
  } from "./base.js";
5
7
  import { loadConfig } from "../config/config.js";
6
8
  import logger from "../utils/logger.js";
@@ -204,13 +206,14 @@ class GoogleProvider extends LLMProvider {
204
206
  const response = await fetch(url, {
205
207
  method: "POST",
206
208
  headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
207
- body: JSON.stringify(body)
209
+ body: JSON.stringify(body),
210
+ signal: withTimeoutSignal(options.signal, STREAM_FETCH_TIMEOUT_MS)
208
211
  });
209
212
  if (!response.ok || !response.body) {
210
213
  const errorText = await response.text();
211
214
  dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });
212
- yield { type: "error", error: `Google API error (${response.status}): ${errorText}` };
213
- return;
215
+ const { createProviderError } = await import("./errorTaxonomy.js");
216
+ throw createProviderError("Google API", response, errorText, { provider: "google", model });
214
217
  }
215
218
  const reader = response.body.getReader();
216
219
  const decoder = new TextDecoder();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/google.ts"],"sourcesContent":["/**\n * TITAN — Google Gemini Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatMessage,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\nimport { mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst COMPONENT = 'Google';\n\n/**\n * When true, every Gemini request body that fails serialization-validation OR\n * gets a non-2xx response is dumped to ~/.titan/debug/gemini-requests/ for\n * post-mortem. Toggled via `GOOGLE_DUMP_REQUEST_BODY=1` env var or the\n * provider's `dumpRequestBody` config flag — keeps it off by default since\n * each dump is a JSON file with full prompt content.\n */\nfunction shouldDumpRequestBody(): boolean {\n if (process.env.GOOGLE_DUMP_REQUEST_BODY === '1' || process.env.GOOGLE_DUMP_REQUEST_BODY === 'true') {\n return true;\n }\n try {\n const cfg = loadConfig();\n const p = (cfg.providers as Record<string, unknown> | undefined)?.google as\n | { dumpRequestBody?: boolean }\n | undefined;\n return Boolean(p?.dumpRequestBody);\n } catch {\n return false;\n }\n}\n\nconst GEMINI_DEBUG_DIR = join(homedir(), '.titan', 'debug', 'gemini-requests');\n\nfunction dumpRequestBody(reason: string, body: unknown, extra?: Record<string, unknown>): void {\n if (!shouldDumpRequestBody()) return;\n try {\n mkdirSync(GEMINI_DEBUG_DIR, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const path = join(GEMINI_DEBUG_DIR, `${stamp}-${reason}.json`);\n writeFileSync(path, JSON.stringify({ reason, body, ...(extra ?? {}) }, null, 2));\n logger.info(COMPONENT, `Dumped Gemini request body → ${path}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to dump Gemini request body: ${(err as Error).message}`);\n }\n}\n\n/**\n * Build the Gemini `contents[]` array from TITAN ChatMessages, with strict\n * pre-serialization validation of `tool` messages.\n *\n * Why this matters:\n * Gemini's `functionResponse` requires a non-empty `name` field paired with\n * a valid `tool_call_id` from a prior assistant turn. If the agent loop\n * ever emits a tool result whose corresponding tool call cannot be located\n * in conversation history, Gemini rejects the whole request with a 400 and\n * the error message is opaque (\"function_response without function_call\").\n *\n * Rather than push the malformed message and let Gemini blow up, we:\n * 1. Build a map of every tool_call.id → name from prior assistant messages.\n * 2. For each `tool` message, ensure (a) the name is non-empty (use the\n * toolCallId map as a backstop) and (b) the toolCallId references a\n * known prior call.\n * 3. Drop or relabel messages that fail validation, with a warning that\n * names the offending message so it shows up in logs.\n * 4. If `dumpRequestBody` is enabled, write the full pre-validation body\n * to disk for inspection before any silent corrections.\n */\nfunction buildContents(messages: ChatMessage[]): { contents: Array<Record<string, unknown>>; corrections: number } {\n // Pass 1: build a lookup of valid tool_call_id → function name from\n // every prior assistant turn that emitted toolCalls.\n const toolCallNameById = new Map<string, string>();\n for (const m of messages) {\n if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {\n for (const tc of m.toolCalls) {\n if (tc.id && tc.function?.name) {\n toolCallNameById.set(tc.id, tc.function.name);\n }\n }\n }\n }\n\n let corrections = 0;\n const contents: Array<Record<string, unknown>> = [];\n\n for (const m of messages.filter((x) => x.role !== 'system')) {\n if (m.role === 'tool') {\n // Validation: name must be non-empty AND toolCallId must reference\n // a known prior call. Either failure → log + best-effort repair.\n const callId = m.toolCallId || '';\n const recordedName = callId ? toolCallNameById.get(callId) : undefined;\n const claimedName = (m.name || '').trim();\n\n if (!recordedName) {\n logger.warn(\n COMPONENT,\n `Malformed tool message: tool_call_id=\"${callId}\" has no matching prior tool_call. ` +\n `name=\"${claimedName}\". Dropping to prevent Gemini 400.`,\n );\n corrections++;\n continue;\n }\n\n const finalName = claimedName || recordedName;\n if (!claimedName) {\n logger.warn(\n COMPONENT,\n `Tool message missing name for tool_call_id=\"${callId}\"; ` +\n `inferred \"${finalName}\" from assistant history.`,\n );\n corrections++;\n } else if (claimedName !== recordedName) {\n logger.warn(\n COMPONENT,\n `Tool message name mismatch for tool_call_id=\"${callId}\": ` +\n `claimed \"${claimedName}\" but tool_call recorded \"${recordedName}\". Using recorded.`,\n );\n corrections++;\n }\n\n contents.push({\n role: 'function' as const,\n parts: [{ functionResponse: { name: recordedName, response: { result: m.content } } }],\n });\n continue;\n }\n\n contents.push({\n role: (m.role === 'assistant' ? 'model' : 'user') as string,\n parts: [{ text: m.content }],\n });\n }\n\n return { contents, corrections };\n}\n\nexport class GoogleProvider extends LLMProvider {\n readonly name = 'google';\n readonly displayName = 'Google (Gemini)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.google;\n return resolveApiKey('google', p.authProfiles || [], p.apiKey || '', 'GOOGLE_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Google API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before sending to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n if (systemInstruction) {\n body.systemInstruction = { parts: [{ text: systemInstruction }] };\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = [{\n functionDeclarations: options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n })),\n }];\n }\n\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-goog-api-key': apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Dump body when the API rejected it so post-mortem has full context\n dumpRequestBody(`http-${response.status}`, body, { errorText, model });\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Google API', response, errorText, { provider: 'google', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const candidates = data.candidates as Array<Record<string, unknown>>;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) {\n textContent += part.text as string;\n }\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fc.name as string,\n arguments: JSON.stringify(fc.args),\n },\n });\n }\n }\n }\n\n const usageMeta = data.usageMetadata as { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined;\n\n return {\n id: uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usageMeta\n ? {\n promptTokens: usageMeta.promptTokenCount || 0,\n completionTokens: usageMeta.candidatesTokenCount || 0,\n totalTokens: usageMeta.totalTokenCount || 0,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `google/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Google API key not configured' }; return; }\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before streaming to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: { maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens), temperature: options.temperature ?? 0.7 },\n };\n if (systemInstruction) body.systemInstruction = { parts: [{ text: systemInstruction }] };\n if (options.tools && options.tools.length > 0) {\n body.tools = [{ functionDeclarations: options.tools.map((t) => ({ name: t.function.name, description: t.function.description, parameters: t.function.parameters })) }];\n }\n\n try {\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });\n yield { type: 'error', error: `Google API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const candidates = chunk.candidates as Array<Record<string, unknown>> | undefined;\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) yield { type: 'text', content: part.text as string };\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fc.name as string, arguments: JSON.stringify(fc.args) } },\n };\n }\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or live discovery fails. Covers Gemini families currently in\n // production as of 2026-05.\n const FALLBACK = [\n // Gemini 3.x (2026)\n 'gemini-3.1-pro',\n 'gemini-3.1-flash',\n 'gemini-3.1-flash-lite',\n 'gemini-3.0-pro',\n 'gemini-3.0-flash',\n // Gemini 2.5 / 2.0\n 'gemini-2.5-pro',\n 'gemini-2.5-flash',\n 'gemini-2.5-flash-lite',\n 'gemini-2.0-flash',\n 'gemini-2.0-flash-lite',\n 'gemini-2.0-flash-thinking-exp',\n 'gemini-2.0-pro-exp',\n // Gemini 1.5 (still production-supported)\n 'gemini-1.5-pro',\n 'gemini-1.5-pro-002',\n 'gemini-1.5-flash',\n 'gemini-1.5-flash-002',\n 'gemini-1.5-flash-8b',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1beta/models. Response shape:\n // { models: [{ name: \"models/gemini-2.5-pro\", supportedGenerationMethods: [\"generateContent\", ...] }] }\n // Filter to models that support generateContent (chat) — skip embedding-only,\n // image-gen, and aqa models so the picker stays useful.\n try {\n const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=200', {\n headers: { 'x-goog-api-key': this.apiKey },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1beta/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as {\n models?: Array<{ name: string; supportedGenerationMethods?: string[] }>;\n };\n const ids = (data.models || [])\n .filter((m) => (m.supportedGenerationMethods || []).includes('generateContent'))\n .map((m) => m.name.replace(/^models\\//, ''))\n .filter((id) => /gemini/i.test(id) && !/embedding|tts|image/i.test(id));\n // Sort: newest gemini families first (2.5 > 2.0 > 1.5).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const url = `https://generativelanguage.googleapis.com/v1beta/models`;\n const response = await fetch(url, {\n headers: { 'x-goog-api-key': this.apiKey },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/google.ts"],"sourcesContent":["/**\n * TITAN — Google Gemini Provider\n */\nimport {\n LLMProvider,\n withTimeoutSignal,\n STREAM_FETCH_TIMEOUT_MS,\n type ChatOptions,\n type ChatMessage,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\nimport { mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst COMPONENT = 'Google';\n\n/**\n * When true, every Gemini request body that fails serialization-validation OR\n * gets a non-2xx response is dumped to ~/.titan/debug/gemini-requests/ for\n * post-mortem. Toggled via `GOOGLE_DUMP_REQUEST_BODY=1` env var or the\n * provider's `dumpRequestBody` config flag — keeps it off by default since\n * each dump is a JSON file with full prompt content.\n */\nfunction shouldDumpRequestBody(): boolean {\n if (process.env.GOOGLE_DUMP_REQUEST_BODY === '1' || process.env.GOOGLE_DUMP_REQUEST_BODY === 'true') {\n return true;\n }\n try {\n const cfg = loadConfig();\n const p = (cfg.providers as Record<string, unknown> | undefined)?.google as\n | { dumpRequestBody?: boolean }\n | undefined;\n return Boolean(p?.dumpRequestBody);\n } catch {\n return false;\n }\n}\n\nconst GEMINI_DEBUG_DIR = join(homedir(), '.titan', 'debug', 'gemini-requests');\n\nfunction dumpRequestBody(reason: string, body: unknown, extra?: Record<string, unknown>): void {\n if (!shouldDumpRequestBody()) return;\n try {\n mkdirSync(GEMINI_DEBUG_DIR, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const path = join(GEMINI_DEBUG_DIR, `${stamp}-${reason}.json`);\n writeFileSync(path, JSON.stringify({ reason, body, ...(extra ?? {}) }, null, 2));\n logger.info(COMPONENT, `Dumped Gemini request body → ${path}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to dump Gemini request body: ${(err as Error).message}`);\n }\n}\n\n/**\n * Build the Gemini `contents[]` array from TITAN ChatMessages, with strict\n * pre-serialization validation of `tool` messages.\n *\n * Why this matters:\n * Gemini's `functionResponse` requires a non-empty `name` field paired with\n * a valid `tool_call_id` from a prior assistant turn. If the agent loop\n * ever emits a tool result whose corresponding tool call cannot be located\n * in conversation history, Gemini rejects the whole request with a 400 and\n * the error message is opaque (\"function_response without function_call\").\n *\n * Rather than push the malformed message and let Gemini blow up, we:\n * 1. Build a map of every tool_call.id → name from prior assistant messages.\n * 2. For each `tool` message, ensure (a) the name is non-empty (use the\n * toolCallId map as a backstop) and (b) the toolCallId references a\n * known prior call.\n * 3. Drop or relabel messages that fail validation, with a warning that\n * names the offending message so it shows up in logs.\n * 4. If `dumpRequestBody` is enabled, write the full pre-validation body\n * to disk for inspection before any silent corrections.\n */\nfunction buildContents(messages: ChatMessage[]): { contents: Array<Record<string, unknown>>; corrections: number } {\n // Pass 1: build a lookup of valid tool_call_id → function name from\n // every prior assistant turn that emitted toolCalls.\n const toolCallNameById = new Map<string, string>();\n for (const m of messages) {\n if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {\n for (const tc of m.toolCalls) {\n if (tc.id && tc.function?.name) {\n toolCallNameById.set(tc.id, tc.function.name);\n }\n }\n }\n }\n\n let corrections = 0;\n const contents: Array<Record<string, unknown>> = [];\n\n for (const m of messages.filter((x) => x.role !== 'system')) {\n if (m.role === 'tool') {\n // Validation: name must be non-empty AND toolCallId must reference\n // a known prior call. Either failure → log + best-effort repair.\n const callId = m.toolCallId || '';\n const recordedName = callId ? toolCallNameById.get(callId) : undefined;\n const claimedName = (m.name || '').trim();\n\n if (!recordedName) {\n logger.warn(\n COMPONENT,\n `Malformed tool message: tool_call_id=\"${callId}\" has no matching prior tool_call. ` +\n `name=\"${claimedName}\". Dropping to prevent Gemini 400.`,\n );\n corrections++;\n continue;\n }\n\n const finalName = claimedName || recordedName;\n if (!claimedName) {\n logger.warn(\n COMPONENT,\n `Tool message missing name for tool_call_id=\"${callId}\"; ` +\n `inferred \"${finalName}\" from assistant history.`,\n );\n corrections++;\n } else if (claimedName !== recordedName) {\n logger.warn(\n COMPONENT,\n `Tool message name mismatch for tool_call_id=\"${callId}\": ` +\n `claimed \"${claimedName}\" but tool_call recorded \"${recordedName}\". Using recorded.`,\n );\n corrections++;\n }\n\n contents.push({\n role: 'function' as const,\n parts: [{ functionResponse: { name: recordedName, response: { result: m.content } } }],\n });\n continue;\n }\n\n contents.push({\n role: (m.role === 'assistant' ? 'model' : 'user') as string,\n parts: [{ text: m.content }],\n });\n }\n\n return { contents, corrections };\n}\n\nexport class GoogleProvider extends LLMProvider {\n readonly name = 'google';\n readonly displayName = 'Google (Gemini)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.google;\n return resolveApiKey('google', p.authProfiles || [], p.apiKey || '', 'GOOGLE_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Google API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before sending to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n if (systemInstruction) {\n body.systemInstruction = { parts: [{ text: systemInstruction }] };\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = [{\n functionDeclarations: options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n })),\n }];\n }\n\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-goog-api-key': apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Dump body when the API rejected it so post-mortem has full context\n dumpRequestBody(`http-${response.status}`, body, { errorText, model });\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Google API', response, errorText, { provider: 'google', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const candidates = data.candidates as Array<Record<string, unknown>>;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) {\n textContent += part.text as string;\n }\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fc.name as string,\n arguments: JSON.stringify(fc.args),\n },\n });\n }\n }\n }\n\n const usageMeta = data.usageMetadata as { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined;\n\n return {\n id: uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usageMeta\n ? {\n promptTokens: usageMeta.promptTokenCount || 0,\n completionTokens: usageMeta.candidatesTokenCount || 0,\n totalTokens: usageMeta.totalTokenCount || 0,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `google/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Google API key not configured' }; return; }\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before streaming to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: { maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens), temperature: options.temperature ?? 0.7 },\n };\n if (systemInstruction) body.systemInstruction = { parts: [{ text: systemInstruction }] };\n if (options.tools && options.tools.length > 0) {\n body.tools = [{ functionDeclarations: options.tools.map((t) => ({ name: t.function.name, description: t.function.description, parameters: t.function.parameters })) }];\n }\n\n try {\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify(body),\n signal: withTimeoutSignal(options.signal, STREAM_FETCH_TIMEOUT_MS),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });\n // v6.5 — THROW (not yield) so the router routes HTTP errors through\n // retry / fallback chain / circuit breaker, like non-streaming chat().\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Google API', response, errorText, { provider: 'google', model });\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const candidates = chunk.candidates as Array<Record<string, unknown>> | undefined;\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) yield { type: 'text', content: part.text as string };\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fc.name as string, arguments: JSON.stringify(fc.args) } },\n };\n }\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or live discovery fails. Covers Gemini families currently in\n // production as of 2026-05.\n const FALLBACK = [\n // Gemini 3.x (2026)\n 'gemini-3.1-pro',\n 'gemini-3.1-flash',\n 'gemini-3.1-flash-lite',\n 'gemini-3.0-pro',\n 'gemini-3.0-flash',\n // Gemini 2.5 / 2.0\n 'gemini-2.5-pro',\n 'gemini-2.5-flash',\n 'gemini-2.5-flash-lite',\n 'gemini-2.0-flash',\n 'gemini-2.0-flash-lite',\n 'gemini-2.0-flash-thinking-exp',\n 'gemini-2.0-pro-exp',\n // Gemini 1.5 (still production-supported)\n 'gemini-1.5-pro',\n 'gemini-1.5-pro-002',\n 'gemini-1.5-flash',\n 'gemini-1.5-flash-002',\n 'gemini-1.5-flash-8b',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1beta/models. Response shape:\n // { models: [{ name: \"models/gemini-2.5-pro\", supportedGenerationMethods: [\"generateContent\", ...] }] }\n // Filter to models that support generateContent (chat) — skip embedding-only,\n // image-gen, and aqa models so the picker stays useful.\n try {\n const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=200', {\n headers: { 'x-goog-api-key': this.apiKey },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1beta/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as {\n models?: Array<{ name: string; supportedGenerationMethods?: string[] }>;\n };\n const ids = (data.models || [])\n .filter((m) => (m.supportedGenerationMethods || []).includes('generateContent'))\n .map((m) => m.name.replace(/^models\\//, ''))\n .filter((id) => /gemini/i.test(id) && !/embedding|tts|image/i.test(id));\n // Sort: newest gemini families first (2.5 > 2.0 > 1.5).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const url = `https://generativelanguage.googleapis.com/v1beta/models`;\n const response = await fetch(url, {\n headers: { 'x-goog-api-key': this.apiKey },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- LLMProvider
3
+ LLMProvider,
4
+ withTimeoutSignal
4
5
  } from "./base.js";
5
6
  import { loadConfig } from "../config/config.js";
6
7
  import logger from "../utils/logger.js";
@@ -622,7 +623,7 @@ ${msgs2[firstUserIdx].content}`;
622
623
  method: "POST",
623
624
  headers: { "Content-Type": "application/json" },
624
625
  body: JSON.stringify(body),
625
- signal: AbortSignal.timeout(streamTimeoutMs)
626
+ signal: withTimeoutSignal(options.signal, streamTimeoutMs)
626
627
  });
627
628
  if (!response.ok || !response.body) {
628
629
  const errorText = await response.text();
@@ -633,16 +634,16 @@ ${msgs2[firstUserIdx].content}`;
633
634
  method: "POST",
634
635
  headers: { "Content-Type": "application/json" },
635
636
  body: JSON.stringify(body),
636
- signal: AbortSignal.timeout(streamTimeoutMs)
637
+ signal: withTimeoutSignal(options.signal, streamTimeoutMs)
637
638
  });
638
639
  if (!response.ok || !response.body) {
639
640
  const retryText = await response.text();
640
- yield { type: "error", error: `Ollama error (${response.status}): ${retryText}` };
641
- return;
641
+ const { createProviderError } = await import("./errorTaxonomy.js");
642
+ throw createProviderError("Ollama", response, retryText, { provider: "ollama", model });
642
643
  }
643
644
  } else {
644
- yield { type: "error", error: `Ollama error (${response.status}): ${errorText}` };
645
- return;
645
+ const { createProviderError } = await import("./errorTaxonomy.js");
646
+ throw createProviderError("Ollama", response, errorText, { provider: "ollama", model });
646
647
  }
647
648
  }
648
649
  const reader = response.body.getReader();