feat: add Kimi Code provider with OAuth device flow authentication#8466
feat: add Kimi Code provider with OAuth device flow authentication#8466jh-block merged 8 commits intoaaif-goose:mainfrom
Conversation
Replace the declarative kimi.json (which incorrectly pointed to api.moonshot.cn with a Moonshot API key) with a hand-coded provider that authenticates via OAuth 2.0 Device Authorization Grant (RFC 8628) against auth.kimi.com, and calls the Anthropic-compatible API at api.kimi.com/coding. - Device flow login with browser open + clipboard copy of user code - Automatic token refresh (5 min before expiry) - Token persisted to ~/.config/goose/kimicode/token.json (0o600) - Persistent device ID in ~/.config/goose/kimicode/device_id - RFC 8628 slow_down handling (interval backoff) - Supports kimi-k2.5 and kimi-k2-thinking models Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccdfbfdb94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .error_for_status() | ||
| .context("token poll request failed")? |
There was a problem hiding this comment.
Parse device-code pending errors before status validation
poll_for_token calls error_for_status() before deserializing the OAuth payload, so any non-2xx token response exits early and never reaches the authorization_pending/slow_down handling below. In OAuth device flow, pending authorization is commonly returned as an error payload, so this can abort goose configure before the user finishes browser approval instead of continuing to poll.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08ce154123
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| registry.register::<GeminiOAuthProvider>(true); | ||
| registry.register::<GithubCopilotProvider>(false); | ||
| registry.register::<GoogleProvider>(true); | ||
| registry.register::<KimiCodeProvider>(true); |
There was a problem hiding this comment.
Preserve backward-compatible alias for
kimi provider
This registers only kimi_code, while this same commit removes the prior declarative provider (declarative/kimi.json had name: "kimi"), so existing configs that still have GOOSE_PROVIDER: kimi now fail with Unknown provider when sessions are created. Please add a compatibility alias or migration path so upgrades do not break previously working user configurations.
Useful? React with 👍 / 👎.
| if let Ok(refreshed) = self.do_refresh_token(&token.refresh_token.clone()).await { | ||
| self.token_cache.save(&refreshed).await?; | ||
| let access = refreshed.access_token.clone(); | ||
| *guard = Some(refreshed); | ||
| return Ok(access); | ||
| } |
There was a problem hiding this comment.
Fall back to current token when refresh fails
When a cached token is within the refresh threshold but still unexpired, any refresh error immediately falls through to full device-code login instead of using the still-valid access token for the current request. That means transient token-endpoint outages can trigger unnecessary interactive auth (or outright failure in headless environments) even though the existing token would still work.
Useful? React with 👍 / 👎.
- Remove error_for_status() before parsing OAuth device flow poll response, so authorization_pending/slow_down errors are handled by the polling logic instead of aborting early - Fall back to still-valid token when refresh fails instead of triggering unnecessary device flow re-authentication Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a56ae0fcf5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let Some(token) = self.token_cache.load().await { | ||
| if let Ok(refreshed) = self.do_refresh_token(&token.refresh_token).await { | ||
| self.token_cache.save(&refreshed).await.map_err(|e| { | ||
| ProviderError::ExecutionError(format!("Failed to save token: {}", e)) | ||
| })?; |
There was a problem hiding this comment.
Fall back to existing token when refresh fails in configure
When configure_oauth finds a cached token, it only handles the refresh-success path and otherwise immediately drops into full device-code login. If the refresh endpoint is temporarily unavailable (or returns an error) but the cached access token is still unexpired, goose configure unnecessarily forces interactive re-auth and can fail in headless/offline contexts even though requests would still work with the current token. This should mirror get_access_token and reuse the still-valid token before starting device flow.
Useful? React with 👍 / 👎.
RFC 8628 device authorization grant requires application/x-www-form-urlencoded, not JSON. Fixes device flow login failing with 400 error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a69f6f7e1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.token_cache | ||
| .save(&token) | ||
| .await | ||
| .map_err(|e| ProviderError::ExecutionError(format!("Failed to save token: {}", e)))?; | ||
| *self.cached_token.lock().await = Some(token); |
There was a problem hiding this comment.
Record OAuth configured state after successful Kimi login
configure_oauth only updates the local token cache and in-memory token, then returns success, but it never persists any config-visible signal (e.g., KIMI_CODE_TOKEN or a kimi_code_configured marker). In the server config path, check_provider_configured treats OAuth providers as configured only when that marker (or required key) exists, so Kimi can still be reported as unconfigured and /config/providers/{name}/models can reject it after a successful login done outside the server OAuth endpoint. Persisting configured state here would keep CLI/server behavior consistent.
Useful? React with 👍 / 👎.
| let mut effective_interval = interval_secs; | ||
| for attempt in 0..MAX_POLL_ATTEMPTS { | ||
| tokio::time::sleep(tokio::time::Duration::from_secs(effective_interval)).await; |
There was a problem hiding this comment.
Poll device flow until server expiry instead of fixed attempts
The polling loop is capped by a hardcoded attempt count (MAX_POLL_ATTEMPTS), which limits auth to roughly 5 minutes at the default interval and can terminate early even when the device code is still valid. Device-code flows are intended to run until the server-declared expiration window closes, so users who need longer approval time (SSO/MFA/device switching) can be forced into unnecessary re-login cycles. Use the device authorization expires_in to compute an absolute deadline and poll until that deadline instead of a fixed attempt cap.
Useful? React with 👍 / 👎.
- configure_oauth: fall back to still-valid token when refresh fails instead of triggering unnecessary device flow re-auth - poll_for_token: use server-provided expires_in deadline instead of fixed MAX_POLL_ATTEMPTS, giving users more time for SSO/MFA approval Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 072be3650e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let mut headers = reqwest::header::HeaderMap::new(); | ||
| headers.insert("X-Msh-Platform", KIMI_MSH_PLATFORM.parse().unwrap()); | ||
| headers.insert("X-Msh-Version", KIMI_MSH_VERSION.parse().unwrap()); | ||
| headers.insert("X-Msh-Device-Id", self.device_id.parse().unwrap()); |
There was a problem hiding this comment.
Validate device ID header value before unwrapping
get_or_create_device_id accepts any non-empty contents from ~/.config/goose/kimicode/device_id, but kimi_headers later does self.device_id.parse().unwrap(). If that file is corrupted or manually edited to include an invalid header value (e.g., control/non-ASCII characters), provider initialization succeeds and every auth/API call panics at runtime instead of returning a recoverable error. Please validate/regenerate invalid IDs or propagate a normal error when building headers.
Useful? React with 👍 / 👎.
|
Re-assigned to myself since the associated issue is assigned to me |
jh-block
left a comment
There was a problem hiding this comment.
just a couple of minor issues. there's also quite a bit of duplication between get_access_token and configure_oauth. maybe configure_oauth can call get_access_token, or a shared helper can be extracted?
eventually we might want to extract a common oauth device flow helper that multiple providers can use, but no need to do that as part of this PR.
| }) | ||
| .send() | ||
| .await | ||
| .context("failed to poll for token")? |
There was a problem hiding this comment.
need a raise_for_status() here to handle possible HTTP errors
There was a problem hiding this comment.
Addressed in commit a5113f5, but not as a literal error_for_status() — the OAuth device flow (RFC 8628) returns authorization_pending and slow_down as 4xx responses with a JSON body, so raising on any non-2xx would break polling entirely (that was the original Codex P1 on the first commit).
The new code in poll_for_token reads the body first, attempts to parse it as PollResp, and only surfaces the HTTP status when parsing fails and the status is non-success. That preserves RFC 8628 semantics while still giving full status + body context on real transport errors.
Covered by:
poll_for_token_handles_authorization_pending_then_success(400 + pending body)poll_for_token_surfaces_http_error_on_unparseable_body(502 + unparseable body)
| let known: Vec<String> = KIMI_CODE_KNOWN_MODELS | ||
| .iter() | ||
| .map(|s| s.to_string()) | ||
| .collect(); |
There was a problem hiding this comment.
this test isn't actually testing anything; it's just testing that a constant contains the values that it contains
There was a problem hiding this comment.
Agreed — removed in commit a5113f5 and replaced with wiremock-backed tests that exercise real refresh/poll behavior:
use_or_refresh_returns_fresh_token_without_calling_endpointuse_or_refresh_falls_back_to_existing_token_when_refresh_failsuse_or_refresh_returns_new_token_on_successful_refreshuse_or_refresh_preserves_refresh_token_when_server_omits_itpoll_for_token_handles_authorization_pending_then_successpoll_for_token_accepts_response_without_refresh_tokenpoll_for_token_surfaces_http_error_on_unparseable_bodykimi_headers_skips_invalid_device_id_without_panicvalidates_device_id_rejects_invalid_bytes
… tests Address PR aaif-goose#8466 review feedback: - Extract `ensure_token` + `use_or_refresh` so `get_access_token` and `configure_oauth` share the cache → refresh → device-flow ladder. - Validate device_id against `HeaderValue::from_str` and regenerate when the on-disk file is corrupted; `kimi_headers` no longer panics. - Persist `kimi_code_configured = true` on successful OAuth so the server's `check_provider_configured` reports the provider as ready. - In `poll_for_token`, parse the body before status checks (preserves RFC 8628 `authorization_pending` / `slow_down` semantics) but surface the HTTP status when the body is unparseable. - Drop the trivial constants test and replace with wiremock-based coverage for refresh fallback, refresh success, pending-then-success polling, invalid device_id headers, and HTTP error surfacing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Follow-ups to the OAuth refactor (aaif-goose#8466): - Name the previously hardcoded fallbacks: DEFAULT_TOKEN_LIFETIME_SECS, DEFAULT_DEVICE_CODE_LIFETIME_SECS, DEFAULT_POLL_INTERVAL_SECS, SLOW_DOWN_BACKOFF_SECS. - Warn when the cached token.json fails to deserialize instead of silently dropping through to device flow. - Move the interactive "Please visit <url>" prompt to stderr so that stdout-parsing CLI workflows are not disturbed. - Emit tracing::info for a new OAuth device-flow login, and tracing::debug for refresh success / failure / fallback — but only for outcome-bearing events. The procedural "trying cache", "token is fresh" lines were noise and have been dropped. - Document the KIMI_CODE_TOKEN ConfigKey as a marker (the actual token is cached on disk), and clarify the kimi_headers fallback comment. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b155ae3df6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let (Some(access_token), Some(refresh_token)) = | ||
| (resp.access_token, resp.refresh_token) | ||
| { |
There was a problem hiding this comment.
Accept device-flow tokens without refresh_token
poll_for_token only returns success when both access_token and refresh_token are present, but OAuth token responses may legitimately omit refresh_token; in that case this code keeps polling until timeout even though authentication already succeeded. This makes goose configure fail for providers/environments that issue access-only token responses.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 3b04aa8. poll_for_token now succeeds on just access_token and defaults refresh_token to empty when the server omits it, per RFC 6749. Covered by new test poll_for_token_accepts_response_without_refresh_token.
| struct RefreshResp { | ||
| access_token: String, | ||
| refresh_token: String, | ||
| expires_in: Option<i64>, |
There was a problem hiding this comment.
Keep prior refresh token when refresh response omits it
do_refresh_token requires refresh_token in the refresh response payload, but OAuth refresh responses are allowed to return only a new access_token. When that happens deserialization fails, use_or_refresh treats refresh as failed, and once the current token expires users are forced back through interactive device auth instead of continuing with the existing refresh token.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 3b04aa8. RefreshResp.refresh_token is now Option<String>, and do_refresh_token falls back to reusing the prior refresh_token when the server omits one, per RFC 6749 §6. Covered by new test use_or_refresh_preserves_refresh_token_when_server_omits_it.
Codex P2 follow-ups on aaif-goose#8466: - `poll_for_token` now succeeds on just `access_token`, defaulting `refresh_token` to empty if the server omits it. - `do_refresh_token` now deserializes `refresh_token` as Option<String> and falls back to reusing the prior refresh_token when the server omits it per RFC 6749 §6. - Added wiremock coverage for both paths. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
|
@jh-block Thanks for the review. Quick status update — all inline feedback is addressed:
Also addressed during this round:
Follow-ups intentionally out of scope (happy to split into separate PRs):
All tests pass, clippy clean. Ready for another look. |
Follow-up to aaif-goose#8466. Hitting the actual Kimi Code API after merge revealed that the `/v1/models` endpoint exposes a single model, `kimi-for-coding`, and silently routes any other model name to it. The previously hardcoded `kimi-k2.5` / `kimi-k2-thinking` entries never existed as distinct models and only misled the configure picker. - Replace hardcoded constants with `kimi-for-coding`. - Add `fetch_live_models` that queries `{api_base}/v1/models` and `fetch_supported_models` now uses the live catalogue, falling back to `KIMI_CODE_KNOWN_MODELS` when the endpoint is unreachable or returns an empty list. - New wiremock tests cover the happy path, 5xx fallback, and empty-list fallback. Existing users with `GOOSE_MODEL: kimi-k2.5` in their config keep working because the server's silent fallback routes them to `kimi-for-coding` — no user-facing migration required. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Follow-up to aaif-goose#8466. Hitting the actual Kimi Code API after merge revealed that `/v1/models` exposes a single model, `kimi-for-coding`, and the inference endpoint silently accepts any `model` field and routes every request to it. The previously hardcoded `kimi-k2.5` / `kimi-k2-thinking` entries never existed as distinct models and only misled the configure picker. - Replace hardcoded defaults with `kimi-for-coding`. - `fetch_supported_models` now queries `{api_base}/v1/models` directly and propagates errors, mirroring the `openai_compatible` provider. No fallback: by the time this runs OAuth has completed, and a failure is better surfaced than hidden behind a stale static list. - New wiremock tests cover the server catalogue response and the 5xx error-propagation path. Existing users with `GOOSE_MODEL: kimi-k2.5` in their config keep working because the server silently routes them to `kimi-for-coding`. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Follow-up to aaif-goose#8466. Hitting the actual Kimi Code API after merge revealed that `/v1/models` exposes a single model, `kimi-for-coding`, and the inference endpoint silently accepts any `model` field and routes every request to it. The previously hardcoded `kimi-k2.5` / `kimi-k2-thinking` entries never existed as distinct models and only misled the configure picker. - Replace hardcoded defaults with `kimi-for-coding`. - `fetch_supported_models` now queries `{api_base}/v1/models` directly and propagates errors, mirroring the `openai_compatible` provider. No fallback: by the time this runs OAuth has completed, and a failure is better surfaced than hidden behind a stale static list. - New wiremock tests cover the server catalogue response and the 5xx error-propagation path. Existing users with `GOOSE_MODEL: kimi-k2.5` in their config keep working because the server silently routes them to `kimi-for-coding`. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Both `kimicode` and `githubcopilot` implemented RFC 8628 device-code polling independently. Their two copies diverged enough to hide bugs: - `githubcopilot::poll_for_access_token` called `error_for_status()` before parsing the JSON body, so RFC 8628 error responses like `authorization_pending` (returned with 4xx status per §3.5) could surface as a plain HTTP error instead of polling continuation. - It also ignored `slow_down` (§3.5: MUST increase interval by 5s) and used a hardcoded 36 × 5s poll window instead of the server's `expires_in`. - `kimicode` handled both correctly — now its implementation is the shared one. Add `providers::oauth_device_flow` with `run_device_flow` + `refresh_device_flow_token` entry points. Providers configure via a `DeviceFlowConfig` that carries endpoint URLs, client_id, scopes, extra headers, and request encoding (`Form` for kimi, `Json` for github). `DeviceFlowTokens` exposes `refresh_token` and `expires_at` as `Option`s so callers can apply their own fallback when the server omits them (RFC 6749 §5.1 / §6). - kimicode: `device_flow_login`, `poll_for_token`, and `do_refresh_token` become thin wrappers over the helper, normalizing the on-disk `KimiToken` shape via `tokens_to_kimi`. - githubcopilot: `login` → `run_device_flow`; `get_device_code` and `poll_for_access_token` removed. The RFC 8628 compliance issues above are fixed as a side effect. Net change: +250 in helper (+ tests), −370 across the two providers (−120 for a net reduction of ~315 lines). Testing: - 9 new wiremock tests in `oauth_device_flow` cover pending → success, `slow_down` backoff, expires_in timeout, HTTP error body, server-side `access_denied`, missing refresh_token, and refresh happy-path. - kimicode tests now focus on Kimi-specific integration (token cache, refresh fallback); duplicated polling tests removed — the helper tests cover equivalent behavior. Follow-up to aaif-goose#8466. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
* main: (37 commits) polish: refine sidebar activity indicators, add placeholder token, and tidy search field (#8606) feat: add /edit command to cli for on-demand prompt editing (#8566) docs(mcp): add Rendex MCP Server extension tutorial (#8541) Lifei/delete tauri backend acp (#8582) chore: set goose binaries as executable in package.json (#8589) feat: add Novita AI as declarative provider (#8432) feat: add Kimi Code provider with OAuth device flow authentication (#8466) fix: chat loading-state model placeholder (#8431) fix: expand tool calls by default when Response Style is Detailed (#8478) fix: create logs dir before writing llm request log (#8522) fix: enable token usage tracking and configurable stream timeout for Ollama provider (#8493) fix tauri-plugin-dialog version constraint to match other plugins (#8542) call goose serve from tauri frontend via goose-acp client (#8549) failed the script when bundle:default fails and cleanup "alpha" (#8580) pass globally unique conversation identifier as sessionId in databricks api call (#8576) fix: use sqlx chrono decode for thread timestamps instead of manual parsing (#8575) docs: remove stale gemini-acp references (#8572) show individual untracked files in git changes widget (#8574) fix: update publishing flow to include new sdk dir (#8573) fix: remove double border on content in chat (#8545) ...
Both `kimicode` and `githubcopilot` implemented RFC 8628 device-code polling independently. Their two copies diverged enough to hide bugs: - `githubcopilot::poll_for_access_token` called `error_for_status()` before parsing the JSON body, so RFC 8628 error responses like `authorization_pending` (returned with 4xx status per §3.5) could surface as a plain HTTP error instead of polling continuation. - It also ignored `slow_down` (§3.5: MUST increase interval by 5s) and used a hardcoded 36 × 5s poll window instead of the server's `expires_in`. - `kimicode` handled both correctly — now its implementation is the shared one. Add `providers::oauth_device_flow` with `run_device_flow` + `refresh_device_flow_token` entry points. Providers configure via a `DeviceFlowConfig` that carries endpoint URLs, client_id, scopes, extra headers, and request encoding (`Form` for kimi, `Json` for github). `DeviceFlowTokens` exposes `refresh_token` and `expires_at` as `Option`s so callers can apply their own fallback when the server omits them (RFC 6749 §5.1 / §6). - kimicode: `device_flow_login`, `poll_for_token`, and `do_refresh_token` become thin wrappers over the helper, normalizing the on-disk `KimiToken` shape via `tokens_to_kimi`. - githubcopilot: `login` → `run_device_flow`; `get_device_code` and `poll_for_access_token` removed. The RFC 8628 compliance issues above are fixed as a side effect. Net change: +250 in helper (+ tests), −370 across the two providers (−120 for a net reduction of ~315 lines). Testing: - 9 new wiremock tests in `oauth_device_flow` cover pending → success, `slow_down` backoff, expires_in timeout, HTTP error body, server-side `access_denied`, missing refresh_token, and refresh happy-path. - kimicode tests now focus on Kimi-specific integration (token cache, refresh fallback); duplicated polling tests removed — the helper tests cover equivalent behavior. Follow-up to aaif-goose#8466. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Both `kimicode` and `githubcopilot` implemented RFC 8628 device-code polling independently. Their two copies diverged enough to hide bugs: - `githubcopilot::poll_for_access_token` called `error_for_status()` before parsing the JSON body, so RFC 8628 error responses like `authorization_pending` (returned with 4xx status per §3.5) could surface as a plain HTTP error instead of polling continuation. - It also ignored `slow_down` (§3.5: MUST increase interval by 5s) and used a hardcoded 36 × 5s poll window instead of the server's `expires_in`. - `kimicode` handled both correctly — now its implementation is the shared one. Add `providers::oauth_device_flow` with `run_device_flow` + `refresh_device_flow_token` entry points. Providers configure via a `DeviceFlowConfig` that carries endpoint URLs, client_id, scopes, extra headers, and request encoding (`Form` for kimi, `Json` for github). `DeviceFlowTokens` exposes `refresh_token` and `expires_at` as `Option`s so callers can apply their own fallback when the server omits them (RFC 6749 §5.1 / §6). - kimicode: `device_flow_login`, `poll_for_token`, and `do_refresh_token` become thin wrappers over the helper, normalizing the on-disk `KimiToken` shape via `tokens_to_kimi`. - githubcopilot: `login` → `run_device_flow`; `get_device_code` and `poll_for_access_token` removed. The RFC 8628 compliance issues above are fixed as a side effect. Net change: +250 in helper (+ tests), −370 across the two providers (−120 for a net reduction of ~315 lines). Testing: - 9 new wiremock tests in `oauth_device_flow` cover pending → success, `slow_down` backoff, expires_in timeout, HTTP error body, server-side `access_denied`, missing refresh_token, and refresh happy-path. - kimicode tests now focus on Kimi-specific integration (token cache, refresh fallback); duplicated polling tests removed — the helper tests cover equivalent behavior. Follow-up to aaif-goose#8466. Signed-off-by: DaeHee Lee <lee111dae11@proton.me>
Summary
Add a Kimi Code provider with OAuth 2.0 Device Authorization Grant (RFC 8628) authentication.
The existing
kimi.jsondeclarative provider was broken — it pointed toapi.moonshot.cn(Moonshot's direct API) with aMOONSHOT_API_KEY, rather than the Kimi Code platform (api.kimi.com/coding) which uses OAuth. This PR replaces it with a hand-coded provider that works correctly.How authentication works:
goose configure→ select "Kimi Code" → browser opens + user code copied to clipboard → user approves onauth.kimi.com→ token saved to~/.config/goose/kimicode/token.json(mode0o600)refresh_tokenFollows the same patterns as
GithubCopilotProvider(device code flow) andAnthropicProvider(Anthropic-compatible SSE streaming).Key implementation details:
https://auth.kimi.com, Client ID:17e5f671-d194-4dfb-9706-5516cb48c098(public client ID, same as kimi-cli and oh-my-pi)slow_downerror handled — polling interval increases by 5 s per server requestkimi-k2.5andkimi-k2-thinkingmodelsreqwest::Clientfor API calls (no per-request client construction)Testing
Manual testing plan:
goose configure→ select "Kimi Code" → complete OAuth device flow in browser~/.config/goose/kimicode/token.jsonexists with0o600permissionskimi-k2.5and confirm streaming responses workexpires_atto a past time in the token file and confirm auto-refresh triggersRelated Issues
Relates to #7150
Discussion: (none)