Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions gateway/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ AGENT_EFFORT=medium
QUICK_ANSWER_TIMEOUT_MS=30000
STORE_PATH=./data/gateway-store.json

# Inference provider. Defaults to "anthropic" (native API, ANTHROPIC_API_KEY).
# Set to "minimax" to route the managed-agent client at MiniMax's
# Anthropic-compatible endpoint instead. When unset, AGENT_MODEL falls back to
# the selected provider's default model (e.g. MiniMax-M3 for minimax).
INFERENCE_PROVIDER=anthropic
# Region for providers that publish regional endpoints (minimax: global_en | cn_zh).
PROVIDER_REGION=global_en
# API key for the MiniMax provider (used when INFERENCE_PROVIDER=minimax).
MINIMAX_API_KEY=

# App connections (OAuth -> per-user vault credential)
STATE_SECRET=generate-a-random-string
PUBLIC_BASE_URL=https://your-gateway.example.com
Expand Down
16 changes: 14 additions & 2 deletions gateway/src/cma.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import Anthropic from "@anthropic-ai/sdk";
import { config } from "./config.js";

/** One client for the whole gateway. Reads ANTHROPIC_API_KEY from the environment. */
export const anthropic = new Anthropic();
/**
* One client for the whole gateway.
*
* Defaults to the native Anthropic API, reading ANTHROPIC_API_KEY from the
* environment. When an Anthropic-compatible provider is selected (see
* providers.ts), its base URL and API key are passed through so the same SDK
* reaches that provider's endpoint.
*/
const providerApiKey = process.env[config.providerApiKeyEnv];
export const anthropic = new Anthropic({
...(config.providerBaseURL ? { baseURL: config.providerBaseURL } : {}),
...(providerApiKey ? { apiKey: providerApiKey } : {}),
});
24 changes: 23 additions & 1 deletion gateway/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import "dotenv/config";
import { InferenceProvider, resolveBaseURL, resolveProvider } from "./providers.js";

export interface GatewayConfig {
port: number;
storePath: string;
/** token -> userId. Parsed from GATEWAY_TOKENS="tokenA:alice,tokenB:bob". */
tokens: Map<string, string>;
/** Selected inference provider (see providers.ts). */
provider: InferenceProvider;
/**
* Anthropic-compatible base URL for the provider/region, or undefined to use
* the SDK default (native Anthropic API).
*/
providerBaseURL?: string;
/** Environment variable that carries the selected provider's API key. */
providerApiKeyEnv: string;
agentModel: string;
agentEffort: "low" | "medium" | "high";
/** How long a /v1/chat/completions call waits before converting to a background task. */
Expand All @@ -29,11 +39,16 @@ function parseTokens(raw: string | undefined): Map<string, string> {
return map;
}

const provider = resolveProvider(process.env.INFERENCE_PROVIDER);

export const config: GatewayConfig = {
port: Number(process.env.PORT ?? 8788),
storePath: process.env.STORE_PATH ?? "./data/gateway-store.json",
tokens: parseTokens(process.env.GATEWAY_TOKENS),
agentModel: process.env.AGENT_MODEL ?? "claude-opus-5",
provider,
providerBaseURL: resolveBaseURL(provider, process.env.PROVIDER_REGION),
providerApiKeyEnv: provider.apiKeyEnv,
agentModel: process.env.AGENT_MODEL ?? provider.defaultModel,
agentEffort: (process.env.AGENT_EFFORT as GatewayConfig["agentEffort"]) ?? "medium",
quickAnswerTimeoutMs: Number(process.env.QUICK_ANSWER_TIMEOUT_MS ?? 30_000),
spawnMode: process.env.SPAWN_MODE !== "false",
Expand All @@ -45,3 +60,10 @@ if (config.tokens.size === 0) {
'Set GATEWAY_TOKENS="sometoken:someUserId" in .env',
);
}

if (!process.env[config.providerApiKeyEnv]) {
console.warn(
`[config] ${config.providerApiKeyEnv} is empty - the ${config.provider.name} ` +
"inference provider will reject requests. Set it in .env",
);
}
85 changes: 85 additions & 0 deletions gateway/src/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Inference provider registry.
*
* The gateway drives its managed-agent sessions through the single
* `@anthropic-ai/sdk` client in cma.ts. Any provider that exposes an
* Anthropic-compatible endpoint can be reached through that same client by
* pointing its `baseURL` at the provider's gateway, so adding one here is a
* table entry rather than a second SDK. Selection is env-driven (see config.ts)
* and defaults to the native Anthropic API, which keeps the existing behaviour.
*/

export interface ProviderRegion {
/** Region id used in PROVIDER_REGION. */
region: string;
/** Anthropic-compatible base URL for this region. */
anthropicBaseURL: string;
}

export interface InferenceProvider {
/** Stable id used in INFERENCE_PROVIDER. */
id: string;
/** Human-readable name. */
name: string;
/** Model id used when AGENT_MODEL is unset. */
defaultModel: string;
/** Environment variable that carries this provider's API key. */
apiKeyEnv: string;
/**
* Anthropic-compatible base URLs by region. The native Anthropic API needs no
* override, so its list is empty and the SDK default is used.
*/
regions: ProviderRegion[];
/** Region chosen when PROVIDER_REGION is unset. */
defaultRegion?: string;
}

export const PROVIDERS: Record<string, InferenceProvider> = {
anthropic: {
id: "anthropic",
name: "Anthropic",
defaultModel: "claude-opus-5",
apiKeyEnv: "ANTHROPIC_API_KEY",
regions: [],
},
minimax: {
id: "minimax",
name: "MiniMax",
defaultModel: "MiniMax-M3",
apiKeyEnv: "MINIMAX_API_KEY",
regions: [
{ region: "global_en", anthropicBaseURL: "https://api.minimax.io/anthropic" },
{ region: "cn_zh", anthropicBaseURL: "https://api.minimaxi.com/anthropic" },
],
defaultRegion: "global_en",
},
};

/** Resolve a provider by id, falling back to Anthropic for unknown ids. */
export function resolveProvider(id: string | undefined): InferenceProvider {
const key = (id ?? "anthropic").toLowerCase();
const provider = PROVIDERS[key];
if (!provider) {
console.warn(`[providers] unknown INFERENCE_PROVIDER "${id}", falling back to anthropic`);
return PROVIDERS.anthropic;
}
return provider;
}

/**
* Anthropic-compatible base URL for the provider/region, or undefined when the
* SDK default should be used (native Anthropic API).
*/
export function resolveBaseURL(provider: InferenceProvider, region: string | undefined): string | undefined {
if (provider.regions.length === 0) return undefined;
const wanted = region ?? provider.defaultRegion;
const match = provider.regions.find((r) => r.region === wanted);
if (!match) {
console.warn(
`[providers] unknown PROVIDER_REGION "${region}" for ${provider.name}, ` +
`using "${provider.regions[0].region}"`,
);
return provider.regions[0].anthropicBaseURL;
}
return match.anthropicBaseURL;
}