fix(connectors): guard pi provider resolution

This commit is contained in:
ZacharyZhang-NY committed 2026-04-22 18:58:30 -04:00
1 parent 0d9f4f15eb
commit 7793f2da35
6 files changed
+107 -17

No files matched your search

+5 -1
View File
@@ -23,6 +23,7 @@
* - `connector-definitions/cloud.ts` — 3 Cloud providers
*/
import type { KnownProvider } from "@mariozechner/pi-ai";
import type { ConnectorId } from "@shared/types";
import { OAUTH_CONNECTORS, API_KEY_CONNECTORS, CLOUD_CONNECTORS } from "./connector-definitions/index";
@@ -67,11 +68,14 @@ export type ConnectorAuth =
| ConnectorAuthAzure
| ConnectorAuthGcpAdc;
export type LocalModelResolverProvider = "fireworks" | "fireworks-firepass";
export type ConnectorProvider = KnownProvider | LocalModelResolverProvider;
export interface ConnectorDefinition {
/** Stable AgentCompany id; the `connectors` table primary key. */
id: ConnectorId;
/** pi-ai `KnownProvider` id (or a locally-resolved provider for Fireworks). */
provider: string;
provider: ConnectorProvider;
/** Human-readable label for the Settings panel and onboarding rail. */
label: string;
/** One-line explanation shown under the label. */
+2 -2
View File
@@ -1,6 +1,6 @@
import { getEnvApiKey } from "@mariozechner/pi-ai";
import type { AppDatabase } from "./database";
import { getConnectorDefinition, parseEnvBindingText } from "./connectors";
import { getConnectorDefinition, isPiKnownProvider, parseEnvBindingText } from "./connectors";
import type { ConnectorId } from "@shared/types";
import type { CredentialStore } from "./credentials";
@@ -51,7 +51,7 @@ export function createConnectorHealthService({
if (stored?.kind === "api_key") {
return { status: "ready", authState: "ok", lastError: null };
}
if (definition.provider && getEnvApiKey(definition.provider as never)) {
if (isPiKnownProvider(definition.provider) && getEnvApiKey(definition.provider)) {
return { status: "ready", authState: "ok", lastError: null };
}
// Re-parse envBindingText on every probe: it is user-editable and must
+27 -3
View File
@@ -1,8 +1,9 @@
import type { CapabilityMatrix, ConnectorRecord } from "@shared/types";
import { CONNECTORS, type ConnectorDefinition } from "./connector-definitions";
import { getProviders, type KnownProvider } from "@mariozechner/pi-ai";
import type { CapabilityMatrix, ConnectorId, ConnectorRecord } from "@shared/types";
import { CONNECTORS, type ConnectorDefinition, type ConnectorProvider, type LocalModelResolverProvider } from "./connector-definitions";
import { buildApiKeyDoc, buildCloudDoc, buildOAuthDoc } from "./connector-definitions-docs";
export { CONNECTORS } from "./connector-definitions";
export type { ConnectorDefinition } from "./connector-definitions";
export type { ConnectorDefinition, ConnectorProvider, LocalModelResolverProvider } from "./connector-definitions";
export { findConnectorDefinition, listFeaturedOnboardingConnectors } from "./connector-definitions-lookup";
const RESERVED_ENV_BINDING_KEYS = new Set([
@@ -17,6 +18,12 @@ const RESERVED_ENV_BINDING_KEYS = new Set([
"PWD",
]);
const LOCAL_MODEL_RESOLVER_PROVIDERS: ReadonlySet<LocalModelResolverProvider> = new Set([
"fireworks",
"fireworks-firepass",
]);
const PI_KNOWN_PROVIDER_IDS: ReadonlySet<string> = new Set(getProviders());
/**
* Pi-mono drives all provider dispatch through pi-ai, so the adapter-era
* capability matrix now collapses to a uniform baseline. These values are
@@ -39,6 +46,23 @@ export function getConnectorDefinition(id: string) {
return definition;
}
export function isPiKnownProvider(provider: string): provider is KnownProvider {
return PI_KNOWN_PROVIDER_IDS.has(provider);
}
export function isLocalModelResolverProvider(provider: string): provider is LocalModelResolverProvider {
return LOCAL_MODEL_RESOLVER_PROVIDERS.has(provider as LocalModelResolverProvider);
}
export function getConnectorProvider(id: ConnectorId): ConnectorProvider {
return getConnectorDefinition(id).provider;
}
export function getConnectorKnownProvider(id: ConnectorId): KnownProvider | null {
const provider = getConnectorProvider(id);
return isPiKnownProvider(provider) ? provider : null;
}
export function parseEnvBindingText(text: string): Record<string, string> {
const bindings: Record<string, string> = {};
for (const rawLine of text.split(/\r?\n/)) {
+5 -8
View File
@@ -1,6 +1,6 @@
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
import { getEnvApiKey } from "@mariozechner/pi-ai";
import { getConnectorDefinition } from "@main/connectors";
import { getConnectorDefinition, isPiKnownProvider } from "@main/connectors";
import { CredentialStore } from "./credential-store";
import type { OAuthCredentialPayload, StoredCredentialPayload } from "./types";
@@ -72,14 +72,11 @@ export class CredentialResolver {
if (!fromEnv) throw new CredentialError("CREDENTIAL_MISSING", connectorId, `Env var ${stored.value.value} is not set.`);
return { apiKey: fromEnv };
}
if (!definition.provider) {
throw new CredentialError("CREDENTIAL_MISSING", connectorId, `No credential or env API key for ${connectorId}.`);
if (isPiKnownProvider(definition.provider)) {
const envKey = getEnvApiKey(definition.provider);
if (envKey) return { apiKey: envKey };
}
const envKey = getEnvApiKey(definition.provider as never);
if (!envKey) {
throw new CredentialError("CREDENTIAL_MISSING", connectorId, `No credential or env API key for ${connectorId}.`);
}
return { apiKey: envKey };
throw new CredentialError("CREDENTIAL_MISSING", connectorId, `No credential or env API key for ${connectorId}.`);
}
case "azure": {
if (stored?.kind !== "azure") {
+12 -3
View File
@@ -17,7 +17,8 @@
*/
import log from "electron-log/main.js";
import { getModel, getModels, type Api, type Model } from "@mariozechner/pi-ai";
import { getModels, type Api, type Model } from "@mariozechner/pi-ai";
import { isPiKnownProvider } from "./connectors";
// Fireworks' OpenAI-compatible root. The OpenAI SDK appends
// `/chat/completions`, `/models`, etc. to this baseURL.
@@ -67,8 +68,12 @@ export function resolveModel(provider: string, modelId: string): Model<Api> | nu
const curated = FIREWORKS_CURATED_MODELS.find((entry) => entry.id === modelId);
return buildFireworksModel(modelId, curated?.name, curated?.contextWindow);
}
if (!isPiKnownProvider(provider)) {
log.warn(`[model-resolver] Unknown pi provider: ${provider}`);
return null;
}
try {
return getModel(provider as never, modelId as never) ?? null;
return getModels(provider).find((model) => model.id === modelId) ?? null;
} catch (err) {
log.warn(`[model-resolver] getModel failed for ${provider}/${modelId}:`, err instanceof Error ? err.message : String(err));
return null;
@@ -82,8 +87,12 @@ export function resolveModels(provider: string): Array<{ id: string; name: strin
if (provider === "fireworks") {
return FIREWORKS_CURATED_MODELS.map(({ id, name }) => ({ id, name }));
}
if (!isPiKnownProvider(provider)) {
log.warn(`[model-resolver] Unknown pi provider: ${provider}`);
return [];
}
try {
const models = getModels(provider as never) ?? [];
const models = getModels(provider);
return models.map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? model.id }));
} catch (err) {
log.warn(`[model-resolver] getModels failed for ${provider}:`, err instanceof Error ? err.message : String(err));
@@ -0,0 +1,56 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { getProviders } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import {
CONNECTORS,
getConnectorKnownProvider,
getConnectorProvider,
isLocalModelResolverProvider,
isPiKnownProvider,
} from "../../src/main/connectors";
import { FIREWORKS_FIREPASS_MODEL_ID, resolveModel, resolveModels } from "../../src/main/model-resolver";
describe("connector provider guard", () => {
it("classifies catalog providers before they enter pi-ai APIs", () => {
const piProviders = new Set(getProviders());
for (const connector of CONNECTORS) {
const provider = getConnectorProvider(connector.id);
expect(provider).toBe(connector.provider);
if (isLocalModelResolverProvider(provider)) {
expect(getConnectorKnownProvider(connector.id)).toBeNull();
} else {
expect(isPiKnownProvider(provider)).toBe(true);
expect(piProviders.has(provider)).toBe(true);
expect(getConnectorKnownProvider(connector.id)).toBe(provider);
}
}
});
it("uses the provider guard in model discovery", () => {
const anthropicModels = resolveModels("anthropic");
const firstAnthropicModel = anthropicModels[0];
if (!firstAnthropicModel) throw new Error("Anthropic model catalog is empty.");
expect(resolveModel("anthropic", firstAnthropicModel.id)?.id).toBe(firstAnthropicModel.id);
expect(resolveModels("not-a-pi-provider")).toEqual([]);
expect(resolveModel("not-a-pi-provider", "model-id")).toBeNull();
expect(resolveModel("fireworks-firepass", "ignored")?.id).toBe(FIREWORKS_FIREPASS_MODEL_ID);
});
it("keeps provider calls free of erased never casts", () => {
const files = [
"src/main/model-resolver.ts",
"src/main/connector-health-service.ts",
"src/main/credentials/credential-resolver.ts",
];
for (const file of files) {
const source = readFileSync(join(process.cwd(), file), "utf8");
expect(source).not.toContain("provider as never");
expect(source).not.toContain("definition.provider as never");
}
});
});