File size: 5,728 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import { ErrorCodes, KimiError } from '#/errors';
import { parseBooleanEnv } from './resolve';
import {
validateConfig,
type KimiConfig,
type ModelAlias,
type ProviderConfig,
type ProviderType,
type ThinkingConfig,
} from './schema';
export const ENV_MODEL_PROVIDER_KEY = '__kimi_env__';
export const ENV_MODEL_ALIAS_KEY = '__kimi_env_model__';
const ALLOWED_TYPES: readonly ProviderType[] = ['kimi', 'anthropic', 'openai'];
const DEFAULT_BASE_URL: Partial<Record<ProviderType, string>> = {
kimi: 'https://api.moonshot.ai/v1',
openai: 'https://api.openai.com/v1',
};
const DEFAULT_MAX_CONTEXT_SIZE = 262144;
const DEFAULT_CAPABILITIES = ['image_in', 'thinking'];
type Env = Readonly<Record<string, string | undefined>>;
function trimmed(value: string | undefined): string | undefined {
const t = value?.trim();
return t === undefined || t.length === 0 ? undefined : t;
}
function fail(message: string): never {
throw new KimiError(ErrorCodes.CONFIG_INVALID, message);
}
function parsePositiveInt(raw: string, varName: string): number {
if (!/^\d+$/.test(raw) || Number(raw) <= 0) {
fail(`${varName} must be a positive integer, got "${raw}".`);
}
return Number(raw);
}
function parseProviderType(raw: string | undefined): ProviderType {
if (raw === undefined) return 'kimi';
const normalized = raw.toLowerCase() as ProviderType;
if (!ALLOWED_TYPES.includes(normalized)) {
fail(
`KIMI_MODEL_PROVIDER_TYPE must be one of ${ALLOWED_TYPES.join(', ')}, got "${raw}".`,
);
}
return normalized;
}
function parseCapabilities(raw: string | undefined): string[] | undefined {
if (raw === undefined) return undefined;
const caps = raw
.split(',')
.map((c) => c.trim().toLowerCase())
.filter((c) => c.length > 0);
return caps.length === 0 ? undefined : caps;
}
function parseBooleanVar(raw: string | undefined, varName: string): boolean | undefined {
const value = trimmed(raw);
if (value === undefined) return undefined;
const parsed = parseBooleanEnv(value);
if (parsed === undefined) {
fail(`${varName} must be a boolean (true/false/1/0/yes/no/on/off), got "${raw}".`);
}
return parsed;
}
export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env): KimiConfig {
const model = trimmed(env['KIMI_MODEL_NAME']);
if (model === undefined) return config;
const apiKey = trimmed(env['KIMI_MODEL_API_KEY']);
if (apiKey === undefined) {
fail('KIMI_MODEL_NAME is set but KIMI_MODEL_API_KEY is missing.');
}
const maxContextRaw = trimmed(env['KIMI_MODEL_MAX_CONTEXT_SIZE']);
const maxContextSize =
maxContextRaw === undefined
? DEFAULT_MAX_CONTEXT_SIZE
: parsePositiveInt(maxContextRaw, 'KIMI_MODEL_MAX_CONTEXT_SIZE');
const type = parseProviderType(trimmed(env['KIMI_MODEL_PROVIDER_TYPE']));
const baseUrl = trimmed(env['KIMI_MODEL_BASE_URL']) ?? DEFAULT_BASE_URL[type];
const provider: ProviderConfig = {
type,
apiKey,
...(baseUrl !== undefined ? { baseUrl } : {}),
};
const maxOutputRaw = trimmed(env['KIMI_MODEL_MAX_OUTPUT_SIZE']);
const maxOutputSize =
maxOutputRaw !== undefined
? parsePositiveInt(maxOutputRaw, 'KIMI_MODEL_MAX_OUTPUT_SIZE')
: undefined;
const capabilities = parseCapabilities(env['KIMI_MODEL_CAPABILITIES']) ?? DEFAULT_CAPABILITIES;
const displayName = trimmed(env['KIMI_MODEL_DISPLAY_NAME']);
const reasoningKey = trimmed(env['KIMI_MODEL_REASONING_KEY']);
const adaptiveThinking = parseBooleanVar(
env['KIMI_MODEL_ADAPTIVE_THINKING'],
'KIMI_MODEL_ADAPTIVE_THINKING',
);
const alias: ModelAlias = {
provider: ENV_MODEL_PROVIDER_KEY,
model,
maxContextSize,
capabilities,
...(displayName !== undefined ? { displayName } : {}),
...(maxOutputSize !== undefined ? { maxOutputSize } : {}),
...(reasoningKey !== undefined ? { reasoningKey } : {}),
...(adaptiveThinking !== undefined ? { adaptiveThinking } : {}),
};
const thinkingEffort = trimmed(env['KIMI_MODEL_THINKING_EFFORT']);
const thinking: ThinkingConfig | undefined =
thinkingEffort !== undefined ? { ...config.thinking, effort: thinkingEffort } : config.thinking;
const merged: KimiConfig = {
...config,
providers: { ...config.providers, [ENV_MODEL_PROVIDER_KEY]: provider },
models: { ...config.models, [ENV_MODEL_ALIAS_KEY]: alias },
defaultModel: ENV_MODEL_ALIAS_KEY,
...(thinking !== undefined ? { thinking } : {}),
};
return validateConfig(merged);
}
export function stripEnvModelConfig(config: KimiConfig): KimiConfig {
const hasProvider = ENV_MODEL_PROVIDER_KEY in config.providers;
const hasModel = config.models !== undefined && ENV_MODEL_ALIAS_KEY in config.models;
const defaultIsEnv = config.defaultModel === ENV_MODEL_ALIAS_KEY;
if (!hasProvider && !hasModel && !defaultIsEnv) return config;
const providers = { ...config.providers };
delete providers[ENV_MODEL_PROVIDER_KEY];
let models = config.models;
if (models !== undefined && ENV_MODEL_ALIAS_KEY in models) {
models = { ...models };
delete models[ENV_MODEL_ALIAS_KEY];
}
return {
...config,
providers,
...(models !== undefined ? { models } : {}),
...(defaultIsEnv ? { defaultModel: rawDefaultModel(config) } : {}),
thinking: rawThinking(config),
};
}
function rawDefaultModel(config: KimiConfig): string | undefined {
const raw = config.raw?.['default_model'];
return typeof raw === 'string' ? raw : undefined;
}
function rawThinking(config: KimiConfig): ThinkingConfig | undefined {
const raw = config.raw?.['thinking'];
return typeof raw === 'object' && raw !== null && !Array.isArray(raw)
? (raw as ThinkingConfig)
: undefined;
}
|