diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index c874d2ca28..ab5c35046c 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -46,6 +46,7 @@ export type { BearerTokenProvider, ModelProvider, OAuthTokenProviderResolver, + ResolvedRuntimeProvider, } from './session/provider-manager'; // ─── Wire records (for in-monorepo consumers like apps/vis) ──────────────── diff --git a/packages/node-sdk/src/index.ts b/packages/node-sdk/src/index.ts index ae3d677d20..2da9d81dc3 100644 --- a/packages/node-sdk/src/index.ts +++ b/packages/node-sdk/src/index.ts @@ -1,6 +1,8 @@ export { KimiHarness } from '#/kimi-harness'; export { Session } from '#/session'; export { KimiAuthFacade } from '#/auth'; +export { KimiForCodingProvider } from '#/kimi-code-model-provider'; +export type { KimiForCodingProviderOptions } from '#/kimi-code-model-provider'; export { applyCatalogProvider, diff --git a/packages/node-sdk/src/kimi-code-model-provider.ts b/packages/node-sdk/src/kimi-code-model-provider.ts new file mode 100644 index 0000000000..0e03944e6f --- /dev/null +++ b/packages/node-sdk/src/kimi-code-model-provider.ts @@ -0,0 +1,117 @@ +import { + ErrorCodes, + KimiError, + resolveKimiHome, + type Logger, + type ModelProvider, + type ResolvedRuntimeProvider, +} from '@moonshot-ai/agent-core'; +import { + createKimiDefaultHeaders, + KIMI_CODE_PROVIDER_NAME, + KimiOAuthToolkit, + kimiCodeBaseUrl, + type KimiHostIdentity, +} from '@moonshot-ai/kimi-code-oauth'; +import type { + ProviderConfig as KosongProviderConfig, + ProviderRequestAuth, +} from '@moonshot-ai/kosong'; +import { APIStatusError, UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong'; + +export interface KimiForCodingProviderOptions extends KimiHostIdentity { + readonly homeDir?: string; + readonly model?: string; + readonly baseUrl?: string; + readonly promptCacheKey?: string; + readonly defaultHeaders?: Record; +} + +export class KimiForCodingProvider implements ModelProvider { + private readonly model: string; + private readonly baseUrl: string; + private readonly promptCacheKey: string | undefined; + private readonly defaultHeaders: Record | undefined; + private readonly toolkit: KimiOAuthToolkit; + private readonly homeDir: string; + private readonly identity: KimiHostIdentity; + + constructor(options: KimiForCodingProviderOptions) { + this.model = options.model ?? 'kimi-for-coding'; + this.baseUrl = options.baseUrl ?? kimiCodeBaseUrl(); + this.promptCacheKey = options.promptCacheKey; + this.defaultHeaders = options.defaultHeaders; + this.homeDir = resolveKimiHome(options.homeDir); + this.identity = { + userAgentProduct: options.userAgentProduct, + version: options.version, + userAgentSuffix: options.userAgentSuffix, + }; + this.toolkit = new KimiOAuthToolkit({ + homeDir: this.homeDir, + identity: this.identity, + }); + } + + get defaultModel(): string { + return this.model; + } + + resolveProviderConfig(model: string): ResolvedRuntimeProvider { + if (model !== this.model) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${model}" is not supported by KimiForCodingProvider.`, + ); + } + + const provider: KosongProviderConfig = { + type: 'kimi', + model: this.model, + baseUrl: this.baseUrl, + generationKwargs: this.promptCacheKey + ? { prompt_cache_key: this.promptCacheKey } + : undefined, + defaultHeaders: { + ...createKimiDefaultHeaders({ + homeDir: this.homeDir, + ...this.identity, + }), + ...this.defaultHeaders, + }, + }; + + return { + providerName: 'kimi-for-coding', + provider, + modelCapabilities: UNKNOWN_CAPABILITY, + }; + } + + resolveAuth(_model: string, _options?: { readonly log?: Logger }) { + return async (request: (auth: ProviderRequestAuth) => Promise): Promise => { + let auth = await this.buildAuth(false); + for (let refreshed = false; ; refreshed = true) { + try { + return await request(auth); + } catch (error) { + const is401 = error instanceof APIStatusError && error.statusCode === 401; + if (!is401) throw error; + if (refreshed) { + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + 'OAuth token was rejected after refresh. Run /login to re-authenticate.', + { cause: error }, + ); + } + auth = await this.buildAuth(true); + } + } + }; + } + + private async buildAuth(force: boolean): Promise { + const apiKey = await this.toolkit.ensureFresh(KIMI_CODE_PROVIDER_NAME, { force }); + return { apiKey }; + } +}