mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-25 00:27:32 +00:00
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
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:
parent
a5960b3905
commit
e22479a62e
5 changed files with 169 additions and 3 deletions
5
.changeset/kap-server-meta-experimental-flags.md
Normal file
5
.changeset/kap-server-meta-experimental-flags.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kap-server": patch
|
||||
---
|
||||
|
||||
Expose the effective experimental-flag map as `experimental_flags` on `GET /api/v1/meta`.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
126
packages/kap-server/test/meta.test.ts
Normal file
126
packages/kap-server/test/meta.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue