feat: per-provider custom base URL, OpenAI Responses only (#445)

* feat: drop OpenAI chat-completions gateway format, keep Responses only

* docs: reframe custom base URL as a universal endpoint override

* docs: show optional base URL in the any-other-provider example
This commit is contained in:
ezl-keygraph
2026-09-03 20:01:32 +05:30
committed by GitHub
parent e92ee61c05
commit 4b8131fdd5
15 changed files with 112 additions and 227 deletions
+3 -87
View File
@@ -78,42 +78,6 @@ 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
* provider has exactly one API in pi's registry.
*/
export const OPENAI_FORMATS = {
'chat-completions': 'openai-completions',
responses: 'openai-responses',
} as const;
export type OpenAiFormat = keyof typeof OPENAI_FORMATS;
/** Format assumed when a gateway is configured but no format is named. */
export const DEFAULT_OPENAI_FORMAT: OpenAiFormat = 'chat-completions';
function isOpenAiFormat(value: string): value is OpenAiFormat {
return value in OPENAI_FORMATS;
}
/**
* Read SHANNON_AI_OPENAI_FORMAT. Unset returns undefined, which lets the caller
* distinguish "not configured" from an explicit choice and reject the variable
* where it has no effect.
*/
export function resolveOpenAiFormat(): OpenAiFormat | undefined {
const raw = process.env.SHANNON_AI_OPENAI_FORMAT?.trim();
if (!raw) return undefined;
if (!isOpenAiFormat(raw)) {
throw new Error(
`SHANNON_AI_OPENAI_FORMAT must be one of: ${Object.keys(OPENAI_FORMATS).join(', ')}. Got "${raw}".`,
);
}
return raw;
}
export interface ModelSpec {
providerId: string;
modelId: string;
@@ -256,26 +220,6 @@ export interface ModelSelection {
readonly credentialSource: 'api-key' | 'pi-auth' | 'ambient';
}
/**
* Point a model descriptor at a gateway.
*
* An OpenAI gateway may serve either wire format, named by
* SHANNON_AI_OPENAI_FORMAT and defaulting to chat completions, which is what
* most gateway software exposes. Switching to completions also drops the stored
* `compat` block: the catalogue's block describes Responses, and an explicit
* entry outranks pi's `detectCompat`, so leaving it would apply Responses
* settings to a completions request. Staying on Responses keeps it, since it
* then describes the format in use. Every other provider has one API and only
* changes address.
*/
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 };
const { compat: _responsesCompat, ...withoutCompat } = model;
return { ...withoutCompat, baseUrl, api: OPENAI_FORMATS['chat-completions'] };
}
/**
* Resolve a model against a runtime.
*
@@ -293,44 +237,17 @@ export function resolveModel(
providerId: string,
modelId: string,
baseUrl: string | undefined,
format: OpenAiFormat = DEFAULT_OPENAI_FORMAT,
): Model<Api> | undefined {
const found = modelRuntime.getModel(providerId, modelId);
if (found) {
return baseUrl ? pointAtGateway(found, providerId, baseUrl, format) : found;
return baseUrl ? { ...found, baseUrl } : found;
}
if (!baseUrl) return undefined;
const reference = modelRuntime.getModels(providerId)[0];
if (!reference) return undefined;
return pointAtGateway({ ...reference, id: modelId, name: modelId }, providerId, baseUrl, format);
}
/**
* Validate SHANNON_AI_OPENAI_FORMAT against the rest of the configuration and
* return the format a gateway run should use.
*
* The variable only reaches a request when both an OpenAI model and a gateway
* are configured, so it is rejected outside that combination rather than
* silently ignored.
*/
export function resolveGatewayFormat(providerId: string, baseUrl: string | undefined): OpenAiFormat {
const configured = resolveOpenAiFormat();
if (!configured) return DEFAULT_OPENAI_FORMAT;
if (providerId !== 'openai') {
throw new Error(
`SHANNON_AI_OPENAI_FORMAT applies to openai models only, but SHANNON_AI_MODEL selects "${providerId}". ` +
`${providerId} serves a single API, so there is no format to choose.`,
);
}
if (!baseUrl) {
throw new Error(
'SHANNON_AI_OPENAI_FORMAT applies to gateway runs only. Set SHANNON_AI_BASE_URL, or unset the format to call OpenAI directly.',
);
}
return configured;
return { ...reference, id: modelId, name: modelId, baseUrl };
}
/**
@@ -340,12 +257,11 @@ export function resolveGatewayFormat(providerId: string, baseUrl: string | undef
export async function resolveModelSelection(): Promise<ModelSelection> {
const { providerId, modelId } = resolveModelSpec();
const credentials = resolveProviderCredentials(providerId);
const format = resolveGatewayFormat(providerId, credentials.baseUrl);
const mountedPiAuth = piAuthPresent();
const modelRuntime = await createModelRuntime(providerId, credentials.apiKey);
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl, format);
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl);
if (!model) {
throw new Error(
`Model not found in pi registry: provider="${providerId}" model="${modelId}". Browse valid providers and models at ${PI_CATALOG_URL}.`,
+5 -24
View File
@@ -40,10 +40,8 @@ import {
createModelRuntime,
GENERIC_API_KEY_ENV,
type ModelSpec,
type OpenAiFormat,
PI_CATALOG_URL,
piAuthPresent,
resolveGatewayFormat,
resolveModel,
resolveModelSpec,
resolveProviderCredentials,
@@ -329,24 +327,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
// needs one API key.
const credentials = resolveProviderCredentials(spec.providerId);
// 3. Wire format for an OpenAI gateway. Rejects a format named where it cannot
// take effect, rather than letting the run proceed on the wrong API.
let format: OpenAiFormat;
try {
format = resolveGatewayFormat(spec.providerId, credentials.baseUrl);
} catch (error) {
return err(
new PentestError(
error instanceof Error ? error.message : String(error),
'config',
false,
{ providerId: spec.providerId },
ErrorCode.AUTH_FAILED,
),
);
}
// With a mounted pi auth.json the env-var checks don't apply — step 5's probe validates it.
// With a mounted pi auth.json the env-var checks don't apply — step 4's probe validates it.
const isBedrock = spec.providerId === 'amazon-bedrock';
const missing =
isBedrock && !piAuthPresent() ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
@@ -362,12 +343,12 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
);
}
// 4. Model must exist in the registry, for every provider — Bedrock IDs are the
// 3. Model must exist in the registry, for every provider — Bedrock IDs are the
// easiest to get wrong, since region prefixes and version suffixes differ per
// model (`us.anthropic.claude-opus-5` exists, bare `anthropic.` does not).
// A custom endpoint is exempt: it may serve models under its own names.
const modelRuntime = await createModelRuntime(spec.providerId, credentials.apiKey);
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl, format);
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl);
if (!baseModel) {
return err(
new PentestError(
@@ -385,10 +366,10 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
);
}
if (credentials.baseUrl && spec.providerId === 'openai') {
logger.info(`Gateway API: ${format} (${baseModel.api})`);
logger.info(`Gateway API: ${baseModel.api}`);
}
// 5. One real request, so a credential the account cannot use fails here
// 4. One real request, so a credential the account cannot use fails here
// rather than partway through the run. Bedrock included: pi resolves the
// bearer token from the primed credential and the region from AWS_REGION,
// so the probe exercises the same auth path the scan will.