pullfrog 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/opencodeShared.d.ts +25 -0
- package/dist/cli.mjs +188 -21
- package/dist/index.js +185 -18
- package/dist/internal.js +22 -0
- package/dist/models.d.ts +27 -1
- package/dist/utils/apiKeys.d.ts +10 -0
- package/dist/utils/runContext.d.ts +7 -0
- package/dist/utils/runContextData.d.ts +3 -0
- package/package.json +1 -1
|
@@ -16,6 +16,30 @@ export type OpenCodeConfig = {
|
|
|
16
16
|
export declare function geminiHighThinkingOverrides(): Record<string, {
|
|
17
17
|
options: object;
|
|
18
18
|
}>;
|
|
19
|
+
interface OpenAICompatibleProviderEntry {
|
|
20
|
+
npm: string;
|
|
21
|
+
name: string;
|
|
22
|
+
options: {
|
|
23
|
+
baseURL: string | undefined;
|
|
24
|
+
apiKey: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
models: Record<string, {
|
|
27
|
+
name: string;
|
|
28
|
+
limit: ModelLimit;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
interface ModelLimit {
|
|
32
|
+
context: number;
|
|
33
|
+
output: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* OpenAI-compatible provider block for OPENCODE_CONFIG_CONTENT. opencode has no
|
|
37
|
+
* native provider for an arbitrary gateway, so inject one via
|
|
38
|
+
* `@ai-sdk/openai-compatible` when the run targets `openai-compatible/<model>`;
|
|
39
|
+
* `{}` otherwise so the caller can spread it unconditionally. base URL + key are
|
|
40
|
+
* guaranteed present by `validateOpenAICompatibleSetup`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function openAICompatibleProvider(model: string | undefined): Record<string, OpenAICompatibleProviderEntry>;
|
|
19
43
|
/**
|
|
20
44
|
* Build the `provider.openrouter.models[id].options` map that pins every Kimi
|
|
21
45
|
* K2 OpenRouter alias away from the Enforcer-less providers via OpenRouter's
|
|
@@ -75,3 +99,4 @@ export declare function installOpencodeCli(params: {
|
|
|
75
99
|
binPath: string;
|
|
76
100
|
}): Promise<string>;
|
|
77
101
|
export declare function autoSelectModel(): string | undefined;
|
|
102
|
+
export {};
|
package/dist/cli.mjs
CHANGED
|
@@ -102119,6 +102119,28 @@ var providers = {
|
|
|
102119
102119
|
}
|
|
102120
102120
|
}
|
|
102121
102121
|
}),
|
|
102122
|
+
"openai-compatible": provider({
|
|
102123
|
+
// "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
|
|
102124
|
+
// menu reads `Custom › OpenAI-compatible` and a second custom backend (a
|
|
102125
|
+
// different wire format, say) slots in beside it without a rename. the
|
|
102126
|
+
// provider KEY stays `openai-compatible` — it's the stored slug and the
|
|
102127
|
+
// `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
|
|
102128
|
+
displayName: "Custom",
|
|
102129
|
+
// bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
|
|
102130
|
+
// Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
|
|
102131
|
+
// key + model ID are all supplied via env; nothing is cataloged or bumped.
|
|
102132
|
+
envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
|
|
102133
|
+
models: {
|
|
102134
|
+
// single routing entry — the actual model ID is read from
|
|
102135
|
+
// OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
|
|
102136
|
+
// via `@ai-sdk/openai-compatible`.
|
|
102137
|
+
byok: {
|
|
102138
|
+
displayName: "OpenAI-compatible",
|
|
102139
|
+
resolve: "openai-compatible",
|
|
102140
|
+
routing: "openai-compatible"
|
|
102141
|
+
}
|
|
102142
|
+
}
|
|
102143
|
+
}),
|
|
102122
102144
|
openrouter: provider({
|
|
102123
102145
|
displayName: "OpenRouter",
|
|
102124
102146
|
envVars: ["OPENROUTER_API_KEY"],
|
|
@@ -102336,6 +102358,12 @@ var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
|
|
|
102336
102358
|
var defaultProxyDisplayName = defaultProxyAlias.displayName;
|
|
102337
102359
|
var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
|
102338
102360
|
var VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
|
|
102361
|
+
var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
|
|
102362
|
+
var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
|
|
102363
|
+
var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
|
|
102364
|
+
var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
|
|
102365
|
+
var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
|
|
102366
|
+
var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
|
|
102339
102367
|
function isBedrockAnthropicId(bedrockModelId) {
|
|
102340
102368
|
return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
|
|
102341
102369
|
}
|
|
@@ -103022,7 +103050,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
103022
103050
|
// package.json
|
|
103023
103051
|
var package_default = {
|
|
103024
103052
|
name: "pullfrog",
|
|
103025
|
-
version: "0.1.
|
|
103053
|
+
version: "0.1.46",
|
|
103026
103054
|
type: "module",
|
|
103027
103055
|
bin: {
|
|
103028
103056
|
pullfrog: "dist/cli.mjs",
|
|
@@ -110769,8 +110797,9 @@ function readModels(cliPath) {
|
|
|
110769
110797
|
stdio: ["ignore", "pipe", "pipe"]
|
|
110770
110798
|
});
|
|
110771
110799
|
if (result.status !== 0) {
|
|
110772
|
-
|
|
110773
|
-
|
|
110800
|
+
const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
|
|
110801
|
+
failure = stderr || result.error?.message;
|
|
110802
|
+
log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
|
|
110774
110803
|
return /* @__PURE__ */ new Set();
|
|
110775
110804
|
}
|
|
110776
110805
|
failure = void 0;
|
|
@@ -110828,6 +110857,29 @@ function geminiHighThinkingOverrides() {
|
|
|
110828
110857
|
])
|
|
110829
110858
|
);
|
|
110830
110859
|
}
|
|
110860
|
+
function openAICompatibleLimit() {
|
|
110861
|
+
return {
|
|
110862
|
+
context: Number(process.env[OPENAI_COMPATIBLE_CONTEXT_ENV]),
|
|
110863
|
+
output: Number(process.env[OPENAI_COMPATIBLE_MAX_OUTPUT_ENV])
|
|
110864
|
+
};
|
|
110865
|
+
}
|
|
110866
|
+
function openAICompatibleProvider(model) {
|
|
110867
|
+
const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
|
|
110868
|
+
if (!model?.startsWith(prefix)) return {};
|
|
110869
|
+
const modelId = model.slice(prefix.length);
|
|
110870
|
+
return {
|
|
110871
|
+
[OPENAI_COMPATIBLE_PROVIDER]: {
|
|
110872
|
+
npm: "@ai-sdk/openai-compatible",
|
|
110873
|
+
name: "OpenAI-compatible",
|
|
110874
|
+
options: {
|
|
110875
|
+
// trailing slash would double up against opencode's `/chat/completions` join
|
|
110876
|
+
baseURL: process.env[OPENAI_COMPATIBLE_BASE_URL_ENV]?.replace(/\/$/, ""),
|
|
110877
|
+
apiKey: process.env[OPENAI_COMPATIBLE_API_KEY_ENV]
|
|
110878
|
+
},
|
|
110879
|
+
models: { [modelId]: { name: modelId, limit: openAICompatibleLimit() } }
|
|
110880
|
+
}
|
|
110881
|
+
};
|
|
110882
|
+
}
|
|
110831
110883
|
var KIMI_ENFORCERLESS_PROVIDERS = ["siliconflow", "together"];
|
|
110832
110884
|
function kimiOpenRouterProviderOverrides() {
|
|
110833
110885
|
return Object.fromEntries(
|
|
@@ -110919,7 +110971,8 @@ function buildSecurityConfig(ctx, model) {
|
|
|
110919
110971
|
google: { models: geminiHighThinkingOverrides() },
|
|
110920
110972
|
openrouter: {
|
|
110921
110973
|
models: { ...deepseekHighEffortOverrides(), ...kimiOpenRouterProviderOverrides() }
|
|
110922
|
-
}
|
|
110974
|
+
},
|
|
110975
|
+
...openAICompatibleProvider(model)
|
|
110923
110976
|
}
|
|
110924
110977
|
};
|
|
110925
110978
|
if (model) {
|
|
@@ -165185,6 +165238,15 @@ function resolveSlug(slug2) {
|
|
|
165185
165238
|
}
|
|
165186
165239
|
return vertexId;
|
|
165187
165240
|
}
|
|
165241
|
+
if (alias?.routing === "openai-compatible") {
|
|
165242
|
+
const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
|
|
165243
|
+
if (!modelId) {
|
|
165244
|
+
throw new Error(
|
|
165245
|
+
`${OPENAI_COMPATIBLE_MODEL_ENV} env var is required when the model is set to "${slug2}". set it to the model ID served by your OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway or DashScope model). see https://docs.pullfrog.com/openai-compatible for setup.`
|
|
165246
|
+
);
|
|
165247
|
+
}
|
|
165248
|
+
return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
|
|
165249
|
+
}
|
|
165188
165250
|
return resolveCliModel(slug2);
|
|
165189
165251
|
}
|
|
165190
165252
|
function resolveModel(ctx) {
|
|
@@ -165234,6 +165296,21 @@ function resolveAgent(ctx) {
|
|
|
165234
165296
|
|
|
165235
165297
|
// utils/apiKeys.ts
|
|
165236
165298
|
var MISSING_KEY_MARKER = "no API key found";
|
|
165299
|
+
var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
165300
|
+
function buildKeyError(params) {
|
|
165301
|
+
return params.secretsUnavailable ? buildSecretsUnavailableError(params) : buildMissingApiKeyError(params);
|
|
165302
|
+
}
|
|
165303
|
+
function buildSecretsUnavailableError(params) {
|
|
165304
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
165305
|
+
const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
|
|
165306
|
+
return [
|
|
165307
|
+
`**Pullfrog ${SECRETS_UNAVAILABLE_MARKER}${modelClause} on this run.** The key is still stored \u2014 the runner just couldn't fetch it, so this is a transient failure on our side, not a missing key.`,
|
|
165308
|
+
"",
|
|
165309
|
+
"**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
|
|
165310
|
+
"",
|
|
165311
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
165312
|
+
].join("\n");
|
|
165313
|
+
}
|
|
165237
165314
|
function buildMissingApiKeyError(params) {
|
|
165238
165315
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
165239
165316
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
@@ -165276,10 +165353,47 @@ add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then
|
|
|
165276
165353
|
|
|
165277
165354
|
for full setup instructions, see https://docs.pullfrog.com/vertex`;
|
|
165278
165355
|
}
|
|
165356
|
+
function buildOpenAICompatibleSetupError(params) {
|
|
165357
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
165358
|
+
return `OpenAI-compatible model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
165359
|
+
|
|
165360
|
+
only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
|
|
165361
|
+
|
|
165362
|
+
${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
|
|
165363
|
+
${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
|
|
165364
|
+
${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
|
|
165365
|
+
${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
|
|
165366
|
+
${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
|
|
165367
|
+
|
|
165368
|
+
set the last two to the real limits of the model your endpoint serves. Pullfrog can't
|
|
165369
|
+
discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
|
|
165370
|
+
capped at 32000 tokens (rejected outright by models with a smaller cap) and
|
|
165371
|
+
auto-compaction is disabled, so long runs grow until your endpoint refuses them.
|
|
165372
|
+
|
|
165373
|
+
for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
|
|
165374
|
+
}
|
|
165279
165375
|
function hasEnvVar3(name) {
|
|
165280
165376
|
const value2 = process.env[name];
|
|
165281
165377
|
return typeof value2 === "string" && value2.length > 0;
|
|
165282
165378
|
}
|
|
165379
|
+
function hasPositiveNumberEnvVar(name) {
|
|
165380
|
+
return Number(process.env[name]) > 0;
|
|
165381
|
+
}
|
|
165382
|
+
function validateOpenAICompatibleSetup(params) {
|
|
165383
|
+
const missing = [];
|
|
165384
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
|
|
165385
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
|
|
165386
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
|
|
165387
|
+
if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
|
|
165388
|
+
missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
|
|
165389
|
+
if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
|
|
165390
|
+
missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
|
|
165391
|
+
if (missing.length > 0) {
|
|
165392
|
+
throw new Error(
|
|
165393
|
+
buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
|
|
165394
|
+
);
|
|
165395
|
+
}
|
|
165396
|
+
}
|
|
165283
165397
|
function validateBedrockSetup(params) {
|
|
165284
165398
|
const hasAuth = hasEnvVar3("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar3("AWS_ACCESS_KEY_ID") && hasEnvVar3("AWS_SECRET_ACCESS_KEY");
|
|
165285
165399
|
const missing = [];
|
|
@@ -165314,6 +165428,10 @@ function validateAgentApiKey(params) {
|
|
|
165314
165428
|
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
165315
165429
|
return;
|
|
165316
165430
|
}
|
|
165431
|
+
if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
|
|
165432
|
+
validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
|
|
165433
|
+
return;
|
|
165434
|
+
}
|
|
165317
165435
|
if (!params.model.includes("/")) {
|
|
165318
165436
|
if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
|
|
165319
165437
|
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
@@ -165326,23 +165444,46 @@ function validateAgentApiKey(params) {
|
|
|
165326
165444
|
if (params.authorized.has(params.model)) return;
|
|
165327
165445
|
const reason = getModelsFailure();
|
|
165328
165446
|
if (reason) throw new Error(reason);
|
|
165447
|
+
if (getModelEnvVars(params.model).some(hasEnvVar3)) return;
|
|
165329
165448
|
throw new Error(
|
|
165330
|
-
|
|
165449
|
+
buildKeyError({
|
|
165450
|
+
owner: params.owner,
|
|
165451
|
+
name: params.name,
|
|
165452
|
+
model: params.model,
|
|
165453
|
+
secretsUnavailable: params.secretsUnavailable
|
|
165454
|
+
})
|
|
165331
165455
|
);
|
|
165332
165456
|
}
|
|
165333
165457
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
165334
165458
|
throw new Error(
|
|
165335
|
-
|
|
165459
|
+
buildKeyError({
|
|
165460
|
+
owner: params.owner,
|
|
165461
|
+
name: params.name,
|
|
165462
|
+
model: params.model,
|
|
165463
|
+
secretsUnavailable: params.secretsUnavailable
|
|
165464
|
+
})
|
|
165336
165465
|
);
|
|
165337
165466
|
}
|
|
165338
165467
|
if (params.agent.name === "opencode") {
|
|
165339
165468
|
if (params.authorized.size > 0) return;
|
|
165340
165469
|
const reason = getModelsFailure();
|
|
165341
165470
|
if (reason) throw new Error(reason);
|
|
165342
|
-
throw new Error(
|
|
165471
|
+
throw new Error(
|
|
165472
|
+
buildKeyError({
|
|
165473
|
+
owner: params.owner,
|
|
165474
|
+
name: params.name,
|
|
165475
|
+
secretsUnavailable: params.secretsUnavailable
|
|
165476
|
+
})
|
|
165477
|
+
);
|
|
165343
165478
|
}
|
|
165344
165479
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
165345
|
-
throw new Error(
|
|
165480
|
+
throw new Error(
|
|
165481
|
+
buildKeyError({
|
|
165482
|
+
owner: params.owner,
|
|
165483
|
+
name: params.name,
|
|
165484
|
+
secretsUnavailable: params.secretsUnavailable
|
|
165485
|
+
})
|
|
165486
|
+
);
|
|
165346
165487
|
}
|
|
165347
165488
|
function isApiKeyAuthError(text) {
|
|
165348
165489
|
if (!text) return false;
|
|
@@ -166027,7 +166168,7 @@ async function persistXrepoLearnings(ctx) {
|
|
|
166027
166168
|
// utils/modelAccess.ts
|
|
166028
166169
|
function decideModelAccess(input) {
|
|
166029
166170
|
if (!input.modelExplicit || !input.model) return { kind: "ok" };
|
|
166030
|
-
const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/"));
|
|
166171
|
+
const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`));
|
|
166031
166172
|
if (input.proxyActive) {
|
|
166032
166173
|
const target = resolveOpenRouterModel(input.model);
|
|
166033
166174
|
if (input.oss) {
|
|
@@ -166572,6 +166713,7 @@ async function mintProxyKey(ctx) {
|
|
|
166572
166713
|
}
|
|
166573
166714
|
}
|
|
166574
166715
|
async function buildProxyTokenHeaders(ctx) {
|
|
166716
|
+
const fundingSource = ctx.oss ? "oss" : "router";
|
|
166575
166717
|
if (ctx.oidcCredentials) {
|
|
166576
166718
|
const creds = ctx.oidcCredentials;
|
|
166577
166719
|
const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
|
|
@@ -166579,11 +166721,17 @@ async function buildProxyTokenHeaders(ctx) {
|
|
|
166579
166721
|
retries: [1e3, 2e3],
|
|
166580
166722
|
bail: (error49) => !isTransientTokenError(error49)
|
|
166581
166723
|
})();
|
|
166582
|
-
return {
|
|
166724
|
+
return {
|
|
166725
|
+
Authorization: `Bearer ${oidcToken}`,
|
|
166726
|
+
"X-Pullfrog-Funding-Source": fundingSource
|
|
166727
|
+
};
|
|
166583
166728
|
}
|
|
166584
166729
|
if (isLocalApiUrl()) {
|
|
166585
166730
|
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
166586
|
-
return {
|
|
166731
|
+
return {
|
|
166732
|
+
"x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}`,
|
|
166733
|
+
"X-Pullfrog-Funding-Source": fundingSource
|
|
166734
|
+
};
|
|
166587
166735
|
}
|
|
166588
166736
|
return null;
|
|
166589
166737
|
}
|
|
@@ -166594,7 +166742,11 @@ async function resolveProxyModel(ctx) {
|
|
|
166594
166742
|
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
166595
166743
|
return;
|
|
166596
166744
|
}
|
|
166597
|
-
const key = await mintProxyKey({
|
|
166745
|
+
const key = await mintProxyKey({
|
|
166746
|
+
oidcCredentials: ctx.oidcCredentials,
|
|
166747
|
+
repo: ctx.repo,
|
|
166748
|
+
oss: ctx.oss
|
|
166749
|
+
});
|
|
166598
166750
|
if (!key) return;
|
|
166599
166751
|
process.env.OPENROUTER_API_KEY = key;
|
|
166600
166752
|
core8.setSecret(key);
|
|
@@ -166825,6 +166977,10 @@ var defaultRunContext = {
|
|
|
166825
166977
|
oss: false,
|
|
166826
166978
|
plan: "none"
|
|
166827
166979
|
};
|
|
166980
|
+
var unknownSecretsRunContext = {
|
|
166981
|
+
...defaultRunContext,
|
|
166982
|
+
secretsUnavailable: true
|
|
166983
|
+
};
|
|
166828
166984
|
async function fetchRunContext(params) {
|
|
166829
166985
|
const timeoutMs = 3e4;
|
|
166830
166986
|
const controller = new AbortController();
|
|
@@ -166843,7 +166999,7 @@ async function fetchRunContext(params) {
|
|
|
166843
166999
|
});
|
|
166844
167000
|
clearTimeout(timeoutId);
|
|
166845
167001
|
if (!response.ok) {
|
|
166846
|
-
return defaultRunContext;
|
|
167002
|
+
return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
|
|
166847
167003
|
}
|
|
166848
167004
|
const data = await response.json();
|
|
166849
167005
|
if (data === null) {
|
|
@@ -166867,11 +167023,12 @@ async function fetchRunContext(params) {
|
|
|
166867
167023
|
oss: data.oss ?? false,
|
|
166868
167024
|
plan: data.plan ?? "none",
|
|
166869
167025
|
proxyModel: data.proxyModel,
|
|
166870
|
-
dbSecrets: data.dbSecrets
|
|
167026
|
+
dbSecrets: data.dbSecrets,
|
|
167027
|
+
secretsUnavailable: data.secretsUnavailable
|
|
166871
167028
|
};
|
|
166872
167029
|
} catch {
|
|
166873
167030
|
clearTimeout(timeoutId);
|
|
166874
|
-
return
|
|
167031
|
+
return unknownSecretsRunContext;
|
|
166875
167032
|
}
|
|
166876
167033
|
}
|
|
166877
167034
|
|
|
@@ -166893,7 +167050,10 @@ async function resolveRunContextData(params) {
|
|
|
166893
167050
|
const repoContext = parseRepoContext();
|
|
166894
167051
|
let oidcToken;
|
|
166895
167052
|
try {
|
|
166896
|
-
oidcToken = await core9.getIDToken("pullfrog-api")
|
|
167053
|
+
oidcToken = await op(() => core9.getIDToken("pullfrog-api"), {
|
|
167054
|
+
name: "OIDC mint",
|
|
167055
|
+
retries: process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? [200, 1e3] : []
|
|
167056
|
+
})();
|
|
166897
167057
|
} catch {
|
|
166898
167058
|
}
|
|
166899
167059
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -166915,7 +167075,10 @@ async function resolveRunContextData(params) {
|
|
|
166915
167075
|
oss: runContext.oss,
|
|
166916
167076
|
plan: runContext.plan,
|
|
166917
167077
|
proxyModel: runContext.proxyModel,
|
|
166918
|
-
dbSecrets: runContext.dbSecrets
|
|
167078
|
+
dbSecrets: runContext.dbSecrets,
|
|
167079
|
+
// a failed mint on a runner that should have been able to mint is the same
|
|
167080
|
+
// outcome as the server-side failure: the run never sees stored secrets.
|
|
167081
|
+
secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0
|
|
166919
167082
|
};
|
|
166920
167083
|
}
|
|
166921
167084
|
|
|
@@ -167080,6 +167243,9 @@ function renderRunError(input) {
|
|
|
167080
167243
|
if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
|
|
167081
167244
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
167082
167245
|
}
|
|
167246
|
+
if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
|
|
167247
|
+
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
167248
|
+
}
|
|
167083
167249
|
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
167084
167250
|
const hangBody = isHang ? formatAgentHangBody({
|
|
167085
167251
|
diagnostic: input.agentDiagnostic,
|
|
@@ -167814,7 +167980,8 @@ async function main() {
|
|
|
167814
167980
|
model: effectiveModel,
|
|
167815
167981
|
authorized: getAuthorizedModels(),
|
|
167816
167982
|
owner: runContext.repo.owner,
|
|
167817
|
-
name: runContext.repo.name
|
|
167983
|
+
name: runContext.repo.name,
|
|
167984
|
+
secretsUnavailable: runContext.secretsUnavailable
|
|
167818
167985
|
});
|
|
167819
167986
|
}
|
|
167820
167987
|
await setupGit({
|
|
@@ -168311,7 +168478,7 @@ function link(text, url4) {
|
|
|
168311
168478
|
return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
|
|
168312
168479
|
}
|
|
168313
168480
|
function buildProviders() {
|
|
168314
|
-
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter"
|
|
168481
|
+
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
|
|
168315
168482
|
const aliases = modelAliases.filter(
|
|
168316
168483
|
(a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
|
|
168317
168484
|
);
|
|
@@ -168331,7 +168498,7 @@ function buildProviders() {
|
|
|
168331
168498
|
hint: a === recommended ? "recommended" : void 0
|
|
168332
168499
|
}))
|
|
168333
168500
|
};
|
|
168334
|
-
});
|
|
168501
|
+
}).filter((p2) => p2.models.length > 0);
|
|
168335
168502
|
}
|
|
168336
168503
|
var CLI_PROVIDERS = buildProviders();
|
|
168337
168504
|
function resolveModelProvider(slug2) {
|
|
@@ -169141,7 +169308,7 @@ async function runCli4(input) {
|
|
|
169141
169308
|
}
|
|
169142
169309
|
|
|
169143
169310
|
// cli.ts
|
|
169144
|
-
var VERSION10 = "0.1.
|
|
169311
|
+
var VERSION10 = "0.1.46";
|
|
169145
169312
|
var bin = basename2(process.argv[1] || "");
|
|
169146
169313
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
169147
169314
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -100190,6 +100190,28 @@ var providers = {
|
|
|
100190
100190
|
}
|
|
100191
100191
|
}
|
|
100192
100192
|
}),
|
|
100193
|
+
"openai-compatible": provider({
|
|
100194
|
+
// "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
|
|
100195
|
+
// menu reads `Custom › OpenAI-compatible` and a second custom backend (a
|
|
100196
|
+
// different wire format, say) slots in beside it without a rename. the
|
|
100197
|
+
// provider KEY stays `openai-compatible` — it's the stored slug and the
|
|
100198
|
+
// `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
|
|
100199
|
+
displayName: "Custom",
|
|
100200
|
+
// bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
|
|
100201
|
+
// Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
|
|
100202
|
+
// key + model ID are all supplied via env; nothing is cataloged or bumped.
|
|
100203
|
+
envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
|
|
100204
|
+
models: {
|
|
100205
|
+
// single routing entry — the actual model ID is read from
|
|
100206
|
+
// OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
|
|
100207
|
+
// via `@ai-sdk/openai-compatible`.
|
|
100208
|
+
byok: {
|
|
100209
|
+
displayName: "OpenAI-compatible",
|
|
100210
|
+
resolve: "openai-compatible",
|
|
100211
|
+
routing: "openai-compatible"
|
|
100212
|
+
}
|
|
100213
|
+
}
|
|
100214
|
+
}),
|
|
100193
100215
|
openrouter: provider({
|
|
100194
100216
|
displayName: "OpenRouter",
|
|
100195
100217
|
envVars: ["OPENROUTER_API_KEY"],
|
|
@@ -100407,6 +100429,12 @@ var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
|
|
|
100407
100429
|
var defaultProxyDisplayName = defaultProxyAlias.displayName;
|
|
100408
100430
|
var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
|
100409
100431
|
var VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
|
|
100432
|
+
var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
|
|
100433
|
+
var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
|
|
100434
|
+
var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
|
|
100435
|
+
var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
|
|
100436
|
+
var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
|
|
100437
|
+
var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
|
|
100410
100438
|
function isBedrockAnthropicId(bedrockModelId) {
|
|
100411
100439
|
return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
|
|
100412
100440
|
}
|
|
@@ -101093,7 +101121,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
101093
101121
|
// package.json
|
|
101094
101122
|
var package_default = {
|
|
101095
101123
|
name: "pullfrog",
|
|
101096
|
-
version: "0.1.
|
|
101124
|
+
version: "0.1.46",
|
|
101097
101125
|
type: "module",
|
|
101098
101126
|
bin: {
|
|
101099
101127
|
pullfrog: "dist/cli.mjs",
|
|
@@ -108882,8 +108910,9 @@ function readModels(cliPath) {
|
|
|
108882
108910
|
stdio: ["ignore", "pipe", "pipe"]
|
|
108883
108911
|
});
|
|
108884
108912
|
if (result.status !== 0) {
|
|
108885
|
-
|
|
108886
|
-
|
|
108913
|
+
const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
|
|
108914
|
+
failure = stderr || result.error?.message;
|
|
108915
|
+
log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
|
|
108887
108916
|
return /* @__PURE__ */ new Set();
|
|
108888
108917
|
}
|
|
108889
108918
|
failure = void 0;
|
|
@@ -108941,6 +108970,29 @@ function geminiHighThinkingOverrides() {
|
|
|
108941
108970
|
])
|
|
108942
108971
|
);
|
|
108943
108972
|
}
|
|
108973
|
+
function openAICompatibleLimit() {
|
|
108974
|
+
return {
|
|
108975
|
+
context: Number(process.env[OPENAI_COMPATIBLE_CONTEXT_ENV]),
|
|
108976
|
+
output: Number(process.env[OPENAI_COMPATIBLE_MAX_OUTPUT_ENV])
|
|
108977
|
+
};
|
|
108978
|
+
}
|
|
108979
|
+
function openAICompatibleProvider(model) {
|
|
108980
|
+
const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
|
|
108981
|
+
if (!model?.startsWith(prefix)) return {};
|
|
108982
|
+
const modelId = model.slice(prefix.length);
|
|
108983
|
+
return {
|
|
108984
|
+
[OPENAI_COMPATIBLE_PROVIDER]: {
|
|
108985
|
+
npm: "@ai-sdk/openai-compatible",
|
|
108986
|
+
name: "OpenAI-compatible",
|
|
108987
|
+
options: {
|
|
108988
|
+
// trailing slash would double up against opencode's `/chat/completions` join
|
|
108989
|
+
baseURL: process.env[OPENAI_COMPATIBLE_BASE_URL_ENV]?.replace(/\/$/, ""),
|
|
108990
|
+
apiKey: process.env[OPENAI_COMPATIBLE_API_KEY_ENV]
|
|
108991
|
+
},
|
|
108992
|
+
models: { [modelId]: { name: modelId, limit: openAICompatibleLimit() } }
|
|
108993
|
+
}
|
|
108994
|
+
};
|
|
108995
|
+
}
|
|
108944
108996
|
var KIMI_ENFORCERLESS_PROVIDERS = ["siliconflow", "together"];
|
|
108945
108997
|
function kimiOpenRouterProviderOverrides() {
|
|
108946
108998
|
return Object.fromEntries(
|
|
@@ -109032,7 +109084,8 @@ function buildSecurityConfig(ctx, model) {
|
|
|
109032
109084
|
google: { models: geminiHighThinkingOverrides() },
|
|
109033
109085
|
openrouter: {
|
|
109034
109086
|
models: { ...deepseekHighEffortOverrides(), ...kimiOpenRouterProviderOverrides() }
|
|
109035
|
-
}
|
|
109087
|
+
},
|
|
109088
|
+
...openAICompatibleProvider(model)
|
|
109036
109089
|
}
|
|
109037
109090
|
};
|
|
109038
109091
|
if (model) {
|
|
@@ -163298,6 +163351,15 @@ function resolveSlug(slug2) {
|
|
|
163298
163351
|
}
|
|
163299
163352
|
return vertexId;
|
|
163300
163353
|
}
|
|
163354
|
+
if (alias?.routing === "openai-compatible") {
|
|
163355
|
+
const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
|
|
163356
|
+
if (!modelId) {
|
|
163357
|
+
throw new Error(
|
|
163358
|
+
`${OPENAI_COMPATIBLE_MODEL_ENV} env var is required when the model is set to "${slug2}". set it to the model ID served by your OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway or DashScope model). see https://docs.pullfrog.com/openai-compatible for setup.`
|
|
163359
|
+
);
|
|
163360
|
+
}
|
|
163361
|
+
return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
|
|
163362
|
+
}
|
|
163301
163363
|
return resolveCliModel(slug2);
|
|
163302
163364
|
}
|
|
163303
163365
|
function resolveModel(ctx) {
|
|
@@ -163347,6 +163409,21 @@ function resolveAgent(ctx) {
|
|
|
163347
163409
|
|
|
163348
163410
|
// utils/apiKeys.ts
|
|
163349
163411
|
var MISSING_KEY_MARKER = "no API key found";
|
|
163412
|
+
var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
163413
|
+
function buildKeyError(params) {
|
|
163414
|
+
return params.secretsUnavailable ? buildSecretsUnavailableError(params) : buildMissingApiKeyError(params);
|
|
163415
|
+
}
|
|
163416
|
+
function buildSecretsUnavailableError(params) {
|
|
163417
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
163418
|
+
const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
|
|
163419
|
+
return [
|
|
163420
|
+
`**Pullfrog ${SECRETS_UNAVAILABLE_MARKER}${modelClause} on this run.** The key is still stored \u2014 the runner just couldn't fetch it, so this is a transient failure on our side, not a missing key.`,
|
|
163421
|
+
"",
|
|
163422
|
+
"**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
|
|
163423
|
+
"",
|
|
163424
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
163425
|
+
].join("\n");
|
|
163426
|
+
}
|
|
163350
163427
|
function buildMissingApiKeyError(params) {
|
|
163351
163428
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
163352
163429
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
@@ -163389,10 +163466,47 @@ add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then
|
|
|
163389
163466
|
|
|
163390
163467
|
for full setup instructions, see https://docs.pullfrog.com/vertex`;
|
|
163391
163468
|
}
|
|
163469
|
+
function buildOpenAICompatibleSetupError(params) {
|
|
163470
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
163471
|
+
return `OpenAI-compatible model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
163472
|
+
|
|
163473
|
+
only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
|
|
163474
|
+
|
|
163475
|
+
${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
|
|
163476
|
+
${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
|
|
163477
|
+
${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
|
|
163478
|
+
${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
|
|
163479
|
+
${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
|
|
163480
|
+
|
|
163481
|
+
set the last two to the real limits of the model your endpoint serves. Pullfrog can't
|
|
163482
|
+
discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
|
|
163483
|
+
capped at 32000 tokens (rejected outright by models with a smaller cap) and
|
|
163484
|
+
auto-compaction is disabled, so long runs grow until your endpoint refuses them.
|
|
163485
|
+
|
|
163486
|
+
for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
|
|
163487
|
+
}
|
|
163392
163488
|
function hasEnvVar3(name) {
|
|
163393
163489
|
const value2 = process.env[name];
|
|
163394
163490
|
return typeof value2 === "string" && value2.length > 0;
|
|
163395
163491
|
}
|
|
163492
|
+
function hasPositiveNumberEnvVar(name) {
|
|
163493
|
+
return Number(process.env[name]) > 0;
|
|
163494
|
+
}
|
|
163495
|
+
function validateOpenAICompatibleSetup(params) {
|
|
163496
|
+
const missing = [];
|
|
163497
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
|
|
163498
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
|
|
163499
|
+
if (!hasEnvVar3(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
|
|
163500
|
+
if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
|
|
163501
|
+
missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
|
|
163502
|
+
if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
|
|
163503
|
+
missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
|
|
163504
|
+
if (missing.length > 0) {
|
|
163505
|
+
throw new Error(
|
|
163506
|
+
buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
|
|
163507
|
+
);
|
|
163508
|
+
}
|
|
163509
|
+
}
|
|
163396
163510
|
function validateBedrockSetup(params) {
|
|
163397
163511
|
const hasAuth = hasEnvVar3("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar3("AWS_ACCESS_KEY_ID") && hasEnvVar3("AWS_SECRET_ACCESS_KEY");
|
|
163398
163512
|
const missing = [];
|
|
@@ -163427,6 +163541,10 @@ function validateAgentApiKey(params) {
|
|
|
163427
163541
|
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
163428
163542
|
return;
|
|
163429
163543
|
}
|
|
163544
|
+
if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
|
|
163545
|
+
validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
|
|
163546
|
+
return;
|
|
163547
|
+
}
|
|
163430
163548
|
if (!params.model.includes("/")) {
|
|
163431
163549
|
if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
|
|
163432
163550
|
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
@@ -163439,23 +163557,46 @@ function validateAgentApiKey(params) {
|
|
|
163439
163557
|
if (params.authorized.has(params.model)) return;
|
|
163440
163558
|
const reason = getModelsFailure();
|
|
163441
163559
|
if (reason) throw new Error(reason);
|
|
163560
|
+
if (getModelEnvVars(params.model).some(hasEnvVar3)) return;
|
|
163442
163561
|
throw new Error(
|
|
163443
|
-
|
|
163562
|
+
buildKeyError({
|
|
163563
|
+
owner: params.owner,
|
|
163564
|
+
name: params.name,
|
|
163565
|
+
model: params.model,
|
|
163566
|
+
secretsUnavailable: params.secretsUnavailable
|
|
163567
|
+
})
|
|
163444
163568
|
);
|
|
163445
163569
|
}
|
|
163446
163570
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
163447
163571
|
throw new Error(
|
|
163448
|
-
|
|
163572
|
+
buildKeyError({
|
|
163573
|
+
owner: params.owner,
|
|
163574
|
+
name: params.name,
|
|
163575
|
+
model: params.model,
|
|
163576
|
+
secretsUnavailable: params.secretsUnavailable
|
|
163577
|
+
})
|
|
163449
163578
|
);
|
|
163450
163579
|
}
|
|
163451
163580
|
if (params.agent.name === "opencode") {
|
|
163452
163581
|
if (params.authorized.size > 0) return;
|
|
163453
163582
|
const reason = getModelsFailure();
|
|
163454
163583
|
if (reason) throw new Error(reason);
|
|
163455
|
-
throw new Error(
|
|
163584
|
+
throw new Error(
|
|
163585
|
+
buildKeyError({
|
|
163586
|
+
owner: params.owner,
|
|
163587
|
+
name: params.name,
|
|
163588
|
+
secretsUnavailable: params.secretsUnavailable
|
|
163589
|
+
})
|
|
163590
|
+
);
|
|
163456
163591
|
}
|
|
163457
163592
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
163458
|
-
throw new Error(
|
|
163593
|
+
throw new Error(
|
|
163594
|
+
buildKeyError({
|
|
163595
|
+
owner: params.owner,
|
|
163596
|
+
name: params.name,
|
|
163597
|
+
secretsUnavailable: params.secretsUnavailable
|
|
163598
|
+
})
|
|
163599
|
+
);
|
|
163459
163600
|
}
|
|
163460
163601
|
function isApiKeyAuthError(text) {
|
|
163461
163602
|
if (!text) return false;
|
|
@@ -164140,7 +164281,7 @@ async function persistXrepoLearnings(ctx) {
|
|
|
164140
164281
|
// utils/modelAccess.ts
|
|
164141
164282
|
function decideModelAccess(input) {
|
|
164142
164283
|
if (!input.modelExplicit || !input.model) return { kind: "ok" };
|
|
164143
|
-
const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/"));
|
|
164284
|
+
const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`));
|
|
164144
164285
|
if (input.proxyActive) {
|
|
164145
164286
|
const target = resolveOpenRouterModel(input.model);
|
|
164146
164287
|
if (input.oss) {
|
|
@@ -164685,6 +164826,7 @@ async function mintProxyKey(ctx) {
|
|
|
164685
164826
|
}
|
|
164686
164827
|
}
|
|
164687
164828
|
async function buildProxyTokenHeaders(ctx) {
|
|
164829
|
+
const fundingSource = ctx.oss ? "oss" : "router";
|
|
164688
164830
|
if (ctx.oidcCredentials) {
|
|
164689
164831
|
const creds = ctx.oidcCredentials;
|
|
164690
164832
|
const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
|
|
@@ -164692,11 +164834,17 @@ async function buildProxyTokenHeaders(ctx) {
|
|
|
164692
164834
|
retries: [1e3, 2e3],
|
|
164693
164835
|
bail: (error49) => !isTransientTokenError(error49)
|
|
164694
164836
|
})();
|
|
164695
|
-
return {
|
|
164837
|
+
return {
|
|
164838
|
+
Authorization: `Bearer ${oidcToken}`,
|
|
164839
|
+
"X-Pullfrog-Funding-Source": fundingSource
|
|
164840
|
+
};
|
|
164696
164841
|
}
|
|
164697
164842
|
if (isLocalApiUrl()) {
|
|
164698
164843
|
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
164699
|
-
return {
|
|
164844
|
+
return {
|
|
164845
|
+
"x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}`,
|
|
164846
|
+
"X-Pullfrog-Funding-Source": fundingSource
|
|
164847
|
+
};
|
|
164700
164848
|
}
|
|
164701
164849
|
return null;
|
|
164702
164850
|
}
|
|
@@ -164707,7 +164855,11 @@ async function resolveProxyModel(ctx) {
|
|
|
164707
164855
|
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
164708
164856
|
return;
|
|
164709
164857
|
}
|
|
164710
|
-
const key = await mintProxyKey({
|
|
164858
|
+
const key = await mintProxyKey({
|
|
164859
|
+
oidcCredentials: ctx.oidcCredentials,
|
|
164860
|
+
repo: ctx.repo,
|
|
164861
|
+
oss: ctx.oss
|
|
164862
|
+
});
|
|
164711
164863
|
if (!key) return;
|
|
164712
164864
|
process.env.OPENROUTER_API_KEY = key;
|
|
164713
164865
|
core8.setSecret(key);
|
|
@@ -164938,6 +165090,10 @@ var defaultRunContext = {
|
|
|
164938
165090
|
oss: false,
|
|
164939
165091
|
plan: "none"
|
|
164940
165092
|
};
|
|
165093
|
+
var unknownSecretsRunContext = {
|
|
165094
|
+
...defaultRunContext,
|
|
165095
|
+
secretsUnavailable: true
|
|
165096
|
+
};
|
|
164941
165097
|
async function fetchRunContext(params) {
|
|
164942
165098
|
const timeoutMs = 3e4;
|
|
164943
165099
|
const controller = new AbortController();
|
|
@@ -164956,7 +165112,7 @@ async function fetchRunContext(params) {
|
|
|
164956
165112
|
});
|
|
164957
165113
|
clearTimeout(timeoutId);
|
|
164958
165114
|
if (!response.ok) {
|
|
164959
|
-
return defaultRunContext;
|
|
165115
|
+
return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
|
|
164960
165116
|
}
|
|
164961
165117
|
const data = await response.json();
|
|
164962
165118
|
if (data === null) {
|
|
@@ -164980,11 +165136,12 @@ async function fetchRunContext(params) {
|
|
|
164980
165136
|
oss: data.oss ?? false,
|
|
164981
165137
|
plan: data.plan ?? "none",
|
|
164982
165138
|
proxyModel: data.proxyModel,
|
|
164983
|
-
dbSecrets: data.dbSecrets
|
|
165139
|
+
dbSecrets: data.dbSecrets,
|
|
165140
|
+
secretsUnavailable: data.secretsUnavailable
|
|
164984
165141
|
};
|
|
164985
165142
|
} catch {
|
|
164986
165143
|
clearTimeout(timeoutId);
|
|
164987
|
-
return
|
|
165144
|
+
return unknownSecretsRunContext;
|
|
164988
165145
|
}
|
|
164989
165146
|
}
|
|
164990
165147
|
|
|
@@ -165006,7 +165163,10 @@ async function resolveRunContextData(params) {
|
|
|
165006
165163
|
const repoContext = parseRepoContext();
|
|
165007
165164
|
let oidcToken;
|
|
165008
165165
|
try {
|
|
165009
|
-
oidcToken = await core9.getIDToken("pullfrog-api")
|
|
165166
|
+
oidcToken = await op(() => core9.getIDToken("pullfrog-api"), {
|
|
165167
|
+
name: "OIDC mint",
|
|
165168
|
+
retries: process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? [200, 1e3] : []
|
|
165169
|
+
})();
|
|
165010
165170
|
} catch {
|
|
165011
165171
|
}
|
|
165012
165172
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -165028,7 +165188,10 @@ async function resolveRunContextData(params) {
|
|
|
165028
165188
|
oss: runContext.oss,
|
|
165029
165189
|
plan: runContext.plan,
|
|
165030
165190
|
proxyModel: runContext.proxyModel,
|
|
165031
|
-
dbSecrets: runContext.dbSecrets
|
|
165191
|
+
dbSecrets: runContext.dbSecrets,
|
|
165192
|
+
// a failed mint on a runner that should have been able to mint is the same
|
|
165193
|
+
// outcome as the server-side failure: the run never sees stored secrets.
|
|
165194
|
+
secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0
|
|
165032
165195
|
};
|
|
165033
165196
|
}
|
|
165034
165197
|
|
|
@@ -165193,6 +165356,9 @@ function renderRunError(input) {
|
|
|
165193
165356
|
if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
|
|
165194
165357
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
165195
165358
|
}
|
|
165359
|
+
if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
|
|
165360
|
+
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
165361
|
+
}
|
|
165196
165362
|
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
165197
165363
|
const hangBody = isHang ? formatAgentHangBody({
|
|
165198
165364
|
diagnostic: input.agentDiagnostic,
|
|
@@ -165927,7 +166093,8 @@ async function main() {
|
|
|
165927
166093
|
model: effectiveModel,
|
|
165928
166094
|
authorized: getAuthorizedModels(),
|
|
165929
166095
|
owner: runContext.repo.owner,
|
|
165930
|
-
name: runContext.repo.name
|
|
166096
|
+
name: runContext.repo.name,
|
|
166097
|
+
secretsUnavailable: runContext.secretsUnavailable
|
|
165931
166098
|
});
|
|
165932
166099
|
}
|
|
165933
166100
|
await setupGit({
|
package/dist/internal.js
CHANGED
|
@@ -381,6 +381,28 @@ var providers = {
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
}),
|
|
384
|
+
"openai-compatible": provider({
|
|
385
|
+
// "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
|
|
386
|
+
// menu reads `Custom › OpenAI-compatible` and a second custom backend (a
|
|
387
|
+
// different wire format, say) slots in beside it without a rename. the
|
|
388
|
+
// provider KEY stays `openai-compatible` — it's the stored slug and the
|
|
389
|
+
// `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
|
|
390
|
+
displayName: "Custom",
|
|
391
|
+
// bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
|
|
392
|
+
// Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
|
|
393
|
+
// key + model ID are all supplied via env; nothing is cataloged or bumped.
|
|
394
|
+
envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
|
|
395
|
+
models: {
|
|
396
|
+
// single routing entry — the actual model ID is read from
|
|
397
|
+
// OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
|
|
398
|
+
// via `@ai-sdk/openai-compatible`.
|
|
399
|
+
byok: {
|
|
400
|
+
displayName: "OpenAI-compatible",
|
|
401
|
+
resolve: "openai-compatible",
|
|
402
|
+
routing: "openai-compatible"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}),
|
|
384
406
|
openrouter: provider({
|
|
385
407
|
displayName: "OpenRouter",
|
|
386
408
|
envVars: ["OPENROUTER_API_KEY"],
|
package/dist/models.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* env var and routes to claude-code for Anthropic IDs or opencode for
|
|
21
21
|
* everything else.
|
|
22
22
|
*/
|
|
23
|
-
export type ModelRouting = "bedrock" | "vertex";
|
|
23
|
+
export type ModelRouting = "bedrock" | "vertex" | "openai-compatible";
|
|
24
24
|
export interface ModelAlias {
|
|
25
25
|
/** stable alias stored in DB, e.g. "anthropic/claude-opus" */
|
|
26
26
|
slug: string;
|
|
@@ -99,6 +99,7 @@ export declare const providers: {
|
|
|
99
99
|
"opencode-go": ProviderConfig;
|
|
100
100
|
bedrock: ProviderConfig;
|
|
101
101
|
vertex: ProviderConfig;
|
|
102
|
+
"openai-compatible": ProviderConfig;
|
|
102
103
|
openrouter: ProviderConfig;
|
|
103
104
|
};
|
|
104
105
|
export type ModelProvider = keyof typeof providers;
|
|
@@ -196,6 +197,31 @@ export declare function getAutoSelectHintModel(): string;
|
|
|
196
197
|
export declare const BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
|
197
198
|
/** env var that supplies the Vertex AI model ID for the `vertex/byok` slug. */
|
|
198
199
|
export declare const VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
|
|
200
|
+
/** provider key + slug prefix for the generic OpenAI-compatible BYOK backend. */
|
|
201
|
+
export declare const OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
|
|
202
|
+
/** base URL of the user's OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway URL). */
|
|
203
|
+
export declare const OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
|
|
204
|
+
/** API key/token for the user's OpenAI-compatible endpoint — the one sensitive secret. */
|
|
205
|
+
export declare const OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
|
|
206
|
+
/** model ID served by the endpoint, supplied for the `openai-compatible/byok` slug. */
|
|
207
|
+
export declare const OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
|
|
208
|
+
/**
|
|
209
|
+
* context-window size of the endpoint's model. required — `validateOpenAICompatibleSetup`
|
|
210
|
+
* rejects the run pre-agent when it's unset or non-numeric. it also gates
|
|
211
|
+
* auto-compaction: opencode's `isOverflow` short-circuits when
|
|
212
|
+
* `limit.context === 0`, which would otherwise let a long session grow until the
|
|
213
|
+
* endpoint rejects it on context length.
|
|
214
|
+
*/
|
|
215
|
+
export declare const OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
|
|
216
|
+
/**
|
|
217
|
+
* max completion tokens the endpoint's model accepts. required — see
|
|
218
|
+
* OPENAI_COMPATIBLE_CONTEXT_ENV. opencode has no models.dev metadata for a
|
|
219
|
+
* user-supplied endpoint, and an undeclared limit makes it send
|
|
220
|
+
* `max_tokens: 32000`, which most models reject outright (gpt-4o and gpt-4o-mini
|
|
221
|
+
* cap at 16384, many open models at 4096/8192). opencode's `limit` requires
|
|
222
|
+
* `context` + `output` together, so the pair is validated and emitted as a unit.
|
|
223
|
+
*/
|
|
224
|
+
export declare const OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
|
|
199
225
|
/**
|
|
200
226
|
* the Bedrock model ID passed to claude-code or opencode is whatever the
|
|
201
227
|
* user set in `BEDROCK_MODEL_ID` — Pullfrog never resolves or upgrades it.
|
package/dist/utils/apiKeys.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marker for the distinct "run-context couldn't hand over your stored secrets"
|
|
3
|
+
* body. surfaced verbatim by `runErrorRenderer` (same contract as
|
|
4
|
+
* `MODEL_ACCESS_MARKER`) because blaming the user for a transient fetch failure
|
|
5
|
+
* on our side is the wrong CTA — their key is configured and still stored.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
1
8
|
/**
|
|
2
9
|
* Validate that the resolved model can actually be served by the chosen
|
|
3
10
|
* agent. For routing slugs (Bedrock / Vertex) the auth shape is multi-var
|
|
@@ -16,6 +23,9 @@ export declare function validateAgentApiKey(params: {
|
|
|
16
23
|
authorized: Set<string>;
|
|
17
24
|
owner: string;
|
|
18
25
|
name: string;
|
|
26
|
+
/** run-context couldn't hand over Pullfrog-stored secrets, so a missing key
|
|
27
|
+
* says nothing about what the user actually configured. */
|
|
28
|
+
secretsUnavailable?: boolean | undefined;
|
|
19
29
|
}): void;
|
|
20
30
|
/**
|
|
21
31
|
* Detect agent-runtime auth failures that should be reformatted as an actionable
|
|
@@ -55,6 +55,13 @@ export interface RunContext {
|
|
|
55
55
|
plan: AccountPlan;
|
|
56
56
|
proxyModel?: string | undefined;
|
|
57
57
|
dbSecrets?: Record<string, string> | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* the server tried and failed to materialize Pullfrog-stored secrets (or we
|
|
60
|
+
* never got a usable response at all). distinct from an absent `dbSecrets`,
|
|
61
|
+
* which legitimately means the user has none stored — without the
|
|
62
|
+
* distinction a transient failure renders as "you have no API key".
|
|
63
|
+
*/
|
|
64
|
+
secretsUnavailable?: boolean | undefined;
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* fetch run context from Pullfrog API
|
|
@@ -13,6 +13,9 @@ export interface RunContextData {
|
|
|
13
13
|
plan: AccountPlan;
|
|
14
14
|
proxyModel?: string | undefined;
|
|
15
15
|
dbSecrets?: Record<string, string> | undefined;
|
|
16
|
+
/** stored secrets couldn't be materialized for this run — not the same as
|
|
17
|
+
* the user having none. see `RunContext.secretsUnavailable`. */
|
|
18
|
+
secretsUnavailable?: boolean | undefined;
|
|
16
19
|
}
|
|
17
20
|
interface ResolveRunContextDataParams {
|
|
18
21
|
octokit: OctokitWithPlugins;
|