feat(ai): add Radius gateway support
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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, ""),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user