Kimi Code joins as an OAuth plan provider: RFC 8628 device flow, verified against the live endpoints
- New oauth/kimi-code-flow.ts mirrors the official kimi-cli: form-encoded device_authorization + token polling at auth.kimi.com with the required X-Msh-* device headers, pure poll interpreter, 15-minute tokens refreshed via refresh_token grant - Agent loop runs chat completions at https://api.kimi.com/coding/v1 with the Bearer access token; default model kimi-for-coding (kimi-for-coding-highspeed for Allegretto+ plans) - Live model listing via /coding/v1/models; credential refresh wired into the turn credential resolver - Migration 057 seeds the provider (disabled) into existing organizations - Wire protocol probed live: device code issued, authorization_pending poll observed, 401 envelope on the coding API confirmed
This commit is contained in:
1 parent
813276de71
commit
327de38e78
14 files changed
+281
-2
No files matched your search
@@ -20,6 +20,7 @@ const PROVIDER_SECRET_SAMPLES: Record<string, string> = {
|
||||
openrouter: "sk-or-live-provider",
|
||||
kimi: "sk-live-provider",
|
||||
"kimi-cn": "sk-live-provider",
|
||||
"kimi-code": "kimi-session-token",
|
||||
"github-copilot": "ghu_live_provider",
|
||||
"google-antigravity": "antigravity-session",
|
||||
"gpt-account": "chatgpt-session",
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { interpretPollResponse } from "../../src/main/services/oauth/github-copilot-flow";
|
||||
import { interpretKimiPollResponse } from "../../src/main/services/oauth/kimi-code-flow";
|
||||
import { extractAccountId } from "../../src/main/services/oauth/openai-codex-flow";
|
||||
import { readProjectId, resolveAntigravityClientSecret } from "../../src/main/services/oauth/google-antigravity-flow";
|
||||
import { generatePkce } from "../../src/main/services/oauth/pkce";
|
||||
@@ -33,6 +34,40 @@ describe("github copilot device-code poll interpreter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("kimi code device-code poll interpreter", () => {
|
||||
test("returns credentials with an expiry margin when the token arrives", () => {
|
||||
const before = Date.now();
|
||||
const decision = interpretKimiPollResponse(200, { access_token: "kimi_access", refresh_token: "kimi_refresh", expires_in: 900 });
|
||||
expect(decision.kind).toBe("token");
|
||||
if (decision.kind === "token") {
|
||||
expect(decision.credentials.access).toBe("kimi_access");
|
||||
expect(decision.credentials.refresh).toBe("kimi_refresh");
|
||||
expect(decision.credentials.expires).toBeGreaterThan(before);
|
||||
expect(decision.credentials.expires).toBeLessThan(before + 900 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a token response missing required fields", () => {
|
||||
expect(() => interpretKimiPollResponse(200, { access_token: "kimi_access" })).toThrow("missing required fields");
|
||||
});
|
||||
|
||||
test("keeps polling while authorization is pending", () => {
|
||||
expect(interpretKimiPollResponse(400, { error: "authorization_pending" })).toEqual({ kind: "pending" });
|
||||
expect(interpretKimiPollResponse(400, { error: "slow_down" })).toEqual({ kind: "pending" });
|
||||
expect(interpretKimiPollResponse(500, {})).toEqual({ kind: "pending" });
|
||||
});
|
||||
|
||||
test("stops on an expired device code", () => {
|
||||
expect(interpretKimiPollResponse(400, { error: "expired_token" })).toEqual({ kind: "expired" });
|
||||
});
|
||||
|
||||
test("fails when the user denies the request", () => {
|
||||
const decision = interpretKimiPollResponse(400, { error: "access_denied", error_description: "user declined" });
|
||||
expect(decision.kind).toBe("error");
|
||||
if (decision.kind === "error") expect(decision.message).toContain("user declined");
|
||||
});
|
||||
});
|
||||
|
||||
describe("openai codex account id extraction", () => {
|
||||
test("reads the chatgpt account id from the token claims", () => {
|
||||
const token = makeJwt({ "https://api.openai.com/auth": { chatgpt_account_id: "acct_42" } });
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("aws sigv4", () => {
|
||||
|
||||
describe("tool-turn router", () => {
|
||||
test("supports every agent-capable provider (incl. cloud and custom) and rejects unknown ones", () => {
|
||||
for (const key of ["openai", "anthropic", "xai", "groq", "mistral", "cerebras", "openrouter", "kimi", "kimi-cn", "gpt-account", "github-copilot", "google-antigravity", "claude-pro", "custom-openai", "custom-anthropic", "azure-openai", "aws-bedrock", "vertex-ai"]) {
|
||||
for (const key of ["openai", "anthropic", "xai", "groq", "mistral", "cerebras", "openrouter", "kimi", "kimi-cn", "gpt-account", "github-copilot", "google-antigravity", "claude-pro", "kimi-code", "custom-openai", "custom-anthropic", "azure-openai", "aws-bedrock", "vertex-ai"]) {
|
||||
expect(supportsToolTurn(key)).toBe(true);
|
||||
expect(typeof selectToolTurn(key)).toBe("function");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user