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:
1 parent
07d9c00fab
commit
a532120649
26 files changed
+504
-18
No files matched your search
@@ -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}`);
|
||||
|
||||
@@ -440,6 +440,7 @@ export class RuntimeBridge {
|
||||
provider: definition.provider,
|
||||
apiKey: resolved.apiKey,
|
||||
extraEnv: resolved.extraEnv ?? {},
|
||||
baseUrl: resolved.baseUrl ?? null,
|
||||
priorMessages,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user