feat(connectors): add Ollama and LM Studio as a local auth kind

Both servers expose OpenAI-compatible /v1/chat/completions and /v1/models,
so we route them through pi-ai's `openai-completions` API the same way
Fireworks does — but with a per-installation base URL the user enters
during onboarding instead of a hardcoded one. The shared schemas grow a
`local` credential payload carrying just the baseUrl; the credential
resolver surfaces it on `ResolvedCredential.baseUrl`, the runtime bridge
forwards it through `prepareRun`, and the worker plumbs it into
`resolveModel` so the streamed Model<openai-completions> targets the
user's server. The local-cost path short-circuits to zero in cost-writer
since users bill their own hardware, and the live model list is fetched
from the saved baseUrl by `fetchLocalConnectorModels` (Ollama and LM
Studio both speak the standard OpenAI catalog shape). Onboarding ships
a new LocalProviderCard and the Settings panel a LocalForm; both default
to the catalog's per-connector localhost URL and accept any custom URL.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-14 14:55:38 -04:00
1 parent 07d9c00fab
commit a532120649
26 files changed
+504 -18

No files matched your search

+19 -1
View File
@@ -1,4 +1,4 @@
import { calculateCost, type Usage, type Api, type Model } from "@mariozechner/pi-ai";
import { calculateCost, type Usage } from "@mariozechner/pi-ai";
import type { AppDatabase } from "@main/database";
import { getConnectorDefinition } from "@main/connectors";
import { resolveModel, resolveModelCostFallback } from "@main/model-resolver";
@@ -17,6 +17,12 @@ interface ObserveInput {
usage: Usage;
}
// Local OpenAI-compatible servers run on the user's own machine, so the
// LLM call has no third-party billing line. Record a zero cost entry
// without invoking pi-ai's calculator (which would require the model's
// baseUrl from the credential — unnecessary work for a flat-zero cost).
const ZERO_COST_PROVIDERS: ReadonlySet<string> = new Set(["ollama", "lm-studio"]);
export class CostWriter {
constructor(private readonly deps: CostWriterDeps) {}
@@ -25,6 +31,18 @@ export class CostWriter {
if (!definition.provider) {
throw new Error(`[cost-writer] connector ${input.connectorId} has no provider mapping`);
}
if (ZERO_COST_PROVIDERS.has(definition.provider)) {
this.deps.db.core.runs.recordCostEntry({
runId: input.runId,
companyId: input.companyId,
agentId: input.agentId,
taskId: input.taskId,
connectorId: input.connectorId,
amountUsd: 0,
unattributed: false,
});
return;
}
const model = resolveModel(definition.provider, input.modelId);
if (!model) {
throw new Error(`[cost-writer] model ${input.modelId} not found for provider ${definition.provider}`);
+1
View File
@@ -440,6 +440,7 @@ export class RuntimeBridge {
provider: definition.provider,
apiKey: resolved.apiKey,
extraEnv: resolved.extraEnv ?? {},
baseUrl: resolved.baseUrl ?? null,
priorMessages,
},
};