-
Notifications
You must be signed in to change notification settings - Fork 975
feat(acp): report Kimi account usage #2716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Expose ACP context usage and Kimi Coding Plan or API-key billing metadata in session usage updates. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,8 @@ import { | |
| toolProgressToSessionUpdate, | ||
| toolResultToSessionUpdate, | ||
| turnEndReasonToStopReason, | ||
| type KimiCodeUsageMeta, | ||
| usageReportToSessionUpdate, | ||
| } from './events-map'; | ||
| import { acpModeToToggles, DEFAULT_MODE_ID, isAcpModeId, type AcpModeId } from './modes'; | ||
| import { outcomeToQuestionAnswer, questionItemToPermissionOptions } from './question'; | ||
|
|
@@ -149,6 +151,17 @@ export class AcpSession { | |
| */ | ||
| private skillCommandMap: ReadonlyMap<string, string> = new Map(); | ||
|
|
||
| /** Cached managed-account usage so opening/finishing turns cannot poll the | ||
| * account endpoint more than once per minute. The cache key follows the | ||
| * selected model/provider, so switching models invalidates it immediately. */ | ||
| private accountUsageCache: | ||
| | { | ||
| readonly key: string; | ||
| readonly expiresAt: number; | ||
| readonly value: KimiCodeUsageMeta | undefined; | ||
| } | ||
| | undefined; | ||
|
|
||
| // One token per in-flight `prompt()` that is still awaiting image compression | ||
| // (before any turn exists). A `session/cancel` in that window has no turn to | ||
| // abort, so it flips every token and each affected `prompt()` returns | ||
|
|
@@ -269,6 +282,109 @@ export class AcpSession { | |
| return this.currentModeIdInternal; | ||
| } | ||
|
|
||
| /** | ||
| * Emit ACP context usage plus Kimi Code account metadata. Account lookup is | ||
| * best-effort: API-key mode never reads or serializes the key, and a failed | ||
| * Coding Plan usage request still reports the billing mode so clients can | ||
| * render an honest account state. | ||
| */ | ||
| async emitUsageReport(): Promise<void> { | ||
| if (typeof this.session.getStatus !== 'function') return; | ||
| try { | ||
| const [status, kimiCode] = await Promise.all([ | ||
| this.session.getStatus(), | ||
| this.loadKimiCodeUsageMeta(), | ||
| ]); | ||
| if ( | ||
| !Number.isFinite(status.contextTokens) || | ||
| status.contextTokens < 0 || | ||
| !Number.isFinite(status.maxContextTokens) || | ||
| status.maxContextTokens <= 0 | ||
| ) { | ||
| return; | ||
| } | ||
| await this.conn.sessionUpdate( | ||
| usageReportToSessionUpdate( | ||
| this.session.id, | ||
| status.contextTokens, | ||
| status.maxContextTokens, | ||
| kimiCode, | ||
| ), | ||
| ); | ||
| } catch (error) { | ||
| log.warn('acp: failed to push usage_update', { | ||
| sessionId: this.session.id, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private async loadKimiCodeUsageMeta(): Promise<KimiCodeUsageMeta | undefined> { | ||
| if (this.harness === undefined || typeof this.harness.getConfig !== 'function') { | ||
| return undefined; | ||
| } | ||
|
|
||
| const config = await this.harness.getConfig(); | ||
| const selectedModelId = this.currentModelIdInternal || config.defaultModel; | ||
| const selectedModel = | ||
| selectedModelId === undefined ? undefined : config.models?.[selectedModelId]; | ||
| const providerId = selectedModel?.provider ?? config.defaultProvider; | ||
| const provider = providerId === undefined ? undefined : config.providers[providerId]; | ||
| const billingMode = | ||
| providerId === 'managed:kimi-code' && provider?.oauth !== undefined | ||
| ? 'coding_plan' | ||
| : typeof provider?.apiKey === 'string' && provider.apiKey.length > 0 | ||
| ? 'api_key' | ||
|
Comment on lines
+333
to
+337
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the legacy adapter, a Kimi provider configured with Useful? React with 👍 / 👎. |
||
| : undefined; | ||
| const cacheKey = `${selectedModelId ?? ''}:${providerId ?? ''}:${billingMode ?? ''}`; | ||
| const now = Date.now(); | ||
| if ( | ||
| this.accountUsageCache?.key === cacheKey && | ||
| this.accountUsageCache.expiresAt > now | ||
| ) { | ||
| return this.accountUsageCache.value; | ||
| } | ||
|
|
||
| let value: KimiCodeUsageMeta | undefined; | ||
| if (billingMode === 'api_key') { | ||
| value = { billingMode }; | ||
| } else if (billingMode === 'coding_plan') { | ||
| value = { billingMode }; | ||
| try { | ||
| const result = await this.harness.auth.getManagedUsage(providerId); | ||
| if (result.kind === 'ok') { | ||
| value = { | ||
| billingMode, | ||
| rateLimits: { | ||
| summary: result.summary, | ||
| limits: result.limits, | ||
| booster: | ||
| result.extraUsage === null | ||
| ? null | ||
| : { | ||
| balanceCents: result.extraUsage.balanceCents, | ||
| totalCents: result.extraUsage.totalCents, | ||
| currency: result.extraUsage.currency, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| log.warn('acp: failed to load Kimi Code managed usage', { | ||
| sessionId: this.session.id, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| this.accountUsageCache = { | ||
| key: cacheKey, | ||
| expiresAt: now + 60_000, | ||
| value, | ||
| }; | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Forward an ACP `session/cancel` notification to the underlying SDK | ||
| * session. The SDK's `cancel()` is idempotent at the RPC layer, so | ||
|
|
@@ -1229,6 +1345,7 @@ export class AcpSession { | |
| if (settled) return; | ||
| if (!isFromMainAgent(event)) return; | ||
| settled = true; | ||
| void this.emitUsageReport(); | ||
| if (event.reason === 'failed') { | ||
| // Failures bubble up via the SDK `error` payload. Phase 11.1 | ||
| // upgrades the prior "log + resolve end_turn" behaviour to | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new reporting is wired only into
@moonshot-ai/acp-adapter, butapps/kimi-code/src/cli/sub/acp.ts:40-44routes ordinarykimi acpinvocations to@moonshot-ai/acp-server; this adapter is used only whenKIMI_CODE_LEGACY_FLAGis enabled. The default server'semitUsageUpdate()still sends context fields without_meta.kimiCode, so nearly all users will not receive the advertised account usage. Implement the metadata and opening update inpackages/acp-serveras well, or route the default command through this implementation.Useful? React with 👍 / 👎.