feat(ai): add Radius gateway support

This commit is contained in:
Armin Ronacher
2026-07-14 11:01:21 +02:00
parent 0e6909f050
commit 961fa6c142
16 changed files with 1483 additions and 11 deletions
@@ -6,10 +6,10 @@ import {
type AnthropicMessagesCompat,
type Api,
type AssistantMessageEventStream,
type BuiltinProvider,
type Context,
getModels,
getProviders,
type KnownProvider,
type Model,
type OAuthProviderInterface,
type OpenAICompletionsCompat,
@@ -29,6 +29,7 @@ import { stripJsonComments } from "../utils/json.ts";
import { normalizePath } from "../utils/paths.ts";
import type { AuthStatus, AuthStorage } from "./auth-storage.ts";
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.ts";
import { registerCustomRadiusOAuthProvider } from "./radius.ts";
import {
clearConfigValueCache,
getConfigValueEnvVarNames,
@@ -224,6 +225,9 @@ const ProviderConfigSchema = Type.Object({
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
apiKey: Type.Optional(Type.String({ minLength: 1 })),
api: Type.Optional(Type.String({ minLength: 1 })),
/** OAuth flavor spoken by this provider's endpoint. Registers a sign-in
* provider with a dynamic model catalog (e.g. a custom Radius gateway). */
oauth: Type.Optional(Type.Literal("radius")),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
compat: Type.Optional(ProviderCompatSchema),
authHeader: Type.Optional(Type.Boolean()),
@@ -453,7 +457,7 @@ export class ModelRegistry {
modelOverrides: Map<string, Map<string, ModelOverride>>,
): Model<Api>[] {
return getProviders().flatMap((provider) => {
const models = getModels(provider as KnownProvider) as Model<Api>[];
const models = getModels(provider as BuiltinProvider) as Model<Api>[];
const providerOverride = overrides.get(provider);
const perModelOverrides = modelOverrides.get(provider);
@@ -537,6 +541,12 @@ export class ModelRegistry {
});
}
if (providerConfig.oauth === "radius") {
// Must run before the modifyModels loop in loadModels() so the
// credential-cached catalog is injected on this load.
registerCustomRadiusOAuthProvider(providerName, providerConfig.name, providerConfig.baseUrl!);
}
this.storeProviderRequestConfig(providerName, providerConfig);
if (providerConfig.modelOverrides) {
@@ -568,7 +578,11 @@ export class ModelRegistry {
const hasModelOverrides =
providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
if (models.length === 0) {
if (providerConfig.oauth && !providerConfig.baseUrl) {
throw new Error(`Provider ${providerName}: "baseUrl" is required when "oauth" is set.`);
}
if (models.length === 0 && !providerConfig.oauth) {
// Override-only config: needs baseUrl, headers, compat, modelOverrides, or some combination.
if (!providerConfig.baseUrl && !providerConfig.headers && !providerConfig.compat && !hasModelOverrides) {
throw new Error(
@@ -614,7 +628,7 @@ export class ModelRegistry {
const getBuiltInDefaults = (providerName: string): { api: string; baseUrl: string } | undefined => {
if (!builtInProviders.has(providerName)) return undefined;
if (builtInDefaultsCache.has(providerName)) return builtInDefaultsCache.get(providerName);
const builtIn = getModels(providerName as KnownProvider) as Model<Api>[];
const builtIn = getModels(providerName as BuiltinProvider) as Model<Api>[];
if (builtIn.length === 0) return undefined;
const defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl };
builtInDefaultsCache.set(providerName, defaults);
@@ -18,6 +18,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
openai: "gpt-5.5",
"azure-openai-responses": "gpt-5.4",
"openai-codex": "gpt-5.5",
radius: "auto",
nvidia: "nvidia/nemotron-3-super-120b-a12b",
deepseek: "deepseek-v4-pro",
google: "gemini-3.1-pro-preview",
@@ -23,6 +23,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
"opencode-go": "OpenCode Go",
openai: "OpenAI",
openrouter: "OpenRouter",
radius: "Radius",
together: "Together AI",
"vercel-ai-gateway": "Vercel AI Gateway",
xai: "xAI",
+34
View File
@@ -0,0 +1,34 @@
/**
* Radius (pi-messages gateway) provider wiring.
*
* The main Radius provider is a built-in OAuth provider in pi-ai; models are
* dynamic, cached on the stored OAuth credential (`gatewayConfig`) and
* injected via the OAuth provider's `modifyModels` hook, so startup, /reload,
* and registry refreshes work without network access. The catalog refreshes
* on login and on every token refresh.
*
* Additional gateways (e.g. a local dev gateway) can be declared in
* models.json with `"oauth": "radius"`; each entry is an independent Radius
* instance with its own credentials and catalog.
*/
import { createRadiusOAuthProvider, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
export const RADIUS_PROVIDER_ID = "radius";
/**
* Register a Radius-style OAuth provider for a custom gateway declared in
* models.json (`"oauth": "radius"`). Runs on every models.json load so the
* registration survives `resetOAuthProviders()` during registry refreshes.
*/
export function registerCustomRadiusOAuthProvider(id: string, name: string | undefined, gateway: string): void {
registerOAuthProvider(
createRadiusOAuthProvider({
id,
name: name ?? id,
// Tolerate an API base URL: the gateway root is what the OAuth and
// config discovery endpoints hang off.
gateway: gateway.replace(/\/v1\/?$/u, ""),
}),
);
}