fix(ai): Ollama local-provider compatibility fixes (takeover of #1854)

Four of the five original fixes, rebased onto current master:

1. FREE_LOCAL_CHAT_PROVIDERS in budget-tracker: local chat providers
   (ollama, llama-server) price at $0 so --max-cost-bounded
   brainstorm/lsd no longer hard-fails on lookupPricing miss.
2. Numeric idea-id coercion in brainstorm validateIdeaShape
   (5 -> "Idea 05") for models that return numeric ids.
3. Ollama recipe gains chat + expansion touchpoints
   (llama3.3/qwen3/deepseek-r1, supports_tools, zero cost).
4. "no LLM available" graceful message now mentions chat_model /
   expansion_model config, not just anthropic_api_key.

The original PR's NO_ANTHROPIC_API_KEY scoping hunk is superseded:
master's probe-based warning (probeChatModel reason 'unavailable' is
anthropic-no-key by construction) already restricts the warning to the
anthropic provider; a comment now documents that. splitProviderModelId
import dropped accordingly.

Supersedes #1854.

Co-authored-by: StarMan <26191190+starm2010@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 16:50:49 -07:00
co-authored by StarMan Claude Fable 5
parent 0612b0daa8
commit 400cef0c26
4 changed files with 40 additions and 1 deletions
+14
View File
@@ -33,6 +33,20 @@ export const ollama: Recipe = {
// OLLAMA_NUM_PARALLEL config; no static cap to declare. v0.32 (#779).
no_batch_cap: true,
},
chat: {
models: ['llama3.3', 'qwen3', 'deepseek-r1'],
supports_tools: true,
supports_subagent_loop: false,
supports_prompt_cache: false,
cost_per_1m_input_usd: 0,
cost_per_1m_output_usd: 0,
price_last_verified: '2026-06-02',
},
expansion: {
models: ['llama3.3', 'qwen3', 'deepseek-r1'],
cost_per_1m_tokens_usd: 0,
price_last_verified: '2026-06-02',
},
},
setup_hint: 'Install Ollama from https://ollama.ai, then `ollama pull nomic-embed-text` and `ollama serve`.',
};
+5
View File
@@ -361,6 +361,11 @@ function isAxisScoreInRange(n: unknown): n is number {
function validateIdeaShape(raw: unknown): { id: string; scores: JudgeAxisScores; note: string } | null {
if (typeof raw !== 'object' || raw === null) return null;
const r = raw as Record<string, unknown>;
// Some models (e.g. Kimi K2) return numeric ids instead of string ids like "Idea 01".
// Coerce to string for compatibility.
if (typeof r.id === 'number') {
r.id = 'Idea ' + String(r.id).padStart(2, '0');
}
if (typeof r.id !== 'string') return null;
const note = typeof r.note === 'string' ? r.note : '';
const s = r.scores;
+17
View File
@@ -155,6 +155,20 @@ const FREE_LOCAL_EMBED_PROVIDERS: ReadonlySet<string> = new Set([
'llama-server',
]);
/**
* Provider id prefixes whose chat models run on local inference (electricity,
* not API tokens) and so price at $0. Without this, a `--max-cost`-bounded
* brainstorm/lsd configured for a local chat provider TX2 hard-fails because
* lookupPricing has no entry for them. Matched against the provider half
* of the `provider:model` string.
*
* Sibling to FREE_LOCAL_EMBED_PROVIDERS and FREE_LOCAL_RERANK_PROVIDERS.
*/
const FREE_LOCAL_CHAT_PROVIDERS: ReadonlySet<string> = new Set([
'ollama',
'llama-server',
]);
/**
* Look up `modelId` in the chat or embedding pricing maps. Returns a
* per-1M-token price tuple, or null when unknown.
@@ -201,6 +215,9 @@ function lookupPricing(modelId: string, kind: BudgetKind): ModelPricing | null {
if (kind === 'rerank' && providerId && FREE_LOCAL_RERANK_PROVIDERS.has(providerId)) {
return { input: 0, output: 0 };
}
if (kind === 'chat' && providerId && FREE_LOCAL_CHAT_PROVIDERS.has(providerId)) {
return { input: 0, output: 0 };
}
return null;
}
+4 -1
View File
@@ -460,6 +460,9 @@ export async function runThink(
// problems when the real cause was a model id the recipe didn't know
// (e.g. a tier-configured model newer than the recipe list). The re-probe
// is pure and cheap (no IO): same predicate tryBuildGatewayClient used.
// NO_ANTHROPIC_API_KEY only fires for the anthropic provider (probe reason
// 'unavailable' is anthropic-no-key by construction) — non-Anthropic
// providers (Ollama, OpenRouter) never see the false warning (#1854).
const probe = probeChatModel(normalizeModelId(modelUsed));
const modelProblem = !probe.ok && probe.reason !== 'unavailable';
warnings.push(
@@ -781,7 +784,7 @@ function buildGracefulMessage(modelStr: string): {
type: 'message',
role: 'assistant',
model: modelStr,
content: [{ type: 'text', text: '(no LLM available — set anthropic_api_key via gbrain config or ANTHROPIC_API_KEY env)' }],
content: [{ type: 'text', text: '(no LLM available — set chat_model, expansion_model, or anthropic_api_key via gbrain config)' }],
usage: { input_tokens: 0, output_tokens: 0 },
stop_reason: 'end_turn',
};