diff --git a/src/index.tsx b/src/index.tsx index ea17545..9ed0b7b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -102,7 +102,7 @@ export function getProvider(id: ProviderId): Provider { /* ────────────────────────── Settings (persisted) ─────────────────────────── */ -export type TransportMode = "direct" | "proxy" +export type TransportMode = "direct" | "proxy" | "gateway" export interface LLMProvidersSettings { providerId: ProviderId @@ -150,7 +150,8 @@ function readFromStorage(): LLMProvidersSettings { baseURL: typeof p.baseURL === "string" && p.baseURL ? p.baseURL : undefined, secretName: typeof p.secretName === "string" && p.secretName ? p.secretName : undefined, - mode: p.mode === "proxy" ? "proxy" : "direct", + mode: + p.mode === "proxy" ? "proxy" : p.mode === "gateway" ? "gateway" : "direct", responseBudget: Number.isFinite(p.responseBudget) && (p.responseBudget as number) > 0 ? (p.responseBudget as number) @@ -229,6 +230,10 @@ export interface BuildAdapterOptions { arcadiaAuthToken?: string /** Sent as X-Tenant-ID on proxy calls. */ arcadiaTenantId?: string + /** Required when mode is "gateway". Base URL of arcadia-llm-gateway, e.g. "https://llm-gateway.arcadia-dev.sky-ai.com". */ + gatewayURL?: string + /** Required when mode is "gateway". Per-app gateway key (agw_...) minted via POST /api/v1/me/keys. */ + gatewayKey?: string } /** @@ -236,9 +241,26 @@ export interface BuildAdapterOptions { * API key from arcadia. */ export async function buildAdapter(opts: BuildAdapterOptions): Promise { - const { settings, resolveSecret, arcadiaBaseURL, arcadiaAuthToken, arcadiaTenantId } = opts + const { + settings, + resolveSecret, + arcadiaBaseURL, + arcadiaAuthToken, + arcadiaTenantId, + gatewayURL, + gatewayKey, + } = opts const provider = getProvider(settings.providerId) + if (settings.mode === "gateway") { + if (!gatewayURL || !gatewayKey) { + throw new Error( + "buildAdapter: gatewayURL and gatewayKey are required in gateway mode.", + ) + } + return makeGatewayAdapter({ gatewayURL, gatewayKey, label: provider.label }) + } + if (settings.mode === "proxy") { if (!arcadiaBaseURL) { throw new Error("buildAdapter: arcadiaBaseURL is required in proxy mode.") @@ -332,6 +354,37 @@ export function makeArcadiaProxyAdapter(opts: ArcadiaProxyAdapterOptions): LLMAd } } +/* ────────────────────────── Gateway adapter ─────────────────────────── */ + +export interface GatewayAdapterOptions { + /** Base URL of arcadia-llm-gateway (no trailing /v1). */ + gatewayURL: string + /** Per-app gateway key (agw_...). Sent as Authorization: Bearer. */ + gatewayKey: string + label?: string +} + +/** + * Adapter that routes through arcadia-llm-gateway's OpenAI-compatible proxy at + * `/v1/chat/completions`, authenticated by the gateway key. The + * gateway resolves the upstream provider (BYO via the user's personal-cloud + * credential, or managed), REDACTS the request body (arcadia-redaction), + * forwards, and meters — so no provider key or unredacted prompt leaves the app. + * + * Covers OpenAI-compatible providers (deepseek, openai, qwen, lmstudio, …), + * which use the gateway's Bearer-authed /v1/chat/completions route. Anthropic + * native (/v1/messages) also expects Bearer auth at the gateway; a dedicated + * messages adapter is a follow-up. + */ +export function makeGatewayAdapter(opts: GatewayAdapterOptions): LLMAdapter { + const baseURL = opts.gatewayURL.replace(/\/+$/, "") + "/v1" + return new OpenAICompatibleAdapter({ + baseURL, + apiKey: opts.gatewayKey, // becomes Authorization: Bearer agw_... + label: opts.label ?? "Arcadia gateway", + }) +} + /* ────────────────────────── Settings card UI ─────────────────────────── */ const labelStyle: React.CSSProperties = {