You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add Kimi Code as a built-in ACP provider, meeting the same bar as claude-code, codex and gemini-cli.
Community PR #4714 by @ysntony is open and implements the core of this. This issue tracks the harness to the full parity contract so "supported" means the same thing it does for the three reference providers.
Parity contract
1. Registry record in openhands-sdk/openhands/sdk/settings/acp_providers.py — every field populated from Kimi's actual server behaviour:
key, display_name
default_command — npx -y --prefer-offline <pkg>@<KIMI_VERSION> (may need a trailing arg, as gemini-cli appends --acp)
binary_name — the wrapper binary _prefer_pinned_binary() looks for; omit it and Kimi always pays the npx path
data_dir_env_var — the env var pointed at a per-conversation dir so credentials, sessions and caches don't leak between runs
api_key_env_var, base_url_env_var (or None)
default_session_mode — the mode that disables permission prompts
agent_name_patterns
supports_set_session_model — does the initial model need a protocol call?
supports_runtime_model_switch — does mid-conversation switching work?
session_meta_key (or None)
available_models + default_model — the model picker
file_secrets — only if Kimi authenticates from a file on disk rather than an env var
Determine the two supports_* flags from Kimi's real behaviour rather than copying a neighbour's values. Reporting supports_runtime_model_switch=False when the server supports it silently drops a user-visible feature.
3. Pin-parity test — [ ] add the key to _REGISTRY_ACP_PACKAGES in tests/cross/test_agent_server_build_metadata.py.
Warning
This one is easy to miss and fails silently. test_agent_server_dockerfile_acp_package_versions_match_registry does registry_package = _REGISTRY_ACP_PACKAGES.get(provider_key) followed by if registry_package is None: continue — a provider present in the Dockerfile but absent from that dict is skipped, not flagged. Its pin can then drift from the registry indefinitely with CI green. #4714 currently adds a Dockerfile arm without a dict entry, so the pin would be uncovered.
clients/typescript/scripts/check-acp-drift.py compares the whole registry field-for-field and runs in typescript-client-ci.yml, so this will fail CI if skipped. #4714 does not currently touch this file.
5. Preinstall decision — [ ] decide explicitly whether Kimi joins the image, and record why.
Important
Registry membership is not image membership. Since #4805, a provider absent from the image is not broken — resolve_acp_command() falls back to the version-pinned npx install, cached under ~/.openhands. Adding Kimi to the default INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli grows every published image for every user, so registry-only is the default and preinstalling is a deliberate, recorded decision. #4714 currently adds a Dockerfile arm; worth confirming that is intended rather than assumed necessary.
Note the default list is also hardcoded in .github/workflows/server.yml and asserted verbatim by a test, so preinstalling means changing both.
6. Tests — [ ] tests/sdk/settings/test_acp_providers.py (its set(ACP_PROVIDERS) == {...} assertion forces this), plus the paths covering the reference providers: tests/sdk/agent/test_acp_agent.py, tests/agent_server/test_credential_binding.py, test_settings_router.py.
7. Live verification — [ ] a real ACP turn against the actual Kimi server: session created, tool call executed, reply returned. The registry record is a claim about server behaviour; only a live run substantiates it.
Notes
@ysntony's PR is the starting point, not a gap list — the items above are what separates a working addition from parity with the three reference providers, and several are repo conventions that aren't discoverable from the diff alone.
What the existing providers actually set
The contract above is abstract; this is the concrete shape, read from the registry on main. Use it to decide what each field should be for this harness — and to see which values are genuinely provider-specific versus conventional.
binary_name and data_dir_env_var are part of the record and easy to overlook.binary_name is what _prefer_pinned_binary() looks for to use an installed binary instead of npx; omit it and the provider always pays the npx path. data_dir_env_var is the env var pointed at a per-conversation directory so credentials, sessions, plugins and caches don't leak between runs.
data_dir_env_var is not always a dedicated variable.gemini-cli uses plain HOME, because the CLI has no config-dir override — which is why isolation for it means relocating HOME rather than setting a scoped var. If this harness has a dedicated config-dir variable, prefer it; if it only respects HOME, say so explicitly on the issue.
All three set both supports_* flags to True. That makes False the value that needs justifying: it should mean this server genuinely cannot do it, verified against the running server, not not implemented yet. A silent False removes the model picker or mid-conversation switching for users of this harness.
session_meta_key is the exception, not the rule — only claude-code sets one ("claudeCode"), because claude-agent-acp ignores session-_meta model selection and needs the model pushed via a protocol call. Default to None unless the server actually reads a _meta key.
default_command may carry arguments, not just a package: gemini-cli appends --acp to put the CLI into ACP mode.
file_secrets, if the CLI authenticates from a file
Only needed when the credential is a file on disk rather than an env var. The two live specs:
// codex — env var points at the DIRECTORY containing auth.json
{ "secret_name": "CODEX_AUTH_JSON", "filename": "auth.json",
"env_var": "CODEX_HOME", "subdir": "codex",
"env_points_to": "dir", "warn_if_unset": [] }
// gemini-cli — env var points at the FILE itself
{ "secret_name": "GOOGLE_APPLICATION_CREDENTIALS_JSON", "filename": "gcloud-credentials.json",
"env_var": "GOOGLE_APPLICATION_CREDENTIALS", "subdir": "gemini-cli",
"env_points_to": "file", "warn_if_unset": ["GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION"] }
env_points_to distinguishes the two shapes and is the field most likely to be set wrong — Codex wants a directory, Gemini wants the file. warn_if_unset names companion variables that are useless alone (Gemini's Vertex project and location). The SDK owns the mechanism — materialising the file under the conversation's durable root, seed-if-absent, setting the env var; the spec is only policy.
Parent epic: #4820
Goal
Add Kimi Code as a built-in ACP provider, meeting the same bar as
claude-code,codexandgemini-cli.Community PR #4714 by @ysntony is open and implements the core of this. This issue tracks the harness to the full parity contract so "supported" means the same thing it does for the three reference providers.
Parity contract
1. Registry record in
openhands-sdk/openhands/sdk/settings/acp_providers.py— every field populated from Kimi's actual server behaviour:key,display_namedefault_command—npx -y --prefer-offline <pkg>@<KIMI_VERSION>(may need a trailing arg, asgemini-cliappends--acp)binary_name— the wrapper binary_prefer_pinned_binary()looks for; omit it and Kimi always pays thenpxpathdata_dir_env_var— the env var pointed at a per-conversation dir so credentials, sessions and caches don't leak between runsapi_key_env_var,base_url_env_var(orNone)default_session_mode— the mode that disables permission promptsagent_name_patternssupports_set_session_model— does the initial model need a protocol call?supports_runtime_model_switch— does mid-conversation switching work?session_meta_key(orNone)available_models+default_model— the model pickerfile_secrets— only if Kimi authenticates from a file on disk rather than an env var2. Version constant — [ ]
KIMI_*_VERSIONalongsideCLAUDE_AGENT_ACP_VERSION/CODEX_ACP_VERSION/GEMINI_CLI_VERSION.3. Pin-parity test — [ ] add the key to
_REGISTRY_ACP_PACKAGESintests/cross/test_agent_server_build_metadata.py.Warning
This one is easy to miss and fails silently.
test_agent_server_dockerfile_acp_package_versions_match_registrydoesregistry_package = _REGISTRY_ACP_PACKAGES.get(provider_key)followed byif registry_package is None: continue— a provider present in the Dockerfile but absent from that dict is skipped, not flagged. Its pin can then drift from the registry indefinitely with CI green. #4714 currently adds a Dockerfile arm without a dict entry, so the pin would be uncovered.4. TypeScript mirror — [ ] update
clients/typescript/src/models/acp-providers.json.5. Preinstall decision — [ ] decide explicitly whether Kimi joins the image, and record why.
Important
Registry membership is not image membership. Since #4805, a provider absent from the image is not broken —
resolve_acp_command()falls back to the version-pinnednpxinstall, cached under~/.openhands. Adding Kimi to the defaultINSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cligrows every published image for every user, so registry-only is the default and preinstalling is a deliberate, recorded decision. #4714 currently adds a Dockerfile arm; worth confirming that is intended rather than assumed necessary.Note the default list is also hardcoded in
.github/workflows/server.ymland asserted verbatim by a test, so preinstalling means changing both.6. Tests — [ ]
tests/sdk/settings/test_acp_providers.py(itsset(ACP_PROVIDERS) == {...}assertion forces this), plus the paths covering the reference providers:tests/sdk/agent/test_acp_agent.py,tests/agent_server/test_credential_binding.py,test_settings_router.py.7. Live verification — [ ] a real ACP turn against the actual Kimi server: session created, tool call executed, reply returned. The registry record is a claim about server behaviour; only a live run substantiates it.
Notes
@ysntony's PR is the starting point, not a gap list — the items above are what separates a working addition from parity with the three reference providers, and several are repo conventions that aren't discoverable from the diff alone.
What the existing providers actually set
The contract above is abstract; this is the concrete shape, read from the registry on
main. Use it to decide what each field should be for this harness — and to see which values are genuinely provider-specific versus conventional.claude-codecodexgemini-clidisplay_namedefault_commandnpx -y --prefer-offline @agentclientprotocol/claude-agent-acp@0.63.0npx -y --prefer-offline @agentclientprotocol/codex-acp@1.1.7npx -y --prefer-offline @google/gemini-cli@0.46.0 --acpbinary_nameclaude-agent-acpcodex-acpgeminiapi_key_env_varANTHROPIC_API_KEYOPENAI_API_KEYGEMINI_API_KEYbase_url_env_varANTHROPIC_BASE_URLOPENAI_BASE_URLGEMINI_BASE_URLdata_dir_env_varCLAUDE_CONFIG_DIRCODEX_HOMEHOMEdefault_session_modebypassPermissionsagent-full-accessdefaultagent_name_patterns["claude-agent"]["codex-acp"]["gemini-cli"]supports_set_session_modelTrueTrueTruesupports_runtime_model_switchTrueTrueTruesession_meta_key"claudeCode"NoneNonedefault_modelopus[1m]gpt-5.5autoavailable_modelsfile_secretsCODEX_AUTH_JSONGOOGLE_APPLICATION_CREDENTIALS_JSONWhat to take from it
binary_nameanddata_dir_env_varare part of the record and easy to overlook.binary_nameis what_prefer_pinned_binary()looks for to use an installed binary instead ofnpx; omit it and the provider always pays thenpxpath.data_dir_env_varis the env var pointed at a per-conversation directory so credentials, sessions, plugins and caches don't leak between runs.data_dir_env_varis not always a dedicated variable.gemini-cliuses plainHOME, because the CLI has no config-dir override — which is why isolation for it means relocatingHOMErather than setting a scoped var. If this harness has a dedicated config-dir variable, prefer it; if it only respectsHOME, say so explicitly on the issue.supports_*flags toTrue. That makesFalsethe value that needs justifying: it should mean this server genuinely cannot do it, verified against the running server, not not implemented yet. A silentFalseremoves the model picker or mid-conversation switching for users of this harness.session_meta_keyis the exception, not the rule — onlyclaude-codesets one ("claudeCode"), becauseclaude-agent-acpignores session-_metamodel selection and needs the model pushed via a protocol call. Default toNoneunless the server actually reads a_metakey.default_commandmay carry arguments, not just a package:gemini-cliappends--acpto put the CLI into ACP mode.file_secrets, if the CLI authenticates from a fileOnly needed when the credential is a file on disk rather than an env var. The two live specs:
env_points_todistinguishes the two shapes and is the field most likely to be set wrong — Codex wants a directory, Gemini wants the file.warn_if_unsetnames companion variables that are useless alone (Gemini's Vertex project and location). The SDK owns the mechanism — materialising the file under the conversation's durable root, seed-if-absent, setting the env var; the spec is only policy.