mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-08-11 05:50:21 +02:00
feat(cli): support any Pi provider via generic SHANNON_AI_API_KEY (#415)
* feat(cli): support any Pi provider via generic SHANNON_AI_API_KEY * docs(cli): point users to pi.dev/models for provider and model ids * docs: document generic provider path and pi.dev catalogue
This commit is contained in:
+15
-8
@@ -3,16 +3,16 @@
|
||||
# Defaults to anthropic:claude-sonnet-4-6.
|
||||
|
||||
# --- Anthropic ---------------------------------------------------------------
|
||||
ANTHROPIC_API_KEY=your-api-key-here
|
||||
SHANNON_AI_API_KEY=your-api-key-here
|
||||
SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
|
||||
|
||||
# --- OpenAI ------------------------------------------------------------------
|
||||
# OPENAI_API_KEY=your-api-key-here
|
||||
# SHANNON_AI_API_KEY=your-api-key-here
|
||||
# SHANNON_AI_MODEL=openai:gpt-5.5
|
||||
|
||||
# --- xAI ---------------------------------------------------------------------
|
||||
# XAI_API_KEY=your-api-key-here
|
||||
# SHANNON_AI_API_KEY=your-api-key-here
|
||||
# SHANNON_AI_MODEL=xai:grok-4.5
|
||||
|
||||
# --- AWS Bedrock -------------------------------------------------------------
|
||||
@@ -24,20 +24,27 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
# --- Custom Base URL ---------------------------------------------------------
|
||||
# Route through a proxy or gateway (LiteLLM, an internal endpoint).
|
||||
# Pick the block matching the API dialect your gateway speaks, and uncomment all
|
||||
# three lines. The provider prefix picks the dialect and which key is sent; the
|
||||
# model id is whatever name your gateway serves it under.
|
||||
# three lines. The provider prefix picks the dialect; the model id is whatever
|
||||
# name your gateway serves it under.
|
||||
|
||||
# Anthropic compatible - Anthropic Messages:
|
||||
# ANTHROPIC_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
# SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
|
||||
# OpenAI compatible - Chat Completions (default) or Responses:
|
||||
# OPENAI_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||
# SHANNON_AI_MODEL=openai:gpt-5.5
|
||||
# SHANNON_AI_OPENAI_FORMAT=responses
|
||||
|
||||
# --- Other -------------------------------------------------------------------
|
||||
# --- Other provider ----------------------------------------------------------
|
||||
# Any other provider the Pi harness supports. Name it in SHANNON_AI_MODEL and
|
||||
# supply the key via the generic SHANNON_AI_API_KEY. Pi validates the provider
|
||||
# and model at preflight.
|
||||
# SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3
|
||||
# SHANNON_AI_API_KEY=your-api-key-here
|
||||
|
||||
# --- Misc --------------------------------------------------------------------
|
||||
# Forward /etc/hosts entries into the worker container.
|
||||
# SHANNON_FORWARD_HOSTS=false
|
||||
|
||||
@@ -73,7 +73,7 @@ Sample penetration test reports from intentionally vulnerable applications, prod
|
||||
|
||||
- **Docker**: required for the worker container.
|
||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||
- **AI provider credentials**: Anthropic, OpenAI, xAI, or AWS Bedrock. Claude models are recommended. For suggested model IDs per provider, plus gateways and custom base URLs, see [AI providers](docs/ai-providers.md#suggested-models).
|
||||
- **AI provider credentials**: Anthropic, OpenAI, xAI, or AWS Bedrock. Any other [Pi-supported provider](https://pi.dev/models) is technically supported too, but not recommended. Claude models are recommended. For suggested model IDs per provider, plus gateways and custom base URLs, see [AI providers](docs/ai-providers.md#suggested-models).
|
||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
||||
|
||||
### Run Shannon
|
||||
@@ -190,7 +190,7 @@ Use these guides for operational detail:
|
||||
| --- | --- |
|
||||
| [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. |
|
||||
| [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, and report filters. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock), and custom gateways. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom gateways. |
|
||||
| [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. |
|
||||
| [Workspaces and resuming](docs/workspaces.md) | Naming workspaces, resuming interrupted scans, and workspace storage. |
|
||||
| [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. |
|
||||
|
||||
@@ -10,13 +10,14 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { type ShannonConfig, saveConfig } from '../config/writer.js';
|
||||
import { type OpenAiFormat, type ProviderId, SUPPORTED_PROVIDERS } from '../model-spec.js';
|
||||
import { CURATED_PROVIDERS, type CuratedProviderId, isCuratedProvider, type OpenAiFormat } from '../model-spec.js';
|
||||
import { requireInteractive } from '../tty.js';
|
||||
|
||||
const SHANNON_HOME = path.join(os.homedir(), '.shannon');
|
||||
|
||||
const CUSTOM_MODEL = '__custom__';
|
||||
const CUSTOM_BASE_URL = '__custom_base_url__';
|
||||
const OTHER_PROVIDER = '__other_provider__';
|
||||
|
||||
/**
|
||||
* Wire formats reachable through the gateway route. The format picks the provider
|
||||
@@ -39,28 +40,34 @@ const GATEWAY_DIALECTS: readonly {
|
||||
{ value: 'openai-responses', label: 'OpenAI Responses', provider: 'openai', format: 'responses' },
|
||||
];
|
||||
|
||||
/** Suggested models per provider, best-first. Free-text entry accepts any model in the provider's catalogue. */
|
||||
const MODEL_SUGGESTIONS: Readonly<Record<ProviderId, readonly string[]>> = {
|
||||
/** Suggested models per curated provider, best-first. Free-text entry accepts any model in the provider's catalogue. */
|
||||
const MODEL_SUGGESTIONS: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
||||
anthropic: ['claude-sonnet-4-6', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-haiku-4-5-20251001'],
|
||||
openai: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.4'],
|
||||
xai: ['grok-4.5'],
|
||||
'amazon-bedrock': ['us.anthropic.claude-sonnet-4-6', 'us.anthropic.claude-opus-4-8', 'us.anthropic.claude-opus-4-7'],
|
||||
};
|
||||
|
||||
/** Placeholder shown in the free-text model ID prompt. */
|
||||
const MODEL_ID_PLACEHOLDER: Readonly<Record<ProviderId, string>> = {
|
||||
/** Placeholder shown in the free-text model ID prompt, per curated provider. */
|
||||
const MODEL_ID_PLACEHOLDER: Readonly<Record<CuratedProviderId, string>> = {
|
||||
anthropic: 'claude-sonnet-4-6',
|
||||
openai: 'gpt-5.6-sol',
|
||||
xai: 'grok-4.5',
|
||||
'amazon-bedrock': 'us.anthropic.claude-opus-4-8',
|
||||
};
|
||||
|
||||
/** Model ID placeholder for a provider, absent when the provider is not curated. */
|
||||
function modelIdPlaceholder(provider: string): string | undefined {
|
||||
return isCuratedProvider(provider) ? MODEL_ID_PLACEHOLDER[provider] : undefined;
|
||||
}
|
||||
|
||||
export async function setup(): Promise<void> {
|
||||
requireInteractive('setup', 'For non-interactive use, export credentials as env vars (e.g. ANTHROPIC_API_KEY).');
|
||||
p.intro('Shannon Setup');
|
||||
|
||||
// 1. Select provider. "Custom Base URL" is a route, not a provider — it asks
|
||||
// which API dialect the gateway speaks and configures that provider.
|
||||
// which API dialect the gateway speaks and configures that provider. "Other
|
||||
// provider" reaches any pi-supported provider Shannon does not curate.
|
||||
const selected = await p.select({
|
||||
message: 'Select your AI provider',
|
||||
options: [
|
||||
@@ -69,14 +76,17 @@ export async function setup(): Promise<void> {
|
||||
{ value: 'xai' as const, label: 'xAI', hint: 'Grok models' },
|
||||
{ value: 'amazon-bedrock' as const, label: 'AWS Bedrock', hint: 'Claude models via AWS' },
|
||||
{ value: CUSTOM_BASE_URL as typeof CUSTOM_BASE_URL, label: 'Custom Base URL', hint: 'your own proxy or gateway' },
|
||||
{
|
||||
value: OTHER_PROVIDER as typeof OTHER_PROVIDER,
|
||||
label: 'Other provider',
|
||||
hint: 'any other Pi-supported provider',
|
||||
},
|
||||
],
|
||||
});
|
||||
if (p.isCancel(selected)) return cancelAndExit();
|
||||
|
||||
// 2. Credentials — and, on the gateway route, the endpoint and its dialect.
|
||||
const gateway = selected === CUSTOM_BASE_URL ? await setupGateway() : undefined;
|
||||
const provider = gateway?.provider ?? (selected as ProviderId);
|
||||
const config = gateway?.config ?? (await setupProvider(provider));
|
||||
const { provider, config, gateway } = await setupSelection(selected);
|
||||
|
||||
// 3. The model that runs every phase.
|
||||
const modelId = await promptModel(provider);
|
||||
@@ -95,7 +105,27 @@ export async function setup(): Promise<void> {
|
||||
p.outro('Run `npx @keygraph/shannon start` to begin a scan.');
|
||||
}
|
||||
|
||||
async function setupProvider(provider: ProviderId): Promise<ShannonConfig> {
|
||||
interface Selection {
|
||||
provider: string;
|
||||
config: ShannonConfig;
|
||||
gateway?: GatewaySetup;
|
||||
}
|
||||
|
||||
/** Resolve the provider selection into a provider id and its credential config. */
|
||||
async function setupSelection(
|
||||
selected: CuratedProviderId | typeof CUSTOM_BASE_URL | typeof OTHER_PROVIDER,
|
||||
): Promise<Selection> {
|
||||
if (selected === CUSTOM_BASE_URL) {
|
||||
const gateway = await setupGateway();
|
||||
return { provider: gateway.provider, config: gateway.config, gateway };
|
||||
}
|
||||
if (selected === OTHER_PROVIDER) {
|
||||
return setupOtherProvider();
|
||||
}
|
||||
return { provider: selected, config: await setupProvider(selected) };
|
||||
}
|
||||
|
||||
async function setupProvider(provider: CuratedProviderId): Promise<ShannonConfig> {
|
||||
switch (provider) {
|
||||
case 'amazon-bedrock':
|
||||
return setupBedrock();
|
||||
@@ -108,6 +138,27 @@ async function setupProvider(provider: ProviderId): Promise<ShannonConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Any pi provider Shannon does not curate. The id is free text — the worker's
|
||||
* preflight validates it — and the key is stored generically as SHANNON_AI_API_KEY.
|
||||
*/
|
||||
async function setupOtherProvider(): Promise<Selection> {
|
||||
p.log.info('Browse supported providers and models at https://pi.dev/models');
|
||||
const provider = await p.text({
|
||||
message: 'Provider ID',
|
||||
validate: (value) => {
|
||||
const id = value?.trim();
|
||||
if (!id) return 'Provider ID is required';
|
||||
if (isCuratedProvider(id)) return `${id} has its own option.`;
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(provider)) return cancelAndExit();
|
||||
|
||||
const apiKey = await promptSecret('Enter the API key');
|
||||
return { provider: provider.trim(), config: { provider: { api_key: apiKey } } };
|
||||
}
|
||||
|
||||
// === Provider Setup Flows ===
|
||||
|
||||
async function setupAnthropic(): Promise<ShannonConfig> {
|
||||
@@ -143,7 +194,7 @@ async function setupBedrock(): Promise<ShannonConfig> {
|
||||
}
|
||||
|
||||
interface GatewaySetup {
|
||||
provider: ProviderId;
|
||||
provider: CuratedProviderId;
|
||||
config: ShannonConfig;
|
||||
baseUrl: string;
|
||||
format?: OpenAiFormat;
|
||||
@@ -195,11 +246,11 @@ async function setupGateway(): Promise<GatewaySetup> {
|
||||
* Ask for the one model that runs every phase. Providers with suggestions offer a
|
||||
* pick list with a free-text escape hatch; the rest go straight to free text.
|
||||
*/
|
||||
async function promptModel(provider: ProviderId): Promise<string> {
|
||||
const suggestions = MODEL_SUGGESTIONS[provider];
|
||||
async function promptModel(provider: string): Promise<string> {
|
||||
const suggestions = isCuratedProvider(provider) ? MODEL_SUGGESTIONS[provider] : [];
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return promptModelId(provider, MODEL_ID_PLACEHOLDER[provider]);
|
||||
return promptModelId(provider, modelIdPlaceholder(provider));
|
||||
}
|
||||
|
||||
const choice = await p.select({
|
||||
@@ -212,7 +263,7 @@ async function promptModel(provider: ProviderId): Promise<string> {
|
||||
if (p.isCancel(choice)) return cancelAndExit();
|
||||
|
||||
if (choice === CUSTOM_MODEL) {
|
||||
return promptModelId(provider, MODEL_ID_PLACEHOLDER[provider]);
|
||||
return promptModelId(provider, modelIdPlaceholder(provider));
|
||||
}
|
||||
return choice as string;
|
||||
}
|
||||
@@ -222,13 +273,13 @@ async function promptModel(provider: ProviderId): Promise<string> {
|
||||
* one. Bedrock model IDs carry their own colons (`…-v1:0`), so only a genuine
|
||||
* provider id counts as a prefix.
|
||||
*/
|
||||
function conflictingProviderPrefix(provider: ProviderId, value: string): string | undefined {
|
||||
function conflictingProviderPrefix(provider: string, value: string): string | undefined {
|
||||
const separator = value.indexOf(':');
|
||||
if (separator === -1) return undefined;
|
||||
|
||||
const head = value.slice(0, separator);
|
||||
if (head === provider) return undefined;
|
||||
return (SUPPORTED_PROVIDERS as readonly string[]).includes(head) ? head : undefined;
|
||||
return (CURATED_PROVIDERS as readonly string[]).includes(head) ? head : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,10 +287,10 @@ function conflictingProviderPrefix(provider: ProviderId, value: string): string
|
||||
* and the caller pairs it with the provider — pasting a full `<provider>:<model>`
|
||||
* spec just has its redundant prefix dropped.
|
||||
*/
|
||||
async function promptModelId(provider: ProviderId, placeholder: string): Promise<string> {
|
||||
async function promptModelId(provider: string, placeholder?: string): Promise<string> {
|
||||
const modelId = await p.text({
|
||||
message: 'Model ID',
|
||||
placeholder,
|
||||
...(placeholder && { placeholder }),
|
||||
validate: (value) => {
|
||||
if (!value) return 'Model ID is required';
|
||||
const conflicting = conflictingProviderPrefix(provider, value);
|
||||
|
||||
@@ -9,7 +9,13 @@ import fs from 'node:fs';
|
||||
import { parse as parseTOML } from 'smol-toml';
|
||||
import { getConfigFile } from '../home.js';
|
||||
import { getMode } from '../mode.js';
|
||||
import { DEFAULT_MODEL_SPEC, type ProviderId, parseModelSpec } from '../model-spec.js';
|
||||
import {
|
||||
type CuratedProviderId,
|
||||
DEFAULT_MODEL_SPEC,
|
||||
GENERIC_API_KEY_ENV,
|
||||
isCuratedProvider,
|
||||
parseModelSpec,
|
||||
} from '../model-spec.js';
|
||||
|
||||
// === TOML ↔ Env Mapping ===
|
||||
|
||||
@@ -42,16 +48,22 @@ const CONFIG_MAP: readonly ConfigMapping[] = [
|
||||
// Bedrock
|
||||
{ env: 'AWS_REGION', toml: 'bedrock.region', type: 'string' },
|
||||
{ env: 'AWS_BEARER_TOKEN_BEDROCK', toml: 'bedrock.token', type: 'string' },
|
||||
|
||||
// Generic — credential for any provider Shannon does not curate
|
||||
{ env: GENERIC_API_KEY_ENV, toml: 'provider.api_key', type: 'string' },
|
||||
] as const;
|
||||
|
||||
/** TOML section holding each provider's credentials, keyed by provider id. */
|
||||
const PROVIDER_SECTIONS: Readonly<Record<ProviderId, string>> = {
|
||||
/** TOML section holding each curated provider's credentials, keyed by provider id. */
|
||||
const PROVIDER_SECTIONS: Readonly<Record<CuratedProviderId, string>> = {
|
||||
anthropic: 'anthropic',
|
||||
openai: 'openai',
|
||||
xai: 'xai',
|
||||
'amazon-bedrock': 'bedrock',
|
||||
};
|
||||
|
||||
/** TOML section holding the generic credential for uncurated providers. */
|
||||
const GENERIC_PROVIDER_SECTION = 'provider';
|
||||
|
||||
// === TOML Parsing ===
|
||||
|
||||
type TOMLValue = string | number | boolean;
|
||||
@@ -128,9 +140,18 @@ function buildSchema(): Map<string, Map<string, TOMLType>> {
|
||||
/**
|
||||
* Check that the section backing the selected provider carries a usable
|
||||
* credential. `core.model` names the provider, so only that section is required;
|
||||
* other providers' sections are ignored and never forwarded.
|
||||
* other providers' sections are ignored and never forwarded. An uncurated
|
||||
* provider draws its credential from the generic [provider] section.
|
||||
*/
|
||||
function validateProviderFields(config: TOMLConfig, providerId: ProviderId, errors: string[]): void {
|
||||
function validateProviderFields(config: TOMLConfig, providerId: string, errors: string[]): void {
|
||||
if (!isCuratedProvider(providerId)) {
|
||||
const section = config[GENERIC_PROVIDER_SECTION] as Record<string, unknown> | undefined;
|
||||
if (!section || !Object.keys(section).includes('api_key')) {
|
||||
errors.push(`[${GENERIC_PROVIDER_SECTION}] requires api_key for provider "${providerId}"`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionName = PROVIDER_SECTIONS[providerId];
|
||||
const section = config[sectionName] as Record<string, unknown> | undefined;
|
||||
const keys = section ? Object.keys(section) : [];
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface ShannonConfig {
|
||||
openai?: { api_key?: string; format?: string };
|
||||
xai?: { api_key?: string };
|
||||
bedrock?: { region?: string; token?: string };
|
||||
/** Generic credential for any provider Shannon does not curate. Maps to SHANNON_AI_API_KEY. */
|
||||
provider?: { api_key?: string };
|
||||
}
|
||||
|
||||
// === File Operations ===
|
||||
|
||||
+33
-19
@@ -9,25 +9,35 @@ import dotenv from 'dotenv';
|
||||
import { resolveConfig } from './config/resolver.js';
|
||||
import { getMode } from './mode.js';
|
||||
import {
|
||||
CURATED_PROVIDERS,
|
||||
type CuratedProviderId,
|
||||
GENERIC_API_KEY_ENV,
|
||||
isCuratedProvider,
|
||||
PROVIDER_API_KEY_ENV,
|
||||
PROVIDER_CREDENTIAL_HINT,
|
||||
PROVIDER_EXTRA_ENV,
|
||||
type ProviderId,
|
||||
resolveModelSpec,
|
||||
SUPPORTED_PROVIDERS,
|
||||
} from './model-spec.js';
|
||||
|
||||
/**
|
||||
* Variables forwarded to every worker container regardless of provider. Each is
|
||||
* forwarded only when set, so an unused one never appears in the container.
|
||||
* SHANNON_AI_API_KEY rides along because it is provider-neutral.
|
||||
*/
|
||||
const COMMON_FORWARD_VARS = ['SHANNON_AI_MODEL', 'SHANNON_AI_BASE_URL', 'SHANNON_AI_OPENAI_FORMAT'] as const;
|
||||
const COMMON_FORWARD_VARS = [
|
||||
'SHANNON_AI_MODEL',
|
||||
'SHANNON_AI_BASE_URL',
|
||||
'SHANNON_AI_OPENAI_FORMAT',
|
||||
GENERIC_API_KEY_ENV,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Credential variables for one provider. Only the selected provider's entries are
|
||||
* forwarded, so a key for an unused provider never enters the scan container.
|
||||
* forwarded, so a key for an unused provider never enters the scan container. An
|
||||
* uncurated provider has none — it relies on the common SHANNON_AI_API_KEY.
|
||||
*/
|
||||
function providerForwardVars(providerId: ProviderId): readonly string[] {
|
||||
function providerForwardVars(providerId: string): readonly string[] {
|
||||
if (!isCuratedProvider(providerId)) return [];
|
||||
return [...PROVIDER_API_KEY_ENV[providerId], ...PROVIDER_EXTRA_ENV[providerId]];
|
||||
}
|
||||
|
||||
@@ -70,22 +80,23 @@ interface CredentialValidation {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the selected provider has a usable credential in the environment. Any
|
||||
* one API key satisfies a key-based provider; Bedrock instead needs every one of
|
||||
* its AWS_ vars.
|
||||
*/
|
||||
function hasCredential(providerId: ProviderId): boolean {
|
||||
/** Whether a curated provider has its own named credential set (API key plus any extra var). */
|
||||
function hasNamedCredential(providerId: CuratedProviderId): boolean {
|
||||
const apiKeys = PROVIDER_API_KEY_ENV[providerId];
|
||||
if (apiKeys.length > 0 && !apiKeys.some((name) => Boolean(process.env[name]))) {
|
||||
return false;
|
||||
}
|
||||
if (!apiKeys.some((name) => Boolean(process.env[name]))) return false;
|
||||
return PROVIDER_EXTRA_ENV[providerId].every((name) => Boolean(process.env[name]));
|
||||
}
|
||||
|
||||
/** Every provider that currently has a complete credential in the environment. */
|
||||
function configuredProviders(): ProviderId[] {
|
||||
return SUPPORTED_PROVIDERS.filter((providerId) => hasCredential(providerId));
|
||||
/** Whether the selected provider has a credential. Bedrock needs its AWS_ vars; the generic key never stands in for it. */
|
||||
function hasCredential(providerId: string): boolean {
|
||||
if (providerId === 'amazon-bedrock') return hasNamedCredential('amazon-bedrock');
|
||||
if (isCuratedProvider(providerId) && hasNamedCredential(providerId)) return true;
|
||||
return Boolean(process.env[GENERIC_API_KEY_ENV]);
|
||||
}
|
||||
|
||||
/** Curated providers with a named credential. The generic key is neutral, so it never counts toward ambiguity. */
|
||||
function configuredProviders(): CuratedProviderId[] {
|
||||
return CURATED_PROVIDERS.filter((providerId) => hasNamedCredential(providerId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +104,7 @@ function configuredProviders(): ProviderId[] {
|
||||
* Runs before any Docker work so mistakes fail immediately.
|
||||
*/
|
||||
export function validateCredentials(): CredentialValidation {
|
||||
// 1. Model selection must parse and name a supported provider
|
||||
// 1. Model selection must parse into a provider and model id
|
||||
const spec = resolveModelSpec();
|
||||
if (typeof spec === 'string') {
|
||||
return { valid: false, error: spec };
|
||||
@@ -101,9 +112,12 @@ export function validateCredentials(): CredentialValidation {
|
||||
|
||||
// 2. The selected provider must have a credential
|
||||
if (!hasCredential(spec.providerId)) {
|
||||
const requirement = isCuratedProvider(spec.providerId)
|
||||
? PROVIDER_CREDENTIAL_HINT[spec.providerId]
|
||||
: GENERIC_API_KEY_ENV;
|
||||
const hint =
|
||||
getMode() === 'local'
|
||||
? `Set ${PROVIDER_CREDENTIAL_HINT[spec.providerId]} in .env or export it.`
|
||||
? `Set ${requirement} in .env or export it.`
|
||||
: `Export the variables or run 'npx @keygraph/shannon setup'.`;
|
||||
return {
|
||||
valid: false,
|
||||
|
||||
+23
-18
@@ -6,24 +6,35 @@
|
||||
* rule are duplicated here deliberately and must stay in sync.
|
||||
*/
|
||||
|
||||
/** Providers Shannon can currently reach. Each is a pi-ai provider id. */
|
||||
export const SUPPORTED_PROVIDERS = ['anthropic', 'openai', 'xai', 'amazon-bedrock'] as const;
|
||||
/**
|
||||
* Providers Shannon curates with their own credential variables, config sections,
|
||||
* and setup flows. Any other pi provider is reachable via the generic credential
|
||||
* path. Mirrors CURATED_PROVIDERS in apps/worker/src/ai/models.ts.
|
||||
*/
|
||||
export const CURATED_PROVIDERS = ['anthropic', 'openai', 'xai', 'amazon-bedrock'] as const;
|
||||
|
||||
export type ProviderId = (typeof SUPPORTED_PROVIDERS)[number];
|
||||
export type CuratedProviderId = (typeof CURATED_PROVIDERS)[number];
|
||||
|
||||
export function isCuratedProvider(value: string): value is CuratedProviderId {
|
||||
return (CURATED_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Generic API key, honored for any provider Shannon does not curate. Mirrors the worker. */
|
||||
export const GENERIC_API_KEY_ENV = 'SHANNON_AI_API_KEY';
|
||||
|
||||
/**
|
||||
* Env vars carrying each provider's API key, in precedence order. Any one of them
|
||||
* satisfies the provider. Mirrors PROVIDER_API_KEY_ENV in apps/worker/src/ai/models.ts.
|
||||
* Env vars carrying each curated provider's API key, in precedence order. Any one of
|
||||
* them satisfies the provider. Mirrors PROVIDER_API_KEY_ENV in apps/worker/src/ai/models.ts.
|
||||
*/
|
||||
export const PROVIDER_API_KEY_ENV: Readonly<Record<ProviderId, readonly string[]>> = {
|
||||
export const PROVIDER_API_KEY_ENV: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
||||
anthropic: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
||||
openai: ['OPENAI_API_KEY'],
|
||||
xai: ['XAI_API_KEY'],
|
||||
'amazon-bedrock': ['AWS_BEARER_TOKEN_BEDROCK'],
|
||||
};
|
||||
|
||||
/** Additional env vars a provider requires beyond its API key. All must be set. */
|
||||
export const PROVIDER_EXTRA_ENV: Readonly<Record<ProviderId, readonly string[]>> = {
|
||||
/** Additional env vars a curated provider requires beyond its API key. All must be set. */
|
||||
export const PROVIDER_EXTRA_ENV: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
||||
anthropic: [],
|
||||
openai: [],
|
||||
xai: [],
|
||||
@@ -31,7 +42,7 @@ export const PROVIDER_EXTRA_ENV: Readonly<Record<ProviderId, readonly string[]>>
|
||||
};
|
||||
|
||||
/** Human-readable credential requirement, used in "nothing configured" errors. */
|
||||
export const PROVIDER_CREDENTIAL_HINT: Readonly<Record<ProviderId, string>> = {
|
||||
export const PROVIDER_CREDENTIAL_HINT: Readonly<Record<CuratedProviderId, string>> = {
|
||||
anthropic: 'ANTHROPIC_API_KEY (or CLAUDE_CODE_OAUTH_TOKEN)',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
@@ -51,18 +62,15 @@ export const OPENAI_FORMATS = ['chat-completions', 'responses'] as const;
|
||||
export type OpenAiFormat = (typeof OPENAI_FORMATS)[number];
|
||||
|
||||
export interface ModelSpec {
|
||||
providerId: ProviderId;
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
function isSupportedProvider(value: string): value is ProviderId {
|
||||
return (SUPPORTED_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `<provider>:<model-id>` spec. Splits on the first colon only, so colons
|
||||
* inside a model ID survive (`amazon-bedrock:us.anthropic.claude-opus-4-5-20251101-v1:0`).
|
||||
* Returns an error string rather than throwing, for the CLI's validation flow.
|
||||
* The provider id is passed through as given — the worker's preflight validates it
|
||||
* against pi. Returns an error string rather than throwing, for the CLI's flow.
|
||||
*/
|
||||
export function parseModelSpec(spec: string): ModelSpec | string {
|
||||
const trimmed = spec.trim();
|
||||
@@ -74,9 +82,6 @@ export function parseModelSpec(spec: string): ModelSpec | string {
|
||||
const modelId = trimmed.slice(separator + 1).trim();
|
||||
if (!providerId || !modelId) return malformed;
|
||||
|
||||
if (!isSupportedProvider(providerId)) {
|
||||
return `Unsupported provider "${providerId}" in SHANNON_AI_MODEL. Supported providers: ${SUPPORTED_PROVIDERS.join(', ')}`;
|
||||
}
|
||||
return { providerId, modelId };
|
||||
}
|
||||
|
||||
|
||||
@@ -24,18 +24,29 @@
|
||||
import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai';
|
||||
import { ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||
|
||||
/** Providers Shannon can currently reach. Each is a pi-ai provider id. */
|
||||
export const SUPPORTED_PROVIDERS = ['anthropic', 'openai', 'xai', 'amazon-bedrock'] as const;
|
||||
/**
|
||||
* Providers Shannon curates with their own credential variables, config sections,
|
||||
* and setup flows. Each is a pi-ai provider id; any other pi provider is still
|
||||
* reachable through the generic credential path below.
|
||||
*/
|
||||
export const CURATED_PROVIDERS = ['anthropic', 'openai', 'xai', 'amazon-bedrock'] as const;
|
||||
|
||||
export type ProviderId = (typeof SUPPORTED_PROVIDERS)[number];
|
||||
export type CuratedProviderId = (typeof CURATED_PROVIDERS)[number];
|
||||
|
||||
function isCuratedProvider(value: string): value is CuratedProviderId {
|
||||
return (CURATED_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Generic API key, honored for any provider Shannon does not curate. */
|
||||
export const GENERIC_API_KEY_ENV = 'SHANNON_AI_API_KEY';
|
||||
|
||||
/**
|
||||
* Env vars carrying each provider's API key, in precedence order. Shannon does not
|
||||
* invent credential names — these are the variables each provider's own tooling
|
||||
* uses. Bedrock pairs its bearer token with AWS_REGION, which is provider config
|
||||
* rather than a credential.
|
||||
* Env vars carrying each curated provider's API key, in precedence order. Shannon
|
||||
* does not invent credential names — these are the variables each provider's own
|
||||
* tooling uses. Bedrock pairs its bearer token with AWS_REGION, which is provider
|
||||
* config rather than a credential.
|
||||
*/
|
||||
export const PROVIDER_API_KEY_ENV: Readonly<Record<ProviderId, readonly string[]>> = {
|
||||
export const PROVIDER_API_KEY_ENV: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
||||
anthropic: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
||||
openai: ['OPENAI_API_KEY'],
|
||||
xai: ['XAI_API_KEY'],
|
||||
@@ -45,6 +56,9 @@ export const PROVIDER_API_KEY_ENV: Readonly<Record<ProviderId, readonly string[]
|
||||
/** Model used when SHANNON_AI_MODEL is unset. */
|
||||
export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
||||
|
||||
/** Browsable pi model catalogue — the source of valid `<provider>:<model-id>` ids. */
|
||||
export const PI_CATALOG_URL = 'https://pi.dev/models';
|
||||
|
||||
/**
|
||||
* Wire formats an OpenAI-compatible gateway may serve, named by
|
||||
* SHANNON_AI_OPENAI_FORMAT. Only `openai` offers a choice: every other supported
|
||||
@@ -82,18 +96,14 @@ export function resolveOpenAiFormat(): OpenAiFormat | undefined {
|
||||
}
|
||||
|
||||
export interface ModelSpec {
|
||||
providerId: ProviderId;
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
function isSupportedProvider(value: string): value is ProviderId {
|
||||
return (SUPPORTED_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `<provider>:<model-id>` spec. Splits on the first colon only, so
|
||||
* colons inside a model ID survive. Throws with the supported provider list on
|
||||
* a malformed or unknown provider.
|
||||
* Parse a `<provider>:<model-id>` spec. Splits on the first colon only, so colons
|
||||
* inside a model ID survive. The provider id is passed through as given — pi's
|
||||
* registry validates it later — so this throws only on a malformed spec.
|
||||
*/
|
||||
export function parseModelSpec(spec: string): ModelSpec {
|
||||
const trimmed = spec.trim();
|
||||
@@ -112,11 +122,6 @@ export function parseModelSpec(spec: string): ModelSpec {
|
||||
`SHANNON_AI_MODEL must be "<provider>:<model-id>", got "${trimmed}". Example: ${DEFAULT_MODEL_SPEC}`,
|
||||
);
|
||||
}
|
||||
if (!isSupportedProvider(providerId)) {
|
||||
throw new Error(
|
||||
`Unsupported provider "${providerId}" in SHANNON_AI_MODEL. Supported providers: ${SUPPORTED_PROVIDERS.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { providerId, modelId };
|
||||
}
|
||||
@@ -133,17 +138,25 @@ export interface ProviderCredentials {
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
/** Collect the API key and optional endpoint override for a provider. */
|
||||
export function resolveProviderCredentials(providerId: ProviderId): ProviderCredentials {
|
||||
/**
|
||||
* Collect the API key and optional endpoint override for a provider. A curated
|
||||
* provider's own variables win, then the generic SHANNON_AI_API_KEY. Bedrock is
|
||||
* excluded — it authenticates through its AWS_ variables, which pi reads directly.
|
||||
*/
|
||||
export function resolveProviderCredentials(providerId: string): ProviderCredentials {
|
||||
const credentials: ProviderCredentials = {};
|
||||
|
||||
for (const name of PROVIDER_API_KEY_ENV[providerId]) {
|
||||
const namedVars = isCuratedProvider(providerId) ? PROVIDER_API_KEY_ENV[providerId] : [];
|
||||
for (const name of namedVars) {
|
||||
const value = process.env[name];
|
||||
if (value) {
|
||||
credentials.apiKey = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!credentials.apiKey && providerId !== 'amazon-bedrock' && process.env[GENERIC_API_KEY_ENV]) {
|
||||
credentials.apiKey = process.env[GENERIC_API_KEY_ENV];
|
||||
}
|
||||
if (process.env.SHANNON_AI_BASE_URL) credentials.baseUrl = process.env.SHANNON_AI_BASE_URL;
|
||||
|
||||
return credentials;
|
||||
@@ -203,7 +216,7 @@ export interface ModelSelection {
|
||||
model: Model<Api>;
|
||||
modelRuntime: ModelRuntime;
|
||||
modelId: string;
|
||||
providerId: ProviderId;
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +231,7 @@ export interface ModelSelection {
|
||||
* then describes the format in use. Every other provider has one API and only
|
||||
* changes address.
|
||||
*/
|
||||
function pointAtGateway(model: Model<Api>, providerId: ProviderId, baseUrl: string, format: OpenAiFormat): Model<Api> {
|
||||
function pointAtGateway(model: Model<Api>, providerId: string, baseUrl: string, format: OpenAiFormat): Model<Api> {
|
||||
if (providerId !== 'openai') return { ...model, baseUrl };
|
||||
if (format === 'responses') return { ...model, baseUrl, api: OPENAI_FORMATS.responses };
|
||||
|
||||
@@ -240,7 +253,7 @@ function pointAtGateway(model: Model<Api>, providerId: ProviderId, baseUrl: stri
|
||||
*/
|
||||
export function resolveModel(
|
||||
modelRuntime: ModelRuntime,
|
||||
providerId: ProviderId,
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
baseUrl: string | undefined,
|
||||
format: OpenAiFormat = DEFAULT_OPENAI_FORMAT,
|
||||
@@ -265,7 +278,7 @@ export function resolveModel(
|
||||
* are configured, so it is rejected outside that combination rather than
|
||||
* silently ignored.
|
||||
*/
|
||||
export function resolveGatewayFormat(providerId: ProviderId, baseUrl: string | undefined): OpenAiFormat {
|
||||
export function resolveGatewayFormat(providerId: string, baseUrl: string | undefined): OpenAiFormat {
|
||||
const configured = resolveOpenAiFormat();
|
||||
if (!configured) return DEFAULT_OPENAI_FORMAT;
|
||||
|
||||
@@ -296,7 +309,9 @@ export async function resolveModelSelection(): Promise<ModelSelection> {
|
||||
|
||||
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl, format);
|
||||
if (!model) {
|
||||
throw new Error(`Model not found in pi registry: provider="${providerId}" model="${modelId}"`);
|
||||
throw new Error(
|
||||
`Model not found in pi registry: provider="${providerId}" model="${modelId}". Browse valid providers and models at ${PI_CATALOG_URL}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -36,10 +36,12 @@ import {
|
||||
} from '@earendil-works/pi-coding-agent';
|
||||
import { glob } from 'zx';
|
||||
import {
|
||||
type CuratedProviderId,
|
||||
createModelRuntime,
|
||||
GENERIC_API_KEY_ENV,
|
||||
type ModelSpec,
|
||||
type OpenAiFormat,
|
||||
type ProviderId,
|
||||
PI_CATALOG_URL,
|
||||
resolveGatewayFormat,
|
||||
resolveModel,
|
||||
resolveModelSpec,
|
||||
@@ -277,16 +279,22 @@ async function probeCredentialsWithPi(
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
/** Credential env var a provider reads, for "credential missing" messages. */
|
||||
const PROVIDER_CREDENTIAL_HINT: Readonly<Record<ProviderId, string>> = {
|
||||
/** Credential env var a curated provider reads, for "credential missing" messages. */
|
||||
const PROVIDER_CREDENTIAL_HINT: Readonly<Record<CuratedProviderId, string>> = {
|
||||
anthropic: 'ANTHROPIC_API_KEY (or CLAUDE_CODE_OAUTH_TOKEN)',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
'amazon-bedrock': 'AWS_BEARER_TOKEN_BEDROCK and AWS_REGION',
|
||||
};
|
||||
|
||||
/** Which variable to set when a provider's credential is missing. */
|
||||
function credentialHint(providerId: string): string {
|
||||
const curated = (PROVIDER_CREDENTIAL_HINT as Record<string, string | undefined>)[providerId];
|
||||
return curated ?? GENERIC_API_KEY_ENV;
|
||||
}
|
||||
|
||||
/** Human-readable label for which credential path a run is using. */
|
||||
function describeAuth(providerId: ProviderId, baseUrl: string | undefined): string {
|
||||
function describeAuth(providerId: string, baseUrl: string | undefined): string {
|
||||
if (baseUrl) return `custom endpoint (${baseUrl})`;
|
||||
if (providerId === 'amazon-bedrock') return 'Bedrock bearer token';
|
||||
return `${providerId} API key`;
|
||||
@@ -338,7 +346,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
if (missing.length > 0 || (!isBedrock && !credentials.apiKey)) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`No credentials found for provider "${spec.providerId}". Set ${PROVIDER_CREDENTIAL_HINT[spec.providerId]} in .env.`,
|
||||
`No credentials found for provider "${spec.providerId}". Set ${credentialHint(spec.providerId)} in .env.`,
|
||||
'config',
|
||||
false,
|
||||
{ providerId: spec.providerId, ...(missing.length > 0 && { missing }) },
|
||||
@@ -356,7 +364,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
if (!baseModel) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL.`,
|
||||
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL — browse valid providers and models at ${PI_CATALOG_URL}.`,
|
||||
'config',
|
||||
false,
|
||||
{ providerId: spec.providerId, modelId: spec.modelId },
|
||||
|
||||
+27
-16
@@ -12,17 +12,29 @@ The provider half decides where the request goes, which credential is used, and
|
||||
|
||||
| Provider | Value | Credential |
|
||||
| --- | --- | --- |
|
||||
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` |
|
||||
| OpenAI | `openai` | `OPENAI_API_KEY` |
|
||||
| xAI | `xai` | `XAI_API_KEY` |
|
||||
| Anthropic | `anthropic` | `SHANNON_AI_API_KEY` (or `CLAUDE_CODE_OAUTH_TOKEN`) |
|
||||
| OpenAI | `openai` | `SHANNON_AI_API_KEY` |
|
||||
| xAI | `xai` | `SHANNON_AI_API_KEY` |
|
||||
| AWS Bedrock | `amazon-bedrock` | `AWS_REGION` and `AWS_BEARER_TOKEN_BEDROCK` |
|
||||
|
||||
Shannon does not invent credential names — each is the variable that provider's own tooling already uses. If `SHANNON_AI_MODEL` is unset, Shannon uses `anthropic:claude-sonnet-4-6`.
|
||||
`SHANNON_AI_API_KEY` holds the key for whichever provider `SHANNON_AI_MODEL` names. Bedrock is the exception — it authenticates through its `AWS_` variables only. If `SHANNON_AI_MODEL` is unset, Shannon uses `anthropic:claude-sonnet-4-6`.
|
||||
|
||||
Anthropic, OpenAI, and xAI also accept their native variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`); if one of those is set, it is used instead of `SHANNON_AI_API_KEY`.
|
||||
|
||||
Shannon forwards only the selected provider's credential into the scan container. Keys for other providers stay on your machine.
|
||||
|
||||
> [!NOTE]
|
||||
> Only the **first** colon separates the provider from the model ID, so Bedrock IDs that contain colons work unchanged: `amazon-bedrock:us.anthropic.claude-opus-4-5-20251101-v1:0`.
|
||||
### Any other provider
|
||||
|
||||
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models). These are technically supported but not recommended. Claude models are best-supported (see the note below).
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=your-api-key # the provider's API key
|
||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||
```
|
||||
|
||||
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
||||
|
||||
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Claude models are the best-supported option. Shannon's evaluations, internal testing, and agent harness are tuned for Claude. Other models are permitted and validated against the harness catalogue, but may not follow Shannon's instructions or tool-use constraints as reliably. Use them at your own risk.
|
||||
@@ -58,21 +70,21 @@ The pattern is learned once: export the provider's key, name the model. Two line
|
||||
Anthropic (default):
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export SHANNON_AI_API_KEY=sk-ant-...
|
||||
export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
```
|
||||
|
||||
OpenAI:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export SHANNON_AI_API_KEY=sk-...
|
||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
||||
```
|
||||
|
||||
xAI:
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY=xai-...
|
||||
export SHANNON_AI_API_KEY=xai-...
|
||||
export SHANNON_AI_MODEL=xai:grok-4.5
|
||||
```
|
||||
|
||||
@@ -96,16 +108,16 @@ To route model traffic through your own infrastructure — a corporate proxy, an
|
||||
|
||||
| Gateway serves | Model prefix | API key |
|
||||
| --- | --- | --- |
|
||||
| Anthropic Messages | `anthropic:` | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI Chat Completions | `openai:` | `OPENAI_API_KEY` |
|
||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `OPENAI_API_KEY` |
|
||||
| Anthropic Messages | `anthropic:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Chat Completions | `openai:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `SHANNON_AI_API_KEY` |
|
||||
|
||||
The model ID is whatever name your gateway serves it under; it does not have to exist in Shannon's catalogue.
|
||||
|
||||
Anthropic Messages:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export SHANNON_AI_API_KEY=sk-ant-...
|
||||
export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
```
|
||||
@@ -113,7 +125,7 @@ export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
OpenAI Chat Completions:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export SHANNON_AI_API_KEY=sk-...
|
||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||
```
|
||||
@@ -136,8 +148,7 @@ The variable is rejected in preflight where it cannot take effect: with a non-`o
|
||||
|
||||
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
||||
|
||||
- **Provider** — always validated against the providers Shannon's harness knows. An unrecognised provider is rejected with the valid list.
|
||||
- **Model ID** — validated against the harness catalogue for that provider, so a typo is caught instantly.
|
||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
|
||||
- **Credential presence** — always validated for the selected provider.
|
||||
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user