refactor(provider): flow branded ProviderID/ModelID through internal signatures (#17182)

This commit is contained in:
Kit Langton
2026-03-12 10:48:17 -04:00
committed by GitHub
parent a4f8d66a9b
commit 1cb7df7159
24 changed files with 227 additions and 205 deletions

View File

@@ -4,6 +4,7 @@ import path from "path"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Env } from "../../src/env"
test("provider loaded from env variable", async () => {
@@ -300,7 +301,7 @@ test("getModel returns model for valid provider/model", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
expect(model).toBeDefined()
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
@@ -327,7 +328,7 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
expect(Provider.getModel("anthropic", "nonexistent-model")).rejects.toThrow()
expect(Provider.getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
},
})
})
@@ -346,7 +347,7 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
expect(Provider.getModel("nonexistent-provider", "some-model")).rejects.toThrow()
expect(Provider.getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
},
})
})
@@ -572,10 +573,10 @@ test("closest finds model by partial match", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const result = await Provider.closest("anthropic", ["sonnet-4"])
const result = await Provider.closest(ProviderID.anthropic, ["sonnet-4"])
expect(result).toBeDefined()
expect(result?.providerID).toBe("anthropic")
expect(result?.modelID).toContain("sonnet-4")
expect(String(result?.providerID)).toBe("anthropic")
expect(String(result?.modelID)).toContain("sonnet-4")
},
})
})
@@ -594,7 +595,7 @@ test("closest returns undefined for nonexistent provider", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const result = await Provider.closest("nonexistent", ["model"])
const result = await Provider.closest(ProviderID.make("nonexistent"), ["model"])
expect(result).toBeUndefined()
},
})
@@ -630,7 +631,7 @@ test("getModel uses realIdByKey for aliased models", async () => {
const providers = await Provider.list()
expect(providers["anthropic"].models["my-sonnet"]).toBeDefined()
const model = await Provider.getModel("anthropic", "my-sonnet")
const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
expect(model).toBeDefined()
expect(String(model.id)).toBe("my-sonnet")
expect(model.name).toBe("My Sonnet Alias")
@@ -933,7 +934,7 @@ test("getSmallModel returns appropriate small model", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const model = await Provider.getSmallModel("anthropic")
const model = await Provider.getSmallModel(ProviderID.anthropic)
expect(model).toBeDefined()
expect(model?.id).toContain("haiku")
},
@@ -958,7 +959,7 @@ test("getSmallModel respects config small_model override", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const model = await Provider.getSmallModel("anthropic")
const model = await Provider.getSmallModel(ProviderID.anthropic)
expect(model).toBeDefined()
expect(String(model?.providerID)).toBe("anthropic")
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
@@ -1466,8 +1467,8 @@ test("getModel returns consistent results", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
const model1 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
const model2 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
expect(model1.providerID).toEqual(model2.providerID)
expect(model1.id).toEqual(model2.id)
expect(model1).toEqual(model2)
@@ -1528,7 +1529,7 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
},
fn: async () => {
try {
await Provider.getModel("anthropic", "claude-sonet-4") // typo: sonet instead of sonnet
await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.data.suggestions).toBeDefined()
@@ -1556,7 +1557,7 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
},
fn: async () => {
try {
await Provider.getModel("antropic", "claude-sonnet-4") // typo: antropic
await Provider.getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.data.suggestions).toBeDefined()
@@ -1580,7 +1581,7 @@ test("getProvider returns undefined for nonexistent provider", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const provider = await Provider.getProvider("nonexistent")
const provider = await Provider.getProvider(ProviderID.make("nonexistent"))
expect(provider).toBeUndefined()
},
})
@@ -1603,7 +1604,7 @@ test("getProvider returns provider info", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const provider = await Provider.getProvider("anthropic")
const provider = await Provider.getProvider(ProviderID.anthropic)
expect(provider).toBeDefined()
expect(String(provider?.id)).toBe("anthropic")
},
@@ -1627,7 +1628,7 @@ test("closest returns undefined when no partial match found", async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const result = await Provider.closest("anthropic", ["nonexistent-xyz-model"])
const result = await Provider.closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
expect(result).toBeUndefined()
},
})
@@ -1651,7 +1652,7 @@ test("closest checks multiple query terms in order", async () => {
},
fn: async () => {
// First term won't match, second will
const result = await Provider.closest("anthropic", ["nonexistent", "haiku"])
const result = await Provider.closest(ProviderID.anthropic, ["nonexistent", "haiku"])
expect(result).toBeDefined()
expect(result?.modelID).toContain("haiku")
},

View File

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { Bus } from "../../src/bus"
import { Instance } from "../../src/project/instance"
import { Pty } from "../../src/pty"
import type { PtyID } from "../../src/pty/schema"
import { tmpdir } from "../fixture/fixture"
import { setTimeout as sleep } from "node:timers/promises"
@@ -14,7 +15,7 @@ const wait = async (fn: () => boolean, ms = 2000) => {
throw new Error("timeout waiting for pty events")
}
const pick = (log: Array<{ type: "created" | "exited" | "deleted"; id: string }>, id: string) => {
const pick = (log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }>, id: PtyID) => {
return log.filter((evt) => evt.id === id).map((evt) => evt.type)
}
@@ -27,23 +28,23 @@ describe("pty", () => {
await Instance.provide({
directory: dir.path,
fn: async () => {
const log: Array<{ type: "created" | "exited" | "deleted"; id: string }> = []
const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
const off = [
Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
]
let id = ""
let id: PtyID | undefined
try {
const info = await Pty.create({ command: "/bin/ls", title: "ls" })
id = info.id
await wait(() => pick(log, id).includes("exited"))
await wait(() => pick(log, id!).includes("exited"))
await Pty.remove(id)
await wait(() => pick(log, id).length >= 3)
expect(pick(log, id)).toEqual(["created", "exited", "deleted"])
await wait(() => pick(log, id!).length >= 3)
expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
} finally {
off.forEach((x) => x())
if (id) await Pty.remove(id)
@@ -60,14 +61,14 @@ describe("pty", () => {
await Instance.provide({
directory: dir.path,
fn: async () => {
const log: Array<{ type: "created" | "exited" | "deleted"; id: string }> = []
const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
const off = [
Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
]
let id = ""
let id: PtyID | undefined
try {
const info = await Pty.create({ command: "/bin/sh", title: "sh" })
id = info.id
@@ -75,8 +76,8 @@ describe("pty", () => {
await sleep(100)
await Pty.remove(id)
await wait(() => pick(log, id).length >= 3)
expect(pick(log, id)).toEqual(["created", "exited", "deleted"])
await wait(() => pick(log, id!).length >= 3)
expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
} finally {
off.forEach((x) => x())
if (id) await Pty.remove(id)

View File

@@ -7,7 +7,7 @@ import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider/provider"
import { ProviderTransform } from "../../src/provider/transform"
import { ModelsDev } from "../../src/provider/models"
import { ProviderID } from "../../src/provider/schema"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
import type { Agent } from "../../src/agent/agent"
@@ -266,7 +266,7 @@ describe("session.llm.stream", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
const sessionID = SessionID.make("session-test-1")
const agent = {
name: "test",
@@ -396,7 +396,7 @@ describe("session.llm.stream", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel("openai", model.id)
const resolved = await Provider.getModel(ProviderID.openai, ModelID.make(model.id))
const sessionID = SessionID.make("session-test-2")
const agent = {
name: "test",
@@ -518,7 +518,7 @@ describe("session.llm.stream", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
const sessionID = SessionID.make("session-test-3")
const agent = {
name: "test",
@@ -619,7 +619,7 @@ describe("session.llm.stream", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
const sessionID = SessionID.make("session-test-4")
const agent = {
name: "test",