feat(kap-server): expose effective experimental flags in /meta (#2417)
Some checks failed
CI / build (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test (5) (push) Has been cancelled
CI / test-pi-tui (push) Has been cancelled
CI / test-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
Release / Release (push) Has been cancelled
Nix Build / Check flake.nix workspace sync (push) Has been cancelled
Release / Deploy docs (push) Has been cancelled
Release / Native release artifact (push) Has been cancelled
Release / Publish native release assets (push) Has been cancelled
Nix Build / nix build .#kimi-code (push) Has been cancelled

This commit is contained in:
liruifengv 2026-08-01 10:22:23 +08:00 committed by GitHub
parent a5960b3905
commit e22479a62e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 169 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kap-server": patch
---
Expose the effective experimental-flag map as `experimental_flags` on `GET /api/v1/meta`.

View file

@ -32,6 +32,17 @@ export const metaResponseSchema = z.object({
* credential. Defaults to false on hardened boots.
*/
dangerous_bypass_auth: z.boolean(),
/**
* Effective experimental-flag state (flag id enabled), resolved by the
* FlagService from every source (master env, per-flag env, the
* `[experimental]` config section, defaults). This is the *effective* state,
* not the persisted config section: a flag enabled via
* `KIMI_CODE_EXPERIMENTAL_*` env appears here but not in `GET /config`'s
* `experimental` map. Computed per request config-section writes flip it
* live. Clients use it to gate experimental UI; older servers omit it, so
* treat absence as "no flags enabled".
*/
experimental_flags: z.record(z.string(), z.boolean()).optional(),
/**
* Backend engine generation serving this API. `'v2'` is the DI × Scope
* engine (`@moonshot-ai/kap-server` / `agent-core-v2`); older servers omit

View file

@ -11,7 +11,10 @@
* must treat unbacked capabilities as not-yet-available until the corresponding
* routes are wired.
*
* **No DI**: pure server-self info; the payload is frozen at registration time.
* **No DI for the static fields**: pure server-self info; that part of the
* payload is frozen at registration time. `experimental_flags` is the
* exception flag state flips live when the `[experimental]` config section
* changes, so it is resolved per request through the injected getter.
*/
import { okEnvelope } from '../envelope';
@ -39,10 +42,17 @@ export interface MetaRouteOptions {
* the web UI can skip the token prompt and connect without a credential.
*/
readonly dangerousBypassAuth: boolean;
/**
* Resolves the effective experimental-flag map (flag id enabled) at
* request time. Backed by `IFlagService.snapshot()` in production; tests may
* stub it. May return a promise the handler awaits it, so flag state
* always reflects the fully loaded config (never pre-load defaults).
*/
readonly getExperimentalFlags: () => Record<string, boolean> | Promise<Record<string, boolean>>;
}
export function registerMetaRoute(app: RouteHost, opts: MetaRouteOptions): void {
const data: MetaResponse = Object.freeze({
const staticData = Object.freeze({
server_version: opts.serverVersion,
capabilities: Object.freeze({
websocket: true as const,
@ -68,6 +78,10 @@ export function registerMetaRoute(app: RouteHost, opts: MetaRouteOptions): void
tags: ['meta'],
},
async (req, reply) => {
const data: MetaResponse = {
...staticData,
experimental_flags: await opts.getExperimentalFlags(),
};
reply.send(okEnvelope(data, req.id));
},
);

View file

@ -9,7 +9,8 @@
* folder picker, the session filesystem, terminals, connections, shutdown).
*/
import type { Scope } from '@moonshot-ai/agent-core-v2';
import { IConfigService, type Scope } from '@moonshot-ai/agent-core-v2';
import { IFlagService } from '@moonshot-ai/agent-core-v2/app/flag/flag';
import type { KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';
import { ulid } from 'ulid';
@ -105,6 +106,15 @@ export async function registerApiV1Routes(
serverId: ulid(),
startedAt: new Date().toISOString(),
dangerousBypassAuth: opts.dangerousBypassAuth === true,
getExperimentalFlags: async () => {
// Same edge-facade contract as the config route: never project
// config-derived state before the initial load settles — an early
// /meta hit would otherwise advertise default/env-only flags and
// hide config-enabled features until the FlagService's change
// watcher catches up.
await core.accessor.get(IConfigService).ready;
return core.accessor.get(IFlagService).snapshot();
},
});
registerAuthRoute(apiV1 as unknown as Parameters<typeof registerAuthRoute>[0], core);

View file

@ -0,0 +1,126 @@
/**
* `/api/v1/meta` tests the static server-self fields are covered by
* `boot.test.ts`; these focus on `experimental_flags`, the effective
* experimental-flag map resolved per request from every flag source (env,
* master env, the `[experimental]` config section, defaults).
*/
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { type RunningServer, startServer } from '../src/start';
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
import { authedFetch } from './helpers/auth';
interface MetaBody {
code: number;
data: { experimental_flags?: Record<string, boolean> };
}
describe('/api/v1/meta experimental_flags', () => {
let server: RunningServer | undefined;
let home: string | undefined;
beforeEach(() => {
// Neutralize flag env vars leaking from the developer shell (e.g. a
// globally exported KIMI_CODE_EXPERIMENTAL_FLAG=1) so each test starts
// from the default-off baseline. The master env can be pinned to '0' (it
// only forces ON), but the per-flag env must be fully ABSENT — an
// explicit '0' is an env override that outranks the config section.
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0');
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL', undefined);
});
afterEach(async () => {
vi.unstubAllEnvs();
if (server !== undefined) {
await server.close();
server = undefined;
}
if (home !== undefined) {
// The query-store cache can still be flushing while we clean up; retry
// instead of flaking on ENOTEMPTY.
await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
home = undefined;
}
});
async function boot(toml?: string): Promise<string> {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-meta-'));
if (toml !== undefined) {
await writeFile(join(home, 'config.toml'), toml, 'utf-8');
}
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
});
return `http://127.0.0.1:${server.port}`;
}
async function getMetaFlags(base: string): Promise<Record<string, boolean>> {
const res = await authedFetch(server as RunningServer, base, '/api/v1/meta');
expect(res.status).toBe(200);
const body = (await res.json()) as MetaBody;
expect(body.code).toBe(0);
expect(body.data.experimental_flags).toBeDefined();
return body.data.experimental_flags as Record<string, boolean>;
}
it('reports registered flags as off by default', async () => {
const base = await boot();
const flags = await getMetaFlags(base);
expect(flags['secondary-model']).toBe(false);
});
it('reports a config-enabled flag from the very first response', async () => {
// Regression for the startup race: FlagService reads the `[experimental]`
// section from a config that loads asynchronously, so the handler awaits
// IConfigService.ready before snapshotting — a persisted flag must be
// visible even to the earliest request.
const base = await boot('[experimental]\nsecondary-model = true\n');
const flags = await getMetaFlags(base);
expect(flags['secondary-model']).toBe(true);
});
it('reflects a flag enabled via its KIMI_CODE_EXPERIMENTAL_* env var', async () => {
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL', '1');
const base = await boot();
const flags = await getMetaFlags(base);
expect(flags['secondary-model']).toBe(true);
});
it('flips live when the [experimental] config section is written via POST /config', async () => {
const base = await boot();
expect((await getMetaFlags(base))['secondary-model']).toBe(false);
const res = await authedFetch(server as RunningServer, base, '/api/v1/config', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ experimental: { 'secondary-model': true } }),
});
expect(res.status).toBe(200);
expect((await getMetaFlags(base))['secondary-model']).toBe(true);
});
it('keeps an env-forced flag on when the config section disables it', async () => {
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL', '1');
const base = await boot();
const res = await authedFetch(server as RunningServer, base, '/api/v1/config', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ experimental: { 'secondary-model': false } }),
});
expect(res.status).toBe(200);
// Env outranks the config section in FlagService resolution.
expect((await getMetaFlags(base))['secondary-model']).toBe(true);
});
});