f3d18401ac
* eager signup credit + free-OpenCode fallback when BYOK has no key
addresses the silent-churn pattern that took out 15 first-run-failure
accounts post-launch: GH Actions secret references resolved to empty
strings (because the secrets didn't exist on the repo), the action
launched Claude Code with no key, the LLM provider 401'd, and the run
died in seconds with a synthetic "Invalid API key" message. those
accounts had no Router credits to fall back to because the lazy claim
required a dashboard visit they never made.
three changes, one PR:
1. Eager $10 signup credit at account creation. Both account-creation
sites (`upsertAccountByClerkId` for dashboard signin, `fetchOrCreateRepo`
for CLI / GH-App-only) now insert the `CreditGrant { reason: "signup" }`
in the same transaction as the `accounts` row. CLI installers who
never sign in get the credit. The dashboard `/signup-credit/claim`
POST stays as an idempotent backstop for accounts created before
this shipped.
2. Free-OpenCode fallback in the action. When the configured BYOK slug
needs a provider key the runner doesn't have, swap to
`opencode/minimax-m2.5-free` before agent selection so the run still
succeeds. Surfaced via a `» fell back from <slug> to <free>` warning
in the action log. Skipped on Router runs (Pullfrog mints the key)
and when no model is configured (auto-select-with-throw still fires
for the genuinely-misconfigured case).
3. New action-test fixture `byok-no-keys-fallback` that empty-strings
every known provider key (matching how GH Actions handles missing
secrets) and asserts the run succeeds with the fallback log line
present. plus a unit test for the helper covering each skip case.
skipping the schema flip from `byok` to `router` — that's coming via
the onboarding-stepper PR (#762).
* fallback: skip Bedrock + surface in PR-comment footer
addresses copilot review on #789 (real bug — parseModel throws on
Bedrock raw IDs that have no slash, would crash before
validateBedrockSetup could surface its own error) and the user-side
ask to make the fallback visible in PR comments.
- selectFallbackModelIfNeeded skips when resolvedModel has no '/' so
Bedrock routing IDs (e.g. us.anthropic.claude-opus-4-7) don't crash
inside hasProviderKey -> parseModel. unit test covers it.
- toolState.modelFallback records the configured slug we fell back
from. set in main.ts when fallback engages.
- buildPullfrogFooter accepts fallbackFrom and renders
"Using `MiniMax M2.5` (free) (credentials for Claude Opus not
configured)" so the substitution is visible in PR comments,
reviews, PR bodies, and error reports.
- threaded through all four action-side footer call sites
(mcp/comment, mcp/pr, mcp/review, utils/errorReport). server-side
call sites in triggerWorkflow.ts / handleWorkflowRunWebhook.ts
fire pre-action and don't have toolState — left as-is.
* fallback footer: use provider display name + document email asymmetry
addresses pullfrog reviewer findings on #789:
- footer now shows 'credentials for Anthropic not configured' (provider
display name from `providers.anthropic.displayName`) instead of the
per-model name. credentials are provider-scoped (ANTHROPIC_API_KEY
covers all Anthropic models), so this matches what the user actually
needs to fix.
- document the intentional asymmetry between eager and lazy signup
credit paths: eager skips both the signupCreditClaimedEmail and the
per-grant team@ alert. comment explains why (the 'new account
created' alert already covers it on the eager path; the user-facing
email assumes a user-initiated action that hasn't happened yet for
CLI/GH-App-only signups).
- skipping the backfill for the 15 historical accounts per user's
earlier decision — they all uninstalled, so the cohort self-selected
out of being reachable.
* fallback: gate on resolvedModel + skip resolveModel re-resolve post-swap
local agnostic fixture run surfaced two real bugs the unit tests didn't
catch:
1. fallback gate was on configuredSlug (=payload.model) but the test
uses PULLFROG_MODEL to set the model, which is read by resolveModel
AFTER its slug arg. configuredSlug stayed undefined → fallback never
fired. drop configuredSlug from the helper signature; gate purely on
resolvedModel since that's the same value regardless of how the
model was specified (DB config vs PULLFROG_MODEL env).
2. when fallback engaged, the post-swap resolveModel({slug: fallback.to})
call was ALSO honoring PULLFROG_MODEL, re-overriding the fallback
target back to the unkeyed model. validateAgentApiKey then threw
"no API key found" against the original model. fix: skip the
re-resolve. fallback.to is already a CLI-ready specifier.
unit tests updated for the new helper signature (8 tests, all pass).
fallback log line confirmed emitted in the local run pre-second-fix;
the second fix unblocks the validation that previously threw.
60 lines
2.8 KiB
TypeScript
60 lines
2.8 KiB
TypeScript
import { hasProviderKey } from "./apiKeys.ts";
|
|
|
|
/**
|
|
* Slug we fall back to when a BYOK-required model is configured but the
|
|
* runner has no provider key in env. Picked because it's free
|
|
* (`isFree: true`, `envVars: []` — see `action/models.ts`), stable, and
|
|
* currently the strongest free OpenCode model in the catalog. If a
|
|
* smarter free model is added later, update this single constant.
|
|
*
|
|
* The slug is intentionally hard-coded and not a config knob — the
|
|
* fallback is a safety net, not a user-facing preference, and adding a
|
|
* config surface here would just push the same "what to fall back to"
|
|
* decision into another setting that goes stale the same way.
|
|
*/
|
|
export const FREE_FALLBACK_SLUG = "opencode/minimax-m2.5-free";
|
|
|
|
export type FallbackDecision = { fallback: false } | { fallback: true; from: string; to: string };
|
|
|
|
/**
|
|
* If the resolved model requires a BYOK key but no provider key is
|
|
* available in env, return `fallback: true` with a free OpenCode slug
|
|
* so the run can still succeed. Caller is responsible for swapping the
|
|
* model state and surfacing the fallback (log line + run summary).
|
|
*
|
|
* Gates on `resolvedModel` directly (not the configured slug) so the
|
|
* decision matches both code paths that reach this point: payload-based
|
|
* config (`repo.model` from DB) and `PULLFROG_MODEL` env var. Both end
|
|
* up in `resolvedModel` after `resolveModel()` runs upstream.
|
|
*
|
|
* Skip cases:
|
|
* - Router / proxy runs (`proxyModel` set): Pullfrog mints the key,
|
|
* no BYOK in play — never fall back.
|
|
* - No resolved model: keeps the existing auto-select-with-throw
|
|
* behavior in `validateAgentApiKey` for the "neither model nor
|
|
* key" case (genuine misconfig the user should see).
|
|
* - Resolved model is itself the free fallback: avoid suggesting we
|
|
* fell back to the model we're already running.
|
|
* - Resolved model is a Bedrock raw ID (no `/`): Bedrock has its own
|
|
* auth shape (`AWS_BEARER_TOKEN_BEDROCK` + region + model ID), and
|
|
* `validateBedrockSetup` already surfaces a tailored error. Skipping
|
|
* here also avoids `parseModel`'s slash requirement crashing inside
|
|
* `hasProviderKey`.
|
|
* - Resolved model has its provider key present: no fallback needed.
|
|
*/
|
|
export function selectFallbackModelIfNeeded(input: {
|
|
resolvedModel: string | undefined;
|
|
proxyModel: string | undefined;
|
|
}): FallbackDecision {
|
|
if (input.proxyModel) return { fallback: false };
|
|
if (!input.resolvedModel) return { fallback: false };
|
|
if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
|
|
if (!input.resolvedModel.includes("/")) return { fallback: false };
|
|
if (hasProviderKey(input.resolvedModel)) return { fallback: false };
|
|
return {
|
|
fallback: true,
|
|
from: input.resolvedModel,
|
|
to: FREE_FALLBACK_SLUG,
|
|
};
|
|
}
|