Skip to content
Open
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
5 changes: 3 additions & 2 deletions packages/opencode/script/seed-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {
Expand Down
14 changes: 8 additions & 6 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<string, any> }> = Object.values(provider.models)
const models = Provider.sort(unsorted)
return models.flatMap((model) => {
const base: ModelOption = {
modelId: `${provider.id}/${model.id}`,
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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(),
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/plugin/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down Expand Up @@ -375,8 +376,8 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {

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",
Expand Down
11 changes: 6 additions & 5 deletions packages/opencode/src/provider/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<Authorization | undefined> => {
Expand All @@ -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(),
}),
Expand Down Expand Up @@ -119,7 +120,7 @@ export namespace ProviderAuth {

export const api = fn(
z.object({
providerID: z.string(),
providerID: ProviderID.zod,
key: z.string(),
}),
async (input) => {
Expand All @@ -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,
}),
)

Expand Down
39 changes: 20 additions & 19 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand All @@ -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: {
Expand Down Expand Up @@ -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 ?? [],
Expand Down Expand Up @@ -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"),
})),
}
}
Expand All @@ -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 ?? {}),
Expand All @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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<T extends { id: string }>(models: T[]) {
return sortBy(
models,
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
Expand All @@ -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
Expand All @@ -1395,24 +1396,24 @@ 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(),
}),
)

export const InitError = NamedError.create(
"ProviderInitError",
z.object({
providerID: z.string(),
providerID: ProviderID.zod,
}),
)
}
26 changes: 26 additions & 0 deletions packages/opencode/src/provider/schema.ts
Original file line number Diff line number Diff line change
@@ -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<ProviderID>()),
})),
)

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<ModelID>()),
})),
)
5 changes: 3 additions & 2 deletions packages/opencode/src/server/routes/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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),
}),
),
Expand Down
Loading
Loading