diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts index 78bbca28661..01e10442048 100644 --- a/packages/opencode/script/seed-e2e.ts +++ b/packages/opencode/script/seed-e2e.ts @@ -13,6 +13,7 @@ const seed = async () => { const { Session } = await import("../src/session") const { MessageID, PartID } = await import("../src/session/schema") const { Project } = await import("../src/project/project") + const { ModelID, ProviderID } = await import("../src/provider/schema") await Instance.provide({ directory: dir, @@ -28,8 +29,8 @@ const seed = async () => { time: { created: now }, agent: "build", model: { - providerID, - modelID, + providerID: ProviderID.make(providerID), + modelID: ModelID.make(modelID), }, } const part = { diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 2552682dbe0..6e6b748ca92 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -35,6 +35,7 @@ import { Hash } from "../util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" +import { ProviderID } from "../provider/schema" import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" @@ -590,7 +591,7 @@ export namespace ACP { } } catch (e) { const error = MessageV2.fromError(e, { - providerID: this.config.defaultModel?.providerID ?? "unknown", + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -655,7 +656,7 @@ export namespace ACP { return result } catch (e) { const error = MessageV2.fromError(e, { - providerID: this.config.defaultModel?.providerID ?? "unknown", + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -700,7 +701,7 @@ export namespace ACP { return response } catch (e) { const error = MessageV2.fromError(e, { - providerID: this.config.defaultModel?.providerID ?? "unknown", + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -765,7 +766,7 @@ export namespace ACP { return mode } catch (e) { const error = MessageV2.fromError(e, { - providerID: this.config.defaultModel?.providerID ?? "unknown", + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -796,7 +797,7 @@ export namespace ACP { return result } catch (e) { const error = MessageV2.fromError(e, { - providerID: this.config.defaultModel?.providerID ?? "unknown", + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -1666,7 +1667,8 @@ export namespace ACP { ): ModelOption[] { const includeVariants = options.includeVariants ?? false return providers.flatMap((provider) => { - const models = Provider.sort(Object.values(provider.models) as any) + const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values(provider.models) + const models = Provider.sort(unsorted) return models.flatMap((model) => { const base: ModelOption = { modelId: `${provider.id}/${model.id}`, diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c53ca04e238..343f434375d 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,6 +1,7 @@ import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" +import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" @@ -34,8 +35,8 @@ export namespace Agent { permission: PermissionNext.Ruleset, model: z .object({ - modelID: z.string(), - providerID: z.string(), + modelID: ModelID.zod, + providerID: ProviderID.zod, }) .optional(), variant: z.string().optional(), diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 5c0140e5704..6b0b73208ec 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -4,6 +4,7 @@ import { Installation } from "../installation" import { Auth, OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { ProviderTransform } from "@/provider/transform" +import { ModelID, ProviderID } from "@/provider/schema" import { setTimeout as sleep } from "node:timers/promises" const log = Log.create({ service: "plugin.codex" }) @@ -375,8 +376,8 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { if (!provider.models["gpt-5.3-codex"]) { const model = { - id: "gpt-5.3-codex", - providerID: "openai", + id: ModelID.make("gpt-5.3-codex"), + providerID: ProviderID.make("openai"), api: { id: "gpt-5.3-codex", url: "https://chatgpt.com/backend-api/codex", diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index e6681ff0891..f6c25432034 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -6,6 +6,7 @@ import { fn } from "@/util/fn" import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin" import { NamedError } from "@opencode-ai/util/error" import { Auth } from "@/auth" +import { ProviderID } from "./schema" export namespace ProviderAuth { const state = Instance.state(async () => { @@ -53,7 +54,7 @@ export namespace ProviderAuth { export const authorize = fn( z.object({ - providerID: z.string(), + providerID: ProviderID.zod, method: z.number(), }), async (input): Promise => { @@ -73,7 +74,7 @@ export namespace ProviderAuth { export const callback = fn( z.object({ - providerID: z.string(), + providerID: ProviderID.zod, method: z.number(), code: z.string().optional(), }), @@ -119,7 +120,7 @@ export namespace ProviderAuth { export const api = fn( z.object({ - providerID: z.string(), + providerID: ProviderID.zod, key: z.string(), }), async (input) => { @@ -133,13 +134,13 @@ export namespace ProviderAuth { export const OauthMissing = NamedError.create( "ProviderAuthOauthMissing", z.object({ - providerID: z.string(), + providerID: ProviderID.zod, }), ) export const OauthCodeMissing = NamedError.create( "ProviderAuthOauthCodeMissing", z.object({ - providerID: z.string(), + providerID: ProviderID.zod, }), ) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ccd3c55b4f1..3cca3afa93e 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -45,6 +45,7 @@ import { fromNodeProviderChain } from "@aws-sdk/credential-providers" import { GoogleAuth } from "google-auth-library" import { ProviderTransform } from "./transform" import { Installation } from "../installation" +import { ModelID, ProviderID } from "./schema" const DEFAULT_CHUNK_TIMEOUT = 120_000 @@ -673,8 +674,8 @@ export namespace Provider { export const Model = z .object({ - id: z.string(), - providerID: z.string(), + id: ModelID.zod, + providerID: ProviderID.zod, api: z.object({ id: z.string(), url: z.string(), @@ -744,7 +745,7 @@ export namespace Provider { export const Info = z .object({ - id: z.string(), + id: ProviderID.zod, name: z.string(), source: z.enum(["env", "config", "custom", "api"]), env: z.string().array(), @@ -759,8 +760,8 @@ export namespace Provider { function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const m: Model = { - id: model.id, - providerID: provider.id, + id: ModelID.make(model.id), + providerID: ProviderID.make(provider.id), name: model.name, family: model.family, api: { @@ -826,7 +827,7 @@ export namespace Provider { export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { return { - id: provider.id, + id: ProviderID.make(provider.id), source: "custom", name: provider.name, env: provider.env ?? [], @@ -866,11 +867,11 @@ export namespace Provider { const githubCopilot = database["github-copilot"] database["github-copilot-enterprise"] = { ...githubCopilot, - id: "github-copilot-enterprise", + id: ProviderID.make("github-copilot-enterprise"), name: "GitHub Copilot Enterprise", models: mapValues(githubCopilot.models, (model) => ({ ...model, - providerID: "github-copilot-enterprise", + providerID: ProviderID.make("github-copilot-enterprise"), })), } } @@ -892,7 +893,7 @@ export namespace Provider { for (const [providerID, provider] of configProviders) { const existing = database[providerID] const parsed: Info = { - id: providerID, + id: ProviderID.make(providerID), name: provider.name ?? existing?.name ?? providerID, env: provider.env ?? existing?.env ?? [], options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), @@ -908,7 +909,7 @@ export namespace Provider { return existingModel?.name ?? modelID }) const parsedModel: Model = { - id: modelID, + id: ModelID.make(modelID), api: { id: model.id ?? existingModel?.api.id ?? modelID, npm: @@ -921,7 +922,7 @@ export namespace Provider { }, status: model.status ?? existingModel?.status ?? "active", name, - providerID, + providerID: ProviderID.make(providerID), capabilities: { temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, @@ -1356,7 +1357,7 @@ export namespace Provider { } const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] - export function sort(models: Model[]) { + export function sort(models: T[]) { return sortBy( models, [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], @@ -1370,11 +1371,11 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const providers = await list() - const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>( + const recent = (await Filesystem.readJson<{ recent?: { providerID: ProviderID; modelID: ModelID }[] }>( path.join(Global.Path.state, "model.json"), ) .then((x) => (Array.isArray(x.recent) ? x.recent : [])) - .catch(() => [])) as { providerID: string; modelID: string }[] + .catch(() => [])) as { providerID: ProviderID; modelID: ModelID }[] for (const entry of recent) { const provider = providers[entry.providerID] if (!provider) continue @@ -1395,16 +1396,16 @@ export namespace Provider { export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { - providerID: providerID, - modelID: rest.join("/"), + providerID: ProviderID.make(providerID), + modelID: ModelID.make(rest.join("/")), } } export const ModelNotFoundError = NamedError.create( "ProviderModelNotFoundError", z.object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, suggestions: z.array(z.string()).optional(), }), ) @@ -1412,7 +1413,7 @@ export namespace Provider { export const InitError = NamedError.create( "ProviderInitError", z.object({ - providerID: z.string(), + providerID: ProviderID.zod, }), ) } diff --git a/packages/opencode/src/provider/schema.ts b/packages/opencode/src/provider/schema.ts new file mode 100644 index 00000000000..4d975b8d7ea --- /dev/null +++ b/packages/opencode/src/provider/schema.ts @@ -0,0 +1,26 @@ +import { Schema } from "effect" +import z from "zod" + +import { withStatics } from "@/util/schema" + +const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID")) + +export type ProviderID = typeof providerIdSchema.Type + +export const ProviderID = providerIdSchema.pipe( + withStatics((schema: typeof providerIdSchema) => ({ + make: (id: string) => schema.makeUnsafe(id), + zod: z.string().pipe(z.custom()), + })), +) + +const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID")) + +export type ModelID = typeof modelIdSchema.Type + +export const ModelID = modelIdSchema.pipe( + withStatics((schema: typeof modelIdSchema) => ({ + make: (id: string) => schema.makeUnsafe(id), + zod: z.string().pipe(z.custom()), + })), +) diff --git a/packages/opencode/src/server/routes/provider.ts b/packages/opencode/src/server/routes/provider.ts index 872b48be79d..fc716d25cb0 100644 --- a/packages/opencode/src/server/routes/provider.ts +++ b/packages/opencode/src/server/routes/provider.ts @@ -5,6 +5,7 @@ import { Config } from "../../config/config" import { Provider } from "../../provider/provider" import { ModelsDev } from "../../provider/models" import { ProviderAuth } from "../../provider/auth" +import { ProviderID } from "../../provider/schema" import { mapValues } from "remeda" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -101,7 +102,7 @@ export const ProviderRoutes = lazy(() => validator( "param", z.object({ - providerID: z.string().meta({ description: "Provider ID" }), + providerID: ProviderID.zod.meta({ description: "Provider ID" }), }), ), validator( @@ -141,7 +142,7 @@ export const ProviderRoutes = lazy(() => validator( "param", z.object({ - providerID: z.string().meta({ description: "Provider ID" }), + providerID: ProviderID.zod.meta({ description: "Provider ID" }), }), ), validator( diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 1c7cba28dc6..f742373743f 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -16,6 +16,7 @@ import { Snapshot } from "@/snapshot" import { Log } from "../../util/log" import { PermissionNext } from "@/permission/next" import { PermissionID } from "@/permission/schema" +import { ModelID, ProviderID } from "@/provider/schema" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -510,8 +511,8 @@ export const SessionRoutes = lazy(() => validator( "json", z.object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, auto: z.boolean().optional().default(false), }), ), diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 14c2346a611..55bcf2dfce1 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -23,6 +23,7 @@ import { Command } from "../command" import { Global } from "../global" import { WorkspaceContext } from "../control-plane/workspace-context" import { WorkspaceID } from "../control-plane/schema" +import { ProviderID } from "../provider/schema" import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware" import { ProjectRoutes } from "./routes/project" import { SessionRoutes } from "./routes/session" @@ -148,7 +149,7 @@ export namespace Server { validator( "param", z.object({ - providerID: z.string(), + providerID: ProviderID.zod, }), ), validator("json", Auth.Info), @@ -180,7 +181,7 @@ export namespace Server { validator( "param", z.object({ - providerID: z.string(), + providerID: ProviderID.zod, }), ), async (c) => { diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 1946eeee96a..8d934c05dab 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -14,6 +14,7 @@ import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" import { Config } from "@/config/config" import { ProviderTransform } from "@/provider/transform" +import { ModelID, ProviderID } from "@/provider/schema" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -298,8 +299,8 @@ When constructing the summary, try to stick to this template: sessionID: SessionID.zod, agent: z.string(), model: z.object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, }), auto: z.boolean(), overflow: z.boolean().optional(), diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 275b50e42b7..0879fe87fd3 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -27,6 +27,7 @@ import { WorkspaceID } from "../control-plane/schema" import { SessionID, MessageID, PartID } from "./schema" import type { Provider } from "@/provider/provider" +import { ModelID, ProviderID } from "@/provider/schema" import { PermissionNext } from "@/permission/next" import { Global } from "@/global" import type { LanguageModelV2Usage } from "@ai-sdk/provider" @@ -875,8 +876,8 @@ export namespace Session { export const initialize = fn( z.object({ sessionID: SessionID.zod, - modelID: z.string(), - providerID: z.string(), + modelID: ModelID.zod, + providerID: ProviderID.zod, messageID: MessageID.zod, }), async (input) => { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 90abf54526a..03ccb44c1ad 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -15,6 +15,7 @@ import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" import { type SystemError } from "bun" import type { Provider } from "@/provider/provider" +import { ModelID, ProviderID } from "@/provider/schema" export namespace MessageV2 { export function isMedia(mime: string) { @@ -213,8 +214,8 @@ export namespace MessageV2 { agent: z.string(), model: z .object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, }) .optional(), command: z.string().optional(), @@ -362,8 +363,8 @@ export namespace MessageV2 { .optional(), agent: z.string(), model: z.object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, }), system: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), @@ -411,8 +412,8 @@ export namespace MessageV2 { ]) .optional(), parentID: MessageID.zod, - modelID: z.string(), - providerID: z.string(), + modelID: ModelID.zod, + providerID: ProviderID.zod, /** * @deprecated */ @@ -824,7 +825,7 @@ export namespace MessageV2 { return result } - export function fromError(e: unknown, ctx: { providerID: string }) { + export function fromError(e: unknown, ctx: { providerID: ProviderID }): NonNullable { switch (true) { case e instanceof DOMException && e.name === "AbortError": return new MessageV2.AbortedError( diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 691057db207..ee5eac08b6b 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,5 +1,6 @@ import z from "zod" import { SessionID } from "./schema" +import { ModelID, ProviderID } from "../provider/schema" import { NamedError } from "@opencode-ai/util/error" export namespace Message { @@ -160,8 +161,8 @@ export namespace Message { assistant: z .object({ system: z.string().array(), - modelID: z.string(), - providerID: z.string(), + modelID: ModelID.zod, + providerID: ProviderID.zod, path: z.object({ cwd: z.string(), root: z.string(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b8be93b6be0..939c50a3d92 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -10,6 +10,7 @@ import { SessionRevert } from "./revert" import { Session } from "." import { Agent } from "../agent/agent" import { Provider } from "../provider/provider" +import { ModelID, ProviderID } from "../provider/schema" import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions, asSchema } from "ai" import { SessionCompaction } from "./compaction" import { Instance } from "../project/instance" @@ -94,8 +95,8 @@ export namespace SessionPrompt { messageID: MessageID.zod.optional(), model: z .object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, }) .optional(), agent: z.string().optional(), @@ -1471,8 +1472,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the agent: z.string(), model: z .object({ - providerID: z.string(), - modelID: z.string(), + providerID: ProviderID.zod, + modelID: ModelID.zod, }) .optional(), command: z.string(), diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 9f8de04f80c..497b6019d3e 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -137,8 +137,8 @@ test("custom agent from config creates new agent", async () => { fn: async () => { const custom = await Agent.get("my_custom_agent") expect(custom).toBeDefined() - expect(custom?.model?.providerID).toBe("openai") - expect(custom?.model?.modelID).toBe("gpt-4") + expect(String(custom?.model?.providerID)).toBe("openai") + expect(String(custom?.model?.modelID)).toBe("gpt-4") expect(custom?.description).toBe("My custom agent") expect(custom?.temperature).toBe(0.5) expect(custom?.topP).toBe(0.9) @@ -166,8 +166,8 @@ test("custom agent config overrides native agent properties", async () => { fn: async () => { const build = await Agent.get("build") expect(build).toBeDefined() - expect(build?.model?.providerID).toBe("anthropic") - expect(build?.model?.modelID).toBe("claude-3") + expect(String(build?.model?.providerID)).toBe("anthropic") + expect(String(build?.model?.modelID)).toBe("claude-3") expect(build?.description).toBe("Custom build agent") expect(build?.temperature).toBe(0.7) expect(build?.color).toBe("#FF0000") diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 4c6eaf8b227..96207f21b27 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -302,8 +302,8 @@ test("getModel returns model for valid provider/model", async () => { fn: async () => { const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514") expect(model).toBeDefined() - expect(model.providerID).toBe("anthropic") - expect(model.id).toBe("claude-sonnet-4-20250514") + expect(String(model.providerID)).toBe("anthropic") + expect(String(model.id)).toBe("claude-sonnet-4-20250514") const language = await Provider.getLanguage(model) expect(language).toBeDefined() }, @@ -353,14 +353,14 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => { test("parseModel correctly parses provider/model string", () => { const result = Provider.parseModel("anthropic/claude-sonnet-4") - expect(result.providerID).toBe("anthropic") - expect(result.modelID).toBe("claude-sonnet-4") + expect(String(result.providerID)).toBe("anthropic") + expect(String(result.modelID)).toBe("claude-sonnet-4") }) test("parseModel handles model IDs with slashes", () => { const result = Provider.parseModel("openrouter/anthropic/claude-3-opus") - expect(result.providerID).toBe("openrouter") - expect(result.modelID).toBe("anthropic/claude-3-opus") + expect(String(result.providerID)).toBe("openrouter") + expect(String(result.modelID)).toBe("anthropic/claude-3-opus") }) test("defaultModel returns first available model when no config set", async () => { @@ -406,8 +406,8 @@ test("defaultModel respects config model setting", async () => { }, fn: async () => { const model = await Provider.defaultModel() - expect(model.providerID).toBe("anthropic") - expect(model.modelID).toBe("claude-sonnet-4-20250514") + expect(String(model.providerID)).toBe("anthropic") + expect(String(model.modelID)).toBe("claude-sonnet-4-20250514") }, }) }) @@ -632,7 +632,7 @@ test("getModel uses realIdByKey for aliased models", async () => { const model = await Provider.getModel("anthropic", "my-sonnet") expect(model).toBeDefined() - expect(model.id).toBe("my-sonnet") + expect(String(model.id)).toBe("my-sonnet") expect(model.name).toBe("My Sonnet Alias") }, }) @@ -960,8 +960,8 @@ test("getSmallModel respects config small_model override", async () => { fn: async () => { const model = await Provider.getSmallModel("anthropic") expect(model).toBeDefined() - expect(model?.providerID).toBe("anthropic") - expect(model?.id).toBe("claude-sonnet-4-20250514") + expect(String(model?.providerID)).toBe("anthropic") + expect(String(model?.id)).toBe("claude-sonnet-4-20250514") }, }) }) @@ -1605,7 +1605,7 @@ test("getProvider returns provider info", async () => { fn: async () => { const provider = await Provider.getProvider("anthropic") expect(provider).toBeDefined() - expect(provider?.id).toBe("anthropic") + expect(String(provider?.id)).toBe("anthropic") }, }) }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9dde1a7131e..ecb3a74804c 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import { ProviderTransform } from "../../src/provider/transform" +import { ModelID, ProviderID } from "../../src/provider/schema" const OUTPUT_TOKEN_MAX = 32000 @@ -740,8 +741,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { const result = ProviderTransform.message( msgs, { - id: "deepseek/deepseek-chat", - providerID: "deepseek", + id: ModelID.make("deepseek/deepseek-chat"), + providerID: ProviderID.make("deepseek"), api: { id: "deepseek-chat", url: "https://api.deepseek.com", @@ -802,8 +803,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { const result = ProviderTransform.message( msgs, { - id: "openai/gpt-4", - providerID: "openai", + id: ModelID.make("openai/gpt-4"), + providerID: ProviderID.make("openai"), api: { id: "gpt-4", url: "https://api.openai.com", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index df9dedb356a..0cc44cac27d 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -7,6 +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 { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" @@ -282,7 +283,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID, modelID: resolved.id }, + model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, variant: "high", } satisfies MessageV2.User @@ -411,7 +412,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: "openai", modelID: resolved.id }, + model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, variant: "high", } satisfies MessageV2.User @@ -534,7 +535,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID, modelID: resolved.id }, + model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, } satisfies MessageV2.User const stream = await LLM.stream({ @@ -635,7 +636,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID, modelID: resolved.id }, + model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, } satisfies MessageV2.User const stream = await LLM.stream({ diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 1a7c75c05f8..e9c6cb729bb 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -2,12 +2,14 @@ import { describe, expect, test } from "bun:test" import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" import type { Provider } from "../../src/provider/provider" +import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema" const sessionID = SessionID.make("session") +const providerID = ProviderID.make("test") const model: Provider.Model = { - id: "test-model", - providerID: "test", + id: ModelID.make("test-model"), + providerID, api: { id: "test-model", url: "https://example.com", @@ -61,7 +63,7 @@ function userInfo(id: string): MessageV2.User { role: "user", time: { created: 0 }, agent: "user", - model: { providerID: "test", modelID: "test" }, + model: { providerID, modelID: ModelID.make("test") }, tools: {}, mode: "", } as unknown as MessageV2.User @@ -795,7 +797,7 @@ describe("session.message-v2.fromError", () => { code: "context_length_exceeded", }, } - const result = MessageV2.fromError(input, { providerID: "test" }) + const result = MessageV2.fromError(input, { providerID }) expect(result).toStrictEqual({ name: "ContextOverflowError", @@ -830,7 +832,7 @@ describe("session.message-v2.fromError", () => { message: item.code === "invalid_prompt" ? item.message : undefined, }, } - const result = MessageV2.fromError(input, { providerID: "test" }) + const result = MessageV2.fromError(input, { providerID }) expect(result).toStrictEqual({ name: "APIError", @@ -862,7 +864,7 @@ describe("session.message-v2.fromError", () => { responseHeaders: { "content-type": "application/json" }, isRetryable: false, }) - const result = MessageV2.fromError(error, { providerID: "test" }) + const result = MessageV2.fromError(error, { providerID }) expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true) }) }) @@ -877,14 +879,14 @@ describe("session.message-v2.fromError", () => { responseHeaders: { "content-type": "application/json" }, isRetryable: false, }), - { providerID: "test" }, + { providerID }, ) expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false) expect(MessageV2.APIError.isInstance(result)).toBe(true) }) test("serializes unknown inputs", () => { - const result = MessageV2.fromError(123, { providerID: "test" }) + const result = MessageV2.fromError(123, { providerID }) expect(result).toStrictEqual({ name: "UnknownError", diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index e8a8c65b03d..3986271dab9 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -2,6 +2,7 @@ import path from "path" import { describe, expect, test } from "bun:test" import { fileURLToPath } from "url" import { Instance } from "../../src/project/instance" +import { ModelID, ProviderID } from "../../src/provider/schema" import { Session } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { SessionPrompt } from "../../src/session/prompt" @@ -173,7 +174,7 @@ describe("session.prompt agent variant", () => { const other = await SessionPrompt.prompt({ sessionID: session.id, agent: "build", - model: { providerID: "opencode", modelID: "kimi-k2.5-free" }, + model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") }, noReply: true, parts: [{ type: "text", text: "hello" }], }) @@ -187,7 +188,7 @@ describe("session.prompt agent variant", () => { parts: [{ type: "text", text: "hello again" }], }) if (match.info.role !== "user") throw new Error("expected user message") - expect(match.info.model).toEqual({ providerID: "openai", modelID: "gpt-5.2" }) + expect(match.info.model).toEqual({ providerID: ProviderID.make("openai"), modelID: ModelID.make("gpt-5.2") }) expect(match.info.variant).toBe("xhigh") const override = await SessionPrompt.prompt({ diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index eba4a995053..621ad99e9b4 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -4,6 +4,9 @@ import { APICallError } from "ai" import { setTimeout as sleep } from "node:timers/promises" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" +import { ProviderID } from "../../src/provider/schema" + +const providerID = ProviderID.make("test") function apiError(headers?: Record): MessageV2.APIError { return new MessageV2.APIError({ @@ -150,7 +153,7 @@ describe("session.message-v2.fromError", () => { .then((res) => res.text()) .catch((e) => e) - const result = MessageV2.fromError(error, { providerID: "test" }) + const result = MessageV2.fromError(error, { providerID }) expect(MessageV2.APIError.isInstance(result)).toBe(true) expect((result as MessageV2.APIError).data.isRetryable).toBe(true) @@ -183,7 +186,7 @@ describe("session.message-v2.fromError", () => { responseBody: '{"error":"boom"}', isRetryable: false, }) - const result = MessageV2.fromError(error, { providerID: "openai" }) as MessageV2.APIError + const result = MessageV2.fromError(error, { providerID: ProviderID.make("openai") }) as MessageV2.APIError expect(result.data.isRetryable).toBe(true) }) }) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index 1a5844bc91a..fb37a3a8dca 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test, beforeEach, afterEach } from "bun:test" import path from "path" import { Session } from "../../src/session" +import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionRevert } from "../../src/session/revert" import { SessionCompaction } from "../../src/session/compaction" import { MessageV2 } from "../../src/session/message-v2" @@ -29,8 +30,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: "openai", - modelID: "gpt-4", + providerID: ProviderID.make("openai"), + modelID: ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -64,8 +65,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: "gpt-4", - providerID: "openai", + modelID: ModelID.make("gpt-4"), + providerID: ProviderID.make("openai"), parentID: userMsg1.id, time: { created: Date.now(), @@ -90,8 +91,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: "openai", - modelID: "gpt-4", + providerID: ProviderID.make("openai"), + modelID: ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -124,8 +125,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: "gpt-4", - providerID: "openai", + modelID: ModelID.make("gpt-4"), + providerID: ProviderID.make("openai"), parentID: userMsg2.id, time: { created: Date.now(), @@ -205,8 +206,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: "openai", - modelID: "gpt-4", + providerID: ProviderID.make("openai"), + modelID: ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -238,8 +239,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: "gpt-4", - providerID: "openai", + modelID: ModelID.make("gpt-4"), + providerID: ProviderID.make("openai"), parentID: userMsg.id, time: { created: Date.now(),