fix: skip API key validation for free opencode models
free models (big-pickle, gpt-5-nano, etc.) define envVars: [] and isFree: true but validateAgentApiKey always required at least one provider key. now the validation is model-aware: free models bypass the check, keyed models validate their specific vars, and auto-select still requires at least one known key. closes #483 Made-with: Cursor
This commit is contained in:
committed by
pullfrog[bot]
parent
8a734c32f4
commit
30d68e53a7
@@ -132065,6 +132065,25 @@ var providers = {
|
||||
}
|
||||
})
|
||||
};
|
||||
function parseModel(slug) {
|
||||
const slashIdx = slug.indexOf("/");
|
||||
if (slashIdx === -1) {
|
||||
throw new Error(`invalid model slug "${slug}" \u2014 expected "provider/model"`);
|
||||
}
|
||||
return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
|
||||
}
|
||||
function getModelEnvVars(slug) {
|
||||
const parsed2 = parseModel(slug);
|
||||
const providerConfig = providers[parsed2.provider];
|
||||
if (!providerConfig) {
|
||||
return [];
|
||||
}
|
||||
const modelConfig = providerConfig.models[parsed2.model];
|
||||
if (modelConfig?.envVars) {
|
||||
return modelConfig.envVars.slice();
|
||||
}
|
||||
return providerConfig.envVars.slice();
|
||||
}
|
||||
var modelAliases = Object.entries(providers).flatMap(
|
||||
([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
|
||||
slug: `${providerKey}/${modelId}`,
|
||||
@@ -149212,10 +149231,18 @@ to fix this, add the required secret to your GitHub repository:
|
||||
|
||||
configure your model at ${settingsUrl}`;
|
||||
}
|
||||
function hasEnvVar(name) {
|
||||
const value2 = process.env[name];
|
||||
return typeof value2 === "string" && value2.length > 0;
|
||||
}
|
||||
function validateAgentApiKey(params) {
|
||||
const hasAnyKey = Object.entries(process.env).some(
|
||||
([key, value2]) => value2 && typeof value2 === "string" && knownApiKeys.has(key)
|
||||
);
|
||||
if (params.model) {
|
||||
const requiredVars = getModelEnvVars(params.model);
|
||||
if (requiredVars.length === 0) return;
|
||||
if (requiredVars.some((v) => hasEnvVar(v))) return;
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
const hasAnyKey = [...knownApiKeys].some((k) => hasEnvVar(k));
|
||||
if (!hasAnyKey) {
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
@@ -149805,7 +149832,7 @@ import { isAbsolute, resolve } from "node:path";
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.180",
|
||||
version: "0.0.181",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -150374,6 +150401,7 @@ async function main() {
|
||||
const agent2 = resolveAgent();
|
||||
validateAgentApiKey({
|
||||
agent: agent2,
|
||||
model: payload.model,
|
||||
owner: runContext.repo.owner,
|
||||
name: runContext.repo.name
|
||||
});
|
||||
|
||||
@@ -151,6 +151,7 @@ export async function main(): Promise<MainResult> {
|
||||
|
||||
validateAgentApiKey({
|
||||
agent,
|
||||
model: payload.model,
|
||||
owner: runContext.repo.owner,
|
||||
name: runContext.repo.name,
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.180",
|
||||
"version": "0.0.181",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -41284,7 +41284,7 @@ var core3 = __toESM(require_core(), 1);
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.180",
|
||||
version: "0.0.181",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { validateAgentApiKey } from "./apiKeys.ts";
|
||||
|
||||
const base = {
|
||||
agent: { name: "opentoad" },
|
||||
owner: "test-owner",
|
||||
name: "test-repo",
|
||||
};
|
||||
|
||||
const savedEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// strip all known provider keys so tests start clean
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (key.endsWith("_API_KEY")) delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...savedEnv };
|
||||
});
|
||||
|
||||
describe("validateAgentApiKey", () => {
|
||||
describe("free model (no keys required)", () => {
|
||||
it("passes with zero env keys", () => {
|
||||
expect(() => validateAgentApiKey({ ...base, model: "opencode/big-pickle" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("passes for other free opencode models", () => {
|
||||
for (const slug of [
|
||||
"opencode/gpt-5-nano",
|
||||
"opencode/mimo-v2-flash-free",
|
||||
"opencode/minimax-m2.5-free",
|
||||
"opencode/nemotron-3-super-free",
|
||||
]) {
|
||||
expect(() => validateAgentApiKey({ ...base, model: slug })).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyed model", () => {
|
||||
it("passes when the required key is present", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "sk-test";
|
||||
expect(() => validateAgentApiKey({ ...base, model: "anthropic/claude-opus" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws when the required key is missing", () => {
|
||||
expect(() => validateAgentApiKey({ ...base, model: "anthropic/claude-opus" })).toThrow(
|
||||
"no API key found"
|
||||
);
|
||||
});
|
||||
|
||||
it("passes for opencode keyed model with OPENCODE_API_KEY", () => {
|
||||
process.env.OPENCODE_API_KEY = "sk-test";
|
||||
expect(() => validateAgentApiKey({ ...base, model: "opencode/claude-opus" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws for opencode keyed model without OPENCODE_API_KEY", () => {
|
||||
expect(() => validateAgentApiKey({ ...base, model: "opencode/claude-opus" })).toThrow(
|
||||
"no API key found"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no model (auto-select)", () => {
|
||||
it("passes when any known provider key is present", () => {
|
||||
process.env.OPENAI_API_KEY = "sk-test";
|
||||
expect(() => validateAgentApiKey({ ...base, model: undefined })).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws when no provider keys are present", () => {
|
||||
expect(() => validateAgentApiKey({ ...base, model: undefined })).toThrow("no API key found");
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-4
@@ -1,4 +1,4 @@
|
||||
import { providers } from "../models.ts";
|
||||
import { getModelEnvVars, providers } from "../models.ts";
|
||||
import { getApiUrl } from "./apiUrl.ts";
|
||||
|
||||
const knownApiKeys: Set<string> = new Set(Object.values(providers).flatMap((p) => [...p.envVars]));
|
||||
@@ -23,15 +23,29 @@ to fix this, add the required secret to your GitHub repository:
|
||||
configure your model at ${settingsUrl}`;
|
||||
}
|
||||
|
||||
function hasEnvVar(name: string): boolean {
|
||||
const value = process.env[name];
|
||||
return typeof value === "string" && value.length > 0;
|
||||
}
|
||||
|
||||
export function validateAgentApiKey(params: {
|
||||
agent: { name: string };
|
||||
model: string | undefined;
|
||||
owner: string;
|
||||
name: string;
|
||||
}): void {
|
||||
const hasAnyKey = Object.entries(process.env).some(
|
||||
([key, value]) => value && typeof value === "string" && knownApiKeys.has(key)
|
||||
);
|
||||
// if a specific model is configured, only check that model's required env vars
|
||||
if (params.model) {
|
||||
const requiredVars = getModelEnvVars(params.model);
|
||||
// free models have no required env vars — skip validation entirely
|
||||
if (requiredVars.length === 0) return;
|
||||
if (requiredVars.some((v) => hasEnvVar(v))) return;
|
||||
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
|
||||
// no model configured — auto-select requires at least one known provider key
|
||||
const hasAnyKey = [...knownApiKeys].some((k) => hasEnvVar(k));
|
||||
if (!hasAnyKey) {
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user