Files
shockbot/utils/agent.ts
T
Colin McDonnell c43ed65c3b Add Vertex AI routing support (#753)
* add Vertex AI routing support

* include Vertex smokes in action CI
2026-05-20 16:49:08 +00:00

140 lines
4.9 KiB
TypeScript

import type { Agent } from "../agents/index.ts";
import { agents } from "../agents/index.ts";
import {
BEDROCK_MODEL_ID_ENV,
getModelProvider,
isBedrockAnthropicId,
isVertexAnthropicId,
resolveCliModel,
resolveDisplayAlias,
VERTEX_MODEL_ID_ENV,
} from "../models.ts";
import { log } from "./cli.ts";
import { VERTEX_SERVICE_ACCOUNT_JSON_ENV } from "./vertex.ts";
function hasEnvVar(name: string): boolean {
const val = process.env[name];
return typeof val === "string" && val.length > 0;
}
function hasClaudeCodeAuth(): boolean {
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
}
function hasBedrockAuth(): boolean {
return (
hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") ||
(hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY"))
);
}
function hasVertexAuth(): boolean {
return hasEnvVar(VERTEX_SERVICE_ACCOUNT_JSON_ENV);
}
/**
* resolve a single slug to its CLI-ready model string. routing aliases
* (e.g. `bedrock/byok`) defer to their backing env var instead of the
* sentinel stored in `resolve`. shared between PULLFROG_MODEL override
* and repo-config slug resolution so both paths get the same routing
* semantics — without this helper, `PULLFROG_MODEL=bedrock/byok` would
* leak the literal sentinel string `"bedrock"` downstream.
*/
function resolveSlug(slug: string): string | undefined {
const alias = resolveDisplayAlias(slug);
if (alias?.routing === "bedrock") {
const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
if (!bedrockId) {
throw new Error(
`${BEDROCK_MODEL_ID_ENV} env var is required when the model is set to "${slug}". ` +
`set it to an AWS Bedrock model ID from the Bedrock console. ` +
`see https://docs.pullfrog.com/bedrock for setup.`
);
}
return bedrockId;
}
if (alias?.routing === "vertex") {
const vertexId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
if (!vertexId) {
throw new Error(
`${VERTEX_MODEL_ID_ENV} env var is required when the model is set to "${slug}". ` +
`set it to a Google Vertex AI model ID from Model Garden. ` +
`see https://docs.pullfrog.com/vertex for setup.`
);
}
return vertexId;
}
return resolveCliModel(slug);
}
/**
* resolve the effective model for this run.
*
* priority:
* 1. PULLFROG_MODEL env var — resolved through the alias registry first,
* so values like "anthropic/claude-opus" become "anthropic/claude-opus-4-7".
* raw specifiers (e.g. "anthropic/claude-opus-4-6") pass through unchanged.
* always wins — bypasses Bedrock routing entirely. to test a different
* Bedrock model, change `BEDROCK_MODEL_ID`, not `PULLFROG_MODEL`.
* 2. slug from repo config / payload → alias registry. routing slugs
* (e.g. `bedrock/byok`) defer to a separate env var (`BEDROCK_MODEL_ID`).
* 3. undefined — agent will auto-select.
*/
export function resolveModel(ctx: { slug?: string | undefined }): string | undefined {
const envModel = process.env.PULLFROG_MODEL?.trim();
if (envModel) {
return resolveSlug(envModel) ?? envModel;
}
if (ctx.slug) {
const resolved = resolveSlug(ctx.slug);
if (resolved) {
return resolved;
}
log.warning(`» unknown model slug "${ctx.slug}" — agent will auto-select`);
}
return undefined;
}
export function resolveAgent(ctx: { model?: string | undefined }): Agent {
// 1. explicit env var override (escape hatch)
const envAgent = process.env.PULLFROG_AGENT?.trim();
if (envAgent) {
if (envAgent in agents) {
return agents[envAgent as keyof typeof agents];
}
log.warning(`» unknown PULLFROG_AGENT="${envAgent}" — falling through to auto-select`);
}
// 2. Bedrock routing: when BEDROCK_MODEL_ID is the resolved model, route
// Anthropic IDs through claude-code (which supports Bedrock natively
// once CLAUDE_CODE_USE_BEDROCK=1) and everything else through opencode's
// `amazon-bedrock` provider.
if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
}
// 3. Vertex routing: same shape as Bedrock, but Anthropic Vertex IDs are
// anchored `claude-*` IDs and non-Anthropic models use opencode's
// `google-vertex` provider.
if (ctx.model && hasVertexAuth() && process.env[VERTEX_MODEL_ID_ENV]?.trim() === ctx.model) {
return isVertexAnthropicId(ctx.model) ? agents.claude : agents.opencode;
}
// 4. if model is Anthropic and Claude Code credentials are available, use Claude Code
if (ctx.model) {
try {
const provider = getModelProvider(ctx.model);
if (provider === "anthropic" && hasClaudeCodeAuth()) {
return agents.claude;
}
} catch {
// invalid model format — fall through
}
}
// 5. default: OpenCode (universal, supports all providers)
return agents.opencode;
}