kimi-code/packages/node-sdk/src/kimi-code-model-provider.ts
Kai 821847cb4b
feat(managed-kimi-code): route anthropic protocol via beta api (#1186)
* feat(managed-kimi-code): route anthropic protocol via beta api

- kosong: add betaApi option to use client.beta.messages.create
- agent-core: thread alias betaApi into the anthropic provider config
- oauth: route managed models on the anthropic protocol through the beta Messages API

* feat(providers): add KIMI_CODE_CUSTOM_HEADERS support

- Add KIMI_CODE_CUSTOM_HEADERS env var for custom outbound LLM headers
- Send User-Agent to non-Kimi providers
- Forward Kimi identity headers to model catalog fetches
- Support defaultHeaders in Google GenAI provider

* feat(agent-core): add protocol attrs to turn and api error telemetry

- Add type/protocol/alias to api_error for per-protocol error attribution

- Add turn_ended event with reason/duration/mode/type/protocol

- Add type/protocol to turn_interrupted

* chore(oauth): remove hardcoded internal dev endpoint from shared OAuth base URLs

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-06-29 14:24:01 +08:00

142 lines
4.3 KiB
TypeScript

import {
ErrorCodes,
KimiError,
resolveKimiHome,
type Logger,
type ModelProvider,
type ResolvedRuntimeProvider,
} from '@moonshot-ai/agent-core';
import {
createKimiDefaultHeaders,
KIMI_CODE_FLOW_CONFIG,
KIMI_CODE_PROVIDER_NAME,
KimiOAuthToolkit,
kimiCodeBaseUrl,
parseKimiCodeCustomHeaders,
resolveKimiCodeOAuthRef,
type KimiHostIdentity,
type ManagedKimiOAuthRef,
} from '@moonshot-ai/kimi-code-oauth';
import type {
ProviderConfig as KosongProviderConfig,
ProviderRequestAuth,
} from '@moonshot-ai/kosong';
import { APIStatusError, UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong';
import { mapOAuthTokenError } from '#/oauth-error';
export interface KimiForCodingProviderOptions extends KimiHostIdentity {
readonly homeDir?: string;
readonly model?: string;
readonly baseUrl?: string;
readonly promptCacheKey?: string;
readonly defaultHeaders?: Record<string, string>;
}
export class KimiForCodingProvider implements ModelProvider {
private readonly model: string;
private readonly baseUrl: string;
private readonly promptCacheKey: string | undefined;
private readonly defaultHeaders: Record<string, string> | undefined;
private readonly toolkit: KimiOAuthToolkit;
private readonly homeDir: string;
private readonly identity: KimiHostIdentity;
private readonly oauthRef: ManagedKimiOAuthRef;
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.oauthRef = resolveKimiCodeOAuthRef({
oauthHost: KIMI_CODE_FLOW_CONFIG.oauthHost,
baseUrl: this.baseUrl,
});
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: {
...parseKimiCodeCustomHeaders(),
...createKimiDefaultHeaders({
homeDir: this.homeDir,
...this.identity,
}),
...this.defaultHeaders,
},
};
return {
providerName: 'kimi-for-coding',
provider,
modelCapabilities: UNKNOWN_CAPABILITY,
type: 'kimi',
protocol: undefined,
};
}
resolveAuth(_model: string, _options?: { readonly log?: Logger }) {
return async <T>(request: (auth: ProviderRequestAuth) => Promise<T>): Promise<T> => {
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<ProviderRequestAuth> {
try {
const apiKey = await this.toolkit.ensureFresh(KIMI_CODE_PROVIDER_NAME, {
force,
oauthRef: this.oauthRef,
});
return { apiKey };
} catch (error) {
// Classify OAuth token failures into the public KimiError protocol so the
// turn surfaces `auth.login_required` / `provider.connection_error`
// instead of collapsing everything to `internal`. Unrecognized errors are
// rethrown raw (see mapOAuthTokenError).
throw mapOAuthTokenError(error, KIMI_CODE_PROVIDER_NAME) ?? error;
}
}
}