Files
curriculum-project-hub/hub/src/settings/runtime.ts
T

129 lines
3.9 KiB
TypeScript

import { InMemoryModelRegistry, type ModelRegistry } from "../agent/models.js";
export type Env = Readonly<Record<string, string | undefined>>;
type EnvSource = Env | (() => Env);
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api";
const DEFAULT_SONNET_MODEL = "anthropic/claude-sonnet-5";
const DEFAULT_SONNET_LABEL = "Claude Sonnet 5";
const DEFAULT_AGENT_MAX_TURNS = 25;
export interface ProviderRuntimeSettings {
readonly id: string;
readonly baseUrl: string;
readonly authToken: string;
readonly anthropicApiKey: string;
readonly sdkEnv: Record<string, string | undefined>;
}
export interface RuntimeScope {
readonly projectId?: string;
}
export interface RunPolicyInput {
readonly projectId: string;
readonly roleId: string;
}
export interface RunPolicy {
readonly maxTurns: number;
}
export interface RuntimeSettings {
provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings>;
modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry>;
runPolicy(input: RunPolicyInput): Promise<RunPolicy>;
}
export class EnvRuntimeSettings implements RuntimeSettings {
private readonly envSource: () => Env;
constructor(env: EnvSource = process.env) {
this.envSource = typeof env === "function" ? env : () => env;
}
async provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings> {
void scope;
if (providerId !== "openrouter") {
throw new Error(`unknown provider runtime settings: ${providerId}`);
}
const env = this.envSource();
const baseUrl = readEnv(env, "ANTHROPIC_BASE_URL") ?? DEFAULT_OPENROUTER_BASE_URL;
const authToken = requireSetting(env, "ANTHROPIC_AUTH_TOKEN");
const anthropicApiKey = readEnv(env, "ANTHROPIC_API_KEY") ?? "";
return {
id: providerId,
baseUrl,
authToken,
anthropicApiKey,
sdkEnv: {
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_AUTH_TOKEN: authToken,
ANTHROPIC_API_KEY: anthropicApiKey,
},
};
}
async modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
void scope;
return createDefaultModelRegistry(this.envSource());
}
async runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
void input;
return {
maxTurns: positiveIntegerEnv(this.envSource(), "HUB_AGENT_MAX_TURNS", DEFAULT_AGENT_MAX_TURNS),
};
}
}
export function createEnvRuntimeSettings(env: EnvSource = process.env): RuntimeSettings {
return new EnvRuntimeSettings(env);
}
export function defaultSonnetModel(env: Env = process.env): string {
return readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL") ?? DEFAULT_SONNET_MODEL;
}
export function createDefaultModelRegistry(env: Env = process.env): InMemoryModelRegistry {
const sonnetModel = defaultSonnetModel(env);
const sonnetLabel =
readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME") ??
(sonnetModel === DEFAULT_SONNET_MODEL ? DEFAULT_SONNET_LABEL : sonnetModel);
return new InMemoryModelRegistry(
[
{ id: sonnetModel, label: sonnetLabel, toolCapable: true },
],
[
{ id: "draft", label: "草稿", defaultModel: sonnetModel },
{ id: "review", label: "审校", defaultModel: sonnetModel },
],
);
}
function readEnv(env: Env, name: string): string | undefined {
const value = env[name]?.trim();
return value === undefined || value === "" ? undefined : value;
}
function requireSetting(env: Env, name: string): string {
const value = readEnv(env, name);
if (value === undefined) {
throw new Error(`missing required runtime setting: ${name}`);
}
return value;
}
function positiveIntegerEnv(env: Env, name: string, fallback: number): number {
const raw = readEnv(env, name);
if (raw === undefined) return fallback;
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`runtime setting ${name} must be a positive integer`);
}
return value;
}