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
1 change: 1 addition & 0 deletions docs/en/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
| `KIMI_MODEL_TOP_P` | Nucleus-sampling `top_p` for every request; `kimi` provider only (global) | Number, e.g. `0.95` |
| `KIMI_MODEL_THINKING_EFFORT` | Force a thinking effort (`thinking.effort`), bypassing the model's declared `support_efforts`; `kimi` provider only | An effort value, e.g. `max` |
| `KIMI_MODEL_THINKING_KEEP` | Preserved-thinking passthrough: `thinking.keep` on `kimi`, a `clear_thinking_20251015` edit on `anthropic`; overrides `[thinking] keep` | A value the API accepts, e.g. `all`; an off-value (`false`/`0`/`no`/`off`/`none`/`null`) disables it |
| `KIMI_CODE_LLM_HEADERS_TIMEOUT_MS` | Max time (ms) an LLM request on the `openai` / `openai-responses` / `anthropic` protocols may wait for the response headers (first byte), replacing the HTTP client's default 300 s headers timeout — raise it for long non-streaming thinking; unset keeps the default; the SDK's own total request timeout (default 10 minutes) still caps each attempt; not supported for `google-genai` (its SDK exposes no dispatcher option) or behind a SOCKS proxy (requests still go through the proxy) | Positive integer no greater than 2147483647; invalid values fail the request |
| `KIMI_CODE_NO_AUTO_UPDATE` | Fully disable the update preflight: no check, background install, or prompt. Legacy alias `KIMI_CLI_NO_AUTO_UPDATE` also honored | Truthy: `1`/`true`/`yes`/`on` |
| `KIMI_DISABLE_CRON` | Disable the scheduled-task tool (`CronCreate` rejects new schedules; existing tasks do not fire) | `1` to disable |

Expand Down
1 change: 1 addition & 0 deletions docs/zh/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ kimi
| `KIMI_MODEL_TOP_P` | 每次请求的核采样 `top_p`,仅对 `kimi` 供应商生效(全局生效) | 数字,如 `0.95` |
| `KIMI_MODEL_THINKING_EFFORT` | 在线上强制使用指定的思考强度,绕过模型声明的 `support_efforts`;仅 `kimi` 供应商生效 | 思考强度值,如 `max` |
| `KIMI_MODEL_THINKING_KEEP` | 保留思考透传;`kimi` 以 `thinking.keep` 发送,`anthropic` 以 `clear_thinking_20251015` 编辑发送;覆盖 `[thinking] keep` | API 接受的值,如 `all`;传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用 |
| `KIMI_CODE_LLM_HEADERS_TIMEOUT_MS` | `openai` / `openai-responses` / `anthropic` 协议的 LLM 请求等待响应头(首字节)的最长时间(毫秒),替代 HTTP 客户端默认的 300 秒响应头超时——长时间非流式 thinking 可调大;未设置保持默认;SDK 自身的总请求超时(默认 10 分钟)仍对单次尝试生效;`google-genai` 不支持(其 SDK 没有 dispatcher 配置项);SOCKS 代理下不生效(请求仍走代理) | 正整数,不超过 2147483647;非法值会使请求失败 |
| `KIMI_CODE_NO_AUTO_UPDATE` | 完全禁用更新预检:不检查、不后台安装、不提示。同时兼容旧名 `KIMI_CLI_NO_AUTO_UPDATE` | 真值:`1`/`true`/`yes`/`on` |
| `KIMI_DISABLE_CRON` | 禁用定时任务工具(`CronCreate` 拒绝新计划,已有任务不触发) | `1` 表示禁用 |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
type LlmRequestEvent,
type ToolCallIdPolicy,
} from '#/llm/requester/requester';
import {
getLlmHeadersTimeoutDispatcher,
resolveLlmHeadersTimeoutMs,
} from '#/llm/requester/timeout';

