titan-agent 6.3.5 → 6.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config.js +18 -2
- package/dist/config/config.js.map +1 -1
- package/dist/providers/defaultModel.js +2 -2
- package/dist/providers/defaultModel.js.map +1 -1
- package/dist/providers/ollama.js +17 -1
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/router.js +7 -3
- package/dist/providers/router.js.map +1 -1
- package/dist/providers/stub.js +1 -6
- package/dist/providers/stub.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
package/dist/config/config.js
CHANGED
|
@@ -158,7 +158,7 @@ async function hasUsableProvider() {
|
|
|
158
158
|
return { ok: true, details: `${key} is set in environment` };
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
const ollamaUrl = providers.ollama?.baseUrl || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
161
|
+
const ollamaUrl = normalizeOllamaBaseUrl(providers.ollama?.baseUrl || process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || "http://localhost:11434");
|
|
162
162
|
try {
|
|
163
163
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
164
164
|
if (res.ok) {
|
|
@@ -182,7 +182,8 @@ function applyEnvOverrides(config) {
|
|
|
182
182
|
ANTHROPIC_API_KEY: (val) => setNested(config, "providers.anthropic.apiKey", val),
|
|
183
183
|
OPENAI_API_KEY: (val) => setNested(config, "providers.openai.apiKey", val),
|
|
184
184
|
GOOGLE_API_KEY: (val) => setNested(config, "providers.google.apiKey", val),
|
|
185
|
-
|
|
185
|
+
OLLAMA_HOST: (val) => setNested(config, "providers.ollama.baseUrl", normalizeOllamaBaseUrl(val)),
|
|
186
|
+
OLLAMA_BASE_URL: (val) => setNested(config, "providers.ollama.baseUrl", normalizeOllamaBaseUrl(val)),
|
|
186
187
|
DISCORD_TOKEN: (val) => setNested(config, "channels.discord.token", val),
|
|
187
188
|
TELEGRAM_TOKEN: (val) => setNested(config, "channels.telegram.token", val),
|
|
188
189
|
SLACK_TOKEN: (val) => setNested(config, "channels.slack.token", val),
|
|
@@ -212,6 +213,21 @@ function isProviderDisabled(config, providerName) {
|
|
|
212
213
|
const provider = providers?.[providerName];
|
|
213
214
|
return provider?.enabled === false;
|
|
214
215
|
}
|
|
216
|
+
function normalizeOllamaBaseUrl(value) {
|
|
217
|
+
let raw = value.trim().replace(/\/+$/, "");
|
|
218
|
+
if (!/^https?:\/\//i.test(raw)) {
|
|
219
|
+
raw = `http://${raw}`;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const url = new URL(raw);
|
|
223
|
+
if (url.hostname === "0.0.0.0") {
|
|
224
|
+
url.hostname = "localhost";
|
|
225
|
+
}
|
|
226
|
+
return url.toString().replace(/\/+$/, "");
|
|
227
|
+
} catch {
|
|
228
|
+
return raw;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
215
231
|
function findDroppedKeys(raw, parsed, pathPrefix, depth = 0) {
|
|
216
232
|
if (depth > 8) return [];
|
|
217
233
|
if (!isPlainObject(raw)) return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = (providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || 'http://localhost:11434';\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', val),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = normalizeOllamaBaseUrl((providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || process.env.OLLAMA_HOST\n || 'http://localhost:11434');\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_HOST: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\nfunction normalizeOllamaBaseUrl(value: string): string {\n let raw = value.trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\//i.test(raw)) {\n raw = `http://${raw}`;\n }\n\n try {\n const url = new URL(raw);\n if (url.hostname === '0.0.0.0') {\n url.hostname = 'localhost';\n }\n return url.toString().replace(/\\/+$/, '');\n } catch {\n return raw;\n }\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -96,7 +96,7 @@ function pickDefaultModel() {
|
|
|
96
96
|
cheap: "stub/echo",
|
|
97
97
|
reasoning: "stub/json"
|
|
98
98
|
},
|
|
99
|
-
reason: "
|
|
99
|
+
reason: "TITAN_STUB_PROVIDER=1 \u2014 using deterministic stub provider (no LLM calls)",
|
|
100
100
|
provider: "stub"
|
|
101
101
|
};
|
|
102
102
|
}
|
|
@@ -123,7 +123,7 @@ function pickDefaultModel() {
|
|
|
123
123
|
return {
|
|
124
124
|
model: OLLAMA_FALLBACK.model,
|
|
125
125
|
aliases: OLLAMA_FALLBACK.aliases,
|
|
126
|
-
reason: "no
|
|
126
|
+
reason: "no direct provider API keys detected \u2014 defaulting to Ollama (configure providers.ollama.baseUrl, OLLAMA_BASE_URL, or OLLAMA_HOST for a remote Ollama host)",
|
|
127
127
|
provider: "ollama"
|
|
128
128
|
};
|
|
129
129
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/providers/defaultModel.ts"],"sourcesContent":["/**\n * TITAN — Default Model Picker (v6.0.1)\n *\n * \"Model-agnostic\" promise: TITAN runs with whatever LLM provider the\n * user has credentials for. If they only have an OpenAI key, TITAN picks\n * OpenAI. If only Anthropic, TITAN picks Anthropic. If nothing's keyed,\n * TITAN falls back to a local Ollama default (still works — the package\n * runs end-to-end on a laptop with no cloud creds).\n *\n * Order of preference (highest tier first):\n *\n * 1. `agent.model` in user config — explicit override, always wins.\n * 2. `ANTHROPIC_API_KEY` → claude-sonnet-4 (strongest agent reasoning)\n * 3. `OPENAI_API_KEY` → gpt-4o\n * 4. `GOOGLE_API_KEY` → gemini-2.5-pro\n * 5. `OPENROUTER_API_KEY` → claude-sonnet via openrouter\n * 6. (anything else with `*_API_KEY` set among the 32 OpenAI-compat\n * providers TITAN ships) → use that\n * 7. Local Ollama → `qwen3.5:cloud` (the standing fast default —\n * routes through `ollama serve` on localhost:11434).\n *\n * The picker is pure (no network, no async). It only inspects\n * `process.env` + the user config blob. It's safe to call from the Zod\n * schema's `.default()` thunks AND from runtime resolution paths.\n *\n * On gateway boot we log the pick + the reason so the user knows which\n * provider TITAN selected.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { shouldUseStubProvider } from './stub.js';\n\nconst TITAN_HOME_DEFAULT = join(homedir(), '.titan');\n\n// ── Provider preference table ───────────────────────────────────\n//\n// Order is intentional: stronger reasoning models first, then size,\n// then breadth. We pin to widely-available checkpoints (no preview-only\n// ids) so the default works out of the box for everyone.\n\ninterface ProviderCandidate {\n name: string;\n envKey: string;\n /** The model id we pick when this env key is present. */\n model: string;\n /** Reasonable fast / smart / cheap fallbacks within this provider. */\n aliases: { fast: string; smart: string; cheap: string; reasoning: string };\n}\n\nconst PROVIDERS: ProviderCandidate[] = [\n {\n name: 'anthropic',\n envKey: 'ANTHROPIC_API_KEY',\n model: 'anthropic/claude-sonnet-4-20250514',\n aliases: {\n fast: 'anthropic/claude-haiku-4-20250514',\n smart: 'anthropic/claude-sonnet-4-20250514',\n cheap: 'anthropic/claude-haiku-4-20250514',\n reasoning: 'anthropic/claude-sonnet-4-20250514',\n },\n },\n {\n name: 'openai',\n envKey: 'OPENAI_API_KEY',\n model: 'openai/gpt-4o',\n aliases: {\n fast: 'openai/gpt-4o-mini',\n smart: 'openai/gpt-4o',\n cheap: 'openai/gpt-4o-mini',\n reasoning: 'openai/gpt-4o',\n },\n },\n {\n name: 'google',\n envKey: 'GOOGLE_API_KEY',\n model: 'google/gemini-2.5-pro',\n aliases: {\n fast: 'google/gemini-2.5-flash',\n smart: 'google/gemini-2.5-pro',\n cheap: 'google/gemini-2.5-flash',\n reasoning: 'google/gemini-2.5-pro',\n },\n },\n {\n name: 'openrouter',\n envKey: 'OPENROUTER_API_KEY',\n model: 'openrouter/anthropic/claude-sonnet-4',\n aliases: {\n fast: 'openrouter/anthropic/claude-haiku-4',\n smart: 'openrouter/anthropic/claude-sonnet-4',\n cheap: 'openrouter/anthropic/claude-haiku-4',\n reasoning: 'openrouter/anthropic/claude-sonnet-4',\n },\n },\n];\n\n// ── Ollama-cloud fallback (works without any cloud key) ──────────\n//\n// These are the same defaults we shipped through v5.x. Kept here as the\n// last-resort floor so a user with NO cloud credentials still gets a\n// working agent out of the box (assuming `ollama serve` is up — which\n// the `titan onboard` wizard helps with).\n\nconst OLLAMA_FALLBACK = {\n model: 'ollama/qwen3.5:cloud',\n aliases: {\n fast: 'ollama/qwen3.5:cloud',\n smart: 'ollama/glm-5:cloud',\n cheap: 'ollama/qwen3.5:cloud',\n reasoning: 'ollama/kimi-k2.6:cloud',\n local: 'ollama/qwen3.5:4b',\n cloud: 'ollama/kimi-k2.6:cloud',\n },\n};\n\n// ── Credential probing ──────────────────────────────────────────\n\nfunction hasNonEmptyEnv(key: string): boolean {\n const v = process.env[key];\n return typeof v === 'string' && v.trim().length > 0;\n}\n\n/** Read user config without depending on the full Zod schema (we may be\n * called from inside the schema itself). Returns the raw JSON blob or\n * null. Best-effort — corrupted config returns null. */\nfunction readUserConfigBlob(): Record<string, unknown> | null {\n const titanHome = process.env.TITAN_HOME && process.env.TITAN_HOME.length > 0\n ? (process.env.TITAN_HOME.startsWith('~/') ? join(homedir(), process.env.TITAN_HOME.slice(2)) : process.env.TITAN_HOME)\n : TITAN_HOME_DEFAULT;\n const path = join(titanHome, 'titan.json');\n if (!existsSync(path)) return null;\n try { return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>; } catch { return null; }\n}\n\n// ── Public picker ───────────────────────────────────────────────\n\nexport interface DefaultModelPick {\n /** The chosen model id (provider/model-name). */\n model: string;\n /** Model aliases for the fast / smart / cheap / reasoning tiers. */\n aliases: Record<string, string>;\n /** Human-readable reason for the pick — printed at gateway boot. */\n reason: string;\n /** Provider name (anthropic / openai / google / openrouter / ollama). */\n provider: string;\n}\n\n/**\n * Decide the default agent model + tier aliases from currently-available\n * credentials. Pure function — only reads `process.env` and (optionally)\n * the on-disk config blob.\n *\n * Returns the same shape regardless of which provider wins so the\n * config schema can plug the pick straight in.\n */\nexport function pickDefaultModel(): DefaultModelPick {\n // 1. Explicit user config wins\n const cfg = readUserConfigBlob();\n const userAgentModel = ((cfg?.agent as Record<string, unknown> | undefined)?.model);\n if (typeof userAgentModel === 'string' && userAgentModel.trim().length > 0) {\n const provider = userAgentModel.split('/')[0] || 'unknown';\n return {\n model: userAgentModel,\n aliases: defaultAliasesForProvider(provider),\n reason: `user config (agent.model=${userAgentModel})`,\n provider,\n };\n }\n\n // 1b. beta.11 — CI stub fallback. When TITAN_STUB_PROVIDER=1 OR\n // when we're in CI without any real provider key, route everything\n // to the deterministic stub so Eval Gate runs can score. Slotted\n // BEFORE the cloud-key checks so an accidental leftover key on a\n // CI runner doesn't suddenly start spending real money. The\n // explicit user-config branch above still beats the stub.\n if (shouldUseStubProvider()) {\n return {\n model: 'stub/echo',\n aliases: {\n fast: 'stub/echo',\n smart: 'stub/echo',\n cheap: 'stub/echo',\n reasoning: 'stub/json',\n },\n reason: 'CI / TITAN_STUB_PROVIDER set — using deterministic stub provider (no LLM calls)',\n provider: 'stub',\n };\n }\n\n // 2. Walk providers in preference order; first env key wins.\n for (const candidate of PROVIDERS) {\n if (hasNonEmptyEnv(candidate.envKey)) {\n return {\n model: candidate.model,\n aliases: candidate.aliases,\n reason: `${candidate.envKey} is set — picked ${candidate.name}`,\n provider: candidate.name,\n };\n }\n }\n\n // 3. Catch-all: any of the 32 OpenAI-compat providers with a key set.\n // Conservative — we only auto-pick when the key pattern is\n // unmistakable (`*_API_KEY` or known names like GROQ_API_KEY).\n const compatHits = Object.keys(process.env)\n .filter(k => k.endsWith('_API_KEY') && (process.env[k] ?? '').trim().length > 0)\n .filter(k => !['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'].includes(k));\n if (compatHits.length > 0) {\n // Pick the first one alphabetically for determinism. The user can\n // always override via config — this is just the bootstrap default.\n const providerName = compatHits[0].replace('_API_KEY', '').toLowerCase();\n // For these we don't know the exact model id; reuse Ollama\n // fallback aliases but flag the provider so the user sees the\n // log line and can correct it in config.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: `detected ${compatHits[0]} but no first-class adapter mapping; falling back to Ollama defaults — override agent.model in config to use ${providerName}`,\n provider: 'ollama',\n };\n }\n\n // 4. Final fallback — local Ollama. Works on a clean laptop with\n // `ollama serve` running and the default models pulled.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: 'no cloud API keys detected — defaulting to local Ollama (set ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY to switch providers)',\n provider: 'ollama',\n };\n}\n\nfunction defaultAliasesForProvider(provider: string): Record<string, string> {\n const match = PROVIDERS.find(p => p.name === provider);\n if (match) return match.aliases;\n return OLLAMA_FALLBACK.aliases;\n}\n\n/**\n * Resolve the default agent model. Thin wrapper used by the schema's\n * `.default()` thunk. Stable across calls within a process (env doesn't\n * change at runtime), so the schema's default is consistent.\n */\nexport function getDefaultModelId(): string {\n return pickDefaultModel().model;\n}\n\n/**\n * Resolve the default tier-alias map. Used by the schema's modelAliases\n * default. Merges with the Ollama floor so any tier name not provided\n * by the chosen provider still resolves to something working.\n */\nexport function getDefaultModelAliases(): Record<string, string> {\n const pick = pickDefaultModel();\n return {\n // Ollama floor — any tier the chosen provider doesn't define\n // still resolves to a working model.\n ...OLLAMA_FALLBACK.aliases,\n // Chosen-provider aliases win for fast / smart / cheap /\n // reasoning. `local` + `cloud` keys stay on the Ollama floor.\n ...pick.aliases,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/providers/defaultModel.ts"],"sourcesContent":["/**\n * TITAN — Default Model Picker (v6.0.1)\n *\n * \"Model-agnostic\" promise: TITAN runs with whatever LLM provider the\n * user has credentials for. If they only have an OpenAI key, TITAN picks\n * OpenAI. If only Anthropic, TITAN picks Anthropic. If nothing's keyed,\n * TITAN falls back to a local Ollama default (still works — the package\n * runs end-to-end on a laptop with no cloud creds).\n *\n * Order of preference (highest tier first):\n *\n * 1. `agent.model` in user config — explicit override, always wins.\n * 2. `ANTHROPIC_API_KEY` → claude-sonnet-4 (strongest agent reasoning)\n * 3. `OPENAI_API_KEY` → gpt-4o\n * 4. `GOOGLE_API_KEY` → gemini-2.5-pro\n * 5. `OPENROUTER_API_KEY` → claude-sonnet via openrouter\n * 6. (anything else with `*_API_KEY` set among the 32 OpenAI-compat\n * providers TITAN ships) → use that\n * 7. Ollama → `qwen3.5:cloud` (the standing fast default —\n * routes through the configured Ollama host, usually Titan PC).\n *\n * The picker is pure (no network, no async). It only inspects\n * `process.env` + the user config blob. It's safe to call from the Zod\n * schema's `.default()` thunks AND from runtime resolution paths.\n *\n * On gateway boot we log the pick + the reason so the user knows which\n * provider TITAN selected.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { shouldUseStubProvider } from './stub.js';\n\nconst TITAN_HOME_DEFAULT = join(homedir(), '.titan');\n\n// ── Provider preference table ───────────────────────────────────\n//\n// Order is intentional: stronger reasoning models first, then size,\n// then breadth. We pin to widely-available checkpoints (no preview-only\n// ids) so the default works out of the box for everyone.\n\ninterface ProviderCandidate {\n name: string;\n envKey: string;\n /** The model id we pick when this env key is present. */\n model: string;\n /** Reasonable fast / smart / cheap fallbacks within this provider. */\n aliases: { fast: string; smart: string; cheap: string; reasoning: string };\n}\n\nconst PROVIDERS: ProviderCandidate[] = [\n {\n name: 'anthropic',\n envKey: 'ANTHROPIC_API_KEY',\n model: 'anthropic/claude-sonnet-4-20250514',\n aliases: {\n fast: 'anthropic/claude-haiku-4-20250514',\n smart: 'anthropic/claude-sonnet-4-20250514',\n cheap: 'anthropic/claude-haiku-4-20250514',\n reasoning: 'anthropic/claude-sonnet-4-20250514',\n },\n },\n {\n name: 'openai',\n envKey: 'OPENAI_API_KEY',\n model: 'openai/gpt-4o',\n aliases: {\n fast: 'openai/gpt-4o-mini',\n smart: 'openai/gpt-4o',\n cheap: 'openai/gpt-4o-mini',\n reasoning: 'openai/gpt-4o',\n },\n },\n {\n name: 'google',\n envKey: 'GOOGLE_API_KEY',\n model: 'google/gemini-2.5-pro',\n aliases: {\n fast: 'google/gemini-2.5-flash',\n smart: 'google/gemini-2.5-pro',\n cheap: 'google/gemini-2.5-flash',\n reasoning: 'google/gemini-2.5-pro',\n },\n },\n {\n name: 'openrouter',\n envKey: 'OPENROUTER_API_KEY',\n model: 'openrouter/anthropic/claude-sonnet-4',\n aliases: {\n fast: 'openrouter/anthropic/claude-haiku-4',\n smart: 'openrouter/anthropic/claude-sonnet-4',\n cheap: 'openrouter/anthropic/claude-haiku-4',\n reasoning: 'openrouter/anthropic/claude-sonnet-4',\n },\n },\n];\n\n// ── Ollama-cloud fallback (works without any direct provider key) ──────────\n//\n// These are the same defaults we shipped through v5.x. Kept here as the\n// last-resort floor so a user with NO direct provider credentials still gets\n// a working agent out of the box through their Ollama surface. On Tony's\n// setup this is Titan PC Ollama, including Ollama cloud models.\n\nconst OLLAMA_FALLBACK = {\n model: 'ollama/qwen3.5:cloud',\n aliases: {\n fast: 'ollama/qwen3.5:cloud',\n smart: 'ollama/glm-5:cloud',\n cheap: 'ollama/qwen3.5:cloud',\n reasoning: 'ollama/kimi-k2.6:cloud',\n local: 'ollama/qwen3.5:4b',\n cloud: 'ollama/kimi-k2.6:cloud',\n },\n};\n\n// ── Credential probing ──────────────────────────────────────────\n\nfunction hasNonEmptyEnv(key: string): boolean {\n const v = process.env[key];\n return typeof v === 'string' && v.trim().length > 0;\n}\n\n/** Read user config without depending on the full Zod schema (we may be\n * called from inside the schema itself). Returns the raw JSON blob or\n * null. Best-effort — corrupted config returns null. */\nfunction readUserConfigBlob(): Record<string, unknown> | null {\n const titanHome = process.env.TITAN_HOME && process.env.TITAN_HOME.length > 0\n ? (process.env.TITAN_HOME.startsWith('~/') ? join(homedir(), process.env.TITAN_HOME.slice(2)) : process.env.TITAN_HOME)\n : TITAN_HOME_DEFAULT;\n const path = join(titanHome, 'titan.json');\n if (!existsSync(path)) return null;\n try { return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>; } catch { return null; }\n}\n\n// ── Public picker ───────────────────────────────────────────────\n\nexport interface DefaultModelPick {\n /** The chosen model id (provider/model-name). */\n model: string;\n /** Model aliases for the fast / smart / cheap / reasoning tiers. */\n aliases: Record<string, string>;\n /** Human-readable reason for the pick — printed at gateway boot. */\n reason: string;\n /** Provider name (anthropic / openai / google / openrouter / ollama). */\n provider: string;\n}\n\n/**\n * Decide the default agent model + tier aliases from currently-available\n * credentials. Pure function — only reads `process.env` and (optionally)\n * the on-disk config blob.\n *\n * Returns the same shape regardless of which provider wins so the\n * config schema can plug the pick straight in.\n */\nexport function pickDefaultModel(): DefaultModelPick {\n // 1. Explicit user config wins\n const cfg = readUserConfigBlob();\n const userAgentModel = ((cfg?.agent as Record<string, unknown> | undefined)?.model);\n if (typeof userAgentModel === 'string' && userAgentModel.trim().length > 0) {\n const provider = userAgentModel.split('/')[0] || 'unknown';\n return {\n model: userAgentModel,\n aliases: defaultAliasesForProvider(provider),\n reason: `user config (agent.model=${userAgentModel})`,\n provider,\n };\n }\n\n // 1b. beta.11 — explicit CI/eval stub fallback. Only\n // TITAN_STUB_PROVIDER=1 routes to the deterministic stub. A plain\n // CI=true environment still defaults to Ollama so provider failures\n // are visible instead of being masked by stub/echo.\n if (shouldUseStubProvider()) {\n return {\n model: 'stub/echo',\n aliases: {\n fast: 'stub/echo',\n smart: 'stub/echo',\n cheap: 'stub/echo',\n reasoning: 'stub/json',\n },\n reason: 'TITAN_STUB_PROVIDER=1 — using deterministic stub provider (no LLM calls)',\n provider: 'stub',\n };\n }\n\n // 2. Walk providers in preference order; first env key wins.\n for (const candidate of PROVIDERS) {\n if (hasNonEmptyEnv(candidate.envKey)) {\n return {\n model: candidate.model,\n aliases: candidate.aliases,\n reason: `${candidate.envKey} is set — picked ${candidate.name}`,\n provider: candidate.name,\n };\n }\n }\n\n // 3. Catch-all: any of the 32 OpenAI-compat providers with a key set.\n // Conservative — we only auto-pick when the key pattern is\n // unmistakable (`*_API_KEY` or known names like GROQ_API_KEY).\n const compatHits = Object.keys(process.env)\n .filter(k => k.endsWith('_API_KEY') && (process.env[k] ?? '').trim().length > 0)\n .filter(k => !['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'].includes(k));\n if (compatHits.length > 0) {\n // Pick the first one alphabetically for determinism. The user can\n // always override via config — this is just the bootstrap default.\n const providerName = compatHits[0].replace('_API_KEY', '').toLowerCase();\n // For these we don't know the exact model id; reuse Ollama\n // fallback aliases but flag the provider so the user sees the\n // log line and can correct it in config.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: `detected ${compatHits[0]} but no first-class adapter mapping; falling back to Ollama defaults — override agent.model in config to use ${providerName}`,\n provider: 'ollama',\n };\n }\n\n // 4. Final fallback — Ollama. Works against the configured Ollama host,\n // including Titan PC's Ollama cloud models.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: 'no direct provider API keys detected — defaulting to Ollama (configure providers.ollama.baseUrl, OLLAMA_BASE_URL, or OLLAMA_HOST for a remote Ollama host)',\n provider: 'ollama',\n };\n}\n\nfunction defaultAliasesForProvider(provider: string): Record<string, string> {\n const match = PROVIDERS.find(p => p.name === provider);\n if (match) return match.aliases;\n return OLLAMA_FALLBACK.aliases;\n}\n\n/**\n * Resolve the default agent model. Thin wrapper used by the schema's\n * `.default()` thunk. Stable across calls within a process (env doesn't\n * change at runtime), so the schema's default is consistent.\n */\nexport function getDefaultModelId(): string {\n return pickDefaultModel().model;\n}\n\n/**\n * Resolve the default tier-alias map. Used by the schema's modelAliases\n * default. Merges with the Ollama floor so any tier name not provided\n * by the chosen provider still resolves to something working.\n */\nexport function getDefaultModelAliases(): Record<string, string> {\n const pick = pickDefaultModel();\n return {\n // Ollama floor — any tier the chosen provider doesn't define\n // still resolves to a working model.\n ...OLLAMA_FALLBACK.aliases,\n // Chosen-provider aliases win for fast / smart / cheap /\n // reasoning. `local` + `cloud` keys stay on the Ollama floor.\n ...pick.aliases,\n };\n}\n"],"mappings":";AA6BA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,6BAA6B;AAEtC,MAAM,qBAAqB,KAAK,QAAQ,GAAG,QAAQ;AAiBnD,MAAM,YAAiC;AAAA,EACnC;AAAA,IACI,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,MACP,WAAW;AAAA,IACf;AAAA,EACJ;AAAA,EACA;AAAA,IACI,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,MACP,WAAW;AAAA,IACf;AAAA,EACJ;AAAA,EACA;AAAA,IACI,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,MACP,WAAW;AAAA,IACf;AAAA,EACJ;AAAA,EACA;AAAA,IACI,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,MACP,WAAW;AAAA,IACf;AAAA,EACJ;AACJ;AASA,MAAM,kBAAkB;AAAA,EACpB,OAAO;AAAA,EACP,SAAS;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,OAAO;AAAA,IACP,OAAO;AAAA,EACX;AACJ;AAIA,SAAS,eAAe,KAAsB;AAC1C,QAAM,IAAI,QAAQ,IAAI,GAAG;AACzB,SAAO,OAAO,MAAM,YAAY,EAAE,KAAK,EAAE,SAAS;AACtD;AAKA,SAAS,qBAAqD;AAC1D,QAAM,YAAY,QAAQ,IAAI,cAAc,QAAQ,IAAI,WAAW,SAAS,IACrE,QAAQ,IAAI,WAAW,WAAW,IAAI,IAAI,KAAK,QAAQ,GAAG,QAAQ,IAAI,WAAW,MAAM,CAAC,CAAC,IAAI,QAAQ,IAAI,aAC1G;AACN,QAAM,OAAO,KAAK,WAAW,YAAY;AACzC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AAAE,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAA8B,QAAQ;AAAE,WAAO;AAAA,EAAM;AAC5G;AAuBO,SAAS,mBAAqC;AAEjD,QAAM,MAAM,mBAAmB;AAC/B,QAAM,iBAAmB,KAAK,OAA+C;AAC7E,MAAI,OAAO,mBAAmB,YAAY,eAAe,KAAK,EAAE,SAAS,GAAG;AACxE,UAAM,WAAW,eAAe,MAAM,GAAG,EAAE,CAAC,KAAK;AACjD,WAAO;AAAA,MACH,OAAO;AAAA,MACP,SAAS,0BAA0B,QAAQ;AAAA,MAC3C,QAAQ,4BAA4B,cAAc;AAAA,MAClD;AAAA,IACJ;AAAA,EACJ;AAMA,MAAI,sBAAsB,GAAG;AACzB,WAAO;AAAA,MACH,OAAO;AAAA,MACP,SAAS;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO;AAAA,QACP,WAAW;AAAA,MACf;AAAA,MACA,QAAQ;AAAA,MACR,UAAU;AAAA,IACd;AAAA,EACJ;AAGA,aAAW,aAAa,WAAW;AAC/B,QAAI,eAAe,UAAU,MAAM,GAAG;AAClC,aAAO;AAAA,QACH,OAAO,UAAU;AAAA,QACjB,SAAS,UAAU;AAAA,QACnB,QAAQ,GAAG,UAAU,MAAM,yBAAoB,UAAU,IAAI;AAAA,QAC7D,UAAU,UAAU;AAAA,MACxB;AAAA,IACJ;AAAA,EACJ;AAKA,QAAM,aAAa,OAAO,KAAK,QAAQ,GAAG,EACrC,OAAO,OAAK,EAAE,SAAS,UAAU,MAAM,QAAQ,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,SAAS,CAAC,EAC9E,OAAO,OAAK,CAAC,CAAC,qBAAqB,kBAAkB,kBAAkB,oBAAoB,EAAE,SAAS,CAAC,CAAC;AAC7G,MAAI,WAAW,SAAS,GAAG;AAGvB,UAAM,eAAe,WAAW,CAAC,EAAE,QAAQ,YAAY,EAAE,EAAE,YAAY;AAIvE,WAAO;AAAA,MACH,OAAO,gBAAgB;AAAA,MACvB,SAAS,gBAAgB;AAAA,MACzB,QAAQ,YAAY,WAAW,CAAC,CAAC,qHAAgH,YAAY;AAAA,MAC7J,UAAU;AAAA,IACd;AAAA,EACJ;AAIA,SAAO;AAAA,IACH,OAAO,gBAAgB;AAAA,IACvB,SAAS,gBAAgB;AAAA,IACzB,QAAQ;AAAA,IACR,UAAU;AAAA,EACd;AACJ;AAEA,SAAS,0BAA0B,UAA0C;AACzE,QAAM,QAAQ,UAAU,KAAK,OAAK,EAAE,SAAS,QAAQ;AACrD,MAAI,MAAO,QAAO,MAAM;AACxB,SAAO,gBAAgB;AAC3B;AAOO,SAAS,oBAA4B;AACxC,SAAO,iBAAiB,EAAE;AAC9B;AAOO,SAAS,yBAAiD;AAC7D,QAAM,OAAO,iBAAiB;AAC9B,SAAO;AAAA;AAAA;AAAA,IAGH,GAAG,gBAAgB;AAAA;AAAA;AAAA,IAGnB,GAAG,KAAK;AAAA,EACZ;AACJ;","names":[]}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -287,12 +287,28 @@ function simplifySchema(schema) {
|
|
|
287
287
|
if (schema.required) clean.required = schema.required;
|
|
288
288
|
return clean;
|
|
289
289
|
}
|
|
290
|
+
function normalizeOllamaBaseUrl(value) {
|
|
291
|
+
let raw = value.trim().replace(/\/+$/, "");
|
|
292
|
+
if (!/^https?:\/\//i.test(raw)) {
|
|
293
|
+
raw = `http://${raw}`;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const url = new URL(raw);
|
|
297
|
+
if (url.hostname === "0.0.0.0") {
|
|
298
|
+
url.hostname = "localhost";
|
|
299
|
+
}
|
|
300
|
+
return url.toString().replace(/\/+$/, "");
|
|
301
|
+
} catch {
|
|
302
|
+
return raw;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
290
305
|
class OllamaProvider extends LLMProvider {
|
|
291
306
|
name = "ollama";
|
|
292
307
|
displayName = "Ollama (Local)";
|
|
293
308
|
get baseUrl() {
|
|
294
309
|
const config = loadConfig();
|
|
295
|
-
|
|
310
|
+
const raw = config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
311
|
+
return normalizeOllamaBaseUrl(raw);
|
|
296
312
|
}
|
|
297
313
|
async chat(options) {
|
|
298
314
|
const model = (options.model || "llama3.1").replace("ollama/", "");
|