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(
|
var modelAliases = Object.entries(providers).flatMap(
|
||||||
([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
|
([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
|
||||||
slug: `${providerKey}/${modelId}`,
|
slug: `${providerKey}/${modelId}`,
|
||||||
@@ -149212,10 +149231,18 @@ to fix this, add the required secret to your GitHub repository:
|
|||||||
|
|
||||||
configure your model at ${settingsUrl}`;
|
configure your model at ${settingsUrl}`;
|
||||||
}
|
}
|
||||||
|
function hasEnvVar(name) {
|
||||||
|
const value2 = process.env[name];
|
||||||
|
return typeof value2 === "string" && value2.length > 0;
|
||||||
|
}
|
||||||
function validateAgentApiKey(params) {
|
function validateAgentApiKey(params) {
|
||||||
const hasAnyKey = Object.entries(process.env).some(
|
if (params.model) {
|
||||||
([key, value2]) => value2 && typeof value2 === "string" && knownApiKeys.has(key)
|
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) {
|
if (!hasAnyKey) {
|
||||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||||
}
|
}
|
||||||
@@ -149805,7 +149832,7 @@ import { isAbsolute, resolve } from "node:path";
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.180",
|
version: "0.0.181",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -150374,6 +150401,7 @@ async function main() {
|
|||||||
const agent2 = resolveAgent();
|
const agent2 = resolveAgent();
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent: agent2,
|
agent: agent2,
|
||||||
|
model: payload.model,
|
||||||
owner: runContext.repo.owner,
|
owner: runContext.repo.owner,
|
||||||
name: runContext.repo.name
|
name: runContext.repo.name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export async function main(): Promise<MainResult> {
|
|||||||
|
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent,
|
agent,
|
||||||
|
model: payload.model,
|
||||||
owner: runContext.repo.owner,
|
owner: runContext.repo.owner,
|
||||||
name: runContext.repo.name,
|
name: runContext.repo.name,
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.180",
|
"version": "0.0.181",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -41284,7 +41284,7 @@ var core3 = __toESM(require_core(), 1);
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.180",
|
version: "0.0.181",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"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";
|
import { getApiUrl } from "./apiUrl.ts";
|
||||||
|
|
||||||
const knownApiKeys: Set<string> = new Set(Object.values(providers).flatMap((p) => [...p.envVars]));
|
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}`;
|
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: {
|
export function validateAgentApiKey(params: {
|
||||||
agent: { name: string };
|
agent: { name: string };
|
||||||
|
model: string | undefined;
|
||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
}): void {
|
}): void {
|
||||||
const hasAnyKey = Object.entries(process.env).some(
|
// if a specific model is configured, only check that model's required env vars
|
||||||
([key, value]) => value && typeof value === "string" && knownApiKeys.has(key)
|
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) {
|
if (!hasAnyKey) {
|
||||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user