Skip to content

symfonic.core.kimi_credentials

kimi_credentials

Kimi credential store — auto-load + auto-refresh of the kimi-cli token.

DEV/TEST USE ONLY. Reads the plaintext OAuth credential file written by a kimi-cli login (~/.kimi-code/credentials/kimi-code.json) and yields an always-valid short-lived access token, refreshing transparently against auth.kimi.com when the cached token is within expiry_skew_seconds of expiry.

Side effect (deliberate and necessary)

The kimi-code OAuth refresh_token ROTATES: each refresh returns a NEW refresh_token and INVALIDATES the previous one. To keep the developer's kimi-cli login working, the rotated token MUST be written back to the credential file. This module therefore mutates the local kimi-cli credential file on refresh. The write is atomic (temp file + os.replace) and guarded by an exclusive file lock so a concurrent kimi-cli refresh cannot corrupt the file or race the rotation.

Tokens are NEVER logged; only a short non-leaking fingerprint is emitted.

KimiCredentialError

Bases: RuntimeError

Raised when a Kimi subscription token cannot be resolved or refreshed.

KimiCredentialStore

KimiCredentialStore(
    credentials_path: Path | None = None,
    *,
    client_id: str = _KIMI_CODE_PUBLIC_CLIENT_ID,
    token_url: str = _DEFAULT_TOKEN_URL,
    expiry_skew_seconds: int = 120,
)

Yields an always-valid kimi-cli access token, refreshing as needed.

Parameters:

Name Type Description Default
credentials_path Path | None

Override the credential file. Defaults to the KIMI_OAUTH_CREDENTIALS_FILE env var, then ~/.kimi-code/credentials/kimi-code.json.

None
client_id str

OAuth device-flow client id. Defaults to the kimi-code CLI's public client id.

_KIMI_CODE_PUBLIC_CLIENT_ID
token_url str

OAuth token endpoint. Defaults to auth.kimi.com.

_DEFAULT_TOKEN_URL
expiry_skew_seconds int

Refresh this many seconds BEFORE the cached token's expires_at so an in-flight request never carries a just-expired token. Default 120s (token life is ~15 min).

120
Source code in src/symfonic/core/kimi_credentials.py
def __init__(
    self,
    credentials_path: Path | None = None,
    *,
    client_id: str = _KIMI_CODE_PUBLIC_CLIENT_ID,
    token_url: str = _DEFAULT_TOKEN_URL,
    expiry_skew_seconds: int = 120,
) -> None:
    self._path = (
        Path(credentials_path).expanduser()
        if credentials_path is not None
        else default_credentials_path()
    )
    self._lock_path = self._path.with_name(self._path.name + ".lock")
    self._client_id = client_id
    self._token_url = token_url
    self._skew = expiry_skew_seconds

access_token

access_token() -> str

Return a valid access token, refreshing if within the skew window.

Source code in src/symfonic/core/kimi_credentials.py
def access_token(self) -> str:
    """Return a valid access token, refreshing if within the skew window."""
    creds = self._load()
    if self._is_fresh(creds):
        return str(creds["access_token"])
    return self._refresh()

peek_access_token

peek_access_token() -> str | None

Return the cached token without refreshing; None if unavailable.

For logging / fingerprinting at construction time — must never raise or trigger a network refresh.

Source code in src/symfonic/core/kimi_credentials.py
def peek_access_token(self) -> str | None:
    """Return the cached token without refreshing; ``None`` if unavailable.

    For logging / fingerprinting at construction time — must never raise or
    trigger a network refresh.
    """
    try:
        creds = self._load()
    except KimiCredentialError:
        return None
    token = creds.get("access_token")
    return str(token) if token else None

default_credentials_path

default_credentials_path() -> Path

Resolve the kimi-cli credential file path (env override -> default).

Source code in src/symfonic/core/kimi_credentials.py
def default_credentials_path() -> Path:
    """Resolve the kimi-cli credential file path (env override -> default)."""
    override = os.environ.get(_CREDENTIALS_ENV)
    if override:
        return Path(override).expanduser()
    return Path.home() / ".kimi-code" / "credentials" / "kimi-code.json"