diff --git a/apps/desktop/src/main/__tests__/commandcode-browser-login-flow.test.ts b/apps/desktop/src/main/__tests__/commandcode-browser-login-flow.test.ts deleted file mode 100644 index 7d9ec577f3..0000000000 --- a/apps/desktop/src/main/__tests__/commandcode-browser-login-flow.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { - CommandCodeBrowserLoginFlow, - type CommandCodeBrowserLoginBridge, - type CommandCodeBrowserLoginCredentials, - type CommandCodeBrowserLoginResult, - type CommandCodeBrowserLoginStartResult, -} from '../../renderer/features/connection-settings/index.js'; - -function deferred() { - let resolve!: (value: T) => void; - let reject!: (reason: unknown) => void; - const promise = new Promise((accept, decline) => { - resolve = accept; - reject = decline; - }); - return { promise, resolve, reject }; -} - -const CREDENTIALS: CommandCodeBrowserLoginCredentials = { - apiKey: 'user_k', - userName: 'joobin', - keyName: 'cli-1', -}; -const STARTED: CommandCodeBrowserLoginStartResult = { - ok: true, - attemptId: 'a1', - authUrl: 'https://commandcode.ai/studio/auth/cli?state=s', -}; - -function harness() { - const starts: ReturnType>[] = []; - const completes: ReturnType>[] = []; - const cancelled: (string | undefined)[] = []; - const bridge: CommandCodeBrowserLoginBridge = { - start: () => { - const next = deferred(); - starts.push(next); - return next.promise; - }, - complete: () => { - const next = deferred(); - completes.push(next); - return next.promise; - }, - cancel: async (attemptId) => { - cancelled.push(attemptId); - }, - }; - const delivered: CommandCodeBrowserLoginCredentials[] = []; - const phases: string[] = []; - const flow = new CommandCodeBrowserLoginFlow(bridge, (credentials) => delivered.push(credentials)); - flow.subscribe(() => phases.push(flow.getState().phase)); - return { flow, starts, completes, cancelled, delivered, phases }; -} - -const settle = () => new Promise((resolve) => setTimeout(resolve, 0)); - -describe('CommandCodeBrowserLoginFlow', () => { - test('walks idle → starting → waiting → filled and hands the credentials out once', async () => { - const { flow, starts, completes, delivered, phases } = harness(); - const run = flow.start({ baseUrl: 'https://api.commandcode.ai/provider/v1' }); - assert.equal(flow.getState().phase, 'starting'); - starts[0]!.resolve(STARTED); - await settle(); - assert.deepEqual(flow.getState(), { phase: 'waiting', attemptId: 'a1', authUrl: STARTED.authUrl }); - completes[0]!.resolve({ ok: true, credentials: CREDENTIALS }); - await run; - assert.deepEqual(flow.getState(), { phase: 'filled', userName: 'joobin' }); - assert.deepEqual(delivered, [CREDENTIALS]); - assert.deepEqual(phases, ['starting', 'waiting', 'filled']); - }); - - test('a second start while one is live is ignored', async () => { - const { flow, starts } = harness(); - void flow.start(); - void flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - void flow.start(); - assert.equal(starts.length, 1); - }); - - test('a failed start reports its reason', async () => { - const { flow, starts } = harness(); - const run = flow.start(); - starts[0]!.resolve({ ok: false, reason: 'port_unavailable' }); - await run; - assert.deepEqual(flow.getState(), { phase: 'failed', reason: 'port_unavailable' }); - }); - - test('a bridge that throws is reported as unavailable, on start and on complete', async () => { - const first = harness(); - const run = first.flow.start(); - first.starts[0]!.reject(new Error('ipc gone')); - await run; - assert.deepEqual(first.flow.getState(), { phase: 'failed', reason: 'unavailable' }); - - const second = harness(); - const run2 = second.flow.start(); - second.starts[0]!.resolve(STARTED); - await settle(); - second.completes[0]!.reject(new Error('ipc gone')); - await run2; - assert.deepEqual(second.flow.getState(), { phase: 'failed', reason: 'unavailable' }); - }); - - test('denied / timeout end in failed; a user cancel from the main side returns to idle', async () => { - for (const reason of ['denied', 'timeout', 'superseded'] as const) { - const { flow, starts, completes } = harness(); - const run = flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - completes[0]!.resolve({ ok: false, reason }); - await run; - assert.deepEqual(flow.getState(), { phase: 'failed', reason }); - } - const { flow, starts, completes } = harness(); - const run = flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - completes[0]!.resolve({ ok: false, reason: 'cancelled' }); - await run; - assert.deepEqual(flow.getState(), { phase: 'idle' }); - }); - - test('cancel drops a late success instead of typing it into the field', async () => { - const { flow, starts, completes, cancelled, delivered } = harness(); - const run = flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - flow.cancel(); - assert.deepEqual(flow.getState(), { phase: 'idle' }); - assert.deepEqual(cancelled, ['a1']); - completes[0]!.resolve({ ok: true, credentials: CREDENTIALS }); - await run; - assert.deepEqual(delivered, [], 'a cancelled attempt must never fill the key'); - assert.deepEqual(flow.getState(), { phase: 'idle' }); - }); - - test('cancel during start releases the listener the bridge bound meanwhile', async () => { - const { flow, starts, cancelled } = harness(); - const run = flow.start(); - flow.cancel(); - starts[0]!.resolve(STARTED); - await run; - assert.deepEqual(cancelled, ['a1']); - assert.deepEqual(flow.getState(), { phase: 'idle' }); - }); - - test('a failed attempt can be retried', async () => { - const { flow, starts, completes } = harness(); - const run = flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - completes[0]!.resolve({ ok: false, reason: 'timeout' }); - await run; - const retry = flow.start(); - starts[1]!.resolve({ ...STARTED, attemptId: 'a2' }); - await settle(); - completes[1]!.resolve({ ok: true, credentials: CREDENTIALS }); - await retry; - assert.equal(flow.getState().phase, 'filled'); - }); - - test('dispose cancels the live attempt and silences listeners', async () => { - const { flow, starts, completes, cancelled, delivered, phases } = harness(); - const run = flow.start(); - starts[0]!.resolve(STARTED); - await settle(); - flow.dispose(); - assert.deepEqual(cancelled, ['a1']); - const seen = phases.length; - completes[0]!.resolve({ ok: true, credentials: CREDENTIALS }); - await run; - assert.deepEqual(delivered, [], 'a disposed flow must never fill the key'); - assert.equal(phases.length, seen, 'no state change may be published after dispose'); - void flow.start(); - assert.equal(starts.length, 1, 'a disposed flow never starts again'); - }); -}); diff --git a/apps/desktop/src/main/__tests__/commandcode-browser-login.test.ts b/apps/desktop/src/main/__tests__/commandcode-browser-login.test.ts deleted file mode 100644 index 0ecbed2d3b..0000000000 --- a/apps/desktop/src/main/__tests__/commandcode-browser-login.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { request as httpRequest } from 'node:http'; -import { createServer as createNetServer, type Server as NetServer } from 'node:net'; -import { afterEach, describe, test } from 'node:test'; -import { - COMMANDCODE_LOGIN_ALLOWED_ORIGINS, - COMMANDCODE_LOGIN_BODY_LIMIT_BYTES, - CommandCodeBrowserLoginController, - buildCommandCodeAuthUrl, - studioBaseForApiBase, -} from '../commandcode-browser-login.js'; - -// Every test binds a real loopback listener and talks to it with real fetch — -// exactly the requests the Studio page makes. Only the browser is replaced. - -const STATE = 'test-state-token'; -const controllers: CommandCodeBrowserLoginController[] = []; -const netServers: NetServer[] = []; - -function makeController( - overrides: Partial[0]> = {}, - opened: string[] = [], -) { - const controller = new CommandCodeBrowserLoginController({ - openExternal: async (url) => { - opened.push(url); - }, - // Off the CLI's range so a developer's own `command-code login` and the - // tests never contend for a port. - startPort: 46_959, - maxPortAttempts: 10, - randomToken: () => STATE, - ...overrides, - }); - controllers.push(controller); - return controller; -} - -afterEach(async () => { - for (const controller of controllers.splice(0)) controller.dispose(); - await Promise.all( - netServers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve()))), - ); -}); - -async function startOk(controller: CommandCodeBrowserLoginController, baseUrl?: string) { - const started = await controller.start(baseUrl === undefined ? {} : { baseUrl }); - assert.equal(started.ok, true, `start failed: ${JSON.stringify(started)}`); - if (!started.ok) throw new Error('unreachable'); - const url = new URL(started.authUrl); - const callback = new URL(url.searchParams.get('callback') ?? ''); - return { ...started, callback: callback.toString(), port: Number(callback.port) }; -} - -function post(url: string, body: unknown, init: RequestInit = {}) { - return fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'text/plain', ...(init.headers ?? {}) }, - body: typeof body === 'string' ? body : JSON.stringify(body), - ...init, - }); -} - -const APPROVED = { - apiKey: 'user_secret_key', - state: STATE, - userId: 'u1', - userName: 'joobin', - keyName: 'maka-desktop', -}; - -/** True when the port could be taken, i.e. no attempt is listening on it. */ -async function isPortFree(port: number): Promise { - for (let attempt = 0; attempt < 20; attempt += 1) { - try { - await occupyPort(port); - return true; - } catch { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - return false; -} - -function occupyPort(port: number): Promise { - return new Promise((resolve, reject) => { - const server = createNetServer(); - server.once('error', reject); - server.listen(port, '127.0.0.1', () => { - netServers.push(server); - resolve(); - }); - }); -} - -describe('studio URL derivation', () => { - test('maps the Provider API base to the Studio that mints its keys', () => { - assert.equal(studioBaseForApiBase(undefined), 'https://commandcode.ai'); - assert.equal( - studioBaseForApiBase('https://api.commandcode.ai/provider/v1'), - 'https://commandcode.ai', - ); - assert.equal( - studioBaseForApiBase('https://staging-api.commandcode.ai/provider/v1'), - 'https://staging.commandcode.ai', - ); - assert.equal(studioBaseForApiBase('http://localhost:8080/provider/v1'), 'http://localhost:3000'); - // A lookalike host must not be trusted as staging. - assert.equal( - studioBaseForApiBase('https://staging-api.commandcode.ai.evil.example/'), - 'https://commandcode.ai', - ); - }); - - test('builds the CLI approval URL with an encoded loopback callback', () => { - const url = new URL( - buildCommandCodeAuthUrl({ studioBase: 'https://commandcode.ai', port: 5959, state: 'a b' }), - ); - assert.equal(url.origin + url.pathname, 'https://commandcode.ai/studio/auth/cli'); - assert.equal(url.searchParams.get('callback'), 'http://localhost:5959/callback'); - assert.equal(url.searchParams.get('state'), 'a b'); - }); -}); - -describe('CommandCodeBrowserLoginController', () => { - test('start binds the first free port in range and opens the approval page there', async () => { - const opened: string[] = []; - const controller = makeController({}, opened); - const started = await startOk(controller); - assert.equal(started.port, 46_959); - assert.deepEqual(opened, [started.authUrl]); - assert.equal(new URL(started.authUrl).searchParams.get('state'), STATE); - }); - - test('an approved callback settles complete() with the delivered credentials and closes the port', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - - const response = await post(started.callback, APPROVED, { - headers: { Origin: 'https://commandcode.ai' }, - }); - assert.equal(response.status, 200); - assert.equal(response.headers.get('access-control-allow-origin'), 'https://commandcode.ai'); - assert.deepEqual(await response.json(), { success: true }); - - assert.deepEqual(await completion, { - ok: true, - credentials: { apiKey: 'user_secret_key', userName: 'joobin', keyName: 'maka-desktop' }, - }); - await assert.rejects(post(started.callback, APPROVED), 'the listener must be gone after settling'); - }); - - test('credentials are delivered once: a second complete() for the same attempt is spent', async () => { - const controller = makeController(); - const started = await startOk(controller); - const first = controller.complete(started.attemptId); - await post(started.callback, APPROVED); - assert.equal((await first).ok, true); - assert.deepEqual(await controller.complete(started.attemptId), { - ok: false, - reason: 'superseded', - }); - assert.deepEqual(await controller.complete('never-issued'), { ok: false, reason: 'superseded' }); - }); - - test('a state mismatch is answered 403 and the attempt keeps waiting', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - - const stale = await post(started.callback, { ...APPROVED, state: 'stale' }); - assert.equal(stale.status, 403); - // A forged denial without the state must not kill the live attempt. - const forgedDenial = await post(started.callback, { error: 'access_denied', state: 'nope' }); - assert.equal(forgedDenial.status, 403); - - const approved = await post(started.callback, APPROVED); - assert.equal(approved.status, 200); - assert.equal((await completion).ok, true); - }); - - test('a denial carrying the real state ends the attempt as denied', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - const response = await post(started.callback, { - error: 'access_denied', - error_description: 'User declined', - state: STATE, - }); - assert.equal(response.status, 200); - assert.deepEqual(await completion, { ok: false, reason: 'denied' }); - }); - - test('the CORS preflight succeeds only for the Studio origins', async () => { - const controller = makeController(); - const started = await startOk(controller); - for (const origin of COMMANDCODE_LOGIN_ALLOWED_ORIGINS) { - const preflight = await fetch(started.callback, { - method: 'OPTIONS', - headers: { Origin: origin, 'Access-Control-Request-Method': 'POST' }, - }); - assert.equal(preflight.status, 204); - assert.equal(preflight.headers.get('access-control-allow-origin'), origin); - } - const foreign = await fetch(started.callback, { - method: 'OPTIONS', - headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' }, - }); - assert.equal(foreign.status, 204); - assert.equal(foreign.headers.get('access-control-allow-origin'), ''); - }); - - test('malformed requests are refused without ending the attempt', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - const origin = `http://127.0.0.1:${started.port}`; - - assert.equal((await fetch(`${origin}/elsewhere`, { method: 'POST', body: '{}' })).status, 404); - assert.equal((await fetch(started.callback)).status, 405); - assert.equal((await post(started.callback, '{not json')).status, 400); - assert.equal((await post(started.callback, '[1,2]')).status, 400); - // Right state, but no key: the shape gate runs after the state gate. - assert.equal((await post(started.callback, { state: STATE, userName: 'x' })).status, 400); - assert.equal( - (await post(started.callback, { ...APPROVED, apiKey: '' })).status, - 400, - 'an empty key is not a credential', - ); - - let settled = false; - void completion.then(() => { - settled = true; - }); - await new Promise((resolve) => setTimeout(resolve, 20)); - assert.equal(settled, false, 'none of the refused requests may settle the attempt'); - controller.cancel(started.attemptId); - assert.deepEqual(await completion, { ok: false, reason: 'cancelled' }); - }); - - test('a request whose Host is not the loopback authority is refused (DNS rebinding)', async () => { - const controller = makeController(); - const started = await startOk(controller); - // fetch forbids overriding Host, so this rides raw node:http. - const rebound = await new Promise((resolve, reject) => { - const request = httpRequest( - { - host: '127.0.0.1', - port: started.port, - path: '/callback', - method: 'POST', - headers: { Host: `evil.example:${started.port}`, 'Content-Type': 'text/plain' }, - }, - (response) => { - response.resume(); - resolve(response.statusCode ?? 0); - }, - ); - request.once('error', reject); - request.end(JSON.stringify(APPROVED)); - }); - assert.equal(rebound, 403); - const viaIp = await post(`http://127.0.0.1:${started.port}/callback`, APPROVED); - assert.equal(viaIp.status, 200, 'both loopback spellings are the bound authority'); - }); - - test('an over-limit body is dropped before it is parsed', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - // CJK padding: the cap is on wire bytes, so this crosses it well before - // its string length would. - const padding = '密'.repeat(COMMANDCODE_LOGIN_BODY_LIMIT_BYTES / 2); - await assert.rejects(post(started.callback, { ...APPROVED, padding })); - - const approved = await post(started.callback, APPROVED); - assert.equal(approved.status, 200); - assert.equal((await completion).ok, true); - }); - - test('the login window elapsing settles the attempt as timeout', async () => { - const controller = makeController({ timeoutMs: 30 }); - const started = await startOk(controller); - assert.deepEqual(await controller.complete(started.attemptId), { - ok: false, - reason: 'timeout', - }); - await assert.rejects(post(started.callback, APPROVED)); - }); - - test('cancel settles the attempt and releases the port', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - controller.cancel(); - assert.deepEqual(await completion, { ok: false, reason: 'cancelled' }); - await assert.rejects(post(started.callback, APPROVED)); - // Cancelling again, or a stranger's id, is a no-op. - controller.cancel(started.attemptId); - controller.cancel('unknown'); - }); - - test('a new start supersedes the live attempt and rebinds the same port', async () => { - const controller = makeController(); - const first = await startOk(controller); - const firstCompletion = controller.complete(first.attemptId); - const second = await startOk(controller); - assert.deepEqual(await firstCompletion, { ok: false, reason: 'superseded' }); - assert.equal(second.port, first.port, 'the superseded listener released its port'); - assert.notEqual(second.attemptId, first.attemptId); - - // The old state no longer opens anything. - const completion = controller.complete(second.attemptId); - assert.equal((await post(second.callback, APPROVED)).status, 200); - assert.equal((await completion).ok, true); - }); - - test('two simultaneous starts leave one live attempt and free the loser\'s port', async () => { - const opened: string[] = []; - const controller = makeController({}, opened); - // Both calls reach `start` before either has bound a port. Reserving the - // attempt only after the bind let both survive, bind two ports, and each - // deliver its own credentials. - const [loser, winner] = await Promise.all([controller.start(), controller.start()]); - assert.deepEqual(loser, { ok: false, reason: 'superseded' }); - assert.equal(winner.ok, true, JSON.stringify(winner)); - if (!winner.ok) throw new Error('unreachable'); - assert.deepEqual(opened, [winner.authUrl], 'only the live attempt opens a browser tab'); - - const free: number[] = []; - for (const port of [46_959, 46_960]) { - if (await isPortFree(port)) free.push(port); - } - assert.equal(free.length, 1, `exactly one port stays bound, free: ${free.join(',')}`); - - const callback = new URL(new URL(winner.authUrl).searchParams.get('callback') ?? ''); - const completion = controller.complete(winner.attemptId); - assert.equal((await post(callback.toString(), APPROVED)).status, 200); - assert.equal((await completion).ok, true); - }); - - test('an occupied port is skipped for the next one in the CLI range', async () => { - await occupyPort(46_959); - const controller = makeController(); - const started = await startOk(controller); - assert.equal(started.port, 46_960); - }); - - test('no free port in range fails the start without opening a browser', async () => { - await occupyPort(46_959); - const opened: string[] = []; - const controller = makeController({ maxPortAttempts: 1 }, opened); - assert.deepEqual(await controller.start(), { ok: false, reason: 'port_unavailable' }); - assert.deepEqual(opened, []); - }); - - test('a browser that cannot open fails the start and releases the port', async () => { - const controller = makeController({ - openExternal: async () => { - throw new Error('no default browser'); - }, - }); - assert.deepEqual(await controller.start(), { ok: false, reason: 'browser_unavailable' }); - // The port is free again for a controller that can open a browser. - const next = makeController(); - const started = await startOk(next); - assert.equal(started.port, 46_959); - }); - - test('a staging Provider API base sends the browser to the staging Studio', async () => { - const controller = makeController(); - const started = await startOk(controller, 'https://staging-api.commandcode.ai/provider/v1'); - assert.equal(new URL(started.authUrl).origin, 'https://staging.commandcode.ai'); - }); - - test('dispose cancels the live attempt and refuses later starts', async () => { - const controller = makeController(); - const started = await startOk(controller); - const completion = controller.complete(started.attemptId); - controller.dispose(); - assert.deepEqual(await completion, { ok: false, reason: 'cancelled' }); - assert.equal((await controller.start()).ok, false); - }); -}); diff --git a/apps/desktop/src/main/__tests__/commandcode-login-ipc-main.test.ts b/apps/desktop/src/main/__tests__/commandcode-login-ipc-main.test.ts deleted file mode 100644 index d9324e8d70..0000000000 --- a/apps/desktop/src/main/__tests__/commandcode-login-ipc-main.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import type { IpcMainInvokeEvent } from 'electron'; -import { - COMMANDCODE_LOGIN_IPC_CHANNELS, - registerCommandCodeLoginIpc, - type CommandCodeLoginIpcDeps, -} from '../commandcode-login-ipc-main.js'; - -type Handler = (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown; -const EVENT = {} as IpcMainInvokeEvent; - -function harness() { - const handlers = new Map(); - const calls: unknown[][] = []; - const controller: CommandCodeLoginIpcDeps['controller'] = { - start: async (input) => { - calls.push(['start', input]); - return { ok: true, attemptId: 'a1', authUrl: 'https://commandcode.ai/x' }; - }, - complete: async (attemptId) => { - calls.push(['complete', attemptId]); - return { ok: false, reason: 'timeout' }; - }, - cancel: (attemptId) => { - calls.push(['cancel', attemptId]); - }, - }; - registerCommandCodeLoginIpc({ - ipcMain: { - handle: (channel: string, handler: Handler) => { - handlers.set(channel, handler); - }, - } as unknown as CommandCodeLoginIpcDeps['ipcMain'], - controller, - }); - const invoke = (channel: string, ...args: unknown[]) => { - const handler = handlers.get(channel); - assert.ok(handler, `no handler for ${channel}`); - return handler(EVENT, ...args); - }; - return { handlers, calls, invoke }; -} - -describe('registerCommandCodeLoginIpc', () => { - test('registers exactly the three shared channels', () => { - const { handlers } = harness(); - assert.deepEqual( - [...handlers.keys()].sort(), - Object.values(COMMANDCODE_LOGIN_IPC_CHANNELS).sort(), - ); - }); - - test('start forwards only a well-formed baseUrl', async () => { - const { calls, invoke } = harness(); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.start, { - baseUrl: 'https://staging-api.commandcode.ai/provider/v1', - }); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.start, { baseUrl: 42 }); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.start, { baseUrl: 'x'.repeat(5_000) }); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.start, 'not an object'); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.start, undefined); - assert.deepEqual(calls, [ - ['start', { baseUrl: 'https://staging-api.commandcode.ai/provider/v1' }], - ['start', {}], - ['start', {}], - ['start', {}], - ['start', {}], - ]); - }); - - test('complete requires a bounded attempt id and otherwise reports superseded', async () => { - const { calls, invoke } = harness(); - assert.deepEqual(await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.complete, 'a1'), { - ok: false, - reason: 'timeout', - }); - assert.deepEqual(await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.complete, 7), { - ok: false, - reason: 'superseded', - }); - assert.deepEqual(await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.complete, ''), { - ok: false, - reason: 'superseded', - }); - assert.deepEqual(calls, [['complete', 'a1']]); - }); - - test('cancel needs a well-formed attempt id; anything else is ignored', async () => { - const { calls, invoke } = harness(); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.cancel, undefined); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.cancel, 'a1'); - await invoke(COMMANDCODE_LOGIN_IPC_CHANNELS.cancel, { attemptId: 'a1' }); - assert.deepEqual(calls, [['cancel', 'a1']]); - }); -}); diff --git a/apps/desktop/src/main/__tests__/connection-settings-locale-render.test.ts b/apps/desktop/src/main/__tests__/connection-settings-locale-render.test.ts index fca1aff52f..a016a627b9 100644 --- a/apps/desktop/src/main/__tests__/connection-settings-locale-render.test.ts +++ b/apps/desktop/src/main/__tests__/connection-settings-locale-render.test.ts @@ -134,8 +134,6 @@ after(async () => { const localeCases = [ { locale: 'zh-CN', direct: '直连', transit: '成员转发', save: '保存供应商', - transportNotice: '这个供应商走官方 CLI 的私有通道', - transportRequired: '请先勾选上面的确认再添加。', slugErrors: { required: '请填写连接标识', format: '连接标识只能包含小写字母、数字和连字符', @@ -145,8 +143,6 @@ const localeCases = [ }, { locale: 'zh-TW', direct: '直接連線', transit: '成員轉送', save: '儲存供應商', - transportNotice: '這個供應商走官方 CLI 的私有通道', - transportRequired: '請先勾選上面的確認再新增。', slugErrors: { required: '請填寫連線標識', format: '連線標識只能包含小寫字母、數字和連字號', @@ -156,8 +152,6 @@ const localeCases = [ }, { locale: 'en', direct: 'Direct', transit: 'Member transit', save: 'Save provider', - transportNotice: "This provider uses the official CLI's private transport", - transportRequired: 'Tick the acknowledgement above before adding.', slugErrors: { required: 'Enter a connection identifier', format: 'Connection identifiers use lowercase letters, digits, and hyphens', @@ -270,52 +264,6 @@ for (const copy of localeCases) { }); } - /** - * Mounted rather than asserted through the draft rule alone: Command Code GO - * takes the quick API-key dialog, which returns its own subtree and submits - * through a route that returns before that rule runs. A test of the rule - * stays green while the notice never renders and the check never fires. - */ - test(`${copy.locale}: Command Code GO states its transport and refuses until answered`, async () => { - const harness = installRenderer(); - const calls: string[] = []; - const bridge = { - create: async () => { calls.push('create'); throw new Error('unexpected create'); }, - fetchModels: async () => { calls.push('fetchModels'); throw new Error('unexpected discovery'); }, - } as unknown as ConnectionsBridge; - await harness.render(copy.locale, createElement(components.AddProviderForm, { - bridge, providerType: 'commandcode-go', existingSlugs: [], - onCancel: unexpectedCall, onCreated: unexpectedCall, - })); - - const checkbox = harness.document.querySelector('input[type="checkbox"]'); - assert.ok(checkbox, 'the transport acknowledgement never rendered'); - assert.equal(checkbox.checked, false); - assert.ok( - harness.document.body.textContent?.includes(copy.transportNotice), - 'the transport notice never rendered', - ); - - // This route submits through the form rather than an `onClick`, and a - // bare `button.click()` does not submit here — asserting on it would pass - // while nothing ran. - const form = harness.document.querySelector('form'); - assert.ok(form, 'missing add-provider form'); - assert.ok( - [...harness.document.querySelectorAll('button')].some( - (button) => button.textContent === copy.save, - ), - 'missing save button', - ); - await act(async () => { - form.dispatchEvent(new globalThis.Event('submit', { bubbles: true, cancelable: true })); - }); - assert.deepEqual(calls, [], 'an unanswered acknowledgement must not reach the provider bridge'); - assert.ok( - harness.document.body.textContent?.includes(copy.transportRequired), - 'refusing the add must say why', - ); - }); } test('zh-TW: expanded Peer Mesh members render localized route states', async () => { diff --git a/apps/desktop/src/main/__tests__/provider-add-submission.test.ts b/apps/desktop/src/main/__tests__/provider-add-submission.test.ts index 26b3f58865..b9bd3fbbd3 100644 --- a/apps/desktop/src/main/__tests__/provider-add-submission.test.ts +++ b/apps/desktop/src/main/__tests__/provider-add-submission.test.ts @@ -26,7 +26,6 @@ import { initialOnboardingModelIds, shouldShowManagedOnboardingOutcomeUnknown, stableOnboardingModels, - providerRequiresAcknowledgement, validateAddProviderDraft, type AddProviderDraft, type AddProviderField, @@ -128,45 +127,12 @@ test('no provider type demands a model id at creation', () => { apiKey: 'sk-test', baseUrl: 'https://example.com/v1', cloudflareAccountId: 'account-id', - // The rule under test is about the model id, so every other gate is - // satisfied here — including the acknowledgement a provider may ask - // for, which has its own test below. - acknowledged: true, }), ); assert.equal(issue, null, `${providerType} refused a draft with no model id`); } }); -/** - * Command Code GO reaches the wire the official CLI uses and presents that - * CLI's identity rather than Maka's. That is stated on the form, and the - * statement is only worth making if the user has to answer it. - */ -test('a provider that states its transport is not added until the user answers', () => { - const providerType: ProviderType = 'commandcode-go'; - assert.equal(providerRequiresAcknowledgement(providerType), true); - assert.deepEqual(validateAddProviderDraft(draft({ providerType, slug: 'cc-go' })), { - field: 'form', - reason: 'acknowledgement', - }); - assert.equal( - validateAddProviderDraft(draft({ providerType, slug: 'cc-go', acknowledged: true })), - null, - ); -}); - -test('no other provider asks for an acknowledgement', () => { - for (const providerType of Object.keys(PROVIDER_REGISTRY) as ProviderType[]) { - if (providerType === 'commandcode-go') continue; - assert.equal( - providerRequiresAcknowledgement(providerType), - false, - `${providerType} unexpectedly asks for an acknowledgement`, - ); - } -}); - // The second. Discovery failures were reported for every provider except the // custom relays, which are the endpoints most likely to be misconfigured. test('a discovery failure reaches the caller for a custom relay', async () => { diff --git a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts index 9d4b91f138..083394ca28 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts @@ -79,7 +79,6 @@ test('registers pure Connection reads for replacement-Host retry', () => { 'connections:getRequestHeaders', 'connections:getSnapshot', 'connections:hasSecret', - 'connections:usage', ]); assert.ok(effects.has('connections:create')); assert.ok(effects.has('connections:onboardingVerify')); @@ -427,41 +426,6 @@ test('reports an existing but unconfigured credential as missing', async () => { ); }); -test('reads connection usage Host-side and rejects an identity with extra keys', async () => { - const handlers = new Map unknown>(); - let readFor: string | undefined; - registerRuntimeHostConnectionsIpc({ - ipcMain: { - handle: (channel, handler) => { - handlers.set(channel, handler as (...args: unknown[]) => unknown); - }, - }, - client: { - loadConnectionCatalog: async () => catalog(), - readConnectionUsage: async (connectionId: string) => { - readFor = connectionId; - return { kind: 'unavailable', reason: 'unsupported' }; - }, - } as never, - emitConnectionListChanged() {}, - }); - - assert.deepEqual( - await handlers.get('connections:usage')?.({}, connectionIdentity()), - { kind: 'unavailable', reason: 'unsupported' }, - ); - assert.equal(readFor, 'connection-1'); - - // The renderer bug this guards: passing the whole projected connection - // instead of the narrow identity. Structural typing lets the extra fields - // through at compile time, so the boundary must refuse them at runtime. - await assert.rejects( - async () => - handlers.get('connections:usage')?.({}, { ...connectionIdentity(), name: 'OpenRouter' }), - /Invalid Connection identity/i, - ); -}); - test('keeps saved custom header values out of the renderer and preserves them by name', async () => { const handlers = new Map unknown>(); let replacedHeaders: unknown; diff --git a/apps/desktop/src/main/commandcode-browser-login.ts b/apps/desktop/src/main/commandcode-browser-login.ts deleted file mode 100644 index b4b5722941..0000000000 --- a/apps/desktop/src/main/commandcode-browser-login.ts +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Browser-assisted Command Code sign-in: the loopback flow the official - * `command-code login` CLI performs, driven from the Desktop main process. - * - * This is not OAuth. Studio (commandcode.ai) opens on an approval page whose - * `callback` query names a loopback URL this module is listening on. When the - * user approves, the Studio page POSTs a freshly minted **API key** to that - * URL as JSON — the same kind of key a user would paste by hand. The flow - * therefore ends with plain credentials the renderer drops into the ordinary - * key field; nothing downstream (creation, credential vault, discovery) knows - * the key arrived this way. - * - * Why main and not the Runtime Host: the browser posts to `localhost`, so the - * listener must share a machine with the browser. The Host may be remote; the - * Desktop never is. - * - * Protocol facts (mirrored from the CLI, no public specification): - * GET {studio}/studio/auth/cli?callback=http://localhost:{port}/callback&state={state} - * POST http://localhost:{port}/callback - * { apiKey, state, userId, userName, keyName } — approved - * { error: 'access_denied', error_description?, state } — denied - * The CLI walks ports from 5959 upward; Studio may check the callback port, - * so this module keeps the same range instead of an ephemeral one. - */ - -import { randomBytes, randomUUID } from 'node:crypto'; -import { - createServer, - type IncomingMessage, - type Server, - type ServerResponse, -} from 'node:http'; - -export const COMMANDCODE_LOGIN_TIMEOUT_MS = 120_000; -export const COMMANDCODE_LOGIN_START_PORT = 5959; -export const COMMANDCODE_LOGIN_MAX_PORT_ATTEMPTS = 10; -/** Cap on the callback body, in wire bytes (not JS string length). */ -export const COMMANDCODE_LOGIN_BODY_LIMIT_BYTES = 10_000; -export const COMMANDCODE_LOGIN_ALLOWED_ORIGINS: readonly string[] = [ - 'https://commandcode.ai', - 'https://staging.commandcode.ai', - 'http://localhost:3000', -]; - -const CALLBACK_PATH = '/callback'; -const STUDIO_AUTH_PATH = '/studio/auth/cli'; -const DEFAULT_STUDIO_BASE = 'https://commandcode.ai'; - -export type CommandCodeBrowserLoginFailureReason = - /** Studio reported the user denied the authorization. */ - | 'denied' - /** No callback arrived within the login window. */ - | 'timeout' - /** Cancelled by the user or torn down with the app. */ - | 'cancelled' - /** A newer attempt replaced this one, or the attempt id is unknown/spent. */ - | 'superseded' - /** No loopback port in the CLI's range could be bound. */ - | 'port_unavailable' - /** The system browser could not be opened. */ - | 'browser_unavailable'; - -export interface CommandCodeBrowserLoginCredentials { - readonly apiKey: string; - readonly userName: string; - readonly keyName: string; -} - -export interface CommandCodeBrowserLoginStartInput { - /** The connection's Provider API base; picks the Studio that mints its keys. */ - readonly baseUrl?: string; -} - -export type CommandCodeBrowserLoginStartResult = - | { - readonly ok: true; - readonly attemptId: string; - /** The approval page, for a "browser did not open?" fallback link. */ - readonly authUrl: string; - } - | { - readonly ok: false; - readonly reason: Extract< - CommandCodeBrowserLoginFailureReason, - 'port_unavailable' | 'browser_unavailable' | 'superseded' - >; - }; - -export type CommandCodeBrowserLoginResult = - | { readonly ok: true; readonly credentials: CommandCodeBrowserLoginCredentials } - | { readonly ok: false; readonly reason: CommandCodeBrowserLoginFailureReason }; - -export interface CommandCodeBrowserLoginDeps { - openExternal(url: string): Promise; - timeoutMs?: number; - startPort?: number; - maxPortAttempts?: number; - randomToken?: (byteLength: number) => string; -} - -/** Studio host that mints keys for a given Provider API base. */ -export function studioBaseForApiBase(apiBase: string | undefined): string { - if (apiBase === undefined) return DEFAULT_STUDIO_BASE; - if (/^https:\/\/staging-api\.commandcode\.ai(?:[/:]|$)/iu.test(apiBase)) { - return 'https://staging.commandcode.ai'; - } - if (/^http:\/\/localhost(?::\d+)?(?:\/|$)/iu.test(apiBase)) return 'http://localhost:3000'; - return DEFAULT_STUDIO_BASE; -} - -export function buildCommandCodeAuthUrl(input: { - studioBase: string; - port: number; - state: string; -}): string { - const callback = `http://localhost:${input.port}${CALLBACK_PATH}`; - const url = new URL(STUDIO_AUTH_PATH, input.studioBase); - url.searchParams.set('callback', callback); - url.searchParams.set('state', input.state); - return url.toString(); -} - -interface Attempt { - readonly id: string; - readonly state: string; - port: number; - server: Server | undefined; - timer: ReturnType | undefined; - readonly settled: Promise; - settle: ((result: CommandCodeBrowserLoginResult) => void) | undefined; - /** `complete()` hands the result out once; a second read is spent. */ - delivered: boolean; -} - -export class CommandCodeBrowserLoginController { - readonly #deps: CommandCodeBrowserLoginDeps; - readonly #attempts = new Map(); - #current: Attempt | undefined; - #disposed = false; - - constructor(deps: CommandCodeBrowserLoginDeps) { - this.#deps = deps; - } - - /** - * Binds the loopback listener, opens the Studio approval page, and returns - * the attempt handle. A live attempt is superseded: one browser tab at a - * time, and its dead port must never be handed back as a fresh link. - */ - async start( - input: CommandCodeBrowserLoginStartInput = {}, - ): Promise { - if (this.#disposed) return { ok: false, reason: 'browser_unavailable' }; - // Reserved before the first await. Binding a port is asynchronous, so a - // second start() that ran the supersession check first would see no - // current attempt, bind a second port beside this one, and leave two live - // attempts each able to deliver its own credentials. - const attempt = this.#reserve(); - - const bound = await this.#bind(attempt); - if (!bound) { - this.#finish(attempt, { ok: false, reason: 'port_unavailable' }); - return { ok: false, reason: 'port_unavailable' }; - } - // A newer start (or a cancel, or dispose) retired this attempt while it - // was binding. Its result has already settled; the port it just took is - // held by nobody, so release it here. - if (attempt.settle === undefined || this.#disposed) { - this.#finish(attempt, { ok: false, reason: 'superseded' }); - return { ok: false, reason: 'superseded' }; - } - - const authUrl = buildCommandCodeAuthUrl({ - studioBase: studioBaseForApiBase(input.baseUrl), - port: attempt.port, - state: attempt.state, - }); - attempt.timer = setTimeout( - () => this.#finish(attempt, { ok: false, reason: 'timeout' }), - this.#deps.timeoutMs ?? COMMANDCODE_LOGIN_TIMEOUT_MS, - ); - attempt.timer.unref?.(); - - try { - await this.#deps.openExternal(authUrl); - } catch { - this.#finish(attempt, { ok: false, reason: 'browser_unavailable' }); - return { ok: false, reason: 'browser_unavailable' }; - } - // The browser may already have posted back while openExternal was - // pending; `complete()` reads the settled result either way. - return { ok: true, attemptId: attempt.id, authUrl }; - } - - /** - * Resolves when the attempt settles: approved, denied, timed out, - * cancelled, or replaced. The credentials are delivered exactly once. - */ - async complete(attemptId: string): Promise { - const attempt = this.#attempts.get(attemptId); - if (attempt === undefined || attempt.delivered) return { ok: false, reason: 'superseded' }; - attempt.delivered = true; - const result = await attempt.settled; - this.#attempts.delete(attemptId); - return result; - } - - cancel(attemptId?: string): void { - const attempt = attemptId === undefined ? this.#current : this.#attempts.get(attemptId); - if (attempt === undefined) return; - this.#finish(attempt, { ok: false, reason: 'cancelled' }); - } - - dispose(): void { - if (this.#disposed) return; - this.#disposed = true; - this.#finish(this.#current, { ok: false, reason: 'cancelled' }); - } - - // --------------------------------------------------------------------- - // Internals - // --------------------------------------------------------------------- - - /** Retires the live attempt and installs a fresh one, without awaiting. */ - #reserve(): Attempt { - let settle!: (result: CommandCodeBrowserLoginResult) => void; - const settled = new Promise((resolve) => { - settle = resolve; - }); - const attempt: Attempt = { - id: randomUUID(), - state: this.#deps.randomToken?.(32) ?? randomBytes(32).toString('base64url'), - port: 0, - server: undefined, - timer: undefined, - settled, - settle, - delivered: false, - }; - this.#finish(this.#current, { ok: false, reason: 'superseded' }); - this.#current = attempt; - this.#attempts.set(attempt.id, attempt); - return attempt; - } - - async #bind(attempt: Attempt): Promise { - const startPort = this.#deps.startPort ?? COMMANDCODE_LOGIN_START_PORT; - const attempts = this.#deps.maxPortAttempts ?? COMMANDCODE_LOGIN_MAX_PORT_ATTEMPTS; - for (let index = 0; index < attempts; index += 1) { - const port = startPort + index; - const server = await listenLoopback(port, (request, response) => - this.#handleCallback(attempt, request, response), - ); - if (server === undefined) continue; - attempt.server = server; - attempt.port = port; - return true; - } - return false; - } - - #handleCallback(attempt: Attempt, request: IncomingMessage, response: ServerResponse): void { - // One-shot responses: the server dies with the attempt, and a client - // pooling the connection would otherwise race its next request against - // the close. - response.setHeader('Connection', 'close'); - response.setHeader('Access-Control-Allow-Origin', corsOrigin(request.headers.origin)); - response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - response.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - response.setHeader('Content-Type', 'application/json'); - response.setHeader('Cache-Control', 'no-store'); - const json = (code: number, body: Record) => { - response.writeHead(code); - response.end(JSON.stringify(body)); - }; - - // DNS-rebinding defense shared with this repo's other loopback listeners: - // only a request addressed to the loopback authority we bound may proceed. - const host = request.headers.host; - if (host !== `localhost:${attempt.port}` && host !== `127.0.0.1:${attempt.port}`) { - json(403, { success: false, error: 'Forbidden' }); - return; - } - if (request.method === 'OPTIONS') { - response.writeHead(204); - response.end(); - return; - } - const path = request.url?.split('?')[0] ?? '/'; - if (path !== CALLBACK_PATH) { - json(404, { success: false, error: 'Not found' }); - return; - } - if (request.method !== 'POST') { - json(405, { success: false, error: 'Method not allowed. Use POST.' }); - return; - } - - let bodyBytes = 0; - const chunks: Buffer[] = []; - request.on('data', (chunk: Buffer) => { - bodyBytes += chunk.length; - if (bodyBytes > COMMANDCODE_LOGIN_BODY_LIMIT_BYTES) { - request.destroy(); - return; - } - chunks.push(chunk); - }); - request.on('end', () => { - // A destroyed (over-limit) request never processes its partial body. - if (request.destroyed) return; - let payload: unknown; - try { - payload = JSON.parse(Buffer.concat(chunks).toString('utf8')); - } catch { - json(400, { success: false, error: 'Invalid JSON' }); - return; - } - if (!isRecord(payload)) { - json(400, { success: false, error: 'Invalid JSON' }); - return; - } - // State FIRST, on every branch. A denial is terminal, and the Studio - // page's POST rides as a CORS simple request the browser sends - // regardless of our origin allowlist — so without this order any web - // page could kill a live login by blindly posting `access_denied`. - if (payload.state !== attempt.state) { - // Not terminal: a stale tab replaying an old state must not end the - // live attempt. Answer and keep waiting, as the CLI does. - json(403, { success: false, error: 'Invalid state token' }); - return; - } - if ('error' in payload) { - json(200, { success: true }); - this.#finish(attempt, { ok: false, reason: 'denied' }); - return; - } - const credentials = readCredentials(payload); - if (credentials === undefined) { - json(400, { success: false, error: 'Missing required fields' }); - return; - } - json(200, { success: true }); - this.#finish(attempt, { ok: true, credentials }); - }); - request.on('error', () => {}); - } - - #finish(attempt: Attempt | undefined, result: CommandCodeBrowserLoginResult): void { - if (attempt === undefined) return; - if (attempt.timer !== undefined) { - clearTimeout(attempt.timer); - attempt.timer = undefined; - } - if (attempt.server !== undefined) { - const server = attempt.server; - attempt.server = undefined; - server.close(); - // The callback response has been flushed by the time we get here; a - // lingering keep-alive socket must not hold the CLI port range. - server.closeAllConnections(); - } - if (this.#current === attempt) this.#current = undefined; - const settle = attempt.settle; - attempt.settle = undefined; - settle?.(result); - } -} - -function listenLoopback( - port: number, - handler: (request: IncomingMessage, response: ServerResponse) => void, -): Promise { - return new Promise((resolve) => { - const server = createServer(handler); - server.once('error', () => resolve(undefined)); - server.listen(port, '127.0.0.1', () => { - server.removeAllListeners('error'); - // A bound server that later errors must not crash the process. - server.on('error', () => {}); - resolve(server); - }); - }); -} - -function corsOrigin(origin: string | undefined): string { - return origin !== undefined && COMMANDCODE_LOGIN_ALLOWED_ORIGINS.includes(origin) ? origin : ''; -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function readCredentials( - payload: Record, -): CommandCodeBrowserLoginCredentials | undefined { - const { apiKey, userName, keyName } = payload; - if (typeof apiKey !== 'string' || apiKey === '') return undefined; - if (typeof userName !== 'string' || typeof keyName !== 'string') return undefined; - return { apiKey, userName, keyName }; -} diff --git a/apps/desktop/src/main/commandcode-login-ipc-main.ts b/apps/desktop/src/main/commandcode-login-ipc-main.ts deleted file mode 100644 index ffd456430a..0000000000 --- a/apps/desktop/src/main/commandcode-login-ipc-main.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type { IpcMain } from 'electron'; -import type { - CommandCodeBrowserLoginController, - CommandCodeBrowserLoginResult, - CommandCodeBrowserLoginStartInput, - CommandCodeBrowserLoginStartResult, -} from './commandcode-browser-login.js'; - -/** Channel names; the preload spells them as literals, as it does for every bridge. */ -export const COMMANDCODE_LOGIN_IPC_CHANNELS = { - start: 'commandcode-login:start', - complete: 'commandcode-login:complete', - cancel: 'commandcode-login:cancel', -} as const; - -/** - * Desktop-local IPC for the browser-assisted Command Code sign-in. Not - * Host-scoped: the loopback listener lives beside the browser, and the key it - * yields goes through the ordinary `connections:*` path afterwards. - */ -export interface CommandCodeLoginIpcDeps { - readonly ipcMain: Pick; - readonly controller: Pick; -} - -const MAX_BASE_URL_CHARS = 2_048; -const MAX_ATTEMPT_ID_CHARS = 128; - -export function registerCommandCodeLoginIpc(deps: CommandCodeLoginIpcDeps): void { - deps.ipcMain.handle( - COMMANDCODE_LOGIN_IPC_CHANNELS.start, - (_event, raw: unknown): Promise => - deps.controller.start(decodeStartInput(raw)), - ); - deps.ipcMain.handle( - COMMANDCODE_LOGIN_IPC_CHANNELS.complete, - async (_event, raw: unknown): Promise => { - const attemptId = decodeAttemptId(raw); - if (attemptId === undefined) return { ok: false, reason: 'superseded' }; - return deps.controller.complete(attemptId); - }, - ); - deps.ipcMain.handle(COMMANDCODE_LOGIN_IPC_CHANNELS.cancel, (_event, raw: unknown): void => { - const attemptId = decodeAttemptId(raw); - if (attemptId !== undefined) deps.controller.cancel(attemptId); - }); -} - -function decodeStartInput(raw: unknown): CommandCodeBrowserLoginStartInput { - if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}; - const baseUrl = (raw as Record).baseUrl; - return typeof baseUrl === 'string' && baseUrl.length > 0 && baseUrl.length <= MAX_BASE_URL_CHARS - ? { baseUrl } - : {}; -} - -function decodeAttemptId(raw: unknown): string | undefined { - return typeof raw === 'string' && raw.length > 0 && raw.length <= MAX_ATTEMPT_ID_CHARS - ? raw - : undefined; -} diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index c61ce4ba32..3847c41e36 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -82,8 +82,6 @@ import { createSettingsStore } from "@maka/storage/settings-store"; import { resolveStorageRoot } from "@maka/storage/root-authority"; import { createMcpOAuthController } from "./mcp-oauth-controller.js"; -import { CommandCodeBrowserLoginController } from "./commandcode-browser-login.js"; -import { registerCommandCodeLoginIpc } from "./commandcode-login-ipc-main.js"; import { createWorkHubControl } from './workhub-control.js'; import { createWorkHubPresentation } from './workhub-presentation.js'; import { createWorkHubRuntime } from './workhub-runtime.js'; @@ -636,11 +634,6 @@ function localSessionTarget(state: RuntimeHostDesktopTargetState): DesktopSessio ...(state.readiness === 'ready' ? { client: state.candidate.client, submit: (input) => state.candidate.submitLocalMessage(input) } : {}) }; } const oauthPresentation = new RuntimeHostOAuthPresentation((url) => shell.openExternal(url)); -// Desktop-local by construction: the Studio page posts the key to a loopback -// port beside the browser, so the listener cannot live in a (possibly remote) Host. -const commandCodeLoginController = new CommandCodeBrowserLoginController({ - openExternal: (url) => shell.openExternal(url), -}); const runtimeHostProfileService = createDesktopRuntimeHostProfileService({ clientDataRoot: userDataDir, startup: runtimeHostStartup, @@ -1962,7 +1955,6 @@ function registerPersistentClientIpc(): void { mainWindowController, resolveLocale: () => desktopLocale.resolve(), }); - registerCommandCodeLoginIpc({ ipcMain, controller: commandCodeLoginController }); registerDesktopRuntimeHostProfileIpc(ipcMain, runtimeHostProfileService); registerDesktopGuestSessionMountIpc( ipcMain, @@ -2201,8 +2193,6 @@ function closeRuntimeHostDesktop(): Promise { } async function disposeRuntimeHostDesktop(): Promise { - // Any in-flight browser sign-in ends here with the app; its loopback port goes with it. - commandCodeLoginController.dispose(); sessionLocal.close(); powerMonitor.off("resume", wakePeerRecoveryAfterResume); clientSettingsWatcher.stop(); diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index 91287fcd4d..fcd970fd39 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -546,12 +546,6 @@ export class DesktopRuntimeHostClient { }); } - readConnectionUsage( - connectionId: string, - ): Promise> { - return this.request("connection.usage.read", { connectionId }); - } - verifyConnectionOnboarding( input: OperationInput<"connection.onboarding.verify">, ): Promise> { diff --git a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts index 1a31becefa..a9d974f7ec 100644 --- a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts @@ -72,7 +72,6 @@ type HostConnectionsClient = Pick< | 'getConnectionRequestHeaders' | 'loadConnectionCatalog' | 'queryCredential' - | 'readConnectionUsage' | 'removeConnection' | 'replaceConnectionRequestHeaders' | 'setCredential' @@ -122,16 +121,6 @@ export function registerRuntimeHostConnectionsIpc( return { names: result.names } satisfies SavedRequestHeaders; }, ); - // Read-only: resolves the credential Host-side and never returns it, so the - // renderer learns the usage figures and nothing more. - handleReconnectableRead( - deps.ipcMain, - 'connections:usage', - async (_event, identity: unknown) => { - const connection = requireConnectionIdentity(await snapshot(), identity); - return deps.client.readConnectionUsage(connection.connectionId); - }, - ); deps.ipcMain.handle( 'connections:setRequestHeaders', async (_event, identity: unknown, rawUpdates: unknown) => { diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index 91cb3ae407..b3a8685a88 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -456,35 +456,6 @@ export type DesktopOAuthAuthorizationResult = | { readonly ok: true; readonly connection: DesktopOAuthConnectionIdentity } | Exclude; -/** - * Browser-assisted Command Code sign-in. Desktop-local, not Host-scoped: the - * Studio page posts the minted key to a loopback port beside the browser, and - * the key then travels through the ordinary `connections` path like a pasted - * one. Mirrored by `features/connection-settings/ports.ts` on the renderer side. - */ -export type DesktopCommandCodeLoginFailureReason = - | 'denied' - | 'timeout' - | 'cancelled' - | 'superseded' - | 'port_unavailable' - | 'browser_unavailable'; -export interface DesktopCommandCodeLoginStartInput { - readonly baseUrl?: string; -} -export type DesktopCommandCodeLoginStartResult = - | { readonly ok: true; readonly attemptId: string; readonly authUrl: string } - | { - readonly ok: false; - readonly reason: 'port_unavailable' | 'browser_unavailable' | 'superseded'; - }; -export type DesktopCommandCodeLoginResult = - | { - readonly ok: true; - readonly credentials: { readonly apiKey: string; readonly userName: string; readonly keyName: string }; - } - | { readonly ok: false; readonly reason: DesktopCommandCodeLoginFailureReason }; - export type DesktopNewTaskHostRef = DesktopRuntimeHostRef; export interface DesktopNewTaskTarget extends DesktopRuntimeHostRef { @@ -1572,8 +1543,6 @@ export interface MakaBridge { test(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity | string, opts?: { model?: string }, host?: DesktopRuntimeHostRef): Promise; fetchModels(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise>; hasSecret(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise; - /** Read-only account usage for a connection, Host-fetched. */ - usage(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise; getRequestHeaders(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise; setRequestHeaders( connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, @@ -1765,12 +1734,6 @@ export interface MakaBridge { refreshTokens(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise; logout(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise; }; - /** Desktop-local browser sign-in for Command Code; see `DesktopCommandCodeLoginResult`. */ - commandCodeLogin: { - start(input: DesktopCommandCodeLoginStartInput): Promise; - complete(attemptId: string): Promise; - cancel(attemptId: string): Promise; - }; githubCopilotSubscription: { connectExistingLogin(host?: DesktopRuntimeHostRef): Promise; getAuthUrl( diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 523085f800..0571aff08f 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -89,11 +89,6 @@ import type { } from './bridge-contract.js'; import type { ExternalSessionImportIpcResult } from './external-session-import-result.js'; import type { RuntimeHostObservationIpcResult } from '../shared/runtime-host-observation-ipc.js'; -import type { - DesktopCommandCodeLoginResult, - DesktopCommandCodeLoginStartInput, - DesktopCommandCodeLoginStartResult, -} from './bridge-contract.js'; import { projectDesktopExternalSessionCatalogItem, type DesktopExternalSessionCatalogItem, @@ -3052,9 +3047,6 @@ const makaBridge = { hasSecret(connection: import('../shared/desktop-connection-snapshot.js').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise { return invokeSelectedRuntimeHost(host, 'connections:hasSecret', connection); }, - usage(connection: import('../shared/desktop-connection-snapshot.js').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise { - return invokeSelectedRuntimeHost(host, 'connections:usage', connection); - }, getRequestHeaders(connection: import('../shared/desktop-connection-snapshot.js').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise { return invokeSelectedRuntimeHost(host, 'connections:getRequestHeaders', connection); }, @@ -3340,17 +3332,6 @@ const makaBridge = { return invokeSelectedRuntimeHost(host, 'xai-oauth:logout', connectionId); }, }, - commandCodeLogin: { - start(input: DesktopCommandCodeLoginStartInput): Promise { - return ipcRenderer.invoke('commandcode-login:start', input); - }, - complete(attemptId: string): Promise { - return ipcRenderer.invoke('commandcode-login:complete', attemptId); - }, - cancel(attemptId: string): Promise { - return ipcRenderer.invoke('commandcode-login:cancel', attemptId); - }, - }, githubCopilotSubscription: { connectExistingLogin(host?: DesktopRuntimeHostRef): Promise { return invokeSelectedRuntimeHost(host, 'github-copilot:connect-existing-login'); diff --git a/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-flow.ts b/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-flow.ts deleted file mode 100644 index c410016ca3..0000000000 --- a/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-flow.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type { - CommandCodeBrowserLoginBridge, - CommandCodeBrowserLoginCredentials, - CommandCodeBrowserLoginFailureReason, - CommandCodeBrowserLoginResult, - CommandCodeBrowserLoginStartInput, -} from './ports.js'; - -/** - * Renderer-side failure vocabulary: the main-process reasons minus the user's - * own cancel (which returns to idle) plus a dead bridge. - */ -export type CommandCodeBrowserLoginFlowFailure = - | Exclude - | 'unavailable'; - -export type CommandCodeBrowserLoginFlowState = - | { readonly phase: 'idle' } - | { readonly phase: 'starting' } - | { readonly phase: 'waiting'; readonly attemptId: string; readonly authUrl: string } - | { readonly phase: 'filled'; readonly userName: string } - | { readonly phase: 'failed'; readonly reason: CommandCodeBrowserLoginFlowFailure }; - -const IDLE: CommandCodeBrowserLoginFlowState = { phase: 'idle' }; - -/** - * The renderer half of the browser-assisted Command Code sign-in: one attempt - * at a time, `start` then a long `complete`, with a generation counter so a - * cancelled or replaced attempt's late result is dropped rather than typed - * into the key field. Framework-free so the machine is testable on its own; - * the hint component subscribes to it. - */ -export class CommandCodeBrowserLoginFlow { - readonly #bridge: CommandCodeBrowserLoginBridge; - readonly #onCredentials: (credentials: CommandCodeBrowserLoginCredentials) => void; - readonly #listeners = new Set<() => void>(); - #state: CommandCodeBrowserLoginFlowState = IDLE; - #generation = 0; - #attemptId: string | undefined; - #disposed = false; - - constructor( - bridge: CommandCodeBrowserLoginBridge, - onCredentials: (credentials: CommandCodeBrowserLoginCredentials) => void, - ) { - this.#bridge = bridge; - this.#onCredentials = onCredentials; - } - - readonly subscribe = (listener: () => void): (() => void) => { - this.#listeners.add(listener); - return () => { - this.#listeners.delete(listener); - }; - }; - - readonly getState = (): CommandCodeBrowserLoginFlowState => this.#state; - - async start(input: CommandCodeBrowserLoginStartInput = {}): Promise { - if (this.#disposed) return; - if (this.#state.phase === 'starting' || this.#state.phase === 'waiting') return; - const generation = ++this.#generation; - this.#set({ phase: 'starting' }); - - let started: Awaited>; - try { - started = await this.#bridge.start(input); - } catch { - if (this.#isStale(generation)) return; - this.#set({ phase: 'failed', reason: 'unavailable' }); - return; - } - if (this.#isStale(generation)) { - // Superseded while binding: the listener is live on a port nobody is - // watching. Release it rather than letting the window run out. - if (started.ok) void this.#bridge.cancel(started.attemptId).catch(() => {}); - return; - } - if (!started.ok) { - this.#set({ phase: 'failed', reason: started.reason }); - return; - } - - this.#attemptId = started.attemptId; - this.#set({ phase: 'waiting', attemptId: started.attemptId, authUrl: started.authUrl }); - let result: CommandCodeBrowserLoginResult | { ok: false; reason: 'unavailable' }; - try { - result = await this.#bridge.complete(started.attemptId); - } catch { - result = { ok: false, reason: 'unavailable' }; - } - if (this.#isStale(generation)) return; - this.#attemptId = undefined; - if (result.ok) { - this.#onCredentials(result.credentials); - this.#set({ phase: 'filled', userName: result.credentials.userName }); - return; - } - // The user's own cancel returns the hint to its resting link; every - // other end is something to tell them about. - if (result.reason === 'cancelled') { - this.#set(IDLE); - return; - } - this.#set({ phase: 'failed', reason: result.reason }); - } - - /** Abandons the live attempt (if any) and returns to the resting link. */ - cancel(): void { - const attemptId = this.#attemptId; - this.#generation += 1; - this.#attemptId = undefined; - if (!this.#disposed) this.#set(IDLE); - if (attemptId !== undefined) void this.#bridge.cancel(attemptId).catch(() => {}); - } - - dispose(): void { - if (this.#disposed) return; - this.cancel(); - this.#disposed = true; - this.#listeners.clear(); - } - - #isStale(generation: number): boolean { - return this.#disposed || this.#generation !== generation; - } - - #set(next: CommandCodeBrowserLoginFlowState): void { - this.#state = next; - for (const listener of [...this.#listeners]) listener(); - } -} diff --git a/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx b/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx deleted file mode 100644 index a9a6c5e5db..0000000000 --- a/apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; -import { Banner, Divider, HStack, Link, Text, VStack } from '@astryxdesign/core'; -import { Button, useUiLocale } from '@maka/ui'; -import type { - CommandCodeBrowserLoginBridge, - CommandCodeBrowserLoginCredentials, -} from './ports.js'; -import { CommandCodeBrowserLoginFlow } from './commandcode-browser-login-flow.js'; -import { getProviderSettingsCopy } from './settings-provider-copy.js'; - -/** - * The "or sign in with your account" block under the Command Code key field: - * an "or" divider, then a small titled area with one secondary button. Pasting - * a key stays the primary path; a successful sign-in only fills that field. - */ -export function CommandCodeBrowserLoginSection(props: { - readonly bridge: CommandCodeBrowserLoginBridge; - readonly baseUrl?: string; - readonly isDisabled?: boolean; - readonly onCredentials: (credentials: CommandCodeBrowserLoginCredentials) => void; -}) { - const locale = useUiLocale(); - const copy = getProviderSettingsCopy(locale).add.browserLogin; - const onCredentialsRef = useRef(props.onCredentials); - onCredentialsRef.current = props.onCredentials; - const flow = useMemo( - () => - new CommandCodeBrowserLoginFlow(props.bridge, (credentials) => - onCredentialsRef.current(credentials), - ), - [props.bridge], - ); - // Cancel, not dispose: the memoised flow survives StrictMode's mount → - // unmount → mount rehearsal, and a disposed instance would ignore every - // later click. cancel() abandons the live attempt and leaves the instance - // usable; a late result is dropped by the generation check. - useEffect(() => () => flow.cancel(), [flow]); - const state = useSyncExternalStore(flow.subscribe, flow.getState, flow.getState); - const start = () => void flow.start(props.baseUrl === undefined ? {} : { baseUrl: props.baseUrl }); - const inFlight = state.phase === 'starting' || state.phase === 'waiting'; - - return ( - - - - {copy.title} - - {copy.description} - - - {state.phase === 'failed' && } - - {inFlight ? ( - <> - - {copy.waiting} - - {state.phase === 'waiting' && ( - - {copy.openAgain} - - )} - - - - ); -} - -function UsageReport(props: { report: Report; copy: UsageCopy }) { - const { report, copy } = props; - // The account line only exists when there is something to put on it. - const badges = [report.accountLabel, report.planLabel].filter( - (value): value is string => typeof value === 'string' && value.length > 0, - ); - const stats = report.stats; - return ( - - {report.partiallyUnauthorized ? ( - // Some endpoints refused the key, so what is shown here is real but - // incomplete. Saying so is the point: otherwise a partial report reads - // as a complete one and the page cannot name the credential as the cause. - - {copy.partiallyUnauthorized} - - ) : null} - {badges.length > 0 ? ( - - {copy.current} - {badges.map((badge) => ( - - {badge} - - ))} - - ) : null} - - {stats ? ( - - {stats.requests !== null ? ( - - ) : null} - {stats.successRate !== null ? ( - - ) : null} - {stats.cost !== null ? ( - - ) : null} - {stats.tokensIn !== null || stats.tokensOut !== null ? ( - - ) : null} - - ) : null} - - {report.windows.map((window) => ( - - ))} - - - - {report.periodEnd !== null ? copy.periodEnd(formatDate(report.periodEnd)) : ''} - - - {copy.updatedAt(formatTime(report.fetchedAt))} - - - - ); -} - -function StatTile(props: { label: string; value: string; detail?: string }) { - return ( - - - {props.label} - - {props.value} - {props.detail ? ( - - {props.detail} - - ) : null} - - ); -} - -/** - * One window as a labelled bar: `used / cap` on the right, and the reset line - * below only when the provider reported a reset. An uncapped window says so - * instead of drawing a bar it cannot fill. - */ -function UsageWindowBar(props: { window: UsageWindow; copy: UsageCopy }) { - const { window: win, copy } = props; - const label = - copy.windowLabels[win.id as keyof UsageCopy['windowLabels']] ?? win.label ?? win.id; - if (win.unlimited === true) { - return ( - - - {label} - - {copy.unlimited} - - - - ); - } - const percent = win.cap > 0 ? Math.min(100, Math.max(0, (win.used / win.cap) * 100)) : 0; - return ( - - - {label} - - {`${formatCredits(win.used)} / ${formatCredits(win.cap)}`} - - -
-
-
- {win.resetsAt !== null ? ( - - {copy.resetsAt(formatDate(win.resetsAt))} - - ) : null} - - ); -} - -function formatCount(value: number): string { - return value.toLocaleString('en-US'); -} - -/** Credits are dollar-denominated; two decimals keeps the bar's arithmetic legible. */ -function formatCredits(value: number): string { - return `$${value.toFixed(2)}`; -} - -function formatTokens(value: number): string { - const million = 1_000_000; - if (value >= million) return `${(value / million).toFixed(1)}M`; - const thousand = 1_000; - if (value >= thousand) return `${(value / thousand).toFixed(1)}K`; - return String(value); -} - -function formatDate(millis: number): string { - return new Date(millis).toLocaleDateString(); -} - -function formatTime(millis: number): string { - return new Date(millis).toLocaleTimeString(); -} diff --git a/apps/desktop/src/renderer/features/connection-settings/index.ts b/apps/desktop/src/renderer/features/connection-settings/index.ts index 9e6ec79ce2..2a886a4654 100644 --- a/apps/desktop/src/renderer/features/connection-settings/index.ts +++ b/apps/desktop/src/renderer/features/connection-settings/index.ts @@ -24,10 +24,6 @@ export { } from './services-context.js'; export type { ApiKeyOnboardingBridge, - CommandCodeBrowserLoginBridge, - CommandCodeBrowserLoginCredentials, - CommandCodeBrowserLoginResult, - CommandCodeBrowserLoginStartResult, ConnectionOAuthBridge, ConnectionOAuthProviderBridge, ConnectionSettingsServices, @@ -41,8 +37,6 @@ export { providerPanelActionErrorMessage, } from './provider-panel-shared.js'; export { OnboardingStepForm } from './onboarding-step-form.js'; -export { CommandCodeBrowserLoginSection } from './commandcode-browser-login-section.js'; -export { CommandCodeBrowserLoginFlow } from './commandcode-browser-login-flow.js'; export { getProviderSettingsCopy, subscriptionActionErrorMessage, subscriptionResultMessage } from './settings-provider-copy.js'; export type { ProviderSettingsCopy } from './settings-provider-copy.js'; export type { @@ -53,4 +47,3 @@ export { GenericProviderMark } from './generic-provider-mark.js'; export { parseContextWindowInput } from './context-window-input.js'; export { CapabilityEditor } from './provider-capability-editor.js'; export { AddModelDialog, ModelParametersDialog } from './provider-add-model-dialog.js'; -export { ConnectionUsageSection } from './connection-usage-card.js'; diff --git a/apps/desktop/src/renderer/features/connection-settings/ports.ts b/apps/desktop/src/renderer/features/connection-settings/ports.ts index 3eb18996a8..3a479464c5 100644 --- a/apps/desktop/src/renderer/features/connection-settings/ports.ts +++ b/apps/desktop/src/renderer/features/connection-settings/ports.ts @@ -28,7 +28,6 @@ import type { UpdateConnectionInput, } from '@maka/core/llm-connections'; import type { SubscriptionActionResult } from '@maka/core/oauth-subscription'; -import type { ConnectionUsageReadResult } from '@maka/runtime-host/protocol'; import type { ConnectionOnboardingSaveInput, ConnectionOnboardingSaveResult, @@ -84,56 +83,9 @@ export interface ConnectionOAuthBridge { }; } -/** - * Browser-assisted Command Code sign-in, as the renderer sees it. The Desktop - * adapter maps the preload's `commandCodeLogin` onto this; the shapes are kept - * in step by hand because a module shared with main/preload would enter the - * renderer's frozen legacy closure. - */ -export type CommandCodeBrowserLoginFailureReason = - | 'denied' - | 'timeout' - | 'cancelled' - | 'superseded' - | 'port_unavailable' - | 'browser_unavailable'; - -export interface CommandCodeBrowserLoginCredentials { - readonly apiKey: string; - readonly userName: string; - readonly keyName: string; -} - -export interface CommandCodeBrowserLoginStartInput { - readonly baseUrl?: string; -} - -export type CommandCodeBrowserLoginStartResult = - | { readonly ok: true; readonly attemptId: string; readonly authUrl: string } - | { - readonly ok: false; - readonly reason: 'port_unavailable' | 'browser_unavailable' | 'superseded'; - }; - -export type CommandCodeBrowserLoginResult = - | { readonly ok: true; readonly credentials: CommandCodeBrowserLoginCredentials } - | { readonly ok: false; readonly reason: CommandCodeBrowserLoginFailureReason }; - -export interface CommandCodeBrowserLoginBridge { - start(input: CommandCodeBrowserLoginStartInput): Promise; - complete(attemptId: string): Promise; - cancel(attemptId: string): Promise; -} - export interface ConnectionsBridge { /** Host-bound account operations; every adapter and fixture must provide them. */ readonly oauth: ConnectionOAuthBridge; - /** - * Browser-assisted Command Code sign-in that fills the key field. Optional: - * fixtures and non-Desktop adapters may have no loopback to offer, and the - * form then shows only the paste path. - */ - readonly commandCodeBrowserLogin?: CommandCodeBrowserLoginBridge; getSnapshot(): Promise; setDefault(connection: DesktopConnectionIdentity | null): Promise; create(input: CreateConnectionInput): Promise; @@ -144,11 +96,6 @@ export interface ConnectionsBridge { Pick >; hasSecret(connection: DesktopConnectionIdentity): Promise; - /** - * Read-only account usage for a connection. Optional: fixtures and adapters - * without a Host surface omit it, and the section then does not render. - */ - usage?(connection: DesktopConnectionIdentity): Promise; getRequestHeaders(connection: DesktopConnectionIdentity): Promise; setRequestHeaders( connection: DesktopConnectionIdentity, diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 66f940d647..9d591e9a0e 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -271,11 +271,6 @@ const zhCopy = { add: { slugIssues: { required: '请填写连接标识', format: '连接标识只能包含小写字母、数字和连字符', too_long: '连接标识不能超过 64 个字符' }, duplicateSlug: '连接标识已存在', cloudflareAccount: '请填写 Cloudflare Account ID', endpointRequired: '这个供应商需要填写服务地址', accountLogin: '请到账号连接完成登录;登录成功后会自动创建模型连接。', - transportNoticeTitle: '这个供应商走官方 CLI 的私有通道', - transportNoticeDetail: - '请求发往该 CLI 使用的接口,并带上它的身份标识,而不是 Maka 的;该接口不属于已公开的 Provider API。', - transportAcknowledgeLabel: '我了解上述情况,并自行承担', - transportAcknowledgeRequired: '请先勾选上面的确认再添加。', apiKeyPlaceholder: '输入或粘贴 API Key', cancel: '取消', accountTitle: '使用账号连接登录', advancedRequest: '高级请求设置', expandAdvancedRequest: '展开高级请求设置', collapseAdvancedRequest: '收起高级请求设置', requestHeaders: '自定义请求头', headerName: '请求头名称', headerValue: '请求头值', retainedHeaderValue: '保留已保存的值', addHeader: '添加请求头', removeHeader: '移除', noRequestHeaders: '未设置自定义请求头。', @@ -284,25 +279,6 @@ const zhCopy = { slug: '连接标识', name: '显示名称', accountIdPlaceholder: '填写账户 ID', saving: '保存中…', save: '保存供应商', keyRequired: (name: string) => `请填写 ${name} API Key`, - browserLogin: { - or: '或', - title: '使用 Command Code 账号登录', - description: '在浏览器中完成授权后,API Key 会自动填入上方输入框。', - action: '在浏览器中登录', - waiting: '正在等待浏览器中完成授权…', - openAgain: '浏览器没有打开?点此打开授权页', - cancel: '取消', - retry: '重试', - filled: (userName: string) => `已填入 ${userName} 的 API Key。`, - failed: { - denied: '授权已被拒绝。可以改为手动粘贴 API Key。', - timeout: '等待授权超时。可以重试,或手动粘贴 API Key。', - superseded: '这次登录已被新的尝试替代。', - port_unavailable: '本机 5959–5968 端口都被占用,请手动粘贴 API Key。', - browser_unavailable: '无法打开浏览器,请手动粘贴 API Key。', - unavailable: '浏览器登录暂不可用,请手动粘贴 API Key。', - }, - }, apiKeyLabel: 'API Key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: '服务地址', defaultModel: '默认模型', defaultModelPlaceholder: '留空即可,保存后自动拉取', defaultModelHelp: '保存后 Maka 会向该端点拉取模型目录。只有当端点不提供目录时,才需要在这里手填一个模型 ID。', stepsAria: '添加连接步骤', stepCredentials: '密钥', stepModels: '选择模型', @@ -495,11 +471,6 @@ const zhTwCopy = { add: { slugIssues: { required: '請填寫連線標識', format: '連線標識只能包含小寫字母、數字和連字號', too_long: '連線標識不能超過 64 個字元' }, duplicateSlug: '連線標識已存在', cloudflareAccount: '請填寫 Cloudflare Account ID', endpointRequired: '這個供應商需要填寫服務地址', accountLogin: '請到帳號連線完成登入;登入成功後會自動建立模型連線。', - transportNoticeTitle: '這個供應商走官方 CLI 的私有通道', - transportNoticeDetail: - '請求發往該 CLI 使用的介面,並帶上它的身分標示,而不是 Maka 的;該介面不屬於已公開的 Provider API。', - transportAcknowledgeLabel: '我了解上述情況,並自行承擔', - transportAcknowledgeRequired: '請先勾選上面的確認再新增。', apiKeyPlaceholder: '輸入或貼上 API Key', cancel: '取消', accountTitle: '使用帳號連線登入', advancedRequest: '高階請求設定', expandAdvancedRequest: '展開高階請求設定', collapseAdvancedRequest: '收起高階請求設定', requestHeaders: '自訂請求頭', headerName: '請求頭名稱', headerValue: '請求頭值', retainedHeaderValue: '保留已儲存的值', addHeader: '新增請求頭', removeHeader: '移除', noRequestHeaders: '未設定自訂請求頭。', @@ -508,25 +479,6 @@ const zhTwCopy = { slug: '連線標識', name: '顯示名稱', accountIdPlaceholder: '填寫帳號 ID', saving: '儲存中…', save: '儲存供應商', keyRequired: (name: string) => `請填寫 ${name} API Key`, - browserLogin: { - or: '或', - title: '使用 Command Code 帳號登入', - description: '在瀏覽器中完成授權後,API Key 會自動填入上方輸入框。', - action: '在瀏覽器中登入', - waiting: '正在等待瀏覽器中完成授權…', - openAgain: '瀏覽器沒有開啟?點此開啟授權頁', - cancel: '取消', - retry: '重試', - filled: (userName: string) => `已填入 ${userName} 的 API Key。`, - failed: { - denied: '授權已被拒絕。可以改為手動貼上 API Key。', - timeout: '等待授權逾時。可以重試,或手動貼上 API Key。', - superseded: '這次登入已被新的嘗試取代。', - port_unavailable: '本機 5959–5968 連接埠都被佔用,請手動貼上 API Key。', - browser_unavailable: '無法開啟瀏覽器,請手動貼上 API Key。', - unavailable: '瀏覽器登入暫不可用,請手動貼上 API Key。', - }, - }, apiKeyLabel: 'API Key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: '服務地址', defaultModel: '預設模型', defaultModelPlaceholder: '留空即可,儲存後自動拉取', defaultModelHelp: '儲存後 Maka 會向該端點拉取模型目錄。只有當端點不提供目錄時,才需要在這裡手填一個模型 ID。', stepsAria: '新增連線步驟', stepCredentials: '金鑰', stepModels: '選擇模型', @@ -720,11 +672,6 @@ const enCopy: ProviderSettingsCopy = { add: { slugIssues: { required: 'Enter a connection identifier', format: 'Connection identifiers use lowercase letters, digits, and hyphens', too_long: 'Connection identifiers are at most 64 characters' }, duplicateSlug: 'Connection identifier already exists', cloudflareAccount: 'Enter the Cloudflare Account ID', endpointRequired: 'This provider requires a service URL', accountLogin: 'Complete sign-in under account connections. A model connection is created automatically afterward.', - transportNoticeTitle: 'This provider uses the official CLI\'s private transport', - transportNoticeDetail: - 'Requests go to the endpoint that CLI uses and carry its identity headers rather than Maka\'s. The endpoint is not part of the published Provider API.', - transportAcknowledgeLabel: 'I understand and accept this for my install', - transportAcknowledgeRequired: 'Tick the acknowledgement above before adding.', apiKeyPlaceholder: 'Enter or paste API key', cancel: 'Cancel', accountTitle: 'Sign in with an account connection', advancedRequest: 'Advanced request settings', expandAdvancedRequest: 'Show advanced request settings', collapseAdvancedRequest: 'Hide advanced request settings', requestHeaders: 'Custom request headers', headerName: 'Header name', headerValue: 'Header value', retainedHeaderValue: 'Keep saved value', addHeader: 'Add header', removeHeader: 'Remove', noRequestHeaders: 'No custom request headers.', @@ -733,25 +680,6 @@ const enCopy: ProviderSettingsCopy = { slug: 'Connection identifier', name: 'Display name', accountIdPlaceholder: 'Enter account ID', saving: 'Saving…', save: 'Save provider', keyRequired: (name: string) => `Enter the ${name} API key`, - browserLogin: { - or: 'or', - title: 'Sign in with your Command Code account', - description: 'Approve in the browser and the API key is filled in above.', - action: 'Sign in in the browser', - waiting: 'Waiting for you to approve in the browser…', - openAgain: 'Browser did not open? Open the approval page', - cancel: 'Cancel', - retry: 'Retry', - filled: (userName: string) => `Filled in an API key for ${userName}.`, - failed: { - denied: 'Authorization was denied. You can paste an API key instead.', - timeout: 'Timed out waiting for approval. Retry, or paste an API key.', - superseded: 'This sign-in was replaced by a newer attempt.', - port_unavailable: 'Ports 5959–5968 are all in use on this machine. Paste an API key instead.', - browser_unavailable: 'The browser could not be opened. Paste an API key instead.', - unavailable: 'Browser sign-in is unavailable right now. Paste an API key instead.', - }, - }, apiKeyLabel: 'API key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: 'Service URL', defaultModel: 'Default model', defaultModelPlaceholder: 'Leave empty — fetched after saving', defaultModelHelp: 'Maka fetches the model catalog from this endpoint after saving. Type a model id here only if the endpoint serves no catalog.', stepsAria: 'Steps to add the connection', stepCredentials: 'Key', stepModels: 'Choose models', diff --git a/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts b/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts index ff32c61afd..a1c410253f 100644 --- a/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts +++ b/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts @@ -28,7 +28,7 @@ import type { type DesktopConnectionSettingsBridge = Pick< MakaBridge, - 'connections' | 'openAiCodex' | 'xaiOAuth' | 'githubCopilotSubscription' | 'commandCodeLogin' + 'connections' | 'openAiCodex' | 'xaiOAuth' | 'githubCopilotSubscription' >; type DesktopOAuthProviderBridge = | MakaBridge['openAiCodex'] @@ -60,11 +60,6 @@ export function createDesktopConnectionSettingsServices( bridge().githubCopilotSubscription.connectExistingLogin(host), }, }, - commandCodeBrowserLogin: { - start: (input) => bridge().commandCodeLogin.start(input), - complete: (attemptId) => bridge().commandCodeLogin.complete(attemptId), - cancel: (attemptId) => bridge().commandCodeLogin.cancel(attemptId), - }, getSnapshot: () => bridge().connections.getSnapshot(undefined, host), setDefault: (connection) => bridge().connections.setDefault(connection, host), setDefaultModel: (input) => bridge().connections.setDefaultModel(input, host), @@ -74,7 +69,6 @@ export function createDesktopConnectionSettingsServices( test: (connection, options) => bridge().connections.test(connection, options, host), fetchModels: (connection) => bridge().connections.fetchModels(connection, host), hasSecret: (connection) => bridge().connections.hasSecret(connection, host), - usage: (connection) => bridge().connections.usage(connection, host), getRequestHeaders: (connection) => bridge().connections.getRequestHeaders(connection, host), setRequestHeaders: (connection, headers) => bridge().connections.setRequestHeaders(connection, headers, host), diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx index dfbfc556ae..90ff855c7f 100644 --- a/apps/desktop/src/renderer/settings/provider-add-form.tsx +++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx @@ -50,7 +50,6 @@ import { PasswordInput } from './password-input'; import { providerDisplay } from './provider-display'; import { useActionGuard } from './use-action-guard'; import { - CommandCodeBrowserLoginSection, OnboardingStepForm, getProviderSettingsCopy, providerPanelActionErrorMessage, @@ -70,7 +69,6 @@ import { initialOnboardingModelIds, shouldShowManagedOnboardingOutcomeUnknown, stableOnboardingModels, - providerRequiresAcknowledgement, validateAddProviderDraft, type AddProviderIssue, } from './provider-add-submission'; @@ -129,19 +127,14 @@ export function AddProviderForm(props: { const [requestHeaders, setRequestHeaders] = useState([]); const [requestBodyText, setRequestBodyText] = useState(''); const [advancedOpen, setAdvancedOpen] = useState(false); - // The acknowledgement rides the existing form state rather than a state of - // its own: this file sits in the frozen renderer zone, where a new hook call - // is debt the architecture gate refuses. const [formState, setFormState] = useState<{ readonly managedPhase: ManagedOnboardingPhase; readonly error: ProviderFormError | null; - readonly acknowledged: boolean; }>(() => ({ managedPhase: { kind: 'input' }, error: null, - acknowledged: false, })); - const { managedPhase, error, acknowledged } = formState; + const { managedPhase, error } = formState; const [busy, setBusy] = useState(false); const submitGuard = useActionGuard<'submit'>(); const addProviderMountedRef = useMountedRef(); @@ -150,30 +143,9 @@ export function AddProviderForm(props: { const requiresBaseUrl = !defaults.baseUrl && !isCloudflareWorkersAi; const showsDefaultModel = recommendedDefaultModel.trim() === ''; const isExperimental = defaults.status === 'phase3-experimental'; - const needsAcknowledgement = providerRequiresAcknowledgement(props.providerType); const supportsApiKey = providerAuthSupportsApiKey(props.providerType); const requiresApiKey = providerAuthRequiresSecret(props.providerType) && supportsApiKey; const usesApiKeyDialog = usesQuickApiKeyDialog(props.providerType); - // Pasting stays the primary path. Command Code GO alone can also mint a key - // through a browser sign-in (the same login the CLI performs); the section - // under the field only fills the field, so everything after — validation, - // creation, discovery — is the paste path. The ordinary Command Code card - // stays a plain API-key form. - const commandCodeBrowserLogin = props.bridge.commandCodeBrowserLogin; - const browserLoginSection = - props.providerType === 'commandcode-go' && commandCodeBrowserLogin ? ( - { - setApiKey(credentials.apiKey); - resetManagedVerification(); - clearFieldError('apiKey'); - }} - /> - ) : undefined; - function setManagedPhase(next: ManagedOnboardingPhase) { setFormState((current) => ({ ...current, managedPhase: next })); } @@ -211,7 +183,6 @@ export function AddProviderForm(props: { if (issue.field === 'apiKey') return copy.keyRequired(display.name); if (issue.field === 'accountId') return copy.cloudflareAccount; if (issue.field === 'baseUrl') return copy.endpointRequired; - if (issue.reason === 'acknowledgement') return copy.transportAcknowledgeRequired; return copy.accountLogin; } @@ -349,13 +320,6 @@ export function AddProviderForm(props: { async function submit() { if (submitGuard.current !== null) return; setError(null); - // Ahead of every route this form takes. The managed-onboarding branch - // below returns before `validateAddProviderDraft` runs, and the quick - // API-key dialog reaches this same function, so a check placed only in - // the draft rule would never fire for the provider that has one. - if (needsAcknowledgement && !acknowledged) { - return setError({ field: 'form', message: copy.transportAcknowledgeRequired }); - } const normalizedApiKey = apiKey.trim(); const normalizedCloudflareAccountId = cloudflareAccountId.trim(); const normalizedDefaultModel = defaultModel.trim(); @@ -391,7 +355,6 @@ export function AddProviderForm(props: { apiKey, cloudflareAccountId, baseUrl, - acknowledged, }); if (issue) return setError({ field: issue.field, message: issueMessage(issue) }); submitGuard.begin('submit'); @@ -436,35 +399,6 @@ export function AddProviderForm(props: { void submit(); } - // Rendered by every route this form can take. The quick API-key dialog - // returns its own subtree, so a notice placed only in the full form would - // never reach the provider that states one. - const transportAcknowledgement = needsAcknowledgement ? ( - - - { - const ticked = next.includes('acknowledged'); - setFormState((current) => ({ - ...current, - acknowledged: ticked, - error: current.error?.field === 'form' ? null : current.error, - })); - }} - isDisabled={busy} - density="compact" - > - - - - ) : null; - const advancedRequestEditor = ( {managedStepper} - {transportAcknowledgement} {advancedRequestEditor} - {browserLoginSection}
{busy ? ( @@ -749,7 +681,6 @@ export function AddProviderForm(props: { title={copy.accountTitle} description={copy.accountDetail} /> )} - {transportAcknowledgement} {supportsApiKey && ( = new Set([ - 'commandcode-go', -]); - -export function providerRequiresAcknowledgement(providerType: ProviderType): boolean { - return PROVIDERS_REQUIRING_ACKNOWLEDGEMENT.has(providerType); } /** @@ -171,9 +151,6 @@ export function validateAddProviderDraft(draft: AddProviderDraft): AddProviderIs // at all, so telling the user to answer a question that would not unblock // them would be the wrong of the two answers. if (defaults.status === 'phase3-experimental') return { field: 'form', reason: 'experimental' }; - if (providerRequiresAcknowledgement(draft.providerType) && draft.acknowledged !== true) { - return { field: 'form', reason: 'acknowledgement' }; - } return null; } diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx index f79878cf8e..37f4c8f4b2 100644 --- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx +++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx @@ -48,7 +48,7 @@ import { PasswordInput } from './password-input'; import { SettingsExpandableRow } from './settings-expandable-row'; import { SettingsActions, SettingsRow, SettingsSection } from './settings-section'; import { providerDisplay } from './provider-display'; -import { CapabilityEditor, AddModelDialog, ConnectionUsageSection, ModelParametersDialog } from '../features/connection-settings'; +import { CapabilityEditor, AddModelDialog, ModelParametersDialog } from '../features/connection-settings'; import { RuntimeHostSettingsGenerationBoundary, useRuntimeHostSettingsErrorReporter, @@ -604,19 +604,6 @@ function ConnectionDetailInner(props: ConnectionDetailProps) { /> )} - {/* Read-only account usage, for the providers that report it. Sits after - the credentials (which is what a usage read needs) and before the - model list. */} - {!retired && props.bridge.usage ? ( - - - - ) : null} {/* Everything below writes to the connection, and a retired one accepts no writes: the catalog refuses a model or request-body change, and the credential vault refuses a request header. Rendering the editors would diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index f7e6b1fc74..3f44d175dc 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -304,10 +304,13 @@ export const PROVIDER_DISPLAY_COPY = { 'zh-TW': { name: 'Command Code', description: '使用 Command Code 方案額度,連線後自動取得模型。', badge: 'Coding' }, en: { name: 'Command Code', description: 'Use your Command Code plan credits. Models are fetched when you connect.', badge: 'Coding' }, }, + // Retired: its transport reached a private endpoint under the official CLI's + // identity. Kept so an existing connection stays identifiable and readable; + // the connection card states that nothing can send through it any more. 'commandcode-go': { - 'zh-CN': { name: 'Command Code GO', description: 'GO 套餐专用:走官方 CLI 的私有通道发送请求。连接后自动获取模型。', badge: 'Coding' }, - 'zh-TW': { name: 'Command Code GO', description: 'GO 方案專用:走官方 CLI 的私有通道傳送請求。連線後自動取得模型。', badge: 'Coding' }, - en: { name: 'Command Code GO', description: 'For the GO plan: sends through the official CLI\'s private transport. Models are fetched when you connect.', badge: 'Coding' }, + 'zh-CN': { name: 'Command Code GO', description: '已停用:该套餐通过官方 CLI 的私有通道访问,已不再支持。', badge: 'Coding' }, + 'zh-TW': { name: 'Command Code GO', description: '已停用:該方案透過官方 CLI 的私有通道存取,已不再支援。', badge: 'Coding' }, + en: { name: 'Command Code GO', description: 'Retired: this plan reached a private endpoint under the official CLI\u2019s identity.', badge: 'Coding' }, }, groq: { 'zh-CN': { name: 'Groq', description: 'LPU 高速推理托管开源模型', badge: 'API' }, diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index ef64cbdf30..bc64fc2e10 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.6.2` (195 component exports). Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 292 files — blocker 0, reimplementation 0, polish 4, aligned 288. +**Totals:** 290 files — blocker 0, reimplementation 0, polish 4, aligned 286. ## Exclusions (explicit) @@ -55,8 +55,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/app-update/ui/app-update-provider.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/client-plugins/client-plugin-root.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/client-plugins/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | -| `apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx` | shell-chrome-or-panel | Banner, Button, Divider, HStack, Link, Text, VStack | aligned — uses Astryx (Banner, Button, Divider, HStack, Link, Text, VStack) | aligned | -| `apps/desktop/src/renderer/features/connection-settings/connection-usage-card.tsx` | other | Button, HStack, Text, VStack | aligned — uses Astryx (Button, HStack, Text, VStack) | aligned | | `apps/desktop/src/renderer/features/connection-settings/generic-provider-mark.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/connection-settings/onboarding-step-form.tsx` | dialog-overlay | VStack | aligned — uses Astryx (VStack) | aligned | | `apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput) | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index fdf83719a3..83a5cd90a7 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -26,8 +26,6 @@ apps/desktop/src/renderer/features/app-update/services-context.tsx apps/desktop/src/renderer/features/app-update/ui/app-update-provider.tsx apps/desktop/src/renderer/features/client-plugins/client-plugin-root.tsx apps/desktop/src/renderer/features/client-plugins/services-context.tsx -apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx -apps/desktop/src/renderer/features/connection-settings/connection-usage-card.tsx apps/desktop/src/renderer/features/connection-settings/generic-provider-mark.tsx apps/desktop/src/renderer/features/connection-settings/onboarding-step-form.tsx apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx diff --git a/packages/core/src/__tests__/model-metadata.test.ts b/packages/core/src/__tests__/model-metadata.test.ts index 7c7641dede..ec0e403128 100644 --- a/packages/core/src/__tests__/model-metadata.test.ts +++ b/packages/core/src/__tests__/model-metadata.test.ts @@ -285,7 +285,7 @@ describe('Volcengine Agent Plan official catalog mirror', () => { }); describe('Command Code static reasoning metadata', () => { - const commandCodeProviders = ['commandcode', 'commandcode-go'] as const; + const commandCodeProviders = ['commandcode'] as const; // The reference table this is ported from (dsh-commandcode-provider's // KNOWN_EFFORTS, re-verified against command-code@1.53.0). const expectedEfforts: Record = { @@ -306,7 +306,7 @@ describe('Command Code static reasoning metadata', () => { 'zai-org/GLM-5.2': ['high', 'max'], }; - it('serves the same table to both Command Code providers', () => { + it('serves the effort table to the Command Code provider', () => { for (const providerType of commandCodeProviders) { for (const [modelId, efforts] of Object.entries(expectedEfforts)) { assert.deepEqual( @@ -329,9 +329,6 @@ describe('Command Code static reasoning metadata', () => { }); it('leaves a model without a declared level uncovered', () => { - assert.equal( - lookupModelMetadata('commandcode-go', 'tencent/hy3-paid').thinkingOptions, - undefined, - ); + assert.equal(lookupModelMetadata('commandcode', 'tencent/hy3-paid').thinkingOptions, undefined); }); }); diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index a8ef42552f..88f9a2dc90 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -228,7 +228,7 @@ describe('retired provider contract', () => { ); it('pins the entries this catalog retires', () => { - assert.deepEqual(retired, ['opencode-free', 'claude-subscription']); + assert.deepEqual(retired, ['opencode-free', 'commandcode-go', 'claude-subscription']); }); it('keeps a retired provider registered but unwired', () => { diff --git a/packages/core/src/connection-usage.ts b/packages/core/src/connection-usage.ts deleted file mode 100644 index 1179cf4fcf..0000000000 --- a/packages/core/src/connection-usage.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * A provider's account usage, in a shape the settings card can render without - * knowing which provider produced it. - * - * Providers differ in what they report — one exposes a five-hour window and a - * weekly one, another only a monthly balance, a third nothing at all. Rather - * than teach the card each dialect, the provider reports a LIST of windows and - * optional stats, and every field is optional: what was not reported is absent, - * which the card renders as "no such row", never as a zero placeholder. A - * reported `0` is a fact (nothing consumed); an absent field is the provider not - * speaking, and the two must not look alike. - */ - -export type UsageUnit = 'credits' | 'usd' | 'tokens'; - -/** - * One quota window — one progress bar. `fiveHour` / `weekly` / `monthly` are all - * this: a label, an amount consumed against a cap, and a reset. The card keys - * its localized labels off `id` and falls back to `label` for a window it does - * not recognize, so a provider may add windows without a card change. - */ -export interface UsageWindow { - /** Stable identity: `fiveHour` | `weekly` | `monthly` | provider-specific. */ - readonly id: string; - /** Provider-supplied label, used when the card has no copy for `id`. */ - readonly label?: string; - readonly used: number; - readonly cap: number; - readonly unit: UsageUnit; - /** Epoch millis the window resets; absent when the provider did not say. */ - readonly resetsAt?: number; - /** - * The window exists but has no cap (spend is open). Distinct from a cap of - * `0`, which is "no allowance left" — the same distinction the provider - * itself draws between an absent window and an uncapped one. - */ - readonly unlimited?: boolean; -} - -/** Aggregate counters. Each is optional: providers report different subsets. */ -export interface UsageStats { - readonly requests?: number; - readonly failed?: number; - /** Percentage in `0..100`, as the provider reports it. */ - readonly successRate?: number; - readonly cost?: number; - readonly tokensIn?: number; - readonly tokensOut?: number; -} - -export interface ConnectionUsageReport { - readonly accountLabel?: string; - readonly planLabel?: string; - readonly stats?: UsageStats; - /** In display order. Empty when the provider reported no windows. */ - readonly windows: readonly UsageWindow[]; - /** Billing period end, when the provider reports one. */ - readonly periodEnd?: number; - /** - * True when SOME endpoints refused the credential (401/403) while others - * answered — a key that is valid but lacks a scope, so part of the account's - * data is missing rather than the whole read failing. The card shows what it - * has and says the rest was refused, instead of silently presenting a partial - * report as if it were complete. - */ - readonly partiallyUnauthorized?: boolean; - readonly fetchedAt: number; -} - -/** - * The outcome of a usage read. `unavailable` is not an error the caller - * surfaces loudly — a credential the provider has not accepted, a fetch that - * never landed, or a provider with no usage endpoint at all — it is the card - * saying it has nothing to show. - */ -export type ConnectionUsageResult = - | { readonly kind: 'report'; readonly report: ConnectionUsageReport } - | { - readonly kind: 'unavailable'; - /** - * Why the read produced nothing. `unauthorized` is deliberately distinct - * from `network`: an expired or under-scoped key and an unreachable host - * are different problems with different fixes, and the settings page that - * shows this is the page where the credential is repaired. Collapsing them - * would tell a user with a dead key to check their connection. - */ - readonly reason: 'no-credential' | 'unauthorized' | 'unsupported' | 'network'; - }; - -import type { ProviderType } from './llm-connections.js'; - -/** - * The providers Maka can read account usage from. The single source of truth - * for both the settings section's visibility and the runtime dispatch, so a - * provider that gains a mapper is offered the card in the same change. - */ -const USAGE_PROVIDERS: ReadonlySet = new Set(['commandcode-go']); - -export function providerReportsUsage(providerType: ProviderType): boolean { - return USAGE_PROVIDERS.has(providerType); -} diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index aa13094d64..3a66da7549 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -489,9 +489,8 @@ function buildStaticModelMetadata(active: ModelsDevMetadata): ModelsDevMetadata return { anthropic: ANTHROPIC_MODEL_OVERRIDES, 'claude-subscription': claudeSubscriptionModelMetadata(active), - // Both Command Code providers ride the same Provider-API effort table. + // The Command Code Provider-API plan rides the same effort table. commandcode: COMMAND_CODE_MODEL_METADATA, - 'commandcode-go': COMMAND_CODE_MODEL_METADATA, 'alibaba-token-plan-cn': { 'qwen3.8-max': { thinkingOptions: { efforts: ['none', 'low', 'medium', 'xhigh'], toggle: true }, diff --git a/packages/core/src/provider-auth.ts b/packages/core/src/provider-auth.ts index 9bbd4ac10c..dd8825494e 100644 --- a/packages/core/src/provider-auth.ts +++ b/packages/core/src/provider-auth.ts @@ -29,12 +29,7 @@ import { * storage coordinator actually gates — an operation with no admission point * does not belong here, however natural it sounds beside these three. */ -export const PROVIDER_AUTH_ACTIONS = [ - 'test_credentials', - 'fetch_models', - 'read_usage', - 'start_oauth', -] as const; +export const PROVIDER_AUTH_ACTIONS = ['test_credentials', 'fetch_models', 'start_oauth'] as const; export type ProviderAuthAction = (typeof PROVIDER_AUTH_ACTIONS)[number]; export interface ProviderAuthContract { @@ -100,7 +95,6 @@ export function deriveProviderAuthContract(input: { fetch_models: canFetchModels && (reachableWithoutSecret || hasSecret), // Reading account usage needs the same reachability as a connection test: // a credential the provider accepts over HTTP. - read_usage: reachableWithoutSecret || hasSecret, }), }; } diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index d3a6d3005f..bfeff09665 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -88,8 +88,6 @@ type ProviderRuntimeAdapterDefinition = | { kind: 'openai-codex'; responses: ProviderResponsesContract } | { kind: 'google'; normalizeBaseUrl?: boolean } | { kind: 'cohere' } - /** The Command Code CLI's `/alpha/generate` wire, used by the GO plan. */ - | { kind: 'commandcode-cli' } | OpenAiCompatibleRuntimeAdapter; export type ProviderRuntimeAdapter = ProviderRuntimeAdapterDefinition & { @@ -1491,20 +1489,26 @@ const providerRegistry = { signupUrl: 'https://commandcode.ai/docs/plans/goat', catalogOrder: 41.5, }, + // Retired rather than removed: an existing connection must stay identifiable + // and must answer `provider_retired` at readiness. Removing the entry would + // leave it *unknown*, which `isConnectionReady` does not reject, so a send + // would be admitted and only fail deep in model construction. Its transport + // presented the official CLI's identity to a private endpoint, which is why + // nothing can send through it any more. 'commandcode-go': { label: 'Command Code GO', - // The API root, not `/provider/v1`: generation posts to `/alpha/generate` - // and discovery reads `/provider/v1/models`, both under it. baseUrl: 'https://api.commandcode.ai', authKind: 'api_key', fallbackModels: [], - status: 'ready', - runtimeAdapter: { kind: 'commandcode-cli' }, - modelDiscovery: { kind: 'protocol', path: 'provider/v1/models' }, + status: 'phase3-experimental', + runtimeAdapter: { kind: 'unavailable' }, + retired: true, + modelDiscovery: { + kind: 'fallback', + reason: 'The GO plan was reached through the official CLI\u2019s private transport.', + }, category: 'overseas', catalogGroup: 'plans', - signupUrl: 'https://commandcode.ai/docs/plans/go', - catalogOrder: 41.6, }, 'cloudflare-workers-ai': { label: cloudflareWorkersAi.name, diff --git a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts index 99acf7b37f..8ebde4f6f8 100644 --- a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts +++ b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts @@ -148,6 +148,19 @@ test('the retired Regenerate grant is dropped from released credentials', async assert.deepEqual(unresolvedPersistedGrants(file), []); }); +test('the retired Command Code GO usage grant is dropped from released credentials', async () => { + const path = await writeAccessFile({ + schemaVersion: 3, + credentials: [storedCredential(['host.status', 'connection.usage.read'])], + sessionGrants: [], + turnAccessRequests: [], + }); + + const file = await readAccessCredentialFile(path); + assert.deepEqual(file.credentials[0]?.grants, ['host.status']); + assert.deepEqual(unresolvedPersistedGrants(file), []); +}); + test('retired WorkHub grants are released without granting active-turn authority', async () => { const original = storedCredential([ 'host.status', diff --git a/packages/runtime-host/src/__tests__/connection-effects-protocol.test.ts b/packages/runtime-host/src/__tests__/connection-effects-protocol.test.ts index e89b633ecd..dd17dec549 100644 --- a/packages/runtime-host/src/__tests__/connection-effects-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/connection-effects-protocol.test.ts @@ -287,135 +287,6 @@ describe('Runtime Host connection effects protocol', () => { test: { ...failedResult.test, statusCode: 600 }, }); }); - test('keeps one exact and bounded connection usage report', () => { - const report = response('connection.usage.read', { - kind: 'report', - report: { - accountLabel: 'joob1nhk13d9', - planLabel: 'Go', - stats: { - requests: 3552, - failed: 5, - successRate: 99.86, - cost: 5.85, - tokensIn: 428_987_308, - tokensOut: 2_971_687, - }, - windows: [ - { - id: 'fiveHour', - label: null, - used: 0.25, - cap: 3, - unit: 'credits', - resetsAt: 1_789_746_210_944, - unlimited: null, - }, - { - id: 'weekly', - label: 'Weekly', - used: 0.38, - cap: 6, - unit: 'credits', - resetsAt: null, - unlimited: true, - }, - ], - periodEnd: 1_790_310_000_000, - partiallyUnauthorized: false, - fetchedAt: 1_789_000_000_000, - }, - }); - assert.deepEqual(decodeHostFrame(report), report); - - // Read-only: the only non-report shapes are an explicit unavailability and - // a rejection. A committed/superseded shape has no meaning here. - const unavailable = response('connection.usage.read', { - kind: 'unavailable', - reason: 'unsupported', - }); - assert.deepEqual(decodeHostFrame(unavailable), unavailable); - const rejected = response('connection.usage.read', { - kind: 'rejected', - reason: 'credential_not_configured', - }); - assert.deepEqual(decodeHostFrame(rejected), rejected); - - assertInvalidResponse('connection.usage.read', { - kind: 'report', - report: { - accountLabel: null, - planLabel: null, - stats: null, - windows: [ - { - id: 'w', - label: null, - used: -1, - cap: 1, - unit: 'credits', - resetsAt: null, - unlimited: null, - }, - ], - periodEnd: null, - fetchedAt: 1, - }, - }); - assertInvalidResponse('connection.usage.read', { - kind: 'unavailable', - reason: 'made-up', - }); - // `unauthorized` is an accepted reason: a rejected credential is reported as - // itself, not folded into `network`. - const unauthorized = response('connection.usage.read', { - kind: 'unavailable', - reason: 'unauthorized', - }); - assert.deepEqual(decodeHostFrame(unauthorized), unauthorized); - // The window array is capped like every other array in this package. - assertInvalidResponse('connection.usage.read', { - kind: 'report', - report: { - accountLabel: null, - planLabel: null, - stats: null, - windows: Array.from({ length: 32 }, (_unused, index) => ({ - id: `w-${index}`, - label: null, - used: 1, - cap: 2, - unit: 'credits', - resetsAt: null, - unlimited: null, - })), - periodEnd: null, - partiallyUnauthorized: false, - fetchedAt: 1, - }, - }); - assertInvalidResponse('connection.usage.read', { - kind: 'report', - report: { - accountLabel: null, - planLabel: null, - stats: null, - windows: [ - { - id: 'w', - label: null, - used: 1, - cap: 1, - unit: 'bananas', - resetsAt: null, - unlimited: null, - }, - ], - periodEnd: null, - fetchedAt: 1, - }, - }); - }); }); function request(operation: string, input: unknown) { diff --git a/packages/runtime-host/src/protocol/connection-effects.ts b/packages/runtime-host/src/protocol/connection-effects.ts index 9c69ae9855..dfcfdd7dd1 100644 --- a/packages/runtime-host/src/protocol/connection-effects.ts +++ b/packages/runtime-host/src/protocol/connection-effects.ts @@ -54,14 +54,6 @@ const EFFECT_ERRORS = [ 'commit_outcome_unknown', ] as const; -/** - * Upper bound on the quota windows one usage report may carry. The producer - * emits three today (5-hour, weekly, monthly); the cap exists because a decoder - * is the boundary that does not trust its producer, matching how every other - * array in this package is bounded before mapping. - */ -const CONNECTION_USAGE_MAX_WINDOWS = 16; - export const CONNECTION_EFFECT_CHANGED_DOMAINS = [ 'connection', 'credential', @@ -217,56 +209,6 @@ export type ConnectionTestRunResult = | ConnectionEffectRejected | ConnectionEffectSuperseded; -export interface ConnectionUsageReadInput { - readonly connectionId: string; -} - -/** One quota window, e.g. the 5-hour, weekly or monthly allowance. */ -export interface ConnectionUsageWindow { - readonly id: string; - readonly label: string | null; - readonly used: number; - readonly cap: number; - readonly unit: 'credits' | 'usd' | 'tokens'; - /** Epoch millis the window resets, or null when the provider did not say. */ - readonly resetsAt: number | null; - readonly unlimited: boolean | null; -} - -/** The aggregate counters a provider reports, each null when unreported. */ -export interface ConnectionUsageStats { - readonly requests: number | null; - readonly failed: number | null; - readonly successRate: number | null; - readonly cost: number | null; - readonly tokensIn: number | null; - readonly tokensOut: number | null; -} - -export interface ConnectionUsageReportProjection { - readonly accountLabel: string | null; - readonly planLabel: string | null; - readonly stats: ConnectionUsageStats | null; - readonly windows: readonly ConnectionUsageWindow[]; - readonly periodEnd: number | null; - /** Some endpoints refused the credential while others answered. */ - readonly partiallyUnauthorized: boolean; - readonly fetchedAt: number; -} - -/** - * Read-only: unlike the fetch/test effects there is no committed state. A host - * that could not reach the provider says `unavailable` with a reason; a host - * that reached it but found no credential says `rejected`. - */ -export type ConnectionUsageReadResult = - | { readonly kind: 'report'; readonly report: ConnectionUsageReportProjection } - | { - readonly kind: 'unavailable'; - readonly reason: 'unsupported' | 'network' | 'no-credential' | 'unauthorized'; - } - | ConnectionEffectRejected; - export const CONNECTION_EFFECT_OPERATION_SPECS = { 'connection.onboarding.save': defineOperation< ConnectionOnboardingSaveInput, @@ -312,17 +254,6 @@ export const CONNECTION_EFFECT_OPERATION_SPECS = { decodeInput: decodeConnectionTestRunInput, decodeOutput: decodeConnectionTestRunResult, }), - 'connection.usage.read': defineOperation< - ConnectionUsageReadInput, - ConnectionUsageReadResult, - (typeof EFFECT_ERRORS)[number] - >({ - mode: 'query', - availability: 'ready', - errors: EFFECT_ERRORS, - decodeInput: decodeConnectionUsageReadInput, - decodeOutput: decodeConnectionUsageReadResult, - }), } as const; export function decodeConnectionOnboardingSaveInput(value: unknown): ConnectionOnboardingSaveInput { @@ -571,107 +502,6 @@ export function decodeConnectionTestRunResult(value: unknown): ConnectionTestRun return decodeNonEffectResult(result, 'connection test result'); } -export function decodeConnectionUsageReadInput(value: unknown): ConnectionUsageReadInput { - const input = requireExactRecord(value, 'connection usage input', ['connectionId']); - return { connectionId: requireEntityId(input.connectionId, 'connectionId') }; -} - -export function decodeConnectionUsageReadResult(value: unknown): ConnectionUsageReadResult { - const result = requireRecord(value, 'connection usage result'); - if (result.kind === 'report') { - const report = requireExactRecord(result, 'connection usage report', ['kind', 'report']); - return { kind: 'report', report: decodeUsageReport(report.report) }; - } - if (result.kind === 'unavailable') { - const unavailable = requireExactRecord(result, 'connection usage unavailable', [ - 'kind', - 'reason', - ]); - if ( - unavailable.reason !== 'unsupported' && - unavailable.reason !== 'network' && - unavailable.reason !== 'no-credential' && - unavailable.reason !== 'unauthorized' - ) { - throw invalidProtocolFrame('Invalid connection usage unavailable reason'); - } - return { kind: 'unavailable', reason: unavailable.reason }; - } - if (result.kind === 'rejected') { - const rejected = requireExactRecord(result, 'connection usage result', ['kind', 'reason']); - return { kind: 'rejected', reason: rejectionReason(rejected.reason) }; - } - throw invalidProtocolFrame('Invalid connection usage result'); -} - -function decodeUsageReport(value: unknown): ConnectionUsageReportProjection { - const report = requireExactRecord(value, 'connection usage report body', [ - 'accountLabel', - 'planLabel', - 'stats', - 'windows', - 'periodEnd', - 'partiallyUnauthorized', - 'fetchedAt', - ]); - if (!Array.isArray(report.windows) || report.windows.length > CONNECTION_USAGE_MAX_WINDOWS) { - throw invalidProtocolFrame('Invalid connection usage windows'); - } - return { - accountLabel: optionalText(report.accountLabel, 'account label'), - planLabel: optionalText(report.planLabel, 'plan label'), - stats: report.stats === null ? null : decodeUsageStats(report.stats), - windows: report.windows.map(decodeUsageWindow), - periodEnd: optionalMillis(report.periodEnd, 'period end'), - partiallyUnauthorized: requireBoolean(report.partiallyUnauthorized, 'partially unauthorized'), - fetchedAt: requireCount(report.fetchedAt, 'usage fetchedAt'), - }; -} - -function decodeUsageStats(value: unknown): ConnectionUsageStats { - const stats = requireExactRecord(value, 'connection usage stats', [ - 'requests', - 'failed', - 'successRate', - 'cost', - 'tokensIn', - 'tokensOut', - ]); - return { - requests: optionalCount(stats.requests, 'requests'), - failed: optionalCount(stats.failed, 'failed'), - successRate: optionalAmount(stats.successRate, 'success rate'), - cost: optionalAmount(stats.cost, 'cost'), - tokensIn: optionalCount(stats.tokensIn, 'tokens in'), - tokensOut: optionalCount(stats.tokensOut, 'tokens out'), - }; -} - -function decodeUsageWindow(value: unknown): ConnectionUsageWindow { - const window = requireExactRecord(value, 'connection usage window', [ - 'id', - 'label', - 'used', - 'cap', - 'unit', - 'resetsAt', - 'unlimited', - ]); - const unit = window.unit; - if (unit !== 'credits' && unit !== 'usd' && unit !== 'tokens') { - throw invalidProtocolFrame('Invalid connection usage unit'); - } - return { - id: requireUtf8String(window.id, 'usage window id', 64), - label: optionalText(window.label, 'usage window label'), - used: requireAmount(window.used, 'usage window used'), - cap: requireAmount(window.cap, 'usage window cap'), - unit, - resetsAt: optionalMillis(window.resetsAt, 'usage window reset'), - unlimited: window.unlimited === null ? null : requireBoolean(window.unlimited, 'unlimited'), - }; -} - function optionalText(value: unknown, label: string): string | null { return value === null ? null : requireUtf8String(value, label, 256); } diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index eeeb2bf852..140db70b4e 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -101,7 +101,10 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 170 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 171 as const; +// 171: Removed the `connection.usage.read` operation along with the Command +// Code GO provider it served. A peer older than this epoch may still advertise +// or submit that operation, which this Host no longer answers. // 169: The message execution query reports an identity the Host can prove was // never admitted as a positive `not_admitted` resolution instead of omitting // it, so silence stops meaning both "not admitted" and "cannot say yet". diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts index f9fc87ff6e..602f702b21 100644 --- a/packages/runtime-host/src/protocol/operations.ts +++ b/packages/runtime-host/src/protocol/operations.ts @@ -268,7 +268,6 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([ 'connection.request-headers.query', 'connection.request-headers.replace', 'connection.test.run', - 'connection.usage.read', 'context.compact', 'context.diagnostics.query', 'credential.vault.delete', diff --git a/packages/runtime-host/src/server/access-credential-store.ts b/packages/runtime-host/src/server/access-credential-store.ts index 177d9e977e..61c16f8bd7 100644 --- a/packages/runtime-host/src/server/access-credential-store.ts +++ b/packages/runtime-host/src/server/access-credential-store.ts @@ -79,6 +79,9 @@ const PERSISTED_GRANT_MIGRATIONS: ReadonlyMap = // Retired with the second execution-inspection contract; no shipped surface // called execution.inspect.resolve. ['execution.inspect.resolve', { kind: 'release' }], + // Retired with the Command Code GO provider, whose private transport the + // usage read required. + ['connection.usage.read', { kind: 'release' }], // Retired in favor of editing and resending the original user message. ['turn.regenerate', { kind: 'release' }], ['deep-research.query', { kind: 'release' }], diff --git a/packages/runtime-host/src/server/connection-effect-coordinator.ts b/packages/runtime-host/src/server/connection-effect-coordinator.ts index 09493cde17..7716f25127 100644 --- a/packages/runtime-host/src/server/connection-effect-coordinator.ts +++ b/packages/runtime-host/src/server/connection-effect-coordinator.ts @@ -29,7 +29,6 @@ import { effectiveBaseUrl, providerFallbackModelIds, } from '@maka/core/llm-connections'; -import type { ConnectionUsageReport } from '@maka/core/connection-usage'; import { createConnectionEffectFetchTransport, type ConnectionEffectFetchTransport, @@ -42,7 +41,6 @@ import { } from '@maka/runtime/subscription-credentials'; import { runConnectionModelDiscoveryEffect } from '@maka/runtime/model-fetcher'; import { runConnectionTestEffect } from '@maka/runtime/test-connection'; -import { fetchConnectionUsage } from '@maka/runtime/connection-usage'; import { type ConnectionEffectErrorKind, type ConnectionModelDiscoveryEffectOutcome, @@ -53,7 +51,6 @@ import { authenticateRuntimePolicyStoresWriter, RuntimePolicyStoreError, type BeginConnectionTestResult, - type BeginConnectionUsageResult, type BeginModelFetchResult, type ConnectionEffectCompletionResult, type ConnectionOnboardingTicket, @@ -71,9 +68,6 @@ import type { ConnectionTestProjection, ConnectionTestRunInput, ConnectionTestRunResult, - ConnectionUsageReadInput, - ConnectionUsageReadResult, - ConnectionUsageReportProjection, OperationOutcome, } from '../protocol/index.js'; import type { ConnectionEffectOperationHandlerMap } from './operation-dispatcher.js'; @@ -114,7 +108,6 @@ export class HostConnectionEffectCoordinator { 'connection.onboarding.verify': (input) => this.#verifyOnboarding(input), 'connection.models.fetch': (input) => this.#fetchModels(input), 'connection.test.run': (input) => this.#testConnection(input), - 'connection.usage.read': (input) => this.#readConnectionUsage(input), }; readonly #stores: RuntimePolicyStoresWriter; @@ -393,40 +386,10 @@ export class HostConnectionEffectCoordinator { }); } - /** - * Read-only: fetch the account's usage and project it. Nothing is committed, - * so no superseded branch exists — the ticket is spent and the report - * returned. - */ - #readConnectionUsage( - input: ConnectionUsageReadInput, - ): Promise> { - return this.#admit(input.connectionId, 'connection.usage.read', async () => { - const prepared = await this.#stores.operations.beginConnectionUsage(input.connectionId); - if (prepared.kind !== 'ready') return preparationResult(prepared); - try { - const result = await this.#withTransport(prepared, (fetch, secret) => - fetchConnectionUsage({ - providerType: prepared.connection.providerType, - apiKey: secret, - baseUrl: effectiveBaseUrl(prepared.connection), - fetch, - }), - ); - return result.kind === 'report' - ? { kind: 'report', report: projectUsageReport(result.report) } - : { kind: 'unavailable', reason: result.reason }; - } finally { - await this.#stores.operations.completeConnectionUsage(prepared.ticket); - } - }); - } - #admit< K extends | 'connection.models.fetch' | 'connection.test.run' - | 'connection.usage.read' | 'connection.onboarding.verify' | 'connection.onboarding.save', >( @@ -560,13 +523,7 @@ function preparationResult( prepared: Exclude, ): Extract; function preparationResult( - prepared: Exclude, -): Extract; -function preparationResult( - prepared: Exclude< - BeginModelFetchResult | BeginConnectionTestResult | BeginConnectionUsageResult, - { readonly kind: 'ready' } - >, + prepared: Exclude, ): Extract { return { kind: 'rejected', @@ -583,35 +540,6 @@ function projectSuperseded( }; } -function projectUsageReport(report: ConnectionUsageReport): ConnectionUsageReportProjection { - return { - accountLabel: report.accountLabel ?? null, - planLabel: report.planLabel ?? null, - stats: report.stats - ? { - requests: report.stats.requests ?? null, - failed: report.stats.failed ?? null, - successRate: report.stats.successRate ?? null, - cost: report.stats.cost ?? null, - tokensIn: report.stats.tokensIn ?? null, - tokensOut: report.stats.tokensOut ?? null, - } - : null, - windows: report.windows.map((window) => ({ - id: window.id, - label: window.label ?? null, - used: window.used, - cap: window.cap, - unit: window.unit, - resetsAt: window.resetsAt ?? null, - unlimited: window.unlimited ?? null, - })), - periodEnd: report.periodEnd ?? null, - partiallyUnauthorized: report.partiallyUnauthorized === true, - fetchedAt: report.fetchedAt, - }; -} - function projectConnectionTest( outcome: ConnectionTestEffectOutcome, checkedAt: number, @@ -663,7 +591,6 @@ function storeFailure< K extends | 'connection.models.fetch' | 'connection.test.run' - | 'connection.usage.read' | 'connection.onboarding.verify' | 'connection.onboarding.save', >(error: unknown): OperationOutcome { @@ -690,7 +617,6 @@ function operationFailure< K extends | 'connection.models.fetch' | 'connection.test.run' - | 'connection.usage.read' | 'connection.onboarding.verify' | 'connection.onboarding.save', >( diff --git a/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts b/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts deleted file mode 100644 index be6b32eb96..0000000000 --- a/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import type { ServerResponse } from 'node:http'; -import { after, afterEach, beforeEach, describe, test } from 'node:test'; -import { - APICallError, - type LanguageModelV4CallOptions, - type LanguageModelV4StreamPart, -} from '@ai-sdk/provider'; -import type { LlmConnection } from '@maka/core/llm-connections'; -import { - buildCommandCodeCliRequest, - COMMANDCODE_CLI_VERSION, - CommandCodeCliLanguageModel, - commandCodeCliHeaders, - mapFinishReason, - projectSlugFromPath, - toolParametersSchema, - wireToolCallIds, -} from '../commandcode-cli-language-model.js'; -import { getAIModel } from '../model-factory.js'; -import { resolveModelRuntime } from '../model-runtime.js'; -import { classifyError } from '../provider-error-classification.js'; -import { testConnection } from '../test-connection.js'; -import { - closeAllJsonServers, - readBody, - respondJson, - startJsonServer, -} from './conformance-harness.js'; - -after(closeAllJsonServers); - -function respondCliStream(response: ServerResponse, events: readonly unknown[]): void { - response.writeHead(200, { 'content-type': 'text/event-stream' }); - for (const event of events) response.write(`data: ${JSON.stringify(event)}\n\n`); - response.end('data: [DONE]\n\n'); -} - -async function collect(stream: ReadableStream) { - const parts: LanguageModelV4StreamPart[] = []; - const reader = stream.getReader(); - for (;;) { - const { done, value } = await reader.read(); - if (done) return parts; - parts.push(value); - } -} - -const USER_HI: LanguageModelV4CallOptions = { - prompt: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], -}; - -describe('request building', () => { - test('sends the official CLI identity headers and derives the project slug', () => { - const headers = commandCodeCliHeaders('user_k', '/Users/me/My Repo'); - assert.equal(headers.authorization, 'Bearer user_k'); - assert.equal(headers['x-command-code-version'], COMMANDCODE_CLI_VERSION); - assert.equal(headers['x-cli-environment'], 'production'); - assert.equal(headers['x-project-slug'], 'users-me-my-repo'); - assert.equal(projectSlugFromPath('C:\\Work\\proj'), 'work-proj'); - assert.equal(projectSlugFromPath('///'), 'project'); - }); - - test('folds system messages, replays paired tool calls with reasoning, aliases overlong ids', () => { - const longId = `call-${'x'.repeat(80)}`; - const { body, warnings } = buildCommandCodeCliRequest( - { - prompt: [ - { role: 'system', content: 'Be terse.' }, - { role: 'system', content: 'Answer in English.' }, - { - role: 'user', - content: [ - { type: 'text', text: 'read it' }, - { - type: 'file', - mediaType: 'image/png', - data: { type: 'data', data: new Uint8Array([1, 2, 3]) }, - }, - { type: 'file', mediaType: 'application/pdf', data: { type: 'data', data: 'AAAA' } }, - { - type: 'file', - mediaType: 'image/jpeg', - data: { type: 'url', url: new URL('https://example.invalid/a.jpg') }, - }, - ], - }, - { - role: 'assistant', - content: [ - { type: 'reasoning', text: 'I should read the file.' }, - { type: 'tool-call', toolCallId: longId, toolName: 'read', input: { path: 'a.txt' } }, - { type: 'tool-call', toolCallId: 'orphan', toolName: 'read', input: {} }, - ], - }, - { - role: 'tool', - content: [ - { - type: 'tool-result', - toolCallId: longId, - toolName: 'read', - output: { type: 'json', value: { ok: true } }, - }, - ], - }, - ], - tools: [ - { - type: 'function', - name: 'read', - description: 'Read a file', - inputSchema: { type: ['object', 'null'], properties: { path: { type: 'string' } } }, - }, - ], - maxOutputTokens: 1_000, - temperature: 0, - topP: 0.5, - toolChoice: { type: 'required' }, - reasoning: 'high', - }, - { modelId: 'deepseek/deepseek-v4.1-flash', workingDir: '/tmp/x', threadId: () => 'thread-1' }, - ); - const params = body.params as Record; - assert.equal(params.model, 'deepseek/deepseek-v4.1-flash'); - assert.equal(params.system, 'Be terse.\n\nAnswer in English.'); - assert.equal(params.max_tokens, 1_000); - assert.equal(params.temperature, 0); - assert.equal(params.stream, true); - assert.equal(params.reasoning_effort, 'high'); - assert.equal(body.threadId, 'thread-1'); - assert.deepEqual(params.tools, [ - { - type: 'function', - name: 'read', - description: 'Read a file', - input_schema: { type: 'object', properties: { path: { type: 'string' } } }, - }, - ]); - assert.deepEqual(params.messages, [ - { - role: 'user', - content: [ - { type: 'text', text: 'read it' }, - { type: 'image', image: 'data:image/png;base64,AQID', mimeType: 'image/png' }, - ], - }, - { - role: 'assistant', - content: [ - { type: 'reasoning', text: 'I should read the file.' }, - { type: 'tool-call', toolCallId: 'cc-1', toolName: 'read', input: { path: 'a.txt' } }, - ], - }, - { - role: 'tool', - content: [ - { - type: 'tool-result', - toolCallId: 'cc-1', - toolName: 'read', - output: { type: 'text', value: '{"ok":true}' }, - }, - ], - }, - ]); - assert.deepEqual(warnings.map((w) => (w.type === 'unsupported' ? w.feature : w.type)).sort(), [ - 'file input application/pdf', - 'image url input', - 'toolChoice', - 'topP', - ]); - }); - - test('reasoning effort: provider options win, provider-default and none are omitted', () => { - const build = (options: Partial) => - ( - buildCommandCodeCliRequest({ ...USER_HI, ...options }, { modelId: 'm' }).body - .params as Record - ).reasoning_effort; - assert.equal(build({}), undefined); - assert.equal(build({ reasoning: 'provider-default' }), undefined); - assert.equal(build({ reasoning: 'none' }), undefined); - assert.equal(build({ reasoning: 'low' }), 'low'); - assert.equal( - build({ - reasoning: 'low', - providerOptions: { 'commandcode-cli': { reasoningEffort: 'xhigh' } }, - }), - 'xhigh', - ); - }); - - test('tool schemas are normalized to a root object type', () => { - assert.deepEqual(toolParametersSchema(undefined), { - type: 'object', - properties: {}, - additionalProperties: true, - }); - assert.deepEqual(toolParametersSchema({ properties: { a: { type: 'string' } } }), { - properties: { a: { type: 'string' } }, - type: 'object', - }); - assert.deepEqual( - toolParametersSchema({ - $ref: '#/$defs/Args', - $defs: { Args: { type: 'object', properties: { a: {} } } }, - }), - { - type: 'object', - properties: { a: {} }, - $defs: { Args: { type: 'object', properties: { a: {} } } }, - }, - ); - assert.deepEqual( - toolParametersSchema({ - anyOf: [ - { type: 'object', properties: { a: {} }, required: ['a'] }, - { type: 'object', properties: { b: {} } }, - ], - }), - { type: 'object', properties: { a: {}, b: {} }, required: ['a'], additionalProperties: true }, - ); - }); - - test('wire ids pass short ids through and alias the rest without collisions', () => { - const long = 'l'.repeat(65); - const wire = wireToolCallIds(new Set(['cc-1', long, 'short'])); - assert.equal(wire.get('cc-1'), 'cc-1'); - assert.equal(wire.get('short'), 'short'); - assert.equal(wire.get(long), 'cc-2'); - }); - - test('finish reasons map onto the unified vocabulary', () => { - assert.deepEqual(mapFinishReason('tool-calls'), { unified: 'tool-calls', raw: 'tool-calls' }); - assert.deepEqual(mapFinishReason('max_tokens'), { unified: 'length', raw: 'max_tokens' }); - assert.deepEqual(mapFinishReason('stop'), { unified: 'stop', raw: 'stop' }); - assert.deepEqual(mapFinishReason(undefined), { unified: 'stop', raw: undefined }); - }); -}); - -describe('streaming against a CLI-shaped server', () => { - test('carries the provider usage body from finish-step into the finish part', async () => { - // The wire splits one call across two events: `finish-step` holds the - // provider's OWN usage body, `finish` holds only normalized totals. The - // provider keys are what telemetry's strict reader looks for, so losing - // them settles every attempt as `usageBasis: 'missing'` — which is what - // left the composer's context gauge stuck at a stale number. - const server = await startJsonServer(async (_request, response) => { - respondCliStream(response, [ - { type: 'text-delta', text: 'ok' }, - { - type: 'finish-step', - finishReason: 'stop', - usage: { - inputTokens: 7611, - outputTokens: 2, - raw: { - prompt_tokens: 7611, - completion_tokens: 2, - prompt_cache_hit_tokens: 7424, - prompt_cache_miss_tokens: 187, - total_tokens: 7613, - }, - }, - }, - { - type: 'finish', - finishReason: 'stop', - totalUsage: { - inputTokens: 7611, - inputTokenDetails: { noCacheTokens: 187, cacheReadTokens: 7424 }, - outputTokens: 2, - totalTokens: 7613, - }, - }, - ]); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'deepseek/deepseek-v4.1-flash', - apiKey: 'user_k', - apiBase: server.url, - workingDir: '/repo', - }); - const { stream } = await model.doStream(USER_HI); - const parts = await collect(stream); - const finish = parts.find((p) => p.type === 'finish'); - assert.ok(finish && finish.type === 'finish'); - assert.equal(finish.usage.inputTokens.total, 7611); - // The provider body, not the normalized envelope: telemetry reads - // `prompt_tokens` off this to decide the attempt reported usage at all. - assert.equal((finish.usage.raw as Record).prompt_tokens, 7611); - }); - - test('maps reasoning, text, a tool call, and usage onto AI SDK stream parts', async () => { - let seenHeaders: Record = {}; - let seenBody: Record = {}; - const server = await startJsonServer(async (request, response) => { - assert.equal(request.url, '/alpha/generate'); - seenHeaders = request.headers; - seenBody = JSON.parse(await readBody(request)) as Record; - respondCliStream(response, [ - { type: 'reasoning-start' }, - { type: 'reasoning-delta', text: 'think ' }, - { type: 'reasoning-delta', text: 'hard' }, - { type: 'reasoning-end' }, - { type: 'text-delta', text: 'Hel' }, - { type: 'text-delta', text: 'lo' }, - { type: 'tool-call', toolCallId: 'call-1', toolName: 'echo', input: { text: 'hi' } }, - { - type: 'finish', - finishReason: 'tool-calls', - totalUsage: { - inputTokens: 100, - outputTokens: 7, - inputTokenDetails: { cacheReadTokens: 40, cacheWriteTokens: 10 }, - outputTokenDetails: { reasoningTokens: 3 }, - }, - }, - ]); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'deepseek/deepseek-v4.1-flash', - apiKey: 'user_k', - apiBase: server.url, - workingDir: '/repo', - }); - const { stream, request } = await model.doStream(USER_HI); - const parts = await collect(stream); - assert.equal(seenHeaders['x-command-code-version'], COMMANDCODE_CLI_VERSION); - assert.equal(seenHeaders['x-project-slug'], 'repo'); - assert.equal(seenHeaders.authorization, 'Bearer user_k'); - assert.equal( - (seenBody.params as Record).model, - 'deepseek/deepseek-v4.1-flash', - ); - assert.deepEqual(request?.body, seenBody); - assert.deepEqual( - parts.map((p) => p.type), - [ - 'stream-start', - 'response-metadata', - 'reasoning-start', - 'reasoning-delta', - 'reasoning-delta', - 'reasoning-end', - 'text-start', - 'text-delta', - 'text-delta', - 'text-end', - 'tool-input-start', - 'tool-input-delta', - 'tool-input-end', - 'tool-call', - 'finish', - ], - ); - const toolCall = parts.find((p) => p.type === 'tool-call'); - assert.deepEqual(toolCall, { - type: 'tool-call', - toolCallId: 'call-1', - toolName: 'echo', - input: '{"text":"hi"}', - }); - const finish = parts.at(-1); - assert.equal(finish?.type, 'finish'); - if (finish?.type !== 'finish') return; - assert.deepEqual(finish.finishReason, { unified: 'tool-calls', raw: 'tool-calls' }); - assert.deepEqual( - { ...finish.usage, raw: undefined }, - { - inputTokens: { total: 100, noCache: 50, cacheRead: 40, cacheWrite: 10 }, - outputTokens: { total: 7, text: undefined, reasoning: 3 }, - raw: undefined, - }, - ); - }); - - test('doGenerate assembles the streamed blocks into content', async () => { - const server = await startJsonServer((_request, response) => { - respondCliStream(response, [ - { type: 'reasoning-delta', text: 'r' }, - { type: 'text-delta', text: 'pong' }, - { type: 'finish', finishReason: 'stop', totalUsage: { inputTokens: 3, outputTokens: 1 } }, - ]); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'm', - apiKey: 'k', - apiBase: server.url, - }); - const result = await model.doGenerate(USER_HI); - assert.deepEqual(result.content, [ - { type: 'reasoning', text: 'r' }, - { type: 'text', text: 'pong' }, - ]); - assert.deepEqual(result.finishReason, { unified: 'stop', raw: 'stop' }); - assert.equal(result.usage.inputTokens.total, 3); - }); - - test('a 403 upgrade_required rejection is a plan (billing) failure, not a bad key', async () => { - const server = await startJsonServer((_request, response) => { - respondJson(response, 403, { - error: { - code: 'upgrade_required', - message: "Your Go plan doesn't include API access. Upgrade to Provider or higher.", - }, - }); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'm', - apiKey: 'k', - apiBase: server.url, - }); - await assert.rejects(model.doStream(USER_HI), (error: unknown) => { - assert.ok(APICallError.isInstance(error)); - assert.equal(error.statusCode, 403); - assert.equal(error.isRetryable, false); - assert.equal(classifyError(error), 'provider_billing'); - return true; - }); - }); - - test('an in-band error event surfaces as an error part carrying its status', async () => { - const server = await startJsonServer((_request, response) => { - respondCliStream(response, [ - { type: 'text-delta', text: 'partial' }, - { - type: 'error', - error: { message: 'insufficient credits', statusCode: 402, code: 'credits' }, - }, - ]); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'm', - apiKey: 'k', - apiBase: server.url, - }); - const parts = await collect((await model.doStream(USER_HI)).stream); - const errorPart = parts.find((p) => p.type === 'error'); - assert.ok(errorPart && errorPart.type === 'error'); - assert.ok(APICallError.isInstance(errorPart.error)); - assert.equal(errorPart.error.statusCode, 402); - assert.equal(classifyError(errorPart.error), 'provider_billing'); - assert.equal(parts.filter((p) => p.type === 'finish').length, 0); - }); - - test('a stream that ends without finish reports a retryable truncation', async () => { - const server = await startJsonServer((_request, response) => { - response.writeHead(200, { 'content-type': 'text/event-stream' }); - response.end(`data: ${JSON.stringify({ type: 'text-delta', text: 'cut' })}\n\n`); - }); - const model = new CommandCodeCliLanguageModel({ - modelId: 'm', - apiKey: 'k', - apiBase: server.url, - }); - const parts = await collect((await model.doStream(USER_HI)).stream); - const last = parts.at(-1); - assert.ok(last && last.type === 'error' && APICallError.isInstance(last.error)); - assert.equal(last.error.isRetryable, true); - }); -}); - -describe('runtime wiring', () => { - const connection: LlmConnection = { - slug: 'cc-go', - name: 'Command Code GO', - providerType: 'commandcode-go', - defaultModel: 'deepseek/deepseek-v4.1-flash', - enabled: true, - createdAt: 1, - updatedAt: 1, - }; - - test('resolves to the CLI wire with reasoning replayed as reasoning blocks', () => { - const runtime = resolveModelRuntime(connection, 'deepseek/deepseek-v4.1-flash'); - assert.equal(runtime.wire, 'commandcode-cli'); - assert.equal(runtime.adapter.kind, 'commandcode-cli'); - assert.deepEqual(runtime.reasoningReplay, { - kind: 'openai-chat-plaintext', - requestField: 'reasoning', - }); - assert.equal(runtime.baseUrl, 'https://api.commandcode.ai'); - }); - - // Choosing this provider is the choice to use this wire: nothing else routes - // here, and no other provider falls back to it. - test('a Command Code GO connection builds the CLI transport', () => { - const model = getAIModel({ connection, apiKey: 'k', modelId: 'deepseek/deepseek-v4.1-flash' }); - assert.ok(model instanceof CommandCodeCliLanguageModel); - }); - - test('an already encoded data URL is not wrapped a second time', () => { - const { body } = buildCommandCodeCliRequest( - { - prompt: [ - { - role: 'user', - content: [ - { - type: 'file', - mediaType: 'image/png', - data: { type: 'data', data: 'data:image/png;base64,AQID' }, - }, - ], - }, - ], - }, - { modelId: 'm' }, - ); - const [message] = body.params.messages as Array<{ content: Array> }>; - assert.deepEqual(message?.content[0], { - type: 'image', - image: 'data:image/png;base64,AQID', - mimeType: 'image/png', - }); - }); - - test('the connection test posts one tiny CLI generate and reads the status', async () => { - const urls: string[] = []; - const server = await startJsonServer(async (request, response) => { - urls.push(String(request.url)); - const body = JSON.parse(await readBody(request)) as { params: Record }; - assert.equal(body.params.max_tokens, 16); - assert.equal(request.headers['x-command-code-version'], COMMANDCODE_CLI_VERSION); - respondCliStream(response, [{ type: 'finish', finishReason: 'stop' }]); - }); - const result = await testConnection( - { ...connection, baseUrl: server.url }, - 'k', - 'deepseek/deepseek-v4.1-flash', - { fetch }, - ); - assert.equal(result.ok, true, JSON.stringify(result)); - assert.deepEqual(urls, ['/alpha/generate']); - }); - - test('the connection test fails on an in-band error behind HTTP 200', async () => { - const server = await startJsonServer((_request, response) => { - respondCliStream(response, [ - { - type: 'error', - error: { message: 'bad key', statusCode: 401, code: 'invalid_key' }, - }, - ]); - }); - const result = await testConnection( - { ...connection, baseUrl: server.url }, - 'k', - 'deepseek/deepseek-v4.1-flash', - { fetch }, - ); - assert.equal(result.ok, false, 'HTTP 200 is only the handshake on this wire'); - assert.equal(result.statusCode, 401); - assert.equal(result.errorClass, 'auth'); - assert.match(result.errorMessage ?? '', /bad key/u); - }); - - test('the connection test fails when the stream never reaches finish', async () => { - const server = await startJsonServer((_request, response) => { - response.writeHead(200, { 'content-type': 'text/event-stream' }); - response.end(`data: ${JSON.stringify({ type: 'text-delta', text: 'cut' })}\n\n`); - }); - const result = await testConnection( - { ...connection, baseUrl: server.url }, - 'k', - 'deepseek/deepseek-v4.1-flash', - { fetch }, - ); - assert.equal(result.ok, false); - assert.equal(result.errorClass, 'network'); - }); - - test('a rate-limited stream error reports the provider, not the credential', async () => { - const server = await startJsonServer((_request, response) => { - respondCliStream(response, [ - { type: 'error', error: { message: 'slow down', statusCode: 429 } }, - ]); - }); - const result = await testConnection( - { ...connection, baseUrl: server.url }, - 'k', - 'deepseek/deepseek-v4.1-flash', - { fetch }, - ); - assert.equal(result.ok, false); - assert.equal(result.errorClass, 'provider_unavailable'); - }); -}); diff --git a/packages/runtime/src/__tests__/connection-usage.test.ts b/packages/runtime/src/__tests__/connection-usage.test.ts deleted file mode 100644 index 93562fe744..0000000000 --- a/packages/runtime/src/__tests__/connection-usage.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { fetchConnectionUsage } from '../connection-usage.js'; - -/** An effect fetch answering the given `path -> json` routes, 404 elsewhere. */ -function routes(answers: Record) { - const calls: string[] = []; - const fetchFn = (async (input: string | URL | Request) => { - const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; - calls.push(new URL(url).pathname); - const body = answers[new URL(url).pathname]; - if (body === undefined) return new Response('not found', { status: 404 }); - return new Response(JSON.stringify(body), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - }) as unknown as typeof globalThis.fetch; - return { fetchFn, calls }; -} - -/** An effect fetch answering a fixed status for every path (to model a 401/403). */ -function statusFetch(status: number) { - return (async () => - new Response(JSON.stringify({ error: 'no' }), { - status, - headers: { 'content-type': 'application/json' }, - })) as unknown as typeof globalThis.fetch; -} - -const GO_OK = { - '/alpha/whoami': { success: true, user: { id: 'u', userName: 'joob1nhk13d9' } }, - '/alpha/usage/summary': { - totalCount: 3552, - failedCount: 5, - successRate: 99.85923423423422, - totalCost: 5.852315282, - totalTokensIn: 428_987_308, - totalTokensOut: 2_971_687, - }, - '/alpha/billing/credits': { - credits: { monthlyCredits: 4.42673573, purchasedCredits: 0, freeCredits: 0 }, - windowLimits: { - limited: true, - fiveHour: { used: 0.256638598, cap: 3, exceeded: false, resetAt: 1_789_746_210_944 }, - weekly: { used: 0.387957292, cap: 6, exceeded: false, resetAt: 1_790_314_995_070 }, - }, - }, - '/alpha/billing/subscriptions': { - success: true, - data: { planId: 'individual-go', currentPeriodEnd: '2026-10-10T10:39:47.000Z' }, - }, -}; - -const CREDENTIAL = { - providerType: 'commandcode-go' as const, - apiKey: 'key', - baseUrl: 'https://api.commandcode.ai', -}; - -test('maps the GO account endpoints into windows and stats', async () => { - const { fetchFn } = routes(GO_OK); - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - const { report } = result; - - assert.equal(report.accountLabel, 'joob1nhk13d9'); - assert.equal(report.planLabel, 'Go'); - assert.deepEqual( - report.windows.map((w) => w.id), - ['fiveHour', 'weekly', 'monthly'], - ); - assert.deepEqual(report.windows[0], { - id: 'fiveHour', - used: 0.256638598, - cap: 3, - unit: 'credits', - resetsAt: 1_789_746_210_944, - }); - // The monthly window's cap is the plan's allowance; consumed is - // cap − remaining balance (10 − 4.42673573). - const monthly = report.windows.find((w) => w.id === 'monthly'); - assert.ok(monthly); - assert.equal(monthly.cap, 10); - assert.ok(Math.abs(monthly.used - 5.57326427) < 1e-6); - assert.equal(monthly.resetsAt, Date.parse('2026-10-10T10:39:47.000Z')); - - assert.deepEqual(report.stats, { - requests: 3552, - failed: 5, - successRate: 99.85923423423422, - cost: 5.852315282, - tokensIn: 428_987_308, - tokensOut: 2_971_687, - }); -}); - -test('omits a window the account never reported rather than zeroing it', async () => { - const { fetchFn } = routes({ - ...GO_OK, - '/alpha/billing/credits': { - credits: { monthlyCredits: 4.42673573 }, - windowLimits: { limited: true, weekly: { used: 1, cap: 6, resetAt: 1_790_314_995_070 } }, - }, - }); - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - assert.deepEqual( - result.report.windows.map((w) => w.id), - ['weekly', 'monthly'], - ); -}); - -test('omits the monthly window when the plan is unknown (no limit to divide by)', async () => { - const { fetchFn } = routes({ - ...GO_OK, - '/alpha/billing/subscriptions': { success: true, data: { planId: 'mystery-plan' } }, - }); - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - assert.deepEqual( - result.report.windows.map((w) => w.id), - ['fiveHour', 'weekly'], - ); - assert.equal(result.report.planLabel, undefined); -}); - -test('degrades per endpoint: one failing endpoint does not sink the report', async () => { - const { fetchFn } = routes({ - '/alpha/whoami': GO_OK['/alpha/whoami'], - '/alpha/billing/credits': GO_OK['/alpha/billing/credits'], - // summary and subscriptions 404 - }); - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - assert.equal(result.report.stats, undefined); - assert.equal(result.report.planLabel, undefined); - assert.deepEqual( - result.report.windows.map((w) => w.id), - ['fiveHour', 'weekly'], - ); -}); - -test('is unavailable when every endpoint fails', async () => { - const { fetchFn } = routes({}); - assert.deepEqual(await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }), { - kind: 'unavailable', - reason: 'network', - }); -}); - -test('is unavailable without a credential, and never fetches', async () => { - const { fetchFn, calls } = routes(GO_OK); - assert.deepEqual( - await fetchConnectionUsage({ ...CREDENTIAL, apiKey: undefined, fetch: fetchFn }), - { kind: 'unavailable', reason: 'no-credential' }, - ); - assert.equal(calls.length, 0); -}); - -test('is unsupported for a provider with no usage endpoints wired', async () => { - const { fetchFn } = routes(GO_OK); - assert.deepEqual( - await fetchConnectionUsage({ - providerType: 'commandcode', - apiKey: 'key', - baseUrl: 'https://api.commandcode.ai/provider/v1', - fetch: fetchFn, - }), - { kind: 'unavailable', reason: 'unsupported' }, - ); -}); - -test('reports a wholly-refused key as unauthorized, not as a network failure', async () => { - // Every endpoint answers 401. The user's fix is the credential on this very - // page, so the reason has to say so rather than blaming the connection. - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: statusFetch(401) }); - assert.deepEqual(result, { kind: 'unavailable', reason: 'unauthorized' }); -}); - -test('still reports a plain transport failure as network', async () => { - const failing = (async () => { - throw new Error('ECONNREFUSED'); - }) as unknown as typeof globalThis.fetch; - assert.deepEqual(await fetchConnectionUsage({ ...CREDENTIAL, fetch: failing }), { - kind: 'unavailable', - reason: 'network', - }); -}); - -test('a partially-scoped key returns what it can and flags the refusal', async () => { - // whoami answers; the billing endpoints 403. The read is real but incomplete, - // so it is reported with the refusal flagged rather than presented as whole. - const fetchFn = (async (input: string | URL | Request) => { - const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; - const path = new URL(url).pathname; - const body = GO_OK[path as keyof typeof GO_OK]; - if (path === '/alpha/whoami') { - return new Response(JSON.stringify(body), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - } - return new Response('forbidden', { status: 403 }); - }) as unknown as typeof globalThis.fetch; - - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - assert.equal(result.report.accountLabel, 'joob1nhk13d9', 'the part that was readable is kept'); - assert.equal(result.report.partiallyUnauthorized, true, 'the refusal must be flagged'); -}); - -test('a fully-readable report does not carry the partial-refusal flag', async () => { - const { fetchFn } = routes(GO_OK); - const result = await fetchConnectionUsage({ ...CREDENTIAL, fetch: fetchFn }); - assert.equal(result.kind, 'report'); - if (result.kind !== 'report') return; - assert.equal(result.report.partiallyUnauthorized, undefined); -}); diff --git a/packages/runtime/src/__tests__/model-factory-thinking.test.ts b/packages/runtime/src/__tests__/model-factory-thinking.test.ts index 9bfc6d9f98..9370b154cf 100644 --- a/packages/runtime/src/__tests__/model-factory-thinking.test.ts +++ b/packages/runtime/src/__tests__/model-factory-thinking.test.ts @@ -1188,53 +1188,37 @@ test('Copilot Messages preserves bearer auth without the generic Anthropic beta } }); -describe('buildProviderOptions: Command Code CLI thinking level', () => { +describe('buildProviderOptions: Command Code thinking level', () => { // The Provider API exposes no reasoning metadata, so these levels come from // the static table in model-metadata.ts, ported from the reference CLI // effort map. The switcher only appears when the table declares the model. - test('selectable efforts surface for the models the CLI wire accepts them on', () => { - for (const providerType of ['commandcode-go', 'commandcode'] as const) { - assert.deepEqual( - [...thinkingVariantsForModel(providerType, 'claude-fable-5-1')], - ['low', 'medium', 'high', 'xhigh', 'max'], - ); - assert.deepEqual( - [...thinkingVariantsForModel(providerType, 'moonshotai/Kimi-K3')], - ['low', 'high', 'max'], - ); - assert.deepEqual( - [...thinkingVariantsForModel(providerType, 'xai/grok-4.6')], - ['low', 'medium', 'high', 'xhigh'], - ); - } + test('selectable efforts surface for the models the table accepts them on', () => { + assert.deepEqual( + [...thinkingVariantsForModel('commandcode', 'claude-fable-5-1')], + ['low', 'medium', 'high', 'xhigh', 'max'], + ); + assert.deepEqual( + [...thinkingVariantsForModel('commandcode', 'moonshotai/Kimi-K3')], + ['low', 'high', 'max'], + ); + assert.deepEqual( + [...thinkingVariantsForModel('commandcode', 'xai/grok-4.6')], + ['low', 'medium', 'high', 'xhigh'], + ); }); test('models that reason automatically offer no selector', () => { - for (const providerType of ['commandcode-go', 'commandcode'] as const) { - // Tencent Hy3/Hy4 with no levels and the GLM-5.x-Fast siblings think with - // a depth Command Code chooses; the CLI omits reasoning_effort for them, - // so the table must stay silent and the picker must stay hidden. - for (const modelId of ['tencent/hy3-paid', 'zai-org/GLM-5.2-Fast', 'zai-org/GLM-5']) { - assert.deepEqual([...thinkingVariantsForModel(providerType, modelId)], []); - assert.deepEqual(buildProviderOptions(conn(providerType), modelId, 'high'), {}); - } + // Tencent Hy3/Hy4 with no levels and the GLM-5.x-Fast siblings think with + // a depth the provider chooses, so the table must stay silent and the + // picker must stay hidden. + for (const modelId of ['tencent/hy3-paid', 'zai-org/GLM-5.2-Fast', 'zai-org/GLM-5']) { + assert.deepEqual([...thinkingVariantsForModel('commandcode', modelId)], []); + assert.deepEqual(buildProviderOptions(conn('commandcode'), modelId, 'high'), {}); } }); test('an unknown model exposes nothing and sends nothing', () => { - assert.deepEqual([...thinkingVariantsForModel('commandcode-go', 'not-a-model')], []); - assert.deepEqual(buildProviderOptions(conn('commandcode-go'), 'not-a-model', 'high'), {}); - }); - - test('the chosen level is forwarded verbatim, including max', () => { - assert.deepEqual(buildProviderOptions(conn('commandcode-go'), 'claude-fable-5-1', 'max'), { - 'commandcode-cli': { reasoningEffort: 'max' }, - }); - assert.deepEqual(buildProviderOptions(conn('commandcode-go'), 'moonshotai/Kimi-K3', 'low'), { - 'commandcode-cli': { reasoningEffort: 'low' }, - }); - // `off` is a discard, and no declared model offers it on this route. - assert.deepEqual(buildProviderOptions(conn('commandcode-go'), 'claude-fable-5-1', 'off'), {}); - assert.deepEqual(buildProviderOptions(conn('commandcode-go'), 'claude-fable-5-1'), {}); + assert.deepEqual([...thinkingVariantsForModel('commandcode', 'not-a-model')], []); + assert.deepEqual(buildProviderOptions(conn('commandcode'), 'not-a-model', 'high'), {}); }); }); diff --git a/packages/runtime/src/__tests__/provider-contract-matrix.ts b/packages/runtime/src/__tests__/provider-contract-matrix.ts index 4061c3515a..1c3397cc8e 100644 --- a/packages/runtime/src/__tests__/provider-contract-matrix.ts +++ b/packages/runtime/src/__tests__/provider-contract-matrix.ts @@ -71,9 +71,6 @@ export type ProviderContractWire = export const SUBSCRIPTION_WIRE_PROVIDER_TYPES: ReadonlySet = new Set([ 'openai-codex', 'github-copilot', - // Not a subscription, but the same shape of exception: a provider-specific - // wire (the CLI's `/alpha/generate`) no generated executor can drive. - 'commandcode-go', ]); /** diff --git a/packages/runtime/src/__tests__/provider-contract-overrides.ts b/packages/runtime/src/__tests__/provider-contract-overrides.ts index 0fe8a69344..4c2c856b1c 100644 --- a/packages/runtime/src/__tests__/provider-contract-overrides.ts +++ b/packages/runtime/src/__tests__/provider-contract-overrides.ts @@ -37,7 +37,6 @@ import { generateText, isStepCount, streamText, tool } from 'ai'; import { z } from 'zod'; import { discoverModels } from './model-discovery-fixture.js'; import { buildProviderOptions, getAIModel } from '../model-factory.js'; -import { COMMANDCODE_CLI_VERSION } from '../commandcode-cli-language-model.js'; import { buildSubscriptionModelFetch } from '../subscription-model-fetch.js'; import { readBody, @@ -71,16 +70,6 @@ export const PROVIDER_CONTRACT_OVERRIDE_BINDINGS: readonly ProviderContractOverr 'GitHub Copilot discovers the account model and completes a reasoning tool loop on its exact wire', run: runGitHubCopilotWire, }, - { - keys: [ - 'commandcode-go:exact-model-id', - 'commandcode-go:tool-loop', - 'commandcode-go:reasoning-replay', - ], - title: - 'Command Code GO completes a reasoning tool loop on /alpha/generate, replaying reasoning and paired tool results as CLI blocks', - run: runCommandCodeCliWire, - }, { keys: ['fireworks-ai:discovery'], title: @@ -1378,110 +1367,3 @@ async function runZenMuxSignedReasoningReplay(): Promise { }, ); } - -async function runCommandCodeCliWire(): Promise { - const modelId = 'deepseek/deepseek-v4.1-flash'; - const requestBodies: Array<{ params: Record }> = []; - const server = await startJsonServer(async (request, response) => { - if (request.method === 'GET') { - respondJson(response, 200, { object: 'list', data: [{ id: modelId }] }); - return; - } - assert.equal(request.url, '/alpha/generate'); - assert.equal(request.headers.authorization, 'Bearer cc-go-key'); - assert.equal(request.headers['x-command-code-version'], COMMANDCODE_CLI_VERSION); - assert.equal(request.headers['x-cli-environment'], 'production'); - requestBodies.push(JSON.parse(await readBody(request)) as { params: Record }); - response.writeHead(200, { 'content-type': 'text/event-stream' }); - const events = - requestBodies.length === 1 - ? [ - { type: 'reasoning-delta', text: 'I should call echo with the requested text.' }, - { - type: 'tool-call', - toolCallId: 'cc-call-1', - toolName: 'echo', - input: { text: 'hello' }, - }, - { - type: 'finish', - finishReason: 'tool-calls', - totalUsage: { inputTokens: 8, outputTokens: 4 }, - }, - ] - : [ - { type: 'text-delta', text: 'Echoed hello.' }, - { - type: 'finish', - finishReason: 'stop', - totalUsage: { inputTokens: 12, outputTokens: 3 }, - }, - ]; - for (const event of events) response.write(`data: ${JSON.stringify(event)}\n\n`); - response.end('data: [DONE]\n\n'); - }); - const connection: LlmConnection = { - slug: 'commandcode-go', - name: 'Command Code GO', - providerType: 'commandcode-go', - baseUrl: server.url, - defaultModel: modelId, - enabled: true, - createdAt: 1, - updatedAt: 1, - }; - connection.models = await discoverModels(connection, 'cc-go-key'); - - const result = await generateText({ - model: getAIModel({ connection, apiKey: 'cc-go-key', modelId, fetch }), - // A model models.dev does not describe resolves no thinking level, so - // the effort rides the adapter's own provider-options key directly. - providerOptions: { 'commandcode-cli': { reasoningEffort: 'medium' } }, - tools: { - echo: tool({ - description: 'Echo text', - inputSchema: z.object({ text: z.string() }), - execute: async ({ text }) => ({ echoed: text }), - }), - }, - stopWhen: isStepCount(3), - prompt: 'Echo hello', - }); - assert.equal(result.text, 'Echoed hello.'); - - assert.equal(requestBodies.length, 2); - const [first, second] = requestBodies as [ - { params: Record }, - { params: Record }, - ]; - assert.equal(first.params.model, modelId); - assert.equal(first.params.reasoning_effort, 'medium'); - assert.deepEqual(first.params.tools, [ - { - type: 'function', - name: 'echo', - description: 'Echo text', - input_schema: (first.params.tools as Array<{ input_schema: unknown }>)[0]?.input_schema, - }, - ]); - const replay = second.params.messages as Array<{ - role: string; - content: Array>; - }>; - const assistant = replay.find((message) => message.role === 'assistant'); - assert.ok(assistant, 'the second request replays the assistant turn'); - assert.deepEqual( - assistant.content.map((block) => block.type), - ['reasoning', 'tool-call'], - 'reasoning is replayed ahead of the paired tool call', - ); - assert.equal(assistant.content[1]?.toolCallId, 'cc-call-1'); - const toolMessage = replay.find((message) => message.role === 'tool'); - assert.ok(toolMessage); - assert.deepEqual(toolMessage.content[0], { - type: 'tool-result', - toolCallId: 'cc-call-1', - toolName: 'echo', - output: { type: 'text', value: '{"echoed":"hello"}' }, - }); -} diff --git a/packages/runtime/src/commandcode-cli-language-model.ts b/packages/runtime/src/commandcode-cli-language-model.ts deleted file mode 100644 index 8c7bc04173..0000000000 --- a/packages/runtime/src/commandcode-cli-language-model.ts +++ /dev/null @@ -1,914 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Command Code CLI transport: the `/alpha/generate` wire, exposed as an AI SDK - * language model. The Go plan's keys are answered by - * `/provider/v1/chat/completions` with `403 upgrade_required`, so this is the - * wire that serves them. - * - * Shape (see also the MIT-licensed `pi-commandcode-provider` and - * `dsh-commandcode-provider`): - * POST {apiBase}/alpha/generate - * body { config, memory, taste, skills, params: { model, messages, tools, - * system, max_tokens, temperature, stream, reasoning_effort? }, threadId } - * events SSE `data:` lines carrying AI SDK stream parts: `text-delta`, - * `reasoning-start|delta|end`, `tool-call`, `finish`, `error`. - * errors 403 `upgrade_required` is a plan gate, not a bad key. - */ - -import { randomUUID } from 'node:crypto'; -import { - APICallError, - type LanguageModelV4, - type LanguageModelV4CallOptions, - type LanguageModelV4Content, - type LanguageModelV4FilePart, - type LanguageModelV4FinishReason, - type LanguageModelV4GenerateResult, - type LanguageModelV4Prompt, - type LanguageModelV4StreamPart, - type LanguageModelV4StreamResult, - type LanguageModelV4ToolResultOutput, - type LanguageModelV4Usage, - type SharedV4Warning, -} from '@ai-sdk/provider'; - -/** The official CLI release whose wire this mirrors; sent as its version header. */ -export const COMMANDCODE_CLI_VERSION = '1.54.0'; -const DEFAULT_MAX_TOKENS = 64_000; -const DEFAULT_TEMPERATURE = 0.3; -/** The gateway rejects `call_id` values longer than this. */ -const MAX_WIRE_TOOL_CALL_ID_LENGTH = 64; -const SCHEMA_NORMALIZE_MAX_DEPTH = 8; - -export function commandCodeCliGenerateUrl(apiBase: string): string { - return `${apiBase.replace(/\/+$/u, '')}/alpha/generate`; -} - -/** The identity headers the official CLI sends; the gateway keys plan gating on them. */ -export function commandCodeCliHeaders(apiKey: string, workingDir: string): Record { - return { - 'content-type': 'application/json', - authorization: `Bearer ${apiKey}`, - 'x-command-code-version': COMMANDCODE_CLI_VERSION, - 'x-cli-environment': 'production', - 'x-project-slug': projectSlugFromPath(workingDir), - 'x-taste-learning': 'true', - 'x-co-flag': 'false', - }; -} - -export function projectSlugFromPath(pathName: string): string { - const slug = pathName - .toLowerCase() - .replace(/^[a-z]:/iu, '') - .replace(/[^a-z0-9]+/gu, '-') - .replace(/^-+|(? Date; - readonly threadId?: () => string; -} - -export interface CommandCodeCliRequestBody { - readonly config: Record; - readonly memory: null; - readonly taste: null; - readonly skills: null; - readonly params: Record; - readonly threadId: string; -} - -export class CommandCodeCliLanguageModel implements LanguageModelV4 { - readonly specificationVersion = 'v4' as const; - readonly provider = 'commandcode-cli'; - readonly modelId: string; - readonly supportedUrls: Record = {}; - readonly #config: CommandCodeCliLanguageModelConfig; - - constructor(config: CommandCodeCliLanguageModelConfig) { - this.#config = config; - this.modelId = config.modelId; - } - - async doGenerate(options: LanguageModelV4CallOptions): Promise { - const { stream, request, response } = await this.doStream(options); - const content: LanguageModelV4Content[] = []; - const warnings: SharedV4Warning[] = []; - let finishReason: LanguageModelV4FinishReason = { unified: 'other', raw: undefined }; - let usage = emptyUsage(); - let openText: { id: string; text: string } | undefined; - let openReasoning: { id: string; text: string } | undefined; - const reader = stream.getReader(); - for (;;) { - const { done, value: part } = await reader.read(); - if (done) break; - switch (part.type) { - case 'stream-start': - warnings.push(...part.warnings); - break; - case 'text-start': - openText = { id: part.id, text: '' }; - break; - case 'text-delta': - if (openText) openText.text += part.delta; - break; - case 'text-end': - if (openText) content.push({ type: 'text', text: openText.text }); - openText = undefined; - break; - case 'reasoning-start': - openReasoning = { id: part.id, text: '' }; - break; - case 'reasoning-delta': - if (openReasoning) openReasoning.text += part.delta; - break; - case 'reasoning-end': - if (openReasoning) content.push({ type: 'reasoning', text: openReasoning.text }); - openReasoning = undefined; - break; - case 'tool-call': - content.push(part); - break; - case 'finish': - finishReason = part.finishReason; - usage = part.usage; - break; - case 'error': - throw part.error; - default: - break; - } - } - return { - content, - finishReason, - usage, - warnings, - request, - response: { headers: response?.headers }, - }; - } - - async doStream(options: LanguageModelV4CallOptions): Promise { - const { body, warnings } = buildCommandCodeCliRequest(options, this.#config); - const url = commandCodeCliGenerateUrl(this.#config.apiBase); - const fetchFn = this.#config.fetch ?? globalThis.fetch; - const headers: Record = { - ...commandCodeCliHeaders(this.#config.apiKey, this.#config.workingDir ?? DEFAULT_WORKING_DIR), - ...definedHeaders(options.headers), - }; - let response: Response; - try { - response = await fetchFn(url, { - method: 'POST', - headers, - body: JSON.stringify(body), - signal: options.abortSignal, - }); - } catch (error) { - if (options.abortSignal?.aborted) throw error; - throw new APICallError({ - message: `Command Code GO request failed: ${errorMessage(error)}`, - url, - requestBodyValues: body, - cause: error, - isRetryable: true, - }); - } - const responseHeaders = headersToRecord(response.headers); - if (!response.ok) { - const responseBody = await response.text().catch(() => ''); - throw new APICallError({ - message: `Command Code GO rejected the request (${response.status}): ${responseBody.slice(0, 500)}`, - url, - requestBodyValues: body, - statusCode: response.status, - responseHeaders, - responseBody, - isRetryable: response.status === 429 || response.status >= 500, - }); - } - if (!response.body) { - throw new APICallError({ - message: 'Command Code GO returned no body', - url, - requestBodyValues: body, - statusCode: response.status, - responseHeaders, - isRetryable: true, - }); - } - const stream = response.body - .pipeThrough(new TextDecoderStream()) - .pipeThrough(sseDataLines()) - .pipeThrough(cliEventsToStreamParts({ warnings, url, body, modelId: this.modelId })); - return { stream, request: { body }, response: { headers: responseHeaders } }; - } -} - -const DEFAULT_WORKING_DIR = 'maka'; - -// --------------------------------------------------------------------------- -// Request -// --------------------------------------------------------------------------- - -export function buildCommandCodeCliRequest( - options: LanguageModelV4CallOptions, - config: Pick, -): { body: CommandCodeCliRequestBody; warnings: SharedV4Warning[] } { - const warnings: SharedV4Warning[] = []; - const unsupported: Array<[string, unknown]> = [ - ['topP', options.topP], - ['topK', options.topK], - ['presencePenalty', options.presencePenalty], - ['frequencyPenalty', options.frequencyPenalty], - ['stopSequences', options.stopSequences], - ['seed', options.seed], - ]; - for (const [setting, value] of unsupported) { - if (value !== undefined) warnings.push({ type: 'unsupported', feature: setting }); - } - if (options.responseFormat && options.responseFormat.type !== 'text') { - warnings.push({ type: 'unsupported', feature: 'responseFormat' }); - } - if (options.toolChoice && options.toolChoice.type !== 'auto') { - warnings.push({ - type: 'unsupported', - feature: 'toolChoice', - details: 'Command Code GO always lets the model choose', - }); - } - - const { system, messages } = convertPrompt(options.prompt, warnings); - const tools = (options.tools ?? []).flatMap((tool) => { - if (tool.type !== 'function') { - warnings.push({ type: 'unsupported', feature: `provider tool ${tool.name}` }); - return []; - } - return [ - { - type: 'function', - name: tool.name, - description: tool.description, - input_schema: toolParametersSchema(tool.inputSchema), - }, - ]; - }); - const reasoningEffort = resolveReasoningEffort(options); - const now = config.now?.() ?? new Date(); - const body: CommandCodeCliRequestBody = { - config: { - workingDir: config.workingDir ?? DEFAULT_WORKING_DIR, - date: now.toISOString().split('T')[0], - environment: `${process.platform}-${process.arch}, Node.js ${process.version}`, - structure: [], - isGitRepo: false, - currentBranch: '', - mainBranch: '', - gitStatus: '', - recentCommits: [], - }, - memory: null, - taste: null, - skills: null, - params: { - model: config.modelId, - messages, - tools, - system, - max_tokens: options.maxOutputTokens ?? DEFAULT_MAX_TOKENS, - temperature: options.temperature ?? DEFAULT_TEMPERATURE, - stream: true, - ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), - }, - threadId: config.threadId?.() ?? randomUUID(), - }; - return { body, warnings }; -} - -function resolveReasoningEffort(options: LanguageModelV4CallOptions): string | undefined { - const override = options.providerOptions?.['commandcode-cli']?.reasoningEffort; - if (typeof override === 'string' && override !== '') return override; - const level = options.reasoning; - if (level === undefined || level === 'provider-default' || level === 'none') return undefined; - return level; -} - -/** - * The CLI wire's message shape, converted from the AI SDK prompt. System - * messages fold into `params.system`; only tool calls with a paired result - * are replayed; assistant reasoning IS replayed (the gateway rebuilds the - * upstream request from these blocks and DeepSeek thinking mode rejects a - * tool loop whose history lacks it). - */ -function convertPrompt( - prompt: LanguageModelV4Prompt, - warnings: SharedV4Warning[], -): { system: string; messages: unknown[] } { - const systemParts: string[] = []; - const callIds = new Set(); - const callNames = new Map(); - const resultIds = new Set(); - for (const message of prompt) { - if (message.role === 'assistant') { - for (const part of message.content) { - if (part.type === 'tool-call') { - callIds.add(part.toolCallId); - callNames.set(part.toolCallId, part.toolName); - } - } - } - if (message.role === 'tool') { - for (const part of message.content) { - if (part.type === 'tool-result') resultIds.add(part.toolCallId); - } - } - } - const paired = new Set([...callIds].filter((id) => resultIds.has(id))); - const wireIds = wireToolCallIds(paired); - - const messages: unknown[] = []; - for (const message of prompt) { - switch (message.role) { - case 'system': - systemParts.push(message.content); - break; - case 'user': { - const parts: unknown[] = []; - for (const part of message.content) { - if (part.type === 'text') { - parts.push({ type: 'text', text: part.text }); - continue; - } - const image = imagePart(part, warnings); - if (image) parts.push(image); - } - if (parts.length > 0) messages.push({ role: 'user', content: parts }); - break; - } - case 'assistant': { - const parts: unknown[] = []; - for (const part of message.content) { - if (part.type === 'text') parts.push({ type: 'text', text: part.text }); - else if (part.type === 'reasoning') parts.push({ type: 'reasoning', text: part.text }); - else if (part.type === 'tool-call' && paired.has(part.toolCallId)) { - parts.push({ - type: 'tool-call', - toolCallId: wireIds.get(part.toolCallId) ?? part.toolCallId, - toolName: part.toolName, - input: recordOrEmpty(part.input), - }); - } - } - if (parts.length > 0) messages.push({ role: 'assistant', content: parts }); - break; - } - case 'tool': { - for (const part of message.content) { - if (part.type !== 'tool-result' || !paired.has(part.toolCallId)) continue; - const { text, isError } = toolResultText(part.output, warnings); - messages.push({ - role: 'tool', - content: [ - { - type: 'tool-result', - toolCallId: wireIds.get(part.toolCallId) ?? part.toolCallId, - toolName: callNames.get(part.toolCallId) || part.toolName || 'unknown', - output: isError - ? { type: 'error-text', value: text } - : { type: 'text', value: text }, - }, - ], - }); - } - break; - } - default: - break; - } - } - return { system: systemParts.join('\n\n'), messages }; -} - -function imagePart( - part: LanguageModelV4FilePart, - warnings: SharedV4Warning[], -): unknown | undefined { - if (!part.mediaType.startsWith('image/')) { - warnings.push({ type: 'unsupported', feature: `file input ${part.mediaType}` }); - return undefined; - } - if (part.data.type !== 'data') { - warnings.push({ - type: 'unsupported', - feature: `image ${part.data.type} input`, - details: 'Command Code GO carries images inline only', - }); - return undefined; - } - const encoded = - typeof part.data.data === 'string' - ? part.data.data - : Buffer.from(part.data.data).toString('base64'); - // The pinned CLI serializes an image block in the AI SDK message shape it - // builds its request from — a data URL under `image`, beside `mimeType` — - // not Anthropic's `source` object. The rest of this wire is AI SDK shaped - // too (`toolCallId`, `toolName`, `input`), and for an unpublished endpoint - // the pinned CLI is the protocol authority. - return { - type: 'image', - image: encoded.startsWith('data:') ? encoded : `data:${part.mediaType};base64,${encoded}`, - mimeType: part.mediaType, - }; -} - -function toolResultText( - output: LanguageModelV4ToolResultOutput, - warnings: SharedV4Warning[], -): { text: string; isError: boolean } { - switch (output.type) { - case 'text': - return { text: output.value, isError: false }; - case 'error-text': - return { text: output.value, isError: true }; - case 'json': - return { text: JSON.stringify(output.value), isError: false }; - case 'error-json': - return { text: JSON.stringify(output.value), isError: true }; - case 'execution-denied': - return { text: output.reason ?? 'Tool execution was denied.', isError: true }; - case 'content': { - const texts: string[] = []; - for (const item of output.value) { - if (item.type === 'text') texts.push(item.text); - else warnings.push({ type: 'unsupported', feature: `tool result ${item.type}` }); - } - return { text: texts.join('\n'), isError: false }; - } - default: - return { text: '', isError: false }; - } -} - -/** Overlong paired ids get a per-request `cc-` alias; the pair resolves through one map. */ -export function wireToolCallIds(paired: ReadonlySet): Map { - const wire = new Map(); - const taken = new Set(); - for (const id of paired) { - if (id.length <= MAX_WIRE_TOOL_CALL_ID_LENGTH) { - wire.set(id, id); - taken.add(id); - } - } - let seq = 1; - for (const id of paired) { - if (wire.has(id)) continue; - let alias = `cc-${seq++}`; - while (taken.has(alias)) alias = `cc-${seq++}`; - wire.set(id, alias); - taken.add(alias); - } - return wire; -} - -/** - * The gateway validates a tool schema's root `type` against the literal - * string "object". Schemas generators emit as `["object","null"]`, a bare - * `$ref`, or a combinator get the declared type they mean. - */ -export function toolParametersSchema(parameters: unknown, depth = 0): Record { - if (!isRecord(parameters)) return { type: 'object', properties: {}, additionalProperties: true }; - if (parameters.type === 'object') return parameters; - if (Array.isArray(parameters.type) && parameters.type.includes('object')) { - return { ...parameters, type: 'object' }; - } - if (parameters.type === undefined || parameters.type === null) { - if (typeof parameters.$ref === 'string' && depth < SCHEMA_NORMALIZE_MAX_DEPTH) { - const target = resolveLocalRef(parameters, parameters.$ref); - if (target !== undefined) { - const { $ref: _ref, ...rest } = parameters; - return toolParametersSchema({ ...target, ...rest }, depth + 1); - } - } - if (isRecord(parameters.properties) || parameters.additionalProperties !== undefined) { - return { ...parameters, type: 'object' }; - } - for (const key of ['allOf', 'anyOf', 'oneOf'] as const) { - const branches = parameters[key]; - if (Array.isArray(branches) && depth < SCHEMA_NORMALIZE_MAX_DEPTH) { - const merged = branches.map((branch) => toolParametersSchema(branch, depth + 1)); - const properties = Object.assign({}, ...merged.map((b) => b.properties ?? {})); - const required = [ - ...new Set(merged.flatMap((b) => (Array.isArray(b.required) ? b.required : []))), - ]; - return { - type: 'object', - properties, - ...(required.length > 0 ? { required } : {}), - additionalProperties: true, - }; - } - } - if (typeof parameters.$ref === 'string') return { ...parameters, type: 'object' }; - } - return { type: 'object', properties: {}, additionalProperties: true }; -} - -function resolveLocalRef( - root: Record, - ref: string, -): Record | undefined { - if (!ref.startsWith('#/')) return undefined; - let current: unknown = root; - for (const segment of ref.slice(2).split('/')) { - if (!isRecord(current)) return undefined; - current = current[segment.replace(/~1/gu, '/').replace(/~0/gu, '~')]; - } - return isRecord(current) ? current : undefined; -} - -// --------------------------------------------------------------------------- -// Response -// --------------------------------------------------------------------------- - -/** Splits SSE text into the JSON payload of each `data:` line, dropping comments and `[DONE]`. */ -/** One SSE line's event payload, or undefined when the line carries none. */ -export function parseCommandCodeCliEventLine(line: string): unknown | undefined { - let trimmed = line.trim(); - if (!trimmed || trimmed.startsWith(':') || trimmed.startsWith('event:')) return undefined; - if (trimmed.startsWith('data:')) trimmed = trimmed.slice(5).trim(); - if (!trimmed || trimmed === '[DONE]') return undefined; - try { - return JSON.parse(trimmed); - } catch { - // A non-JSON line is not an event on this wire. - return undefined; - } -} - -export interface CommandCodeCliStreamOutcome { - /** The wire said the turn ended. An unterminated stream was truncated. */ - readonly finished: boolean; - /** The in-band `error` event that ended the turn, if one arrived. */ - readonly error?: { readonly message: string; readonly statusCode?: number }; -} - -/** - * Reads one complete CLI stream body the way {@link CommandCodeCliLanguageModel} - * reads it incrementally. HTTP 200 is only the handshake on this wire, so a - * caller that stops at the status (the connection probe) would accept a body - * whose first event is a rejection. - */ -export function summarizeCommandCodeCliStream(body: string): CommandCodeCliStreamOutcome { - for (const line of body.split('\n')) { - const event = parseCommandCodeCliEventLine(line); - if (!isRecord(event)) continue; - if (event.type === 'error') { - const { message, statusCode } = streamErrorFacts(event); - return { - finished: false, - error: { message, ...(statusCode === undefined ? {} : { statusCode }) }, - }; - } - if (event.type === 'finish') return { finished: true }; - } - return { finished: false }; -} - -function sseDataLines(): TransformStream { - let buffer = ''; - const emit = (line: string, controller: TransformStreamDefaultController) => { - const event = parseCommandCodeCliEventLine(line); - if (event !== undefined) controller.enqueue(event); - }; - return new TransformStream({ - transform(chunk, controller) { - buffer += chunk; - const lines = buffer.split('\n'); - buffer = lines.pop() ?? ''; - for (const line of lines) emit(line, controller); - }, - flush(controller) { - if (buffer) emit(buffer, controller); - }, - }); -} - -function cliEventsToStreamParts(input: { - warnings: SharedV4Warning[]; - url: string; - body: unknown; - modelId: string; -}): TransformStream { - let textId: string | undefined; - let reasoningId: string | undefined; - let nextId = 0; - let finished = false; - // The wire splits one call's usage across two events: `finish-step` carries - // the provider's OWN body (`prompt_tokens`, `prompt_cache_hit_tokens`, …) - // under `usage.raw`, and `finish` carries only the normalized totals. The - // provider keys are what telemetry's cache accounting reads, so keep the - // body here until the finish event can carry it out. - let providerUsageRaw: Record | undefined; - const closeText = (controller: TransformStreamDefaultController) => { - if (textId === undefined) return; - controller.enqueue({ type: 'text-end', id: textId }); - textId = undefined; - }; - const closeReasoning = ( - controller: TransformStreamDefaultController, - ) => { - if (reasoningId === undefined) return; - controller.enqueue({ type: 'reasoning-end', id: reasoningId }); - reasoningId = undefined; - }; - return new TransformStream({ - start(controller) { - controller.enqueue({ type: 'stream-start', warnings: input.warnings }); - controller.enqueue({ type: 'response-metadata', modelId: input.modelId }); - }, - transform(event, controller) { - if (!isRecord(event) || finished) return; - switch (event.type) { - case 'text-delta': { - closeReasoning(controller); - if (textId === undefined) { - textId = `text-${nextId++}`; - controller.enqueue({ type: 'text-start', id: textId }); - } - controller.enqueue({ - type: 'text-delta', - id: textId, - delta: stringValue(event.text) ?? '', - }); - break; - } - case 'text-end': - closeText(controller); - break; - case 'reasoning-start': - closeText(controller); - break; - case 'reasoning-delta': { - closeText(controller); - if (reasoningId === undefined) { - reasoningId = `reasoning-${nextId++}`; - controller.enqueue({ type: 'reasoning-start', id: reasoningId }); - } - controller.enqueue({ - type: 'reasoning-delta', - id: reasoningId, - delta: stringValue(event.text) ?? '', - }); - break; - } - case 'reasoning-end': - closeReasoning(controller); - break; - case 'tool-call': { - closeText(controller); - closeReasoning(controller); - const id = stringValue(event.toolCallId) ?? randomUUID(); - const toolName = stringValue(event.toolName) ?? ''; - const args = JSON.stringify(recordOrEmpty(event.input ?? event.args ?? event.arguments)); - controller.enqueue({ type: 'tool-input-start', id, toolName }); - controller.enqueue({ type: 'tool-input-delta', id, delta: args }); - controller.enqueue({ type: 'tool-input-end', id }); - controller.enqueue({ type: 'tool-call', toolCallId: id, toolName, input: args }); - break; - } - case 'finish-step': { - // Carries the provider's own usage body; `finish` normalizes it away. - const usage = isRecord(event.usage) ? event.usage : undefined; - if (isRecord(usage?.raw)) providerUsageRaw = usage.raw; - break; - } - case 'finish': { - closeText(controller); - closeReasoning(controller); - finished = true; - controller.enqueue({ - type: 'finish', - usage: usageFromEvent(event.totalUsage, providerUsageRaw), - finishReason: mapFinishReason(event.finishReason), - }); - break; - } - case 'error': { - closeText(controller); - closeReasoning(controller); - finished = true; - controller.enqueue({ type: 'error', error: streamErrorToApiCallError(event, input) }); - break; - } - default: - break; - } - }, - flush(controller) { - closeText(controller); - closeReasoning(controller); - if (!finished) { - // The connection closed before the wire said it was done: report the - // truncation instead of a clean stop so the caller can retry. - controller.enqueue({ - type: 'error', - error: new APICallError({ - message: 'Command Code GO stream ended without a finish event', - url: input.url, - requestBodyValues: input.body, - isRetryable: true, - }), - }); - } - }, - }); -} - -function usageFromEvent( - value: unknown, - providerRaw?: Record, -): LanguageModelV4Usage { - if (!isRecord(value)) return emptyUsage(); - const inputDetails = isRecord(value.inputTokenDetails) ? value.inputTokenDetails : undefined; - const outputDetails = isRecord(value.outputTokenDetails) ? value.outputTokenDetails : undefined; - const total = numberValue(value.inputTokens); - const cacheRead = numberValue(inputDetails?.cacheReadTokens); - const cacheWrite = numberValue(inputDetails?.cacheWriteTokens); - const noCache = - numberValue(inputDetails?.noCacheTokens) ?? - (total === undefined ? undefined : Math.max(0, total - (cacheRead ?? 0) - (cacheWrite ?? 0))); - return { - inputTokens: { total, noCache, cacheRead, cacheWrite }, - outputTokens: { - total: numberValue(value.outputTokens), - text: undefined, - reasoning: numberValue(outputDetails?.reasoningTokens), - }, - // `raw` must stay the PROVIDER's own usage body, which is where the - // OpenAI/Anthropic cache keys live (`prompt_tokens`, - // `prompt_tokens_details.cached_tokens`, `cache_read_input_tokens`, …). - // Only `finish-step` carries that body; `finish.totalUsage` is already - // normalized, so carrying the envelope here left telemetry's strict reader - // looking for `prompt_tokens` on an object that had only `inputTokens` — - // every attempt settled as `usageBasis: 'missing'` and the composer's - // context gauge never moved. - // - // The fallback keeps a body-less event readable on the normalized keys - // rather than handing telemetry `undefined`. - raw: (providerRaw ?? value) as LanguageModelV4Usage['raw'], - }; -} - -function emptyUsage(): LanguageModelV4Usage { - return { - inputTokens: { - total: undefined, - noCache: undefined, - cacheRead: undefined, - cacheWrite: undefined, - }, - outputTokens: { total: undefined, text: undefined, reasoning: undefined }, - }; -} - -export function mapFinishReason(reason: unknown): LanguageModelV4FinishReason { - const raw = stringValue(reason); - if (raw === 'tool-calls' || raw === 'tool_calls') return { unified: 'tool-calls', raw }; - if ( - raw === 'length' || - raw === 'max_tokens' || - raw === 'max-tokens' || - raw === 'max_output_tokens' - ) { - return { unified: 'length', raw }; - } - if (raw === 'content-filter' || raw === 'content_filter') - return { unified: 'content-filter', raw }; - if (raw === 'error') return { unified: 'error', raw }; - return { unified: 'stop', raw }; -} - -/** - * An in-band `error` event becomes the same error shape a rejected HTTP - * response produces, so the runtime's provider-error classification reads - * status and structured code from one place. - */ -/** What one in-band `error` event states, shared by the stream and the probe. */ -function streamErrorFacts(event: Record): { - message: string; - statusCode?: number; - detail?: Record; - explicitRetryable?: boolean; -} { - const detail = isRecord(event.error) ? event.error : undefined; - const message = - stringValue(detail?.message) ?? - stringValue(event.message) ?? - (detail ? JSON.stringify(detail) : stringValue(event.error)) ?? - 'Stream error'; - const statusCode = numberValue(detail?.statusCode) ?? numberValue(detail?.status); - return { - message, - ...(statusCode !== undefined ? { statusCode } : {}), - ...(detail !== undefined ? { detail } : {}), - ...(typeof detail?.isRetryable === 'boolean' ? { explicitRetryable: detail.isRetryable } : {}), - }; -} - -function streamErrorToApiCallError( - event: Record, - input: { url: string; body: unknown }, -): APICallError { - const { message, statusCode, detail, explicitRetryable } = streamErrorFacts(event); - const isRetryable = - explicitRetryable ?? - (statusCode !== undefined ? statusCode === 429 || statusCode >= 500 : false); - return new APICallError({ - message: `Command Code GO stream error: ${message}`, - url: input.url, - requestBodyValues: input.body, - ...(statusCode !== undefined ? { statusCode } : {}), - responseBody: JSON.stringify({ error: detail ?? { message } }), - isRetryable, - data: detail, - }); -} - -// --------------------------------------------------------------------------- -// Small helpers -// --------------------------------------------------------------------------- - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function stringValue(value: unknown): string | undefined { - return typeof value === 'string' ? value : undefined; -} - -function numberValue(value: unknown): number | undefined { - return typeof value === 'number' && Number.isFinite(value) ? value : undefined; -} - -function recordOrEmpty(value: unknown): Record { - if (isRecord(value)) return value; - if (typeof value === 'string') { - try { - const parsed: unknown = JSON.parse(value); - if (isRecord(parsed)) return parsed; - } catch { - // Not JSON: the model produced no usable arguments. - } - } - return {}; -} - -function definedHeaders( - headers: Record | undefined, -): Record { - const out: Record = {}; - for (const [key, value] of Object.entries(headers ?? {})) { - if (value !== undefined) out[key] = value; - } - return out; -} - -function headersToRecord(headers: Headers): Record { - const out: Record = {}; - headers.forEach((value, key) => { - out[key] = value; - }); - return out; -} - -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/packages/runtime/src/connection-usage.ts b/packages/runtime/src/connection-usage.ts deleted file mode 100644 index 5535089557..0000000000 --- a/packages/runtime/src/connection-usage.ts +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Provider account usage, normalized into `@maka/core/connection-usage`. - * - * The dispatch is one `switch`: each provider that reports usage owns a mapper - * from its own endpoints to the shared window list, and every other provider - * answers `unavailable: unsupported`. The card that renders the result never - * learns the provider — only the windows and stats this produces. - */ -import type { ProviderType } from '@maka/core/llm-connections'; -import type { - ConnectionUsageReport, - ConnectionUsageResult, - UsageStats, - UsageWindow, -} from '@maka/core/connection-usage'; -import { providerReportsUsage } from '@maka/core/connection-usage'; -import { commandCodeCliHeaders } from './commandcode-cli-language-model.js'; -import { fetchForConnectionEffect, type ConnectionEffectFetch } from './connection-effect-fetch.js'; - -export interface ConnectionUsageInput { - readonly providerType: ProviderType; - readonly apiKey: string | undefined; - readonly baseUrl: string | undefined; - /** Injected for tests; production passes nothing and the global fetch is used. */ - readonly fetch?: ConnectionEffectFetch; -} - -export async function fetchConnectionUsage( - input: ConnectionUsageInput, -): Promise { - if (!providerReportsUsage(input.providerType)) { - return { kind: 'unavailable', reason: 'unsupported' }; - } - switch (input.providerType) { - case 'commandcode-go': - return fetchCommandCodeGoUsage(input); - // `commandcode` (the Provider-API plan) is deliberately NOT wired yet: its - // usage lives behind the same `/alpha/*` endpoints, but only a GO-tier key - // has been verified against them. Wiring it from inference alone could - // mis-report a plan's windows, which is worse than showing nothing. - default: - return { kind: 'unavailable', reason: 'unsupported' }; - } -} - -/** - * Command Code's account endpoints, fetched together with per-endpoint - * degradation: whichever endpoints answer, their facts land in the report, and - * only a total failure (no credential, or nothing answered) becomes - * `unavailable`. - */ -async function fetchCommandCodeGoUsage( - input: ConnectionUsageInput, -): Promise { - if (!input.apiKey) return { kind: 'unavailable', reason: 'no-credential' }; - const base = (input.baseUrl ?? 'https://api.commandcode.ai').replace(/\/+$/u, ''); - const headers = commandCodeCliHeaders(input.apiKey, 'maka'); - // One endpoint's outcome. A 401/403 is kept as `unauthorized` rather than - // flattened into "no data": the caller has to be able to tell a dead key from - // an unreachable host, because only one of the two is fixed on this page. - type Endpoint = - | { ok: true; body: Record | undefined } - | { ok: false; unauthorized: boolean }; - const get = async (path: string): Promise => { - try { - const response = await fetchForConnectionEffect(input.fetch, `${base}${path}`, { headers }); - if (!response.ok) { - return { ok: false, unauthorized: response.status === 401 || response.status === 403 }; - } - return { - ok: true, - body: (await response.readJson()) as Record | undefined, - }; - } catch { - return { ok: false, unauthorized: false }; - } - }; - - const [whoami, summary, credits, subscriptions] = await Promise.all([ - get('/alpha/whoami'), - get('/alpha/usage/summary'), - get('/alpha/billing/credits'), - get('/alpha/billing/subscriptions'), - ]); - const endpoints = [whoami, summary, credits, subscriptions]; - const answered = endpoints.filter((endpoint) => endpoint.ok) as Array< - Extract - >; - const refused = endpoints.some((endpoint) => !endpoint.ok && endpoint.unauthorized); - if (answered.length === 0) { - // Every endpoint failed at once. If any rejection was an auth rejection, the - // credential is the problem, not the network. - return { kind: 'unavailable', reason: refused ? 'unauthorized' : 'network' }; - } - // Some answered and some refused: a key that is valid but lacks a scope. The - // read is reported (so the card shows what it has) with the refusal flagged, so - // it can also say why the rest is missing — otherwise a partial report reads - // as a complete one and the page cannot name the credential as the cause. - const partiallyUnauthorized = refused; - const body = (endpoint: Extract) => endpoint.body; - const [whoamiBody, summaryBody, creditsBody, subscriptionsBody] = [ - whoami.ok ? body(whoami) : undefined, - summary.ok ? body(summary) : undefined, - credits.ok ? body(credits) : undefined, - subscriptions.ok ? body(subscriptions) : undefined, - ]; - - const plan = subscriptionFacts(subscriptionsBody); - const windowLimits = record(record(creditsBody)?.windowLimits); - const windows: UsageWindow[] = []; - const fiveHour = parseWindow('fiveHour', windowLimits?.fiveHour); - if (fiveHour) windows.push(fiveHour); - const weekly = parseWindow('weekly', windowLimits?.weekly); - if (weekly) windows.push(weekly); - const monthly = parseMonthly(creditsBody, plan); - if (monthly) windows.push(monthly); - - const stats = parseStats(summaryBody); - const report: ConnectionUsageReport = { - ...(accountLabel(whoamiBody) !== undefined ? { accountLabel: accountLabel(whoamiBody)! } : {}), - ...(plan.name !== undefined ? { planLabel: plan.name } : {}), - ...(stats !== undefined ? { stats } : {}), - windows, - ...(plan.periodEnd !== undefined ? { periodEnd: plan.periodEnd } : {}), - ...(partiallyUnauthorized ? { partiallyUnauthorized: true } : {}), - fetchedAt: Date.now(), - }; - return { kind: 'report', report }; -} - -/** - * `command-code` subscription plans and their monthly credit allowance. The - * billing endpoint reports the monthly REMAINING balance, not the total, so the - * monthly window's cap has to come from the plan — the same table the official - * CLI resolves `getPlanInfo` from. Longest-prefix match so `individual-pro-v1` - * wins over `individual-pro`. - */ -const SUBSCRIPTION_PLAN_MONTHLY_CREDITS: Readonly> = { - 'individual-go': 10, - 'individual-goat': 70, - 'individual-pro-v1': 80, - 'individual-pro': 30, - 'individual-provider': 15, - 'individual-max': 150, - 'individual-ultra': 300, - 'teams-pro': 40, -}; -const SUBSCRIPTION_PLAN_NAMES: Readonly> = { - 'individual-go': 'Go', - 'individual-goat': 'GOAT', - 'individual-pro': 'Pro', - 'individual-provider': 'Provider', - 'individual-max': 'Max', - 'individual-ultra': 'Ultra', - 'teams-pro': 'Teams Pro', -}; - -interface PlanFacts { - readonly name?: string; - readonly monthlyCredits?: number; - readonly periodEnd?: number; -} - -function subscriptionFacts(subscriptions: Record | undefined): PlanFacts { - const data = record(subscriptions?.data); - const planId = text(data?.planId); - const periodEnd = date(data?.currentPeriodEnd); - if (planId === undefined) return periodEnd === undefined ? {} : { periodEnd }; - const prefixes = Object.keys(SUBSCRIPTION_PLAN_MONTHLY_CREDITS).sort( - (a, b) => b.length - a.length, - ); - const prefix = prefixes.find((candidate) => planId.startsWith(candidate)); - return { - ...(prefix === undefined ? {} : { name: SUBSCRIPTION_PLAN_NAMES[prefix] }), - ...(prefix === undefined ? {} : { monthlyCredits: SUBSCRIPTION_PLAN_MONTHLY_CREDITS[prefix] }), - ...(periodEnd === undefined ? {} : { periodEnd }), - }; -} - -function parseWindow(id: 'fiveHour' | 'weekly', value: unknown): UsageWindow | undefined { - const block = record(value); - if (block === undefined) return undefined; - const used = number(block.used); - const cap = number(block.cap); - if (used === undefined && cap === undefined) return undefined; - const resetAt = number(block.resetAt); - return { - id, - used: used ?? 0, - cap: cap ?? 0, - unit: 'credits', - ...(resetAt !== undefined && resetAt > 0 ? { resetsAt: resetAt } : {}), - }; -} - -/** - * The monthly window: cap from the plan, consumed = cap − remaining balance. - * Absent unless both the plan's total and the reported remaining balance are - * known — an unknown plan would otherwise render `limit − 0`, i.e. a confident - * "100% used" that is really "we do not know the limit". - */ -function parseMonthly( - credits: Record | undefined, - plan: PlanFacts, -): UsageWindow | undefined { - const remaining = number(record(record(credits)?.credits)?.monthlyCredits); - if (plan.monthlyCredits === undefined || remaining === undefined) return undefined; - const used = Math.max(0, plan.monthlyCredits - remaining); - return { - id: 'monthly', - used, - cap: plan.monthlyCredits, - unit: 'credits', - ...(plan.periodEnd !== undefined ? { resetsAt: plan.periodEnd } : {}), - }; -} - -function parseStats(summary: Record | undefined): UsageStats | undefined { - if (summary === undefined) return undefined; - const stats: UsageStats = { - ...(number(summary.totalCount) !== undefined ? { requests: number(summary.totalCount)! } : {}), - ...(number(summary.failedCount) !== undefined ? { failed: number(summary.failedCount)! } : {}), - ...(number(summary.successRate) !== undefined - ? { successRate: number(summary.successRate)! } - : {}), - ...(number(summary.totalCost) !== undefined ? { cost: number(summary.totalCost)! } : {}), - ...(number(summary.totalTokensIn) !== undefined - ? { tokensIn: number(summary.totalTokensIn)! } - : {}), - ...(number(summary.totalTokensOut) !== undefined - ? { tokensOut: number(summary.totalTokensOut)! } - : {}), - }; - return Object.keys(stats).length > 0 ? stats : undefined; -} - -function accountLabel(whoami: Record | undefined): string | undefined { - const user = record(whoami?.user); - return text(user?.userName) ?? text(user?.name); -} - -function record(value: unknown): Record | undefined { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function number(value: unknown): number | undefined { - return typeof value === 'number' && Number.isFinite(value) ? value : undefined; -} - -function text(value: unknown): string | undefined { - return typeof value === 'string' && value.length > 0 ? value : undefined; -} - -function date(value: unknown): number | undefined { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string') { - const parsed = Date.parse(value); - if (Number.isFinite(parsed)) return parsed; - } - return undefined; -} diff --git a/packages/runtime/src/model-factory.ts b/packages/runtime/src/model-factory.ts index 407621d8de..c1eb52253b 100644 --- a/packages/runtime/src/model-factory.ts +++ b/packages/runtime/src/model-factory.ts @@ -61,7 +61,6 @@ import { type ResolvedModelRuntime, } from './model-runtime.js'; import { openAiCodexHeaders } from './subscription-auth.js'; -import { CommandCodeCliLanguageModel } from './commandcode-cli-language-model.js'; import { createRequestCustomizationFetch } from './request-customization-fetch.js'; import { createStreamUsageFallbackFetch } from './stream-usage-fallback-fetch.js'; import { withOpenCodeSessionHeader } from './opencode-session-header.js'; @@ -176,14 +175,6 @@ export function getAIModel(input: ModelFactoryInput): LanguageModelV4 { case 'cohere': return createCohere({ apiKey, baseURL, fetch: requestFetch })(modelId); - case 'commandcode-cli': - return new CommandCodeCliLanguageModel({ - modelId, - apiKey, - apiBase: baseURL, - fetch: requestFetch, - }); - case 'openai-compatible': { if (adapter.requireBaseUrl && !baseURL) { throw new Error( @@ -799,14 +790,6 @@ function buildFamilyWire( ? { thinking: { type: 'disabled' as const } } : {}, }; - case 'commandcode-cli': - // The CLI wire takes `reasoning_effort` as the CLI's own effort words. - // `max` is one of them (claude-fable-5-1, moonshotai/Kimi-K3, …), so the - // chosen level is forwarded verbatim — rounding it down would silently - // send a weaker request than the user asked for. - return level === undefined || level === 'off' - ? {} - : { 'commandcode-cli': { reasoningEffort: level } }; default: return {}; } diff --git a/packages/runtime/src/model-fetcher.ts b/packages/runtime/src/model-fetcher.ts index 8b140ae226..eafe33e807 100644 --- a/packages/runtime/src/model-fetcher.ts +++ b/packages/runtime/src/model-fetcher.ts @@ -219,10 +219,7 @@ async function fetchProviderModelsStrict( return filterDiscoveredModels(models, discovery.filter); } case 'openai': - case 'openai-compatible': - // The CLI transport lists models through the Provider API's `/models`, - // which every plan may read. - case 'commandcode-cli': { + case 'openai-compatible': { const r = await fetchForConnectionEffect( fetchFn, modelListUrl(baseUrl, discovery.path, discovery.query), diff --git a/packages/runtime/src/model-runtime.ts b/packages/runtime/src/model-runtime.ts index 644e954424..72dad8e5c7 100644 --- a/packages/runtime/src/model-runtime.ts +++ b/packages/runtime/src/model-runtime.ts @@ -43,8 +43,7 @@ export type ModelRuntimeWire = | 'openai-chat' | 'openai-responses' | 'google-generate' - | 'cohere-v2' - | 'commandcode-cli'; + | 'cohere-v2'; export type ReasoningReplayContract = | { kind: 'none' } @@ -80,14 +79,6 @@ type ModelRuntimeCall = wire: 'cohere-v2'; adapter: Extract; reasoningReplay: { kind: 'none' }; - } - | { - wire: 'commandcode-cli'; - adapter: Extract; - // The gateway rebuilds the upstream request from the replayed blocks and - // DeepSeek thinking mode rejects a tool loop whose history lacks its - // reasoning, so assistant reasoning is replayed as a `reasoning` block. - reasoningReplay: { kind: 'openai-chat-plaintext'; requestField: 'reasoning' }; }; export type ResolvedModelRuntime = ModelRuntimeCall & { @@ -229,14 +220,6 @@ function adapterCalls(adapter: ProviderRuntimeAdapter): ModelRuntimeCall[] { return [{ adapter, wire: 'google-generate', reasoningReplay: { kind: 'none' } }]; case 'cohere': return [{ adapter, wire: 'cohere-v2', reasoningReplay: { kind: 'none' } }]; - case 'commandcode-cli': - return [ - { - adapter, - wire: 'commandcode-cli', - reasoningReplay: { kind: 'openai-chat-plaintext', requestField: 'reasoning' }, - }, - ]; case 'openai-codex': return [ { diff --git a/packages/runtime/src/test-connection.ts b/packages/runtime/src/test-connection.ts index 29519d0c59..67e9bf890c 100644 --- a/packages/runtime/src/test-connection.ts +++ b/packages/runtime/src/test-connection.ts @@ -18,12 +18,6 @@ */ import { randomUUID } from 'node:crypto'; -import { - buildCommandCodeCliRequest, - commandCodeCliGenerateUrl, - commandCodeCliHeaders, - summarizeCommandCodeCliStream, -} from './commandcode-cli-language-model.js'; import { PROVIDER_REGISTRY, effectiveBaseUrl, @@ -256,65 +250,7 @@ async function testConnectionModel( ); case 'cohere': return await probeCohere(baseUrl, secret, testModel, t0, fetchFn); - case 'commandcode-cli': - return await probeCommandCodeCli(baseUrl, secret, testModel, t0, fetchFn, requestHeaders); - } -} - -async function probeCommandCodeCli( - baseUrl: string, - apiKey: string, - model: string, - t0: number, - fetchFn: ConnectionEffectFetch | undefined, - requestHeaders: Readonly> | undefined, -): Promise { - const { body } = buildCommandCodeCliRequest( - { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }], maxOutputTokens: 16 }, - { modelId: model }, - ); - const r = await fetchForConnectionEffect(fetchFn, commandCodeCliGenerateUrl(baseUrl), { - method: 'POST', - headers: { ...commandCodeCliHeaders(apiKey, 'maka'), ...(requestHeaders ?? {}) }, - body: JSON.stringify(body), - timeoutMs: CONNECTION_TEST_TIMEOUT_MS, - }); - if (!r.ok) return httpFailure(r, t0); - // HTTP 200 is only the handshake on this wire. The generation adapter fails - // the send on an in-band `error` event and on a stream that ends without a - // `finish`, so a probe that stopped at the status would store a connection - // as verified whose very next send is rejected. - const outcome = summarizeCommandCodeCliStream( - await r.readText(CONNECTION_EFFECT_JSON_BODY_MAX_BYTES), - ); - const latencyMs = Date.now() - t0; - if (outcome.error) { - const { message, statusCode } = outcome.error; - return { - ok: false, - latencyMs, - errorMessage: message.slice(0, 200), - ...(statusCode === undefined ? {} : { statusCode }), - errorClass: commandCodeCliStreamErrorClass(statusCode), - }; - } - if (!outcome.finished) { - return { - ok: false, - latencyMs, - errorMessage: 'The Command Code GO stream ended before the turn finished', - errorClass: 'network', - }; } - return { ok: true, latencyMs, modelTested: model }; -} - -function commandCodeCliStreamErrorClass(statusCode: number | undefined): ConnectionTestErrorClass { - if (statusCode === 401 || statusCode === 403) return 'auth'; - if (statusCode === 429 || (statusCode !== undefined && statusCode >= 500)) { - return 'provider_unavailable'; - } - return 'unknown'; } async function probeGitHubCopilot( diff --git a/packages/storage/src/runtime-policy-stores.ts b/packages/storage/src/runtime-policy-stores.ts index 0f8303d61e..2d4cbd6c02 100644 --- a/packages/storage/src/runtime-policy-stores.ts +++ b/packages/storage/src/runtime-policy-stores.ts @@ -52,7 +52,6 @@ export { } from './runtime-policy/errors.js'; export type { BeginConnectionTestResult, - BeginConnectionUsageResult, BeginInteractiveOAuthLoginResult, BeginModelFetchResult, CompareAndSetOAuthCredentialInput, @@ -66,7 +65,6 @@ export type { ConnectionEffectPreparationFailure, ConnectionOnboardingTicket, ConnectionTestTicket, - ConnectionUsageTicket, InteractiveOAuthLoginCompletionResult, InteractiveOAuthLoginProvider, InteractiveOAuthLoginInput, @@ -269,8 +267,6 @@ function createWriterFacade(coordinator: RuntimePolicyCoordinator): RuntimePolic coordinator.beginConnectionTest(connectionId, modelId), completeConnectionTest: (ticket, result) => coordinator.completeConnectionTest(ticket, result), - beginConnectionUsage: (connectionId) => coordinator.beginConnectionUsage(connectionId), - completeConnectionUsage: (ticket) => coordinator.completeConnectionUsage(ticket), }, }; freezeFacade(stores); diff --git a/packages/storage/src/runtime-policy/coordinator.ts b/packages/storage/src/runtime-policy/coordinator.ts index 5269c08320..c28b796bfc 100644 --- a/packages/storage/src/runtime-policy/coordinator.ts +++ b/packages/storage/src/runtime-policy/coordinator.ts @@ -110,7 +110,6 @@ import { connectionRequestHeadersLocator, type CredentialStatusQueryResult, type BeginConnectionTestResult, - type BeginConnectionUsageResult, type BoundCredentialMaterialExportResult, type BeginModelFetchResult, type BeginInteractiveOAuthLoginResult, @@ -123,7 +122,6 @@ import { type CommitConnectionOnboardingResult, type ConnectionOnboardingTicket, type ConnectionTestTicket, - type ConnectionUsageTicket, type InteractiveOAuthLoginCompletionResult, type InteractiveOAuthLoginInput, type InteractiveOAuthLoginProvider, @@ -171,7 +169,7 @@ interface PreparedConnectionMaterial { readonly networkProxy: RuntimePolicy['networkProxy']; } -type ConnectionTicketKind = 'model_fetch' | 'connection_test' | 'connection_usage'; +type ConnectionTicketKind = 'model_fetch' | 'connection_test'; type TicketState = 'available' | 'in_flight' | 'consumed'; type EffectiveProxyConfigurationBasis = @@ -207,9 +205,6 @@ type SemanticConnectionBasis = readonly kind: 'connection_test'; readonly requestBodyOverlayJson: string; readonly model: ConnectionTestModelBasis; - }) - | (CommonSemanticConnectionBasis & { - readonly kind: 'connection_usage'; }); interface ConnectionTicketRecord { @@ -1664,30 +1659,6 @@ export class RuntimePolicyCoordinator { ); } - beginConnectionUsage(connectionId: string): Promise { - return this.inLane(async (root) => { - const prepared = await this.prepareConnectionOperation(root, connectionId, 'read_usage'); - if (prepared.kind !== 'ready') return prepared; - const ticket = this.issueTicket('connection_usage', connectionUsageSemanticBasis(prepared)); - return deepFreeze({ - kind: 'ready' as const, - ticket: ticket as ConnectionUsageTicket, - connection: structuredClone(prepared.connection), - secretMaterial: prepared.secretMaterial, - networkProxy: structuredClone(prepared.networkProxy), - }); - }); - } - - /** - * Read-only counterpart of `completeConnectionTest`: nothing was written, so - * there is no catalog state to revalidate — the ticket is simply spent. - */ - async completeConnectionUsage(ticket: ConnectionUsageTicket): Promise { - const claimed = this.claimTicket(ticket, 'connection_usage'); - await this.completeClaimedTicket(claimed, async () => undefined); - } - private async prepareConnectionOperation( root: string, connectionId: string, @@ -2322,12 +2293,6 @@ function connectionTestSemanticBasis( }; } -function connectionUsageSemanticBasis( - prepared: PreparedConnectionMaterial, -): Extract { - return { kind: 'connection_usage', ...commonSemanticConnectionBasis(prepared) }; -} - function isCanonicalConnectionTestModel( connection: ConnectionCatalogEntry, modelId: string, @@ -2473,8 +2438,6 @@ function ticketLabel(kind: ConnectionTicketKind): string { return 'model fetch'; case 'connection_test': return 'connection test'; - case 'connection_usage': - return 'connection usage'; } } diff --git a/packages/storage/src/runtime-policy/operations.ts b/packages/storage/src/runtime-policy/operations.ts index 9811902796..15ea920e58 100644 --- a/packages/storage/src/runtime-policy/operations.ts +++ b/packages/storage/src/runtime-policy/operations.ts @@ -161,10 +161,6 @@ export interface ConnectionTestTicket { * consumed at completion and never validated against the catalog, because there * is no state to guard. */ -export interface ConnectionUsageTicket { - readonly [operationTicketBrand]: 'connection_usage'; -} - export interface InteractiveOAuthLoginTicket { readonly [operationTicketBrand]: 'interactive_oauth_login'; } @@ -275,16 +271,6 @@ export type BeginConnectionTestResult = readonly networkProxy: RuntimePolicy['networkProxy']; }; -export type BeginConnectionUsageResult = - | ConnectionEffectPreparationFailure - | { - readonly kind: 'ready'; - readonly ticket: ConnectionUsageTicket; - readonly connection: ConnectionCatalogEntry; - readonly secretMaterial: RuntimePolicyOperationSecretMaterial; - readonly networkProxy: RuntimePolicy['networkProxy']; - }; - export type ConnectionEffectCompletionResult = | { readonly kind: 'committed'; readonly snapshot: ConnectionCatalogSnapshot } | { @@ -465,12 +451,6 @@ export interface RuntimePolicyOperationCoordinator { ticket: ConnectionTestTicket, result: ConnectionTestSummary, ): Promise; - beginConnectionUsage(connectionId: string): Promise; - /** - * Read-only: the usage report is returned to the caller and nothing is - * written, so there is nothing to revalidate. The ticket is simply spent. - */ - completeConnectionUsage(ticket: ConnectionUsageTicket): Promise; } export function connectionCredentialLocator(