kimi-cli 0.41__py3-none-any.whl → 0.42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kimi-cli might be problematic. Click here for more details.

kimi_cli/CHANGELOG.md CHANGED
@@ -9,6 +9,21 @@ Internal builds may append content to the Unreleased section.
9
9
  Only write entries that are worth mentioning to users.
10
10
  -->
11
11
 
12
+ ## [0.42] - 2025-10-28
13
+
14
+ ### Added
15
+
16
+ - Support Ctrl-J or Alt-Enter to insert a new line
17
+
18
+ ### Changed
19
+
20
+ - Change mode switch shortcut from Ctrl-K to Ctrl-X
21
+ - Improve overall robustness
22
+
23
+ ### Fixed
24
+
25
+ - Fix ACP server `no attribute` error
26
+
12
27
  ## [0.41] - 2025-10-26
13
28
 
14
29
  ### Fixed
kimi_cli/__init__.py CHANGED
@@ -1,125 +1,155 @@
1
1
  import contextlib
2
2
  import os
3
3
  import warnings
4
+ from collections.abc import Generator
4
5
  from pathlib import Path
5
- from typing import Any, Literal
6
+ from typing import Any
6
7
 
7
- import click
8
8
  from pydantic import SecretStr
9
9
 
10
10
  from kimi_cli.agentspec import DEFAULT_AGENT_FILE
11
- from kimi_cli.config import Config, LLMModel, LLMProvider
11
+ from kimi_cli.config import LLMModel, LLMProvider, load_config
12
12
  from kimi_cli.llm import augment_provider_with_env_vars, create_llm
13
- from kimi_cli.metadata import Session
14
- from kimi_cli.soul.agent import load_agent_with_mcp
13
+ from kimi_cli.session import Session
14
+ from kimi_cli.soul.agent import load_agent
15
15
  from kimi_cli.soul.context import Context
16
- from kimi_cli.soul.globals import AgentGlobals
17
16
  from kimi_cli.soul.kimisoul import KimiSoul
17
+ from kimi_cli.soul.runtime import Runtime
18
18
  from kimi_cli.ui.acp import ACPServer
19
19
  from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
20
20
  from kimi_cli.ui.shell import ShellApp
21
21
  from kimi_cli.utils.logging import StreamToLogger, logger
22
22
 
23
- UIMode = Literal["shell", "print", "acp"]
24
-
25
-
26
- async def kimi_run(
27
- *,
28
- config: Config,
29
- model_name: str | None,
30
- work_dir: Path,
31
- session: Session,
32
- command: str | None = None,
33
- agent_file: Path = DEFAULT_AGENT_FILE,
34
- ui: UIMode = "shell",
35
- input_format: InputFormat | None = None,
36
- output_format: OutputFormat | None = None,
37
- mcp_configs: list[dict[str, Any]] | None = None,
38
- yolo: bool = False,
39
- ) -> bool:
40
- """Run Kimi CLI."""
41
- model: LLMModel | None = None
42
- provider: LLMProvider | None = None
43
-
44
- # try to use config file
45
- if not model_name and config.default_model:
46
- # no --model specified && default model is set in config
47
- model = config.models[config.default_model]
48
- provider = config.providers[model.provider]
49
- if model_name and model_name in config.models:
50
- # --model specified && model is set in config
51
- model = config.models[model_name]
52
- provider = config.providers[model.provider]
53
-
54
- if not model:
55
- model = LLMModel(provider="", model="", max_context_size=100_000)
56
- provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr(""))
57
-
58
- # try overwrite with environment variables
59
- assert provider is not None
60
- assert model is not None
61
- augment_provider_with_env_vars(provider, model)
62
-
63
- if not provider.base_url or not model.model:
64
- llm = None
65
- else:
66
- logger.info("Using LLM provider: {provider}", provider=provider)
67
- logger.info("Using LLM model: {model}", model=model)
68
- stream = ui != "print" # use non-streaming mode only for print UI
69
- llm = create_llm(provider, model, stream=stream, session_id=session.id)
70
-
71
- agent_globals = await AgentGlobals.create(config, llm, session, yolo)
72
- try:
73
- agent = await load_agent_with_mcp(agent_file, agent_globals, mcp_configs or [])
74
- except ValueError as e:
75
- raise click.BadParameter(f"Failed to load agent: {e}") from e
76
-
77
- if command is not None:
78
- command = command.strip()
79
- if not command:
80
- raise click.BadParameter("Command cannot be empty")
81
-
82
- context = Context(session.history_file)
83
- await context.restore()
84
-
85
- soul = KimiSoul(
86
- agent,
87
- agent_globals,
88
- context=context,
89
- loop_control=config.loop_control,
90
- )
91
-
92
- original_cwd = Path.cwd()
93
- os.chdir(work_dir)
94
-
95
- try:
96
- if ui == "shell":
23
+
24
+ class KimiCLI:
25
+ @staticmethod
26
+ async def create(
27
+ session: Session,
28
+ *,
29
+ yolo: bool = False,
30
+ stream: bool = True, # TODO: remove this when we have a correct print mode impl
31
+ mcp_configs: list[dict[str, Any]] | None = None,
32
+ config_file: Path | None = None,
33
+ model_name: str | None = None,
34
+ agent_file: Path | None = None,
35
+ ) -> "KimiCLI":
36
+ """
37
+ Create a KimiCLI instance.
38
+
39
+ Args:
40
+ session (Session): A session created by `Session.create` or `Session.continue_`.
41
+ yolo (bool, optional): Approve all actions without confirmation. Defaults to False.
42
+ stream (bool, optional): Use stream mode when calling LLM API. Defaults to True.
43
+ config_file (Path | None, optional): Path to the configuration file. Defaults to None.
44
+ model_name (str | None, optional): Name of the model to use. Defaults to None.
45
+ agent_file (Path | None, optional): Path to the agent file. Defaults to None.
46
+
47
+ Raises:
48
+ FileNotFoundError: When the agent file is not found.
49
+ ConfigError(KimiCLIException): When the configuration is invalid.
50
+ AgentSpecError(KimiCLIException): When the agent specification is invalid.
51
+ """
52
+ config = load_config(config_file)
53
+ logger.info("Loaded config: {config}", config=config)
54
+
55
+ model: LLMModel | None = None
56
+ provider: LLMProvider | None = None
57
+
58
+ # try to use config file
59
+ if not model_name and config.default_model:
60
+ # no --model specified && default model is set in config
61
+ model = config.models[config.default_model]
62
+ provider = config.providers[model.provider]
63
+ if model_name and model_name in config.models:
64
+ # --model specified && model is set in config
65
+ model = config.models[model_name]
66
+ provider = config.providers[model.provider]
67
+
68
+ if not model:
69
+ model = LLMModel(provider="", model="", max_context_size=100_000)
70
+ provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr(""))
71
+
72
+ # try overwrite with environment variables
73
+ assert provider is not None
74
+ assert model is not None
75
+ augment_provider_with_env_vars(provider, model)
76
+
77
+ if not provider.base_url or not model.model:
78
+ llm = None
79
+ else:
80
+ logger.info("Using LLM provider: {provider}", provider=provider)
81
+ logger.info("Using LLM model: {model}", model=model)
82
+ llm = create_llm(provider, model, stream=stream, session_id=session.id)
83
+
84
+ runtime = await Runtime.create(config, llm, session, yolo)
85
+
86
+ if agent_file is None:
87
+ agent_file = DEFAULT_AGENT_FILE
88
+ agent = await load_agent(agent_file, runtime, mcp_configs=mcp_configs or [])
89
+
90
+ context = Context(session.history_file)
91
+ await context.restore()
92
+
93
+ soul = KimiSoul(
94
+ agent,
95
+ runtime,
96
+ context=context,
97
+ )
98
+ return KimiCLI(soul, session)
99
+
100
+ def __init__(self, soul: KimiSoul, session: Session) -> None:
101
+ self._soul = soul
102
+ self._session = session
103
+
104
+ @property
105
+ def soul(self) -> KimiSoul:
106
+ """Get the KimiSoul instance."""
107
+ return self._soul
108
+
109
+ @property
110
+ def session(self) -> Session:
111
+ """Get the Session instance."""
112
+ return self._session
113
+
114
+ @contextlib.contextmanager
115
+ def _app_env(self) -> Generator[None]:
116
+ original_cwd = Path.cwd()
117
+ os.chdir(self._session.work_dir)
118
+ try:
119
+ # to ignore possible warnings from dateparser
120
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
121
+ with contextlib.redirect_stderr(StreamToLogger()):
122
+ yield
123
+ finally:
124
+ os.chdir(original_cwd)
125
+
126
+ async def run_shell_mode(self, command: str | None = None) -> bool:
127
+ with self._app_env():
97
128
  app = ShellApp(
98
- soul,
129
+ self._soul,
99
130
  welcome_info={
100
- "Directory": str(work_dir),
101
- "Session": session.id,
131
+ "Directory": str(self._session.work_dir),
132
+ "Session": self._session.id,
102
133
  },
103
134
  )