import {
normalizeToolCallIdsForProvider,
Expand Down Expand Up @@ -83,12 +87,16 @@ function buildDefaultHeaders(
}

function createClient(model: LlmModel, headers: Record<string, string> | undefined): Anthropic {
const headersTimeoutMs = resolveLlmHeadersTimeoutMs();
const dispatcher =
headersTimeoutMs === undefined ? undefined : getLlmHeadersTimeoutDispatcher(headersTimeoutMs);
return new Anthropic({
apiKey: model.apiKey ?? 'unused',
authToken: null,
baseURL: model.baseUrl ?? null,
defaultHeaders: buildDefaultHeaders(headers),
maxRetries: 0,
fetchOptions: dispatcher === undefined ? undefined : { dispatcher },
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
type LlmRequestEvent,
type ToolCallIdPolicy,
} from '#/llm/requester/requester';
import {
getLlmHeadersTimeoutDispatcher,
resolveLlmHeadersTimeoutMs,
} from '#/llm/requester/timeout';

import {
normalizeToolCallIdsForProvider,
Expand Down Expand Up @@ -49,11 +53,15 @@ const OPENAI_RESPONSES_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
};

function createClient(model: LlmModel, headers: Record<string, string> | undefined): OpenAI {
const headersTimeoutMs = resolveLlmHeadersTimeoutMs();
const dispatcher =
headersTimeoutMs === undefined ? undefined : getLlmHeadersTimeoutDispatcher(headersTimeoutMs);
return new OpenAI({
apiKey: model.apiKey ?? 'unused',
baseURL: model.baseUrl,
defaultHeaders: headers,
maxRetries: 0,
fetchOptions: dispatcher === undefined ? undefined : { dispatcher },
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
type LlmRequestEvent,
type ToolCallIdPolicy,
} from '#/llm/requester/requester';
import {
getLlmHeadersTimeoutDispatcher,
resolveLlmHeadersTimeoutMs,
} from '#/llm/requester/timeout';

import {
normalizeToolCallIdsForProvider,
Expand Down Expand Up @@ -50,11 +54,15 @@ const OPENAI_CHAT_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
};

function createClient(model: LlmModel, headers: Record<string, string> | undefined): OpenAI {
const headersTimeoutMs = resolveLlmHeadersTimeoutMs();
const dispatcher =
headersTimeoutMs === undefined ? undefined : getLlmHeadersTimeoutDispatcher(headersTimeoutMs);
return new OpenAI({
apiKey: model.apiKey ?? 'unused',
baseURL: model.baseUrl,
defaultHeaders: headers,
maxRetries: 0,
fetchOptions: dispatcher === undefined ? undefined : { dispatcher },
});
}

Expand Down
123 changes: 123 additions & 0 deletions packages/agent-core-v2/src/human/llm/requester/timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Agent, EnvHttpProxyAgent, type Dispatcher } from 'undici';

export const LLM_HEADERS_TIMEOUT_ENV = 'KIMI_CODE_LLM_HEADERS_TIMEOUT_MS';

const MAX_HEADERS_TIMEOUT_MS = 2 ** 31 - 1;

type Env = Readonly<Record<string, string | undefined>>;

export function resolveLlmHeadersTimeoutMs(env: Env = process.env): number | undefined {
const raw = env[LLM_HEADERS_TIMEOUT_ENV];
if (raw === undefined || raw.trim() === '') return undefined;
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0 || value > MAX_HEADERS_TIMEOUT_MS) {
throw new Error(
`${LLM_HEADERS_TIMEOUT_ENV} must be a positive integer no greater than ${MAX_HEADERS_TIMEOUT_MS}, got ${JSON.stringify(raw)}.`,
);
}
return value;
}

const SOCKS_SCHEMES = new Set(['socks', 'socks4', 'socks4a', 'socks5', 'socks5h']);
const LOOPBACK_NO_PROXY = ['localhost', '127.0.0.1', '::1', '[::1]'] as const;

function schemeOf(value: string): string | undefined {
return /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
}

function firstNonBlank(env: Env, keys: readonly string[]): string | undefined {
for (const key of keys) {
const value = env[key]?.trim();
if (value !== undefined && value.length > 0) return value;
}
return undefined;
}

function httpSchemeValue(value: string | undefined): string | undefined {
return value !== undefined && !SOCKS_SCHEMES.has(schemeOf(value) ?? '') ? value : undefined;
}

function resolveHttpProxyUrls(env: Env): { httpProxy?: string; httpsProxy?: string } | undefined {
const allProxy = httpSchemeValue(firstNonBlank(env, ['all_proxy', 'ALL_PROXY']));
const httpProxy = httpSchemeValue(firstNonBlank(env, ['http_proxy', 'HTTP_PROXY'])) ?? allProxy;
const httpsProxy = httpSchemeValue(firstNonBlank(env, ['https_proxy', 'HTTPS_PROXY'])) ?? allProxy;
if (httpProxy === undefined && httpsProxy === undefined) return undefined;
return { httpProxy, httpsProxy };
}

function hasSocksProxy(env: Env): boolean {
return [
firstNonBlank(env, ['all_proxy', 'ALL_PROXY']),
firstNonBlank(env, ['https_proxy', 'HTTPS_PROXY']),
firstNonBlank(env, ['http_proxy', 'HTTP_PROXY']),
].some((value) => value !== undefined && SOCKS_SCHEMES.has(schemeOf(value) ?? ''));
}

function resolveNoProxy(env: Env): string {
const raw =
[env['no_proxy'], env['NO_PROXY']].find((value) => (value?.trim() ?? '').length > 0) ?? '';
const hosts = raw
.split(',')
.map((host) => host.trim())
.filter((host) => host.length > 0);
if (hosts.includes('*')) return '*';
for (const loopback of LOOPBACK_NO_PROXY) {
if (!hosts.includes(loopback)) hosts.push(loopback);
}
return hosts.join(',');
}

let warnedSocksProxy = false;
let warnedInvalidProxy = false;
let cached: { readonly key: string; readonly dispatcher: Dispatcher | undefined } | undefined;

export function getLlmHeadersTimeoutDispatcher(
timeoutMs: number,
env: Env = process.env,
): Dispatcher | undefined {
const key = JSON.stringify([
timeoutMs,
env['http_proxy'],
env['HTTP_PROXY'],
env['https_proxy'],
env['HTTPS_PROXY'],
env['all_proxy'],
env['ALL_PROXY'],
env['no_proxy'],
env['NO_PROXY'],
]);
if (cached?.key === key) return cached.dispatcher;
let dispatcher: Dispatcher | undefined;
const httpProxyUrls = resolveHttpProxyUrls(env);
if (httpProxyUrls !== undefined) {
try {
dispatcher = new EnvHttpProxyAgent({
httpProxy: httpProxyUrls.httpProxy ?? '',
httpsProxy: httpProxyUrls.httpsProxy ?? '',
noProxy: resolveNoProxy(env),
headersTimeout: timeoutMs,
});
} catch (error) {
if (!warnedInvalidProxy) {
warnedInvalidProxy = true;
const reason = error instanceof Error ? error.message : String(error);
process.stderr.write(
`kimi: ${LLM_HEADERS_TIMEOUT_ENV}: ignoring invalid proxy configuration (${reason}); requests keep the default headers timeout\n`,
);
}
dispatcher = undefined;
}
} else if (hasSocksProxy(env)) {
if (!warnedSocksProxy) {
warnedSocksProxy = true;
process.stderr.write(
`kimi: ${LLM_HEADERS_TIMEOUT_ENV} is not supported with SOCKS proxies; requests keep the default headers timeout\n`,
);
}
dispatcher = undefined;
} else {
dispatcher = new Agent({ headersTimeout: timeoutMs });
}
cached = { key, dispatcher };
return dispatcher;
}
12 changes: 6 additions & 6 deletions packages/agent-core-v2/src/human/test/llm/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ describe('convertOpenAIError', () => {
expect(convertOpenAIError(raw)).toMatchObject({ kind: 'context_overflow', statusCode: 400 });
});

it('maps 413 too-large messages to request_too_large', () => {
const raw = new RawOpenAISDKAPIError(
it('maps remaining status errors to their kinds', () => {
const tooLarge = new RawOpenAISDKAPIError(
413,
{ message: 'request entity too large' },
undefined,
new Headers(),
);
expect(convertOpenAIError(raw)).toMatchObject({ kind: 'request_too_large', statusCode: 413 });
});

it('maps remaining status errors to their kinds', () => {
expect(convertOpenAIError(tooLarge)).toMatchObject({
kind: 'request_too_large',
statusCode: 413,
});
const overloaded = new RawOpenAISDKAPIError(529, {}, 'overloaded', new Headers());
expect(convertOpenAIError(overloaded)).toMatchObject({ kind: 'overloaded', statusCode: 529 });
const generic = new RawOpenAISDKAPIError(500, {}, 'server error', new Headers());
Expand Down
Loading
Loading