From 3d695d4baa1f74a9c0a5b71478bed8f2e1aa6321 Mon Sep 17 00:00:00 2001 From: Joob1n Date: Sun, 20 Sep 2026 23:16:57 +0800 Subject: [PATCH 1/4] chore(provider): remove the Command Code GO provider Command Code GO did not use a published API. It sent requests to the official CLI's private `/alpha/generate` endpoint behind the same identity headers that CLI presents (`x-command-code-version`, `x-cli-environment`, `x-taste-learning` and friends), and its account-usage card read the same `/alpha/*` endpoints with the same headers. The provider was withdrawn from the registry for that reason. Removing the provider takes its whole surface with it, because everything else existed only to serve it: - the `commandcode-cli` Runtime adapter, its `/alpha/generate` wire, and the CLI identity headers it forged; - the browser-assisted sign-in (`commandcode-browser-login`, `commandcode-login-ipc-main`, its renderer flow and section) that minted a key through the CLI's own loopback login; - the account-usage read end to end: the `connection.usage.read` operation and its Host coordinator, the storage `connection_usage` ticket machinery, the `read_usage` ProviderAuthAction, the runtime fetch/parse, the core report types, and the settings usage card; - the transport-acknowledgement gate, which existed only to tell a user that one provider presented another client's identity, and so is now an empty set. The ordinary `commandcode` provider stays: it is a plain API-key connection over the published Provider API, and it keeps the shared reasoning-effort table. The `connection.usage.read` operation is gone, so the compatibility epoch moves 169 -> 170; a peer older than this epoch may still advertise or submit it. Generated-by: Maka --- .../commandcode-browser-login-flow.test.ts | 201 ---- .../commandcode-browser-login.test.ts | 405 -------- .../commandcode-login-ipc-main.test.ts | 115 --- .../connection-settings-locale-render.test.ts | 52 - .../__tests__/provider-add-submission.test.ts | 34 - .../runtime-host-connections-ipc-main.test.ts | 36 - .../src/main/commandcode-browser-login.ts | 420 -------- .../src/main/commandcode-login-ipc-main.ts | 80 -- apps/desktop/src/main/runtime-host-boot.ts | 10 - apps/desktop/src/main/runtime-host-client.ts | 6 - .../main/runtime-host-connections-ipc-main.ts | 11 - apps/desktop/src/preload/bridge-contract.d.ts | 37 - apps/desktop/src/preload/preload.ts | 19 - .../commandcode-browser-login-flow.ts | 151 --- .../commandcode-browser-login-section.tsx | 102 -- .../connection-usage-card.tsx | 304 ------ .../features/connection-settings/index.ts | 7 - .../features/connection-settings/ports.ts | 53 - .../settings-provider-copy.ts | 15 - .../create-connection-settings-services.ts | 8 +- .../renderer/settings/provider-add-form.tsx | 71 +- .../settings/provider-add-submission.ts | 25 +- .../settings/provider-connection-detail.tsx | 15 +- .../settings/provider-display-copy.ts | 5 - docs/astryx-surface-file-inventory.md | 1 - .../core/src/__tests__/model-metadata.test.ts | 9 +- packages/core/src/connection-usage.ts | 120 --- packages/core/src/model-metadata.ts | 3 +- packages/core/src/provider-auth.ts | 8 +- packages/core/src/provider-registry.ts | 17 - .../connection-effects-protocol.test.ts | 129 --- .../src/protocol/connection-effects.ts | 170 ---- packages/runtime-host/src/protocol/index.ts | 5 +- .../runtime-host/src/protocol/operations.ts | 1 - .../server/connection-effect-coordinator.ts | 76 +- .../commandcode-cli-language-model.test.ts | 609 ------------ .../src/__tests__/connection-usage.test.ts | 242 ----- .../__tests__/model-factory-thinking.test.ts | 60 +- .../src/__tests__/provider-contract-matrix.ts | 3 - .../__tests__/provider-contract-overrides.ts | 118 --- .../src/commandcode-cli-language-model.ts | 914 ------------------ packages/runtime/src/connection-usage.ts | 282 ------ packages/runtime/src/model-factory.ts | 17 - packages/runtime/src/model-fetcher.ts | 5 +- packages/runtime/src/model-runtime.ts | 19 +- packages/runtime/src/test-connection.ts | 64 -- packages/storage/src/runtime-policy-stores.ts | 4 - .../storage/src/runtime-policy/coordinator.ts | 39 +- .../storage/src/runtime-policy/operations.ts | 20 - 49 files changed, 39 insertions(+), 5078 deletions(-) delete mode 100644 apps/desktop/src/main/__tests__/commandcode-browser-login-flow.test.ts delete mode 100644 apps/desktop/src/main/__tests__/commandcode-browser-login.test.ts delete mode 100644 apps/desktop/src/main/__tests__/commandcode-login-ipc-main.test.ts delete mode 100644 apps/desktop/src/main/commandcode-browser-login.ts delete mode 100644 apps/desktop/src/main/commandcode-login-ipc-main.ts delete mode 100644 apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-flow.ts delete mode 100644 apps/desktop/src/renderer/features/connection-settings/commandcode-browser-login-section.tsx delete mode 100644 apps/desktop/src/renderer/features/connection-settings/connection-usage-card.tsx delete mode 100644 packages/core/src/connection-usage.ts delete mode 100644 packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts delete mode 100644 packages/runtime/src/__tests__/connection-usage.test.ts delete mode 100644 packages/runtime/src/commandcode-cli-language-model.ts delete mode 100644 packages/runtime/src/connection-usage.ts 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..37b483b73e 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: '未设置自定义请求头。', @@ -495,11 +490,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: '未設定自訂請求頭。', @@ -720,11 +710,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.', 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..2b2b3d33d2 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -304,11 +304,6 @@ 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' }, }, - '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' }, - }, groq: { 'zh-CN': { name: 'Groq', description: 'LPU 高速推理托管开源模型', badge: 'API' }, 'zh-TW': { name: 'Groq', description: 'LPU 高速推理託管開源模型', badge: 'API' }, diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index ef64cbdf30..0f6dc76116 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -55,7 +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 | 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/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..be3bfb0d34 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,21 +1489,6 @@ const providerRegistry = { signupUrl: 'https://commandcode.ai/docs/plans/goat', catalogOrder: 41.5, }, - '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' }, - category: 'overseas', - catalogGroup: 'plans', - signupUrl: 'https://commandcode.ai/docs/plans/go', - catalogOrder: 41.6, - }, 'cloudflare-workers-ai': { label: cloudflareWorkersAi.name, baseUrl: '', 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/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( From 06fafb3cacf1d6f873b99d04786904f4ef0b58ad Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 21 Sep 2026 10:12:09 +0800 Subject: [PATCH 2/4] fix(provider): retire Command Code GO instead of removing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review [P2] on #5545: deleting the entry outright leaves an existing connection *unknown* rather than *retired*. `isRetiredProvider` answers false for a type no longer in the registry, so the readiness guard never fires, a connection with a default model and an enabled model list reports `ready: true`, and the send is admitted only to throw "Unknown provider type" deep in model construction — the exact failure `connection-readiness.ts` documents above that guard. Keep the entry with `retired: true` and `runtimeAdapter: unavailable`, the pattern #5544 used for OpenCode Free. A stored row stays identifiable and displayable, readiness answers `provider_retired`, and the user lands on the existing `blocked:all_connections_retired` state with its "Add a model connection" call to action. `provider-catalog-contract` already pins the retired set and asserts each entry keeps no Runtime adapter and stays out of the add catalog, so this restores that entry rather than adding new machinery. Also regenerate the Astryx surface inventory, which still listed the two files this PR deletes and failed the CI step ahead of Typecheck. Generated-by: Maka --- .../settings/provider-display-copy.ts | 8 +++++++ docs/astryx-surface-file-inventory.md | 3 +-- docs/astryx-surface-file-inventory.paths | 2 -- .../provider-catalog-contract.test.ts | 2 +- packages/core/src/provider-registry.ts | 21 +++++++++++++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 2b2b3d33d2..3f44d175dc 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -304,6 +304,14 @@ 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: '已停用:该套餐通过官方 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' }, 'zh-TW': { name: 'Groq', description: 'LPU 高速推理託管開源模型', badge: 'API' }, diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 0f6dc76116..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,7 +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/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__/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/provider-registry.ts b/packages/core/src/provider-registry.ts index be3bfb0d34..bfeff09665 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -1489,6 +1489,27 @@ 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', + baseUrl: 'https://api.commandcode.ai', + authKind: 'api_key', + fallbackModels: [], + 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', + }, 'cloudflare-workers-ai': { label: cloudflareWorkersAi.name, baseUrl: '', From 9c36da2264e9d7929f9b73529ab6a46c956d8a20 Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 21 Sep 2026 10:42:03 +0800 Subject: [PATCH 3/4] chore(desktop): drop the browser-login copy left by the GO removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sign-in section that read `browserLogin` is deleted, so its three locale blocks — including the "sign in with your Command Code account" title and the port-range failure text — are unreachable. Remove them with it. --- .../settings-provider-copy.ts | 57 ------------------- 1 file changed, 57 deletions(-) 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 37b483b73e..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 @@ -279,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: '选择模型', @@ -498,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: '選擇模型', @@ -718,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', From 8cb346832dfd2e46a3392aa4c463b434882cbe1d Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 21 Sep 2026 11:40:17 +0800 Subject: [PATCH 4/4] fix(runtime-host): release the retired usage grant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on this branch failed in `Qualify durable state against the published baseline`: The released access credential carries grants this build cannot account for: connection.usage.read Removing an operation leaves its grant in every credential a released build already wrote, and `unresolvedPersistedGrants` reports a stored grant that the protocol no longer defines and no migration entry names. The released fixture carries `connection.usage.read`, so the forward roll refused it. Add the `release` entry, which drops the grant on decode and records that its authority went nowhere — the same treatment `turn.regenerate` and `execution.inspect.resolve` got when they were removed. Without it the grant survives in the record and is reported as unaccounted for on every read. Generated-by: Maka --- .../access-credential-grant-migration.test.ts | 13 +++++++++++++ .../src/server/access-credential-store.ts | 3 +++ 2 files changed, 16 insertions(+) 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/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' }],