104
- # to ignore possible warnings from dateparser
105
- warnings.filterwarnings("ignore", category=DeprecationWarning)
106
- with contextlib.redirect_stderr(StreamToLogger()):
107
- return await app.run(command)
108
- elif ui == "print":
109
- soul._approval.set_yolo(True) # print mode implies yolo mode
135
+ return await app.run(command)
136
+
137
+ async def run_print_mode(
138
+ self,
139
+ input_format: InputFormat,
140
+ output_format: OutputFormat,
141
+ command: str | None = None,
142
+ ) -> bool:
143
+ with self._app_env():
110
144
  app = PrintApp(
111
- soul,
112
- input_format or "text",
113
- output_format or "text",
114
- context.file_backend,
145
+ self._soul,
146
+ input_format,
147
+ output_format,
148
+ self._session.history_file,
115
149
  )
116
150
  return await app.run(command)
117
- elif ui == "acp":
118
- if command is not None:
119
- logger.warning("ACP server ignores command argument")
120
- app = ACPServer(soul)
151
+
152
+ async def run_acp_server(self) -> bool:
153
+ with self._app_env():
154
+ app = ACPServer(self._soul)
121
155
  return await app.run()
122
- else:
123
- raise click.BadParameter(f"Invalid UI mode: {ui}")
124
- finally:
125
- os.chdir(original_cwd)
@@ -19,6 +19,6 @@ agent:
19
19
  - "kimi_cli.tools.web:SearchWeb"
20
20
  - "kimi_cli.tools.web:FetchURL"
21
21
  subagents:
22
- koder:
22
+ coder:
23
23
  path: ./sub.yaml
24
24
  description: "Good at general software engineering tasks."
kimi_cli/agentspec.py CHANGED
@@ -4,12 +4,14 @@ from typing import Any, NamedTuple
4
4
  import yaml
5
5
  from pydantic import BaseModel, Field
6
6
 
7
+ from kimi_cli.exception import AgentSpecError
8
+
7
9
 
8
10
  def get_agents_dir() -> Path:
9
11
  return Path(__file__).parent / "agents"
10
12
 
11
13
 
12
- DEFAULT_AGENT_FILE = get_agents_dir() / "koder" / "agent.yaml"
14
+ DEFAULT_AGENT_FILE = get_agents_dir() / "default" / "agent.yaml"
13
15
 
14
16
 
15
17
  class AgentSpec(BaseModel):
@@ -47,15 +49,21 @@ class ResolvedAgentSpec(NamedTuple):
47
49
 
48
50
 
49
51
  def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
50
- """Load agent specification from file."""
52
+ """
53
+ Load agent specification from file.
54
+
55
+ Raises:
56
+ FileNotFoundError: If the agent spec file is not found.
57
+ AgentSpecError: If the agent spec is not valid.
58
+ """
51
59
  agent_spec = _load_agent_spec(agent_file)
52
60
  assert agent_spec.extend is None, "agent extension should be recursively resolved"
53
61
  if agent_spec.name is None:
54
- raise ValueError("Agent name is required")
62
+ raise AgentSpecError("Agent name is required")
55
63
  if agent_spec.system_prompt_path is None:
56
- raise ValueError("System prompt path is required")
64
+ raise AgentSpecError("System prompt path is required")
57
65
  if agent_spec.tools is None:
58
- raise ValueError("Tools are required")
66
+ raise AgentSpecError("Tools are required")
59
67
  return ResolvedAgentSpec(
60
68
  name=agent_spec.name,
61
69
  system_prompt_path=agent_spec.system_prompt_path,
@@ -68,12 +76,15 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
68
76
 
69
77
  def _load_agent_spec(agent_file: Path) -> AgentSpec:
70
78
  assert agent_file.is_file(), "expect agent file to exist"
71
- with open(agent_file, encoding="utf-8") as f:
72
- data: dict[str, Any] = yaml.safe_load(f)
79
+ try:
80
+ with open(agent_file, encoding="utf-8") as f:
81
+ data: dict[str, Any] = yaml.safe_load(f)
82
+ except yaml.YAMLError as e:
83
+ raise AgentSpecError(f"Invalid YAML in agent spec file: {e}") from e
73
84
 
74
85
  version = data.get("version", 1)
75
86
  if version != 1:
76
- raise ValueError(f"Unsupported agent spec version: {version}")
87
+ raise AgentSpecError(f"Unsupported agent spec version: {version}")
77
88
 
78
89
  agent_spec = AgentSpec(**data.get("agent", {}))
79
90
  if agent_spec.system_prompt_path is not None:
kimi_cli/cli.py CHANGED
@@ -1,15 +1,15 @@
1
1
  import asyncio
2
2
  import json
3
3
  import sys
4
+ from collections.abc import Callable
4
5
  from pathlib import Path
6
+ from typing import Any, Literal, get_args
5
7
 
6
8
  import click
7
9
 
8
- from kimi_cli import UIMode, kimi_run
9
- from kimi_cli.agentspec import DEFAULT_AGENT_FILE
10
- from kimi_cli.config import ConfigError, load_config
10
+ from kimi_cli import KimiCLI
11
11
  from kimi_cli.constant import VERSION
12
- from kimi_cli.metadata import continue_session, new_session
12
+ from kimi_cli.session import Session
13
13
  from kimi_cli.share import get_share_dir
14
14
  from kimi_cli.ui.print import InputFormat, OutputFormat
15
15
  from kimi_cli.utils.logging import logger
@@ -21,6 +21,9 @@ class Reload(Exception):
21
21
  pass
22
22
 
23
23
 
24
+ UIMode = Literal["shell", "print", "acp"]
25
+
26
+
24
27
  @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
25
28
  @click.version_option(VERSION)
26
29
  @click.option(
@@ -38,8 +41,8 @@ class Reload(Exception):
38
41
  @click.option(
39
42
  "--agent-file",
40
43
  type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
41
- default=DEFAULT_AGENT_FILE,
42
- help="Custom agent specification file. Default: builtin Kimi Koder.",
44
+ default=None,
45
+ help="Custom agent specification file. Default: builtin default agent.",
43
46
  )
44
47
  @click.option(
45
48
  "--model",
@@ -77,7 +80,7 @@ class Reload(Exception):
77
80
  @click.option(
78
81
  "--ui",
79
82
  "ui",
80
- type=click.Choice(["shell", "print", "acp"]),
83
+ type=click.Choice(get_args(UIMode)),
81
84
  default="shell",
82
85
  help="UI mode to use. Default: shell.",
83
86
  )
@@ -85,7 +88,7 @@ class Reload(Exception):
85
88
  "--print",
86
89
  "ui",
87
90
  flag_value="print",
88
- help="Run in print mode. Shortcut for `--ui print`.",
91
+ help="Run in print mode. Shortcut for `--ui print`. Note: print mode implicitly adds `--yolo`.",
89
92
  )
90
93
  @click.option(
91
94
  "--acp",
@@ -95,7 +98,7 @@ class Reload(Exception):
95
98
  )
96
99
  @click.option(
97
100
  "--input-format",
98
- type=click.Choice(["text", "stream-json"]),
101
+ type=click.Choice(get_args(InputFormat)),
99
102
  default=None,
100
103
  help=(
101
104
  "Input format to use. Must be used with `--print` "
@@ -105,7 +108,7 @@ class Reload(Exception):
105
108
  )
106
109
  @click.option(
107
110
  "--output-format",
108
- type=click.Choice(["text", "stream-json"]),
111
+ type=click.Choice(get_args(OutputFormat)),
109
112
  default=None,
110
113
  help="Output format to use. Must be used with `--print`. Default: text.",
111
114
  )
@@ -140,7 +143,7 @@ class Reload(Exception):
140
143
  def kimi(
141
144
  verbose: bool,
142
145
  debug: bool,
143
- agent_file: Path,
146
+ agent_file: Path | None,
144
147
  model_name: str | None,
145
148
  work_dir: Path,
146
149
  continue_: bool,
@@ -153,7 +156,11 @@ def kimi(
153
156
  yolo: bool,
154
157
  ):
155
158
  """Kimi, your next CLI agent."""
156
- echo = click.echo if verbose else lambda *args, **kwargs: None
159
+
160
+ def _noop_echo(*args: Any, **kwargs: Any):
161
+ pass
162
+
163
+ echo: Callable[..., None] = click.echo if verbose else _noop_echo
157
164
 
158
165
  logger.add(
159
166
  get_share_dir() / "logs" / "kimi.log",
@@ -163,19 +170,23 @@ def kimi(
163
170
  )
164
171
 
165
172
  work_dir = work_dir.absolute()
166
-
167
173
  if continue_:
168
- session = continue_session(work_dir)
174
+ session = Session.continue_(work_dir)
169
175
  if session is None:
170
176
  raise click.BadOptionUsage(
171
177
  "--continue", "No previous session found for the working directory"
172
178
  )
173
179
  echo(f"✓ Continuing previous session: {session.id}")
174
180
  else:
175
- session = new_session(work_dir)
181
+ session = Session.create(work_dir)
176
182
  echo(f"✓ Created new session: {session.id}")
177
183
  echo(f"✓ Session history file: {session.history_file}")
178
184
 
185
+ if command is not None:
186
+ command = command.strip()
187
+ if not command:
188
+ raise click.BadOptionUsage("--command", "Command cannot be empty")
189
+
179
190
  if input_format is not None and ui != "print":
180
191
  raise click.BadOptionUsage(
181
192
  "--input-format",
@@ -188,7 +199,7 @@ def kimi(
188
199
  )
189
200
 
190
201
  try:
191
- mcp_configs = [json.loads(conf.read_text()) for conf in mcp_config_file]
202
+ mcp_configs = [json.loads(conf.read_text(encoding="utf-8")) for conf in mcp_config_file]
192
203
  except json.JSONDecodeError as e:
193
204
  raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
194
205
 
@@ -197,29 +208,32 @@ def kimi(
197
208
  except json.JSONDecodeError as e:
198
209
  raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
199
210
 
211
+ async def _run() -> bool:
212
+ instance = await KimiCLI.create(
213
+ session,
214
+ yolo=yolo or (ui == "print"), # print mode implies yolo
215
+ stream=ui != "print", # use non-streaming mode only for print UI
216
+ mcp_configs=mcp_configs,
217
+ model_name=model_name,
218
+ agent_file=agent_file,
219
+ )
220
+ match ui:
221
+ case "shell":
222
+ return await instance.run_shell_mode(command)
223
+ case "print":
224
+ return await instance.run_print_mode(
225
+ input_format or "text",
226
+ output_format or "text",
227
+ command,
228
+ )
229
+ case "acp":
230
+ if command is not None:
231
+ logger.warning("ACP server ignores command argument")
232
+ return await instance.run_acp_server()
233
+
200
234
  while True:
201
235
  try:
202
- try:
203
- config = load_config()
204
- except ConfigError as e:
205
- raise click.ClickException(f"Failed to load config: {e}") from e
206
- echo(f"✓ Loaded config: {config}")
207
-
208
- succeeded = asyncio.run(
209
- kimi_run(
210
- config=config,
211
- model_name=model_name,
212
- work_dir=work_dir,
213
- session=session,
214
- command=command,
215
- agent_file=agent_file,
216
- ui=ui,
217
- input_format=input_format,
218
- output_format=output_format,
219
- mcp_configs=mcp_configs,
220
- yolo=yolo,
221
- )
222
- )
236
+ succeeded = asyncio.run(_run())
223
237
  if not succeeded:
224
238
  sys.exit(1)
225
239
  break
kimi_cli/config.py CHANGED
@@ -4,6 +4,7 @@ from typing import Literal, Self
4
4
 
5
5
  from pydantic import BaseModel, Field, SecretStr, ValidationError, field_serializer, model_validator
6
6
 
7
+ from kimi_cli.exception import ConfigError
7
8
  from kimi_cli.share import get_share_dir
8
9
  from kimi_cli.utils.logging import logger
9
10
 
@@ -17,6 +18,8 @@ class LLMProvider(BaseModel):
17
18
  """API base URL"""
18
19
  api_key: SecretStr
19
20
  """API key"""
21
+ custom_headers: dict[str, str] = Field(default_factory=dict)
22
+ """Custom headers to include in API requests"""
20
23
 
21
24
  @field_serializer("api_key", when_used="json")
22
25
  def dump_secret(self, v: SecretStr):
@@ -50,6 +53,8 @@ class MoonshotSearchConfig(BaseModel):
50
53
  """Base URL for Moonshot Search service."""
51
54
  api_key: SecretStr
52
55
  """API key for Moonshot Search service."""
56
+ custom_headers: dict[str, str] = Field(default_factory=dict)
57
+ """Custom headers to include in API requests."""
53
58
 
54
59
  @field_serializer("api_key", when_used="json")
55
60
  def dump_secret(self, v: SecretStr):
@@ -99,13 +104,21 @@ def get_default_config() -> Config:
99
104
  )
100
105
 
101
106
 
102
- def load_config() -> Config:
103
- """Load configuration from config file.
107
+ def load_config(config_file: Path | None = None) -> Config:
108
+ """
109
+ Load configuration from config file.
110
+ If the config file does not exist, create it with default configuration.
111
+
112
+ Args:
113
+ config_file (Path | None): Path to the configuration file. If None, use default path.
104
114
 
105
115
  Returns:
106
116
  Validated Config object.
117
+
118
+ Raises:
119
+ ConfigError: If the configuration file is invalid.
107
120
  """
108
- config_file = get_config_file()
121
+ config_file = config_file or get_config_file()
109
122
  logger.debug("Loading config from file: {file}", file=config_file)
110
123
 
111
124
  if not config_file.exists():
@@ -119,20 +132,21 @@ def load_config() -> Config:
119
132
  with open(config_file, encoding="utf-8") as f:
120
133
  data = json.load(f)
121
134
  return Config(**data)
122
- except (json.JSONDecodeError, ValidationError) as e:
123
- raise ConfigError(f"Invalid configuration file: {config_file}") from e
124
-
135
+ except json.JSONDecodeError as e:
136
+ raise ConfigError(f"Invalid JSON in configuration file: {e}") from e
137
+ except ValidationError as e:
138
+ raise ConfigError(f"Invalid configuration file: {e}") from e
125
139
 
126
- class ConfigError(Exception):
127
- """Configuration error."""
128
-
129
- def __init__(self, message: str):
130
- super().__init__(message)
131
140
 
141
+ def save_config(config: Config, config_file: Path | None = None):
142
+ """
143
+ Save configuration to config file.
132
144
 
133
- def save_config(config: Config):
134
- """Save configuration to config file."""
135
- config_file = get_config_file()
145
+ Args:
146
+ config (Config): Config object to save.
147
+ config_file (Path | None): Path to the configuration file. If None, use default path.
148
+ """
149
+ config_file = config_file or get_config_file()
136
150
  logger.debug("Saving config to file: {file}", file=config_file)
137
151
  with open(config_file, "w", encoding="utf-8") as f:
138
152
  f.write(config.model_dump_json(indent=2, exclude_none=True))
kimi_cli/exception.py ADDED
@@ -0,0 +1,16 @@
1
+ class KimiCLIException(Exception):
2
+ """Base exception class for Kimi CLI."""
3
+
4
+ pass
5
+
6
+
7
+ class ConfigError(KimiCLIException):
8
+ """Configuration error."""
9
+
10
+ pass
11
+
12
+
13
+ class AgentSpecError(KimiCLIException):
14
+ """Agent specification error."""
15
+
16
+ pass
kimi_cli/llm.py CHANGED
@@ -52,6 +52,7 @@ def create_llm(
52
52
  stream=stream,
53
53
  default_headers={
54
54
  "User-Agent": USER_AGENT,
55
+ **provider.custom_headers,
55
56
  },
56
57
  )
57
58
  if session_id: