diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c654e704..d3577cb8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ file is the human-readable summary of what changed and why. ### Gateway +- **The SSO provider is now AIGW** — signing in with SSO writes the gateway + provider as `aigw`, shown as "AIGW" in the model picker, replacing the + previous identity. Existing installs keep working: the old provider id stays + recognized everywhere it is probed, the desktop's silent key refresh re-keys + it too, and configs converge on the new id the next time the hub or a + configure flow rewrites them. + ([#340](https://github.com/quickbeard/codev-code/pull/340)) - **Per-model context limits** — configuring the gateway (the TUI `/configure` dialog, `codev configure`, or the desktop sign-in) used to stamp every model with the same 196,608-token window, so differently sized models all showed diff --git a/packages/app/src/components/gateway/gateway-configure-gate.tsx b/packages/app/src/components/gateway/gateway-configure-gate.tsx index 2551a31600..5223299986 100644 --- a/packages/app/src/components/gateway/gateway-configure-gate.tsx +++ b/packages/app/src/components/gateway/gateway-configure-gate.tsx @@ -137,10 +137,15 @@ export function GatewayConfigureGate() { if (result.status === "refreshed") { // Only the credential changed — the keyless config block still points at // the same gateway and model — so merge it into the auth registry and - // reload the server, skipping the config patch the full flow does. + // reload the server, skipping the config patch the full flow does. Every + // id the controller names gets the key (the current SSO identity plus + // its pre-rename spellings), so a legacy config block is re-keyed too. const client = serverSDK().client - await client.auth - .set({ providerID: result.providerID, auth: { type: "api", key: result.apiKey } }) + await Promise.all( + result.providerIDs.map((providerID) => + client.auth.set({ providerID, auth: { type: "api", key: result.apiKey } }), + ), + ) .then(() => client.global.dispose()) .catch(() => undefined) return diff --git a/packages/app/src/gateway/hidden-providers.test.ts b/packages/app/src/gateway/hidden-providers.test.ts index 36f499aafa..fa43df4188 100644 --- a/packages/app/src/gateway/hidden-providers.test.ts +++ b/packages/app/src/gateway/hidden-providers.test.ts @@ -38,12 +38,12 @@ describe("filterHiddenProviders", () => { }) test("keeps other connected providers and is idempotent", () => { - const input = list([provider("opencode", 0), provider("netgate", 3)], ["opencode", "netgate"], {}) + const input = list([provider("opencode", 0), provider("aigw", 3)], ["opencode", "aigw"], {}) const once = filterHiddenProviders(input) - expect(once.connected).toEqual(["netgate"]) + expect(once.connected).toEqual(["aigw"]) const twice = filterHiddenProviders(once) - expect(twice.connected).toEqual(["netgate"]) - expect([...twice.all.keys()]).toEqual(["netgate"]) + expect(twice.connected).toEqual(["aigw"]) + expect([...twice.all.keys()]).toEqual(["aigw"]) }) test("handles an empty catalog", () => { diff --git a/packages/app/src/gateway/types.ts b/packages/app/src/gateway/types.ts index 9001641c3d..5602011598 100644 --- a/packages/app/src/gateway/types.ts +++ b/packages/app/src/gateway/types.ts @@ -48,7 +48,10 @@ export type GatewayKeyCheck = | { status: "ok" } // A rejected key was re-minted silently; the renderer merges the credential // into the local server's auth registry, since only it writes to the server. - | { status: "refreshed"; providerID: string; apiKey: string } + // Every id in providerIDs gets the key: the current SSO identity plus its + // pre-rename spellings, so a config block written before an identity rename + // is re-keyed too instead of 401ing on a credential nothing updates. + | { status: "refreshed"; providerIDs: string[]; apiKey: string } // No key, or one that cannot be replaced without a browser sign-in. | { status: "needs-login" } diff --git a/packages/codev-gateway/src/auth-file.ts b/packages/codev-gateway/src/auth-file.ts index b21b5e9e55..a636f73b01 100644 --- a/packages/codev-gateway/src/auth-file.ts +++ b/packages/codev-gateway/src/auth-file.ts @@ -23,7 +23,7 @@ export interface ApiKeyCreds { baseUrl?: string model?: string // Set only by the hub's manual-key path, where the user names their own - // provider. Absent ⇒ the key is SSO-issued and takes the netGate default. + // provider. Absent ⇒ the key is SSO-issued and takes the AIGW default. providerId?: string providerName?: string } diff --git a/packages/codev-gateway/src/config-block.ts b/packages/codev-gateway/src/config-block.ts index 8c8321314a..29a1e51448 100644 --- a/packages/codev-gateway/src/config-block.ts +++ b/packages/codev-gateway/src/config-block.ts @@ -22,8 +22,10 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[]; // src/lib/configure.ts configureOpenCodeKind), so hub-managed and // self-configured installs are indistinguishable — the hub marks a config as // CoDev-managed by probing `provider.` against its candidate set (saved -// id, `netgate`, `ai-gateway`, legacy `aigateway`; hub PR #212), so hubs older -// than that PR cannot attribute a `netgate` config. The per-model `limit` is +// id, `aigw`, `ai-gateway`, legacy `netgate` and `aigateway`; hub PR #212 +// introduced the probe set), so hubs older than that PR cannot attribute a +// config written here, and hubs predating the AIGW rename cannot attribute an +// `aigw` one. The per-model `limit` is // mandatory: without it opencode sizes the context window to 0 and disables // auto-compaction; the modality block is mandatory for the same reason (hub PR // #219) — see MODEL_MODALITIES. Each model's numbers come from diff --git a/packages/codev-gateway/src/index.ts b/packages/codev-gateway/src/index.ts index a28f04808b..6f32b4eafd 100644 --- a/packages/codev-gateway/src/index.ts +++ b/packages/codev-gateway/src/index.ts @@ -26,6 +26,7 @@ export { buildCustomProviderConfig, buildGatewayConfig, normalizeGatewayBaseUrl export { CUSTOM_FALLBACK_PROVIDER, GATEWAY_PROVIDER, + LEGACY_GATEWAY_PROVIDER_IDS, providerFromName, slugifyProviderName, type ProviderIdentity, diff --git a/packages/codev-gateway/src/provider.ts b/packages/codev-gateway/src/provider.ts index 3d257da9b2..ed534de13c 100644 --- a/packages/codev-gateway/src/provider.ts +++ b/packages/codev-gateway/src/provider.ts @@ -10,10 +10,17 @@ export interface ProviderIdentity { // SSO-issued gateway keys (the configure gate's SSO flow, including the // manual-paste branch for accounts without a provisioned key). export const GATEWAY_PROVIDER: ProviderIdentity = { - id: "netgate", - name: "netGate", + id: "aigw", + name: "AIGW", } +// The SSO identity before the AIGW rename. Recognition-only: the candidate +// sets (config attribution, the inline-key migration, the upload readers) +// still match it and the desktop's silent key refresh re-keys it, but nothing +// writes new config blocks under this id any more — pre-rename installs keep +// working and converge on the new id at their next full config rewrite. +export const LEGACY_GATEWAY_PROVIDER_IDS: string[] = ["netgate"] + // The custom-provider flow's fallback, used when the user leaves the provider // name blank or types something that yields no usable id. export const CUSTOM_FALLBACK_PROVIDER: ProviderIdentity = { diff --git a/packages/codev-gateway/test/config-block.test.ts b/packages/codev-gateway/test/config-block.test.ts index 70df717cfb..ef5bf4b2cc 100644 --- a/packages/codev-gateway/test/config-block.test.ts +++ b/packages/codev-gateway/test/config-block.test.ts @@ -47,9 +47,9 @@ describe("buildGatewayConfig", () => { expect(config).toEqual({ compaction: { auto: true, reserved: 40000 }, provider: { - netgate: { + aigw: { npm: "@ai-sdk/openai-compatible", - name: "netGate", + name: "AIGW", options: { baseURL: "https://gw.test/v1" }, models: { "MiniMax/MiniMax-M2.7": { @@ -77,7 +77,7 @@ describe("buildGatewayConfig", () => { // so declaredInput's clamp pins input at the window itself. test("writes the table window for the known model", () => { const config = buildGatewayConfig({ baseUrl: "https://gw.test", model: "MiniMax/MiniMax-M3" }) - expect(config.provider.netgate.models["MiniMax/MiniMax-M3"].limit).toEqual({ + expect(config.provider.aigw.models["MiniMax/MiniMax-M3"].limit).toEqual({ context: 262144, input: 262144, output: 65536, @@ -87,7 +87,7 @@ describe("buildGatewayConfig", () => { // Without the declaration a custom-provider model resolves to text-only and // the session replaces an attached image with an error note (hub PR #219). test("declares image input so attachments are not stripped", () => { - const models = buildGatewayConfig({ baseUrl: "https://gw.test", model: "m", models: ["m", "n"] }).provider.netgate + const models = buildGatewayConfig({ baseUrl: "https://gw.test", model: "m", models: ["m", "n"] }).provider.aigw .models for (const id of ["m", "n"]) { expect(models[id].attachment).toBe(true) @@ -97,9 +97,9 @@ describe("buildGatewayConfig", () => { test("falls back to a single-model map and dedupes the default", () => { const config = buildGatewayConfig({ baseUrl: "https://gw.test/v1", model: "m" }) - expect(Object.keys(config.provider.netgate.models)).toEqual(["m"]) + expect(Object.keys(config.provider.aigw.models)).toEqual(["m"]) const withDupe = buildGatewayConfig({ baseUrl: "https://gw.test", model: "m", models: ["m", "n"] }) - expect(Object.keys(withDupe.provider.netgate.models)).toEqual(["m", "n"]) + expect(Object.keys(withDupe.provider.aigw.models)).toEqual(["m", "n"]) }) // A pinned default outranks the TUI's persisted selection on every startup, @@ -113,7 +113,7 @@ describe("buildGatewayConfig", () => { // id); an inline key would outrank it and re-expose the credential in the // config file. test("writes no apiKey unless one is passed", () => { - expect(buildGatewayConfig({ baseUrl: "https://gw.test", model: "m" }).provider.netgate.options).not.toHaveProperty( + expect(buildGatewayConfig({ baseUrl: "https://gw.test", model: "m" }).provider.aigw.options).not.toHaveProperty( "apiKey", ) }) @@ -122,7 +122,7 @@ describe("buildGatewayConfig", () => { // placeholder, since the guest has no auth store of its own. test("keeps an explicitly passed apiKey for the sandbox placeholder", () => { const options = buildGatewayConfig({ apiKey: "broker-secret", baseUrl: "https://gw.test", model: "m" }).provider - .netgate.options + .aigw.options expect(options.apiKey).toBe("broker-secret") }) }) diff --git a/packages/codev-gateway/test/provider.test.ts b/packages/codev-gateway/test/provider.test.ts index ed88456e25..d27ad699b6 100644 --- a/packages/codev-gateway/test/provider.test.ts +++ b/packages/codev-gateway/test/provider.test.ts @@ -3,7 +3,7 @@ import { CUSTOM_FALLBACK_PROVIDER, GATEWAY_PROVIDER, providerFromName, slugifyPr describe("identities", () => { test("match the hub's built-ins", () => { - expect(GATEWAY_PROVIDER).toEqual({ id: "netgate", name: "netGate" }) + expect(GATEWAY_PROVIDER).toEqual({ id: "aigw", name: "AIGW" }) expect(CUSTOM_FALLBACK_PROVIDER).toEqual({ id: "ai-gateway", name: "AI Gateway" }) }) }) diff --git a/packages/codev-upload/src/config.ts b/packages/codev-upload/src/config.ts index 00971469a2..50f71cb3af 100644 --- a/packages/codev-upload/src/config.ts +++ b/packages/codev-upload/src/config.ts @@ -20,9 +20,9 @@ function configPath(): string | null { } // Saved provider id first (a manually-named provider), then the built-in -// identities, then the pre-rename legacy id (hub codevProviderIds parity). +// identities, then the pre-rename legacy ids (hub codevProviderIds parity). function providerCandidates(): string[] { - const ids = ["netgate", "ai-gateway", "aigateway"] + const ids = ["aigw", "netgate", "ai-gateway", "aigateway"] const saved = loadApiKey()?.providerId if (saved && !ids.includes(saved)) ids.unshift(saved) return ids diff --git a/packages/codev-upload/src/nudge.ts b/packages/codev-upload/src/nudge.ts index 08f51c762d..49b4be941b 100644 --- a/packages/codev-upload/src/nudge.ts +++ b/packages/codev-upload/src/nudge.ts @@ -1,5 +1,5 @@ import { readFileSync } from "node:fs" -import { GATEWAY_PROVIDER, loadApiKey } from "@codev/gateway" +import { GATEWAY_PROVIDER, LEGACY_GATEWAY_PROVIDER_IDS, loadApiKey } from "@codev/gateway" import { lastUploadStatusPath } from "./paths" // The sign-in nudge: the auto-upload daemon needs a live CoDev SSO session, @@ -52,14 +52,15 @@ export function readLastUploadStatus(): UploadStatus | null { // in order: // - auto-upload not opted out (a user who disabled it asked for silence); // - a gateway api key exists AND is SSO-origin (no provider id, or the -// netgate identity). A manually-named provider may belong to someone -// with no CoDev SSO account at all — nagging them to sign in to -// something they can't would be wrong; +// AIGW identity, current or pre-rename). A manually-named provider may +// belong to someone with no CoDev SSO account at all — nagging them to +// sign in to something they can't would be wrong; // - the last daemon run recorded the not-logged-in stall. export function shouldNudgeSignIn(env: NodeJS.ProcessEnv = process.env): boolean { if (autoUploadDisabled(env)) return false const key = loadApiKey() if (!key?.apiKey) return false - if (key.providerId && key.providerId !== GATEWAY_PROVIDER.id) return false + if (key.providerId && key.providerId !== GATEWAY_PROVIDER.id && !LEGACY_GATEWAY_PROVIDER_IDS.includes(key.providerId)) + return false return readLastUploadStatus()?.skipped === "not-logged-in" } diff --git a/packages/codev-upload/test/nudge.test.ts b/packages/codev-upload/test/nudge.test.ts index ce00172869..ea80960daf 100644 --- a/packages/codev-upload/test/nudge.test.ts +++ b/packages/codev-upload/test/nudge.test.ts @@ -51,7 +51,13 @@ describe("shouldNudgeSignIn", () => { expect(shouldNudgeSignIn()).toBe(false) }) - test("explicit netgate provider id still counts as SSO-origin", () => { + test("explicit aigw provider id still counts as SSO-origin", () => { + saveApiKey({ apiKey: "sk-x", providerId: "aigw", providerName: "AIGW" }) + recordStall() + expect(shouldNudgeSignIn()).toBe(true) + }) + + test("the pre-rename netgate id still counts as SSO-origin", () => { saveApiKey({ apiKey: "sk-x", providerId: "netgate", providerName: "netGate" }) recordStall() expect(shouldNudgeSignIn()).toBe(true) diff --git a/packages/desktop/src/main/gateway-sso/controller.test.ts b/packages/desktop/src/main/gateway-sso/controller.test.ts index 244b8c49b4..e433fc90c6 100644 --- a/packages/desktop/src/main/gateway-sso/controller.test.ts +++ b/packages/desktop/src/main/gateway-sso/controller.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { GATEWAY_PROVIDER, type AuthData } from "@codev/gateway" +import { GATEWAY_PROVIDER, LEGACY_GATEWAY_PROVIDER_IDS, type AuthData } from "@codev/gateway" import { createGatewaySsoController } from "./controller" const auth: AuthData = { @@ -120,7 +120,7 @@ describe("gateway sso controller", () => { expect(controller.fetchSetup()).rejects.toThrow("backend down") }) - test("complete writes the auth file and returns the key beside a keyless netgate config", async () => { + test("complete writes the auth file and returns the key beside a keyless aigw config", async () => { const savedKeys: unknown[] = [] const cachedWindows: unknown[] = [] const controller = controllerWith({ @@ -147,14 +147,14 @@ describe("gateway sso controller", () => { expect(savedKeys).toEqual([{ apiKey: "sk-live", baseUrl: undefined, model: "model-a" }]) // Gateway-reported windows were cached for the config block's limitsFor. expect(cachedWindows).toEqual([{ "model-a": { context: 300000 } }]) - expect(result.providerID).toBe("netgate") + expect(result.providerID).toBe("aigw") // The key rides beside the config for the renderer's auth.set; the config // block itself must not carry it (codev.json stays credential-free). expect(result.apiKey).toBe("sk-live") const provider = (result.config as { provider: Record }> }).provider - expect(Object.keys(provider)).toEqual(["netgate"]) - expect(Object.keys(provider.netgate.models)).toEqual(["model-a", "model-b"]) - expect((provider.netgate as { options: Record }).options).not.toHaveProperty("apiKey") + expect(Object.keys(provider)).toEqual(["aigw"]) + expect(Object.keys(provider.aigw.models)).toEqual(["model-a", "model-b"]) + expect((provider.aigw as { options: Record }).options).not.toHaveProperty("apiKey") }) // The sandboxes read the auth file at boot, so they have to be told a login @@ -251,7 +251,8 @@ describe("gateway sso controller", () => { expect(await controller.ensureKey()).toEqual({ status: "refreshed", - providerID: GATEWAY_PROVIDER.id, + // Legacy ids ride along so a pre-rename config block is re-keyed too. + providerIDs: [GATEWAY_PROVIDER.id, ...LEGACY_GATEWAY_PROVIDER_IDS], apiKey: "sk-new", }) expect(saved?.apiKey).toBe("sk-new") diff --git a/packages/desktop/src/main/gateway-sso/controller.ts b/packages/desktop/src/main/gateway-sso/controller.ts index 4784dec046..2941957e8e 100644 --- a/packages/desktop/src/main/gateway-sso/controller.ts +++ b/packages/desktop/src/main/gateway-sso/controller.ts @@ -6,6 +6,7 @@ import { fetchModels, fetchModelWindows, GATEWAY_PROVIDER, + LEGACY_GATEWAY_PROVIDER_IDS, loadApiKey, loadGatewayUrl, login, @@ -235,7 +236,14 @@ export function createGatewaySsoController(deps: Deps) { deps.onConfigured?.() // The renderer owns writes to the local server, so hand it the credential // to merge into the auth registry the same way the configure flow does. - return { status: "refreshed", providerID: GATEWAY_PROVIDER.id, apiKey: refreshed } + // Legacy ids ride along: a config block written before an identity + // rename still reads its key from the old id, and this refresh is the + // only thing that updates it. + return { + status: "refreshed", + providerIDs: [GATEWAY_PROVIDER.id, ...LEGACY_GATEWAY_PROVIDER_IDS], + apiKey: refreshed, + } }, setManualApiKey(key: string) { if (!key.length) throw new Error("empty API key") diff --git a/packages/opencode/src/config/gateway-key-migration.ts b/packages/opencode/src/config/gateway-key-migration.ts index 989960bfb0..6da4f70294 100644 --- a/packages/opencode/src/config/gateway-key-migration.ts +++ b/packages/opencode/src/config/gateway-key-migration.ts @@ -21,10 +21,10 @@ import { isRecord } from "@/util/record" // refresh — this simply migrates it again on the next launch. // // The candidate ids mirror the hub's CoDev-managed probe set (hub -// src/lib/provider.ts): the SSO identity, the custom-flow fallback, its legacy -// spelling, plus the user-named provider id the hub's manual-key path records -// in the shared ~/.codev(-hub)/auth.json. -const CANDIDATE_IDS = ["netgate", "ai-gateway", "aigateway"] +// src/lib/provider.ts): the SSO identity, its pre-rename spelling, the +// custom-flow fallback, its legacy spelling, plus the user-named provider id +// the hub's manual-key path records in the shared ~/.codev(-hub)/auth.json. +const CANDIDATE_IDS = ["aigw", "netgate", "ai-gateway", "aigateway"] function candidateIds() { const saved = loadApiKey()?.providerId diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 102dd8c305..f09037e38a 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -672,7 +672,7 @@ it.instance( }, ) -// The netGate and custom-provider config blocks written by @codev/gateway +// The AIGW and custom-provider config blocks written by @codev/gateway // (config-block MODEL_MODALITIES) exist to flip these two flags: without the // declaration a config model is text-only and ProviderTransform.message drops // attached images before the request. @@ -680,7 +680,7 @@ it.instance( "declared image modality makes a custom-provider model attachment capable", Effect.gen(function* () { const providers = yield* list - const models = providers[ProviderV2.ID.make("netgate")].models + const models = providers[ProviderV2.ID.make("aigw")].models const bare = models["bare-model"] expect(bare.capabilities.attachment).toBe(false) expect(bare.capabilities.input.image).toBe(false) @@ -693,8 +693,8 @@ it.instance( { config: { provider: { - netgate: { - name: "netGate", + aigw: { + name: "AIGW", npm: "@ai-sdk/openai-compatible", env: [], models: { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9956f96961..4b9bbdcfaa 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5681,14 +5681,14 @@ describe("ProviderTransform.options - kimi family adaptive thinking", () => { }) }) -// The behavior the netGate/custom-provider config block buys: a model entry +// The behavior the AIGW/custom-provider config block buys: a model entry // with no `modalities` resolves image input to false, and the image never // reaches the endpoint. See @codev/gateway config-block MODEL_MODALITIES. describe("ProviderTransform.message - image input on a custom provider", () => { const createModel = (image: boolean) => ({ id: "MiniMax/MiniMax-M2.7", - providerID: "netgate", + providerID: "aigw", api: { id: "MiniMax/MiniMax-M2.7", url: "https://gw.test/v1", diff --git a/packages/tui/src/component/dialog-configure.tsx b/packages/tui/src/component/dialog-configure.tsx index 88bb6bbc6c..54c50cb4b1 100644 --- a/packages/tui/src/component/dialog-configure.tsx +++ b/packages/tui/src/component/dialog-configure.tsx @@ -32,7 +32,7 @@ import { useToast } from "../ui/toast" // Fork-only: the standalone configure flow — the in-agent equivalent of // `codevhub config`. Login with SSO (gateway key minted via the CoDev backend, -// keyless hub-shaped provider.netgate block written with the key stored in +// keyless hub-shaped provider.aigw block written with the key stored in // opencode's auth.json, session + key also in the hub-shared // ~/.codev/auth.json), set up a custom provider by API key (the extended // "Other" flow from the connect dialog), or open upstream's connect dialog for diff --git a/packages/tui/test/component/use-connected.test.ts b/packages/tui/test/component/use-connected.test.ts index 198942e18d..018ece8d4c 100644 --- a/packages/tui/test/component/use-connected.test.ts +++ b/packages/tui/test/component/use-connected.test.ts @@ -6,7 +6,7 @@ const zenFree = { models: { "big-pickle": { cost: { input: 0 } } }, } const gateway = { - id: "netgate", + id: "aigw", models: { "MiniMax/MiniMax-M3": {} }, }