Skip to content

feat: add Kimi Code provider with OAuth device flow authentication#8466

Merged
jh-block merged 8 commits intoaaif-goose:mainfrom
soilSpoon:feat/kimi-code-oauth-provider
Apr 16, 2026
Merged

feat: add Kimi Code provider with OAuth device flow authentication#8466
jh-block merged 8 commits intoaaif-goose:mainfrom
soilSpoon:feat/kimi-code-oauth-provider

Conversation

@soilSpoon
Copy link
Copy Markdown
Contributor

Summary

Add a Kimi Code provider with OAuth 2.0 Device Authorization Grant (RFC 8628) authentication.

The existing kimi.json declarative provider was broken — it pointed to api.moonshot.cn (Moonshot's direct API) with a MOONSHOT_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:

  1. First use: goose configure → select "Kimi Code" → browser opens + user code copied to clipboard → user approves on auth.kimi.com → token saved to ~/.config/goose/kimicode/token.json (mode 0o600)
  2. Subsequent requests: access token reused from memory/disk cache, auto-refreshed 5 min before expiry via refresh_token
  3. Token fully expired: device flow restarts automatically

Follows the same patterns as GithubCopilotProvider (device code flow) and AnthropicProvider (Anthropic-compatible SSE streaming).

Key implementation details:

  • OAuth host: https://auth.kimi.com, Client ID: 17e5f671-d194-4dfb-9706-5516cb48c098 (public client ID, same as kimi-cli and oh-my-pi)
  • RFC 8628 slow_down error handled — polling interval increases by 5 s per server request
  • Supports kimi-k2.5 and kimi-k2-thinking models
  • Reuses the provider's reqwest::Client for API calls (no per-request client construction)

Testing

Manual testing plan:

  • goose configure → select "Kimi Code" → complete OAuth device flow in browser
  • Verify ~/.config/goose/kimicode/token.json exists with 0o600 permissions
  • Run a session with kimi-k2.5 and confirm streaming responses work
  • Verify token refresh: manually set expires_at to a past time in the token file and confirm auto-refresh triggers

Related Issues

Relates to #7150
Discussion: (none)

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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/goose/src/providers/kimicode.rs Outdated
Comment on lines +280 to +281
.error_for_status()
.context("token poll request failed")?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread crates/goose/src/providers/kimicode.rs Outdated
Comment on lines +168 to +173
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/goose/src/providers/kimicode.rs Outdated
Comment on lines +502 to +506
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))
})?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/goose/src/providers/kimicode.rs Outdated
Comment on lines +516 to +520
self.token_cache
.save(&token)
.await
.map_err(|e| ProviderError::ExecutionError(format!("Failed to save token: {}", e)))?;
*self.cached_token.lock().await = Some(token);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +271 to +273
let mut effective_interval = interval_secs;
for attempt in 0..MAX_POLL_ATTEMPTS {
tokio::time::sleep(tokio::time::Duration::from_secs(effective_interval)).await;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/goose/src/providers/kimicode.rs Outdated
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());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jh-block jh-block assigned jh-block and unassigned DOsinga Apr 15, 2026
@jh-block
Copy link
Copy Markdown
Collaborator

Re-assigned to myself since the associated issue is assigned to me

Copy link
Copy Markdown
Collaborator

@jh-block jh-block left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/goose/src/providers/kimicode.rs Outdated
})
.send()
.await
.context("failed to poll for token")?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need a raise_for_status() here to handle possible HTTP errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread crates/goose/src/providers/kimicode.rs Outdated
let known: Vec<String> = KIMI_CODE_KNOWN_MODELS
.iter()
.map(|s| s.to_string())
.collect();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test isn't actually testing anything; it's just testing that a constant contains the values that it contains

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_endpoint
  • use_or_refresh_falls_back_to_existing_token_when_refresh_fails
  • use_or_refresh_returns_new_token_on_successful_refresh
  • use_or_refresh_preserves_refresh_token_when_server_omits_it
  • poll_for_token_handles_authorization_pending_then_success
  • poll_for_token_accepts_response_without_refresh_token
  • poll_for_token_surfaces_http_error_on_unparseable_body
  • kimi_headers_skips_invalid_device_id_without_panic
  • validates_device_id_rejects_invalid_bytes

soilSpoon and others added 2 commits April 16, 2026 17:01
… 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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/goose/src/providers/kimicode.rs Outdated
Comment on lines +380 to +382
if let (Some(access_token), Some(refresh_token)) =
(resp.access_token, resp.refresh_token)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +418 to +421
struct RefreshResp {
access_token: String,
refresh_token: String,
expires_in: Option<i64>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@soilSpoon
Copy link
Copy Markdown
Contributor Author

@jh-block Thanks for the review. Quick status update — all inline feedback is addressed:

  • Duplication between get_access_token and configure_oauth — extracted ensure_token (the cache → refresh → device-flow ladder) and use_or_refresh (per-token freshness/refresh/fallback) in commit a5113f5. Both entry points now go through ensure_token, so the duplicated branching is gone.
  • raise_for_status() on the poll request — replied inline. Short version: a plain error_for_status() would break RFC 8628, so I read the body first and only surface the HTTP status when parsing fails AND status is non-2xx. Covered by poll_for_token_surfaces_http_error_on_unparseable_body and poll_for_token_handles_authorization_pending_then_success.
  • Trivial constants-only test — removed and replaced with 9 wiremock-backed tests.

Also addressed during this round:

  • Device-code panic on corrupted device_id file → validated at load and regenerated.
  • Missing kimi_code_configured marker → persisted on successful configure_oauth so the server's check_provider_configured reports the provider as ready.
  • Refresh-failure fallback → reuses the still-unexpired token instead of forcing device flow.
  • Hardcoded attempt cap → replaced with a deadline derived from expires_in.
  • poll_for_token no longer requires refresh_token in the response, and do_refresh_token reuses the prior refresh_token when the server omits one (both per RFC 6749; 3b04aa8).
  • Named constants for fallback timeouts, warn on corrupted token.json, eprintln! instead of println!, and outcome-focused tracing on refresh/fallback/device-flow events.

Follow-ups intentionally out of scope (happy to split into separate PRs):

  • Shared RFC 8628 device-flow helper for kimicode + githubcopilot — matches your suggestion. Only these two currently use device flow (chatgpt_codex and gemini_oauth use authorization-code variants).
  • Typed request struct for the stream field (currently payload.as_object_mut().unwrap().insert(...)) — touches shared formats::anthropic::create_request.
  • Optional process-level single-flight protection against concurrent device-flow logins (file lock on token.json).

All tests pass, clippy clean. Ready for another look.

@jh-block jh-block added this pull request to the merge queue Apr 16, 2026
Merged via the queue into aaif-goose:main with commit 93e6f8d Apr 16, 2026
20 checks passed
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 16, 2026
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>
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 16, 2026
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>
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 16, 2026
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>
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 16, 2026
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>
michaelneale added a commit that referenced this pull request Apr 17, 2026
* 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)
  ...
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 17, 2026
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>
soilSpoon added a commit to soilSpoon/goose that referenced this pull request Apr 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants