Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions packages/app/src/components/gateway/gateway-configure-gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/gateway/hidden-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/gateway/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
2 changes: 1 addition & 1 deletion packages/codev-gateway/src/auth-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 4 additions & 2 deletions packages/codev-gateway/src/config-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<id>` 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
Expand Down
1 change: 1 addition & 0 deletions packages/codev-gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export { buildCustomProviderConfig, buildGatewayConfig, normalizeGatewayBaseUrl
export {
CUSTOM_FALLBACK_PROVIDER,
GATEWAY_PROVIDER,
LEGACY_GATEWAY_PROVIDER_IDS,
providerFromName,
slugifyProviderName,
type ProviderIdentity,
Expand Down
11 changes: 9 additions & 2 deletions packages/codev-gateway/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
16 changes: 8 additions & 8 deletions packages/codev-gateway/test/config-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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",
)
})
Expand All @@ -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")
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/codev-gateway/test/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/codev-upload/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions packages/codev-upload/src/nudge.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion packages/codev-upload/test/nudge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 8 additions & 7 deletions packages/desktop/src/main/gateway-sso/controller.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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({
Expand All @@ -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<string, { models: Record<string, unknown> }> }).provider
expect(Object.keys(provider)).toEqual(["netgate"])
expect(Object.keys(provider.netgate.models)).toEqual(["model-a", "model-b"])
expect((provider.netgate as { options: Record<string, unknown> }).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<string, unknown> }).options).not.toHaveProperty("apiKey")
})

// The sandboxes read the auth file at boot, so they have to be told a login
Expand Down Expand Up @@ -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")
Expand Down
10 changes: 9 additions & 1 deletion packages/desktop/src/main/gateway-sso/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fetchModels,
fetchModelWindows,
GATEWAY_PROVIDER,
LEGACY_GATEWAY_PROVIDER_IDS,
loadApiKey,
loadGatewayUrl,
login,
Expand Down Expand Up @@ -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")
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/config/gateway-key-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,15 +672,15 @@ 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.
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)
Expand All @@ -693,8 +693,8 @@ it.instance(
{
config: {
provider: {
netgate: {
name: "netGate",
aigw: {
name: "AIGW",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/component/dialog-configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/test/component/use-connected.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const zenFree = {
models: { "big-pickle": { cost: { input: 0 } } },
}
const gateway = {
id: "netgate",
id: "aigw",
models: { "MiniMax/MiniMax-M3": {} },
}

Expand Down
Loading