fix(connectors): guard pi provider resolution
This commit is contained in:
1 parent
0d9f4f15eb
commit
7793f2da35
6 files changed
+107
-17
No files matched your search
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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/)) {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user