diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 777226e87..6e781d74b 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -103,6 +103,7 @@ export function definitions() { export interface PublishOptions { readonly id?: ID readonly metadata?: Record + readonly location?: Location.Ref } export interface Interface { @@ -236,7 +237,7 @@ export const layer = Layer.effect( function publish(definition: D, data: Data, options?: PublishOptions) { return Effect.gen(function* () { - const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) + const location = options?.location ?? Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) const event = { id: options?.id ?? ID.create(), ...(options?.metadata ? { metadata: options.metadata } : {}), diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 2b6225bcf..f715e8c24 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -12,6 +12,7 @@ import type { Prompt } from "./session/prompt" import { EventV2 } from "./event" import { ProviderV2 } from "./provider" import { Database } from "./database/database" +import { SessionProjector } from "./session/projector" import { SessionMessageTable, SessionTable } from "./session/sql" import { SessionSchema } from "./session/schema" import { AbsolutePath, RelativePath } from "./schema" @@ -253,4 +254,8 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer), Layer.orDie) +export const defaultLayer = layer.pipe( + Layer.provide(SessionProjector.defaultLayer), + Layer.provide(Database.defaultLayer), + Layer.orDie, +) diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 825c49025..c8b4aac50 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -397,7 +397,6 @@ export const All = Schema.Union( mode: "oneOf", }, ).pipe(Schema.toTaggedUnion("type")) - export type Event = typeof All.Type export type Type = Event["type"] diff --git a/packages/core/src/session/legacy.ts b/packages/core/src/session/legacy.ts index 5b804d9ae..015fa8094 100644 --- a/packages/core/src/session/legacy.ts +++ b/packages/core/src/session/legacy.ts @@ -1,11 +1,16 @@ export * as SessionLegacy from "./legacy" import { Effect, Schema, Types } from "effect" -import { withStatics } from "../schema" +import { EventV2 } from "../event" +import { PermissionV2 } from "../permission" +import { ProjectV2 } from "../project" +import { ProviderV2 } from "../provider" +import { optionalOmitUndefined, withStatics } from "../schema" import { Identifier } from "../util/identifier" import { NonNegativeInt } from "../schema" import { NamedError } from "../util/error" import { SessionSchema } from "./schema" +import { WorkspaceV2 } from "../workspace" export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( Schema.brand("MessageID"), @@ -19,27 +24,6 @@ export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( ) export type PartID = typeof PartID.Type -export const ProviderID = Schema.String.pipe( - Schema.brand("ProviderID"), - withStatics((schema) => ({ - opencode: schema.make("opencode"), - anthropic: schema.make("anthropic"), - openai: schema.make("openai"), - google: schema.make("google"), - googleVertex: schema.make("google-vertex"), - githubCopilot: schema.make("github-copilot"), - amazonBedrock: schema.make("amazon-bedrock"), - azure: schema.make("azure"), - openrouter: schema.make("openrouter"), - mistral: schema.make("mistral"), - gitlab: schema.make("gitlab"), - })), -) -export type ProviderID = typeof ProviderID.Type - -export const ModelID = Schema.String.pipe(Schema.brand("ModelID")) -export type ModelID = typeof ModelID.Type - export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) export const AuthError = NamedError.create("ProviderAuthError", { @@ -213,8 +197,8 @@ export const SubtaskPart = Schema.Struct({ agent: Schema.String, model: Schema.optional( Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, }), ), command: Schema.optional(Schema.String), @@ -357,8 +341,8 @@ export const User = Schema.Struct({ ), agent: Schema.String, model: Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, variant: Schema.optional(Schema.String), }), system: Schema.optional(Schema.String), @@ -453,8 +437,8 @@ export const SubtaskPartInput = Schema.Struct({ agent: Schema.String, model: Schema.optional( Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, }), ), command: Schema.optional(Schema.String), @@ -470,8 +454,8 @@ export const Assistant = Schema.Struct({ }), error: Schema.optional(AssistantErrorSchema), parentID: MessageID, - modelID: ModelID, - providerID: ProviderID, + modelID: ProviderV2.ModelID, + providerID: ProviderV2.ID, mode: Schema.String, agent: Schema.String, path: Schema.Struct({ @@ -509,3 +493,132 @@ export type WithParts = { info: Info parts: Part[] } + +const options = { + sync: { + aggregate: "sessionID", + version: 1, + }, +} as const + +const SessionSummary = Schema.Struct({ + additions: Schema.Finite, + deletions: Schema.Finite, + files: Schema.Finite, + diffs: optionalOmitUndefined(Schema.Array(FileDiff)), +}) + +const SessionTokens = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}) + +const SessionShare = Schema.Struct({ + url: Schema.String, +}) + +const SessionRevert = Schema.Struct({ + messageID: MessageID, + partID: optionalOmitUndefined(PartID), + snapshot: optionalOmitUndefined(Schema.String), + diff: optionalOmitUndefined(Schema.String), +}) + +const SessionModel = Schema.Struct({ + id: ProviderV2.ModelID, + providerID: ProviderV2.ID, + variant: optionalOmitUndefined(Schema.String), +}) + +export const SessionInfo = Schema.Struct({ + id: SessionSchema.ID, + slug: Schema.String, + projectID: ProjectV2.ID, + workspaceID: optionalOmitUndefined(WorkspaceV2.ID), + directory: Schema.String, + path: optionalOmitUndefined(Schema.String), + parentID: optionalOmitUndefined(SessionSchema.ID), + summary: optionalOmitUndefined(SessionSummary), + cost: optionalOmitUndefined(Schema.Finite), + tokens: optionalOmitUndefined(SessionTokens), + share: optionalOmitUndefined(SessionShare), + title: Schema.String, + agent: optionalOmitUndefined(Schema.String), + model: optionalOmitUndefined(SessionModel), + version: Schema.String, + time: Schema.Struct({ + created: NonNegativeInt, + updated: NonNegativeInt, + compacting: optionalOmitUndefined(NonNegativeInt), + archived: optionalOmitUndefined(Schema.Finite), + }), + permission: optionalOmitUndefined(PermissionV2.Ruleset), + revert: optionalOmitUndefined(SessionRevert), +}).annotate({ identifier: "Session" }) +export type SessionInfo = typeof SessionInfo.Type + +export const Event = { + Created: EventV2.define({ + type: "session.created", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + Updated: EventV2.define({ + type: "session.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + Deleted: EventV2.define({ + type: "session.deleted", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + MessageUpdated: EventV2.define({ + type: "message.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: Info, + }, + }), + MessageRemoved: EventV2.define({ + type: "message.removed", + ...options, + schema: { + sessionID: SessionSchema.ID, + messageID: MessageID, + }, + }), + PartUpdated: EventV2.define({ + type: "message.part.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + part: Part, + time: Schema.Finite, + }, + }), + PartRemoved: EventV2.define({ + type: "message.part.removed", + ...options, + schema: { + sessionID: SessionSchema.ID, + messageID: MessageID, + partID: PartID, + }, + }), +} diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 0d0d318fd..99fc3243c 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -279,7 +279,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { id: event.data.callID, name: event.data.name, time: { created: event.data.timestamp }, - state: { status: "pending", input: "" }, + state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }), }) as DraftTool, ) }), diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 72d90b4f4..d58d41301 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -1,19 +1,105 @@ export * as SessionProjector from "./projector" -import { and, eq } from "drizzle-orm" +import { and, eq, sql } from "drizzle-orm" import { DateTime, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { EventV2 } from "../event" import { SessionEvent } from "./event" +import { SessionLegacy } from "./legacy" +import { WorkspaceTable } from "../control-plane/workspace.sql" import { SessionMessage } from "./message" import { SessionMessageUpdater } from "./message-updater" -import { SessionMessageTable, SessionTable } from "./sql" +import { MessageTable, PartTable, SessionMessageTable, SessionTable } from "./sql" +import type { DeepMutable } from "../schema" type DatabaseService = Database.Interface["db"] const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) const encodeMessage = Schema.encodeSync(SessionMessage.Message) +type Usage = { + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { read: number; write: number } + } +} + +function usage(part: typeof SessionLegacy.Event.PartUpdated.Type["data"]["part"] | unknown): Usage | undefined { + if (typeof part !== "object" || part === null) return undefined + const value = part as Record + if (value.type !== "step-finish") return undefined + if (!("cost" in value) || !("tokens" in value)) return undefined + return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] } +} + +function sessionRow(info: SessionLegacy.SessionInfo): typeof SessionTable.$inferInsert { + return { + id: info.id, + project_id: info.projectID, + workspace_id: info.workspaceID, + parent_id: info.parentID, + slug: info.slug, + directory: info.directory, + path: info.path, + title: info.title, + agent: info.agent, + model: info.model, + version: info.version, + share_url: info.share?.url, + summary_additions: info.summary?.additions, + summary_deletions: info.summary?.deletions, + summary_files: info.summary?.files, + summary_diffs: info.summary?.diffs ? [...info.summary.diffs] : undefined, + cost: info.cost ?? 0, + tokens_input: (info.tokens ?? { input: 0 }).input, + tokens_output: (info.tokens ?? { output: 0 }).output, + tokens_reasoning: (info.tokens ?? { reasoning: 0 }).reasoning, + tokens_cache_read: (info.tokens ?? { cache: { read: 0 } }).cache.read, + tokens_cache_write: (info.tokens ?? { cache: { write: 0 } }).cache.write, + revert: info.revert ?? null, + permission: info.permission ? [...info.permission] : undefined, + time_created: info.time.created, + time_updated: info.time.updated, + time_compacting: info.time.compacting, + time_archived: info.time.archived, + } +} + +function messageData(info: typeof SessionLegacy.Event.MessageUpdated.Type["data"]["info"]): typeof MessageTable.$inferInsert.data { + const { id: _, sessionID: __, ...rest } = info + return rest as DeepMutable +} + +function partData(part: typeof SessionLegacy.Event.PartUpdated.Type["data"]["part"]): typeof PartTable.$inferInsert.data { + const { id: _, messageID: __, sessionID: ___, ...rest } = part + return rest as DeepMutable +} + +function applyUsage( + db: DatabaseService, + sessionID: typeof SessionLegacy.Event.MessageUpdated.Type["data"]["sessionID"], + value: Usage, + sign = 1, +) { + return db + .update(SessionTable) + .set({ + cost: sql`${SessionTable.cost} + ${value.cost * sign}`, + tokens_input: sql`${SessionTable.tokens_input} + ${value.tokens.input * sign}`, + tokens_output: sql`${SessionTable.tokens_output} + ${value.tokens.output * sign}`, + tokens_reasoning: sql`${SessionTable.tokens_reasoning} + ${value.tokens.reasoning * sign}`, + tokens_cache_read: sql`${SessionTable.tokens_cache_read} + ${value.tokens.cache.read * sign}`, + tokens_cache_write: sql`${SessionTable.tokens_cache_write} + ${value.tokens.cache.write * sign}`, + time_updated: sql`${SessionTable.time_updated}`, + }) + .where(eq(SessionTable.id, sessionID)) + .run() + .pipe(Effect.orDie) +} + function run(db: DatabaseService, event: SessionEvent.Event) { return Effect.gen(function* () { const adapter: SessionMessageUpdater.Adapter = { @@ -175,91 +261,186 @@ export const layer = Layer.effectDiscard( Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - yield* events.project(SessionEvent.AgentSwitched, (event) => + yield* events.project(SessionLegacy.Event.Created, (event) => Effect.gen(function* () { - const message = Schema.encodeSync(SessionMessage.AgentSwitched)( - new SessionMessage.AgentSwitched({ - id: event.id, - type: "agent-switched", - metadata: event.metadata, - agent: event.data.agent, - time: { created: event.data.timestamp }, - }), - ) - const data = { metadata: message.metadata, agent: message.agent, time: message.time } + yield* db.insert(SessionTable).values(sessionRow(event.data.info)).run().pipe(Effect.orDie) + if (event.data.info.workspaceID) { + yield* db + .update(WorkspaceTable) + .set({ time_used: Date.now() }) + .where(eq(WorkspaceTable.id, event.data.info.workspaceID)) + .run() + .pipe(Effect.orDie) + } + }), + ) + yield* events.project(SessionLegacy.Event.Updated, (event) => + db + .update(SessionTable) + .set(sessionRow(event.data.info)) + .where(eq(SessionTable.id, event.data.sessionID)) + .run() + .pipe(Effect.orDie), + ) + yield* events.project(SessionLegacy.Event.Deleted, (event) => + db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), + ) + yield* events.project(SessionLegacy.Event.MessageUpdated, (event) => + Effect.gen(function* () { + const time_created = event.data.info.time.created + const id = event.data.info.id + const sessionID = event.data.info.sessionID + const data = messageData(event.data.info) yield* db - .update(SessionTable) - .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) - .where(eq(SessionTable.id, event.data.sessionID)) - .run() - .pipe(Effect.orDie) - yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(event.id), - session_id: event.data.sessionID, - type: "agent-switched", - time_created: DateTime.toEpochMillis(event.data.timestamp), - data, - }, - ]) + .insert(MessageTable) + .values({ id, session_id: sessionID, time_created, data }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data } }) .run() .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.ModelSwitched, (event) => + yield* events.project(SessionLegacy.Event.MessageRemoved, (event) => Effect.gen(function* () { - const message = Schema.encodeSync(SessionMessage.ModelSwitched)( - new SessionMessage.ModelSwitched({ - id: event.id, - type: "model-switched", - metadata: event.metadata, - model: event.data.model, - time: { created: event.data.timestamp }, - }), - ) - const data = { metadata: message.metadata, model: message.model, time: message.time } - yield* db - .update(SessionTable) - .set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) - .where(eq(SessionTable.id, event.data.sessionID)) - .run() + const rows = yield* db + .select() + .from(PartTable) + .where(and(eq(PartTable.message_id, event.data.messageID), eq(PartTable.session_id, event.data.sessionID))) + .all() .pipe(Effect.orDie) + for (const row of rows) { + const previous = usage(row.data) + if (previous) yield* applyUsage(db, event.data.sessionID, previous, -1) + } yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(event.id), - session_id: event.data.sessionID, - type: "model-switched", - time_created: DateTime.toEpochMillis(event.data.timestamp), - data, - }, - ]) + .delete(MessageTable) + .where(and(eq(MessageTable.id, event.data.messageID), eq(MessageTable.session_id, event.data.sessionID))) .run() .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.Prompted, (event) => run(db, event)) - yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Retried, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event)) + yield* events.project(SessionLegacy.Event.PartRemoved, (event) => + Effect.gen(function* () { + const row = yield* db + .select() + .from(PartTable) + .where(and(eq(PartTable.id, event.data.partID), eq(PartTable.session_id, event.data.sessionID))) + .get() + .pipe(Effect.orDie) + const previous = row && usage(row.data) + if (previous) yield* applyUsage(db, event.data.sessionID, previous, -1) + yield* db + .delete(PartTable) + .where(and(eq(PartTable.id, event.data.partID), eq(PartTable.session_id, event.data.sessionID))) + .run() + .pipe(Effect.orDie) + }), + ) + yield* events.project(SessionLegacy.Event.PartUpdated, (event) => + Effect.gen(function* () { + const id = event.data.part.id + const messageID = event.data.part.messageID + const sessionID = event.data.part.sessionID + const data = partData(event.data.part) + const row = yield* db.select().from(PartTable).where(eq(PartTable.id, id)).get().pipe(Effect.orDie) + yield* db + .insert(PartTable) + .values({ id, message_id: messageID, session_id: sessionID, time_created: event.data.time, data }) + .onConflictDoUpdate({ target: PartTable.id, set: { data } }) + .run() + .pipe(Effect.orDie) + const previous = row && usage(row.data) + const next = usage(event.data.part) + if (previous) yield* applyUsage(db, row.session_id, previous, -1) + if (next) yield* applyUsage(db, sessionID, next) + }), + ) + // session.next.* projectors are disabled while the v2 message projection is stabilized. + // The events still publish through EventV2 and fan out through the opencode bridge. + // yield* events.project(SessionEvent.AgentSwitched, (event) => + // Effect.gen(function* () { + // const message = Schema.encodeSync(SessionMessage.AgentSwitched)( + // new SessionMessage.AgentSwitched({ + // id: event.id, + // type: "agent-switched", + // metadata: event.metadata, + // agent: event.data.agent, + // time: { created: event.data.timestamp }, + // }), + // ) + // const data = { metadata: message.metadata, agent: message.agent, time: message.time } + // yield* db + // .update(SessionTable) + // .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) + // .where(eq(SessionTable.id, event.data.sessionID)) + // .run() + // .pipe(Effect.orDie) + // yield* db + // .insert(SessionMessageTable) + // .values([ + // { + // id: SessionMessage.ID.make(event.id), + // session_id: event.data.sessionID, + // type: "agent-switched", + // time_created: DateTime.toEpochMillis(event.data.timestamp), + // data, + // }, + // ]) + // .run() + // .pipe(Effect.orDie) + // }), + // ) + // yield* events.project(SessionEvent.ModelSwitched, (event) => + // Effect.gen(function* () { + // const message = Schema.encodeSync(SessionMessage.ModelSwitched)( + // new SessionMessage.ModelSwitched({ + // id: event.id, + // type: "model-switched", + // metadata: event.metadata, + // model: event.data.model, + // time: { created: event.data.timestamp }, + // }), + // ) + // const data = { metadata: message.metadata, model: message.model, time: message.time } + // yield* db + // .update(SessionTable) + // .set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) + // .where(eq(SessionTable.id, event.data.sessionID)) + // .run() + // .pipe(Effect.orDie) + // yield* db + // .insert(SessionMessageTable) + // .values([ + // { + // id: SessionMessage.ID.make(event.id), + // session_id: event.data.sessionID, + // type: "model-switched", + // time_created: DateTime.toEpochMillis(event.data.timestamp), + // data, + // }, + // ]) + // .run() + // .pipe(Effect.orDie) + // }), + // ) + // yield* events.project(SessionEvent.Prompted, (event) => run(db, event)) + // yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) + // yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) + // yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event)) + // yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event)) + // yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) + // yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) + // yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) + // yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) + // yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) + // yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) + // yield* events.project(SessionEvent.Retried, (event) => run(db, event)) + // yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) + // yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event)) }), ) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 8b74b9c9b..ce564e3ed 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -41,7 +41,7 @@ import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { ACPRuntime } from "./runtime" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../provider/schema" + import { MessageV2 } from "@/session/message-v2" import { ConfigMCP } from "@/config/mcp" import { Todo } from "@/session/todo" @@ -51,6 +51,7 @@ import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, T import { applyPatch } from "diff" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { ShellID } from "@/tool/shell/id" +import { ProviderV2 } from "@opencode-ai/core/provider" type ModeOption = { id: string; name: string; description?: string } type ModelOption = { modelId: string; name: string } @@ -62,8 +63,8 @@ const log = Log.create({ service: "acp-agent" }) async function getContextLimit( sdk: OpencodeClient, - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, directory: string, ): Promise { const providers = await sdk.config @@ -104,7 +105,7 @@ async function sendUsageUpdate( const msg = lastAssistant.info if (!msg.providerID || !msg.modelID) return - const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory) + const size = await getContextLimit(sdk, ProviderV2.ID.make(msg.providerID), ProviderV2.ModelID.make(msg.modelID), directory) if (!size) { // Cannot calculate usage without known context size @@ -579,7 +580,7 @@ export class Agent implements ACPAgent { } } catch (e) { const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + providerID: ProviderV2.ID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -619,7 +620,7 @@ export class Agent implements ACPAgent { return result } catch (e) { const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + providerID: ProviderV2.ID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -664,7 +665,7 @@ export class Agent implements ACPAgent { return response } catch (e) { const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + providerID: ProviderV2.ID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -718,7 +719,7 @@ export class Agent implements ACPAgent { return mode } catch (e) { const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + providerID: ProviderV2.ID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -752,7 +753,7 @@ export class Agent implements ACPAgent { return result } catch (e) { const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + providerID: ProviderV2.ID.make(this.config.defaultModel?.providerID ?? "unknown"), }) if (LoadAPIKeyError.isInstance(error)) { throw RequestError.authRequired() @@ -1531,8 +1532,8 @@ export class Agent implements ACPAgent { if (lastUser?.role !== "user") return this.sessionManager.setModel(sessionId, { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), + providerID: ProviderV2.ID.make(lastUser.model.providerID), + modelID: ProviderV2.ModelID.make(lastUser.model.modelID), }) this.sessionManager.setVariant(sessionId, lastUser.model.variant) if (lastUser.agent) { @@ -1658,7 +1659,7 @@ function imageContents(attachments: Array<{ mime: string; url: string }>): ToolC }) } -async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> { +async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }> { const sdk = config.sdk const configured = config.defaultModel if (configured) return configured @@ -1700,8 +1701,8 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider const [best] = Provider.sort(Object.values(opencodeProvider.models)) if (best) { return { - providerID: ProviderID.make(best.providerID), - modelID: ModelID.make(best.id), + providerID: ProviderV2.ID.make(best.providerID), + modelID: ProviderV2.ModelID.make(best.id), } } } @@ -1710,8 +1711,8 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider const [best] = Provider.sort(models) if (best) { return { - providerID: ProviderID.make(best.providerID), - modelID: ModelID.make(best.id), + providerID: ProviderV2.ID.make(best.providerID), + modelID: ProviderV2.ModelID.make(best.id), } } @@ -1723,7 +1724,7 @@ async function lastUsedModel( sdk: OpencodeClient, directory: string, providers: Array<{ id: string; models: Record }>, -): Promise<{ providerID: ProviderID; modelID: ModelID } | undefined> { +): Promise<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } | undefined> { const session = await sdk.session .list({ directory, roots: true, limit: 1 }, { throwOnError: true }) .then((x) => x.data?.[0]) @@ -1745,8 +1746,8 @@ async function lastUsedModel( const provider = providers.find((entry) => entry.id === lastUser.model.providerID) if (!provider?.models[lastUser.model.modelID]) return return { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), + providerID: ProviderV2.ID.make(lastUser.model.providerID), + modelID: ProviderV2.ModelID.make(lastUser.model.modelID), } } @@ -1810,7 +1811,7 @@ function sortProvidersByName(providers: T[]): T[] { function modelVariantsFromProviders( providers: Array<{ id: string; models: Record }> }>, - model: { providerID: ProviderID; modelID: ModelID }, + model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, ): string[] { const provider = providers.find((entry) => entry.id === model.providerID) if (!provider) return [] @@ -1844,7 +1845,7 @@ function buildAvailableModels( } function formatModelIdWithVariant( - model: { providerID: ProviderID; modelID: ModelID }, + model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, variant: string | undefined, availableVariants: string[], includeVariant: boolean, @@ -1861,7 +1862,7 @@ function formatModelIdWithVariant( } function buildVariantMeta(input: { - model: { providerID: ProviderID; modelID: ModelID } + model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } variant?: string availableVariants: string[] }) { @@ -1877,7 +1878,7 @@ function buildVariantMeta(input: { function parseModelSelection( modelId: string, providers: Array<{ id: string; models: Record }> }>, -): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } { +): { model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }; variant?: string } { const parsed = Provider.parseModel(modelId) const provider = providers.find((p) => p.id === parsed.providerID) if (!provider) { @@ -1897,7 +1898,7 @@ function parseModelSelection( const baseModelInfo = provider.models[baseModelId] if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { return { - model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) }, + model: { providerID: parsed.providerID, modelID: ProviderV2.ModelID.make(baseModelId) }, variant: candidateVariant, } } diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts index 2c3e886bc..58f139e01 100644 --- a/packages/opencode/src/acp/types.ts +++ b/packages/opencode/src/acp/types.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@agentclientprotocol/sdk" import type { OpencodeClient } from "@opencode-ai/sdk/v2" -import type { ProviderID, ModelID } from "../provider/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" export interface ACPSessionState { id: string @@ -8,8 +8,8 @@ export interface ACPSessionState { mcpServers: McpServer[] createdAt: Date model?: { - providerID: ProviderID - modelID: ModelID + providerID: ProviderV2.ID + modelID: ProviderV2.ModelID } variant?: string modeId?: string @@ -18,7 +18,7 @@ export interface ACPSessionState { export interface ACPConfig { sdk: OpencodeClient defaultModel?: { - providerID: ProviderID - modelID: ModelID + providerID: ProviderV2.ID + modelID: ProviderV2.ModelID } } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 064a59f59..9dba3445b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,7 +1,7 @@ import { Config } from "@/config/config" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../provider/schema" + import { generateObject, streamObject, type ModelMessage } from "ai" import { Truncate } from "@/tool/truncate" import { Auth } from "../auth" @@ -25,6 +25,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" import { type DeepMutable } from "@opencode-ai/core/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" export const Info = Schema.Struct({ name: Schema.String, @@ -38,8 +39,8 @@ export const Info = Schema.Struct({ permission: Permission.Ruleset, model: Schema.optional( Schema.Struct({ - modelID: ModelID, - providerID: ProviderID, + modelID: ProviderV2.ModelID, + providerID: ProviderV2.ID, }), ), variant: Schema.optional(Schema.String), @@ -62,7 +63,7 @@ export interface Interface { readonly defaultAgent: () => Effect.Effect readonly generate: (input: { description: string - model?: { providerID: ProviderID; modelID: ModelID } + model?: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } }) => Effect.Effect< { identifier: string @@ -383,7 +384,7 @@ export const layer = Layer.effect( }), generate: Effect.fn("Agent.generate")(function* (input: { description: string - model?: { providerID: ProviderID; modelID: ModelID } + model?: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } }) { const cfg = yield* config.get() const model = input.model ?? (yield* provider.defaultModel()) diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 73ec18d73..920cccd64 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -2,6 +2,7 @@ import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "eff import { EffectBridge } from "@/effect/bridge" import * as Log from "@opencode-ai/core/util/log" import { BusEvent } from "./bus-event" +import { EventV2 } from "@opencode-ai/core/event" import { GlobalBus } from "./global" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" @@ -12,7 +13,13 @@ import { InstanceRef } from "@/effect/instance-ref" const log = Log.create({ service: "bus" }) -type BusProperties> = Schema.Schema.Type +type BusDefinition = BusEvent.Definition | EventV2.Definition +type BusSchema = D extends { data: infer S extends Schema.Top } + ? S + : D extends { properties: infer S extends Schema.Top } + ? S + : never +type BusProperties = Schema.Schema.Type> export const InstanceDisposed = BusEvent.define( "server.instance.disposed", @@ -21,7 +28,7 @@ export const InstanceDisposed = BusEvent.define( }), ) -type Payload = { +type Payload = { id: string type: D["type"] properties: BusProperties @@ -33,7 +40,7 @@ type State = { } export interface Interface { - readonly publish: ( + readonly publish: ( def: D, properties: BusProperties, options?: { id?: string }, @@ -44,11 +51,11 @@ export interface Interface { // Stream-returning shape acquired the subscription lazily on first pull, // opening a race window during which publishes were lost — see // test/bus/bus-effect.test.ts RACE tests. - readonly subscribe: ( + readonly subscribe: ( def: D, ) => Effect.Effect>, never, Scope.Scope> readonly subscribeAll: () => Effect.Effect, never, Scope.Scope> - readonly subscribeCallback: ( + readonly subscribeCallback: ( def: D, callback: (event: Payload) => unknown, ) => Effect.Effect<() => void> @@ -86,7 +93,7 @@ export const layer = Layer.effect( }), ) - function getOrCreate(state: State, def: D) { + function getOrCreate(state: State, def: D) { return Effect.gen(function* () { let ps = state.typed.get(def.type) if (!ps) { @@ -97,7 +104,7 @@ export const layer = Layer.effect( }) } - function publish(def: D, properties: BusProperties, options?: { id?: string }) { + function publish(def: D, properties: BusProperties, options?: { id?: string }) { return Effect.gen(function* () { const s = yield* InstanceState.get(state) const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties } @@ -120,7 +127,7 @@ export const layer = Layer.effect( }) } - const subscribe = ( + const subscribe = ( def: D, ): Effect.Effect>, never, Scope.Scope> => Effect.gen(function* () { @@ -169,7 +176,7 @@ export const layer = Layer.effect( }) } - const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* ( + const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* ( def: D, callback: (event: Payload) => unknown, ) { diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index 909b0b40b..3349d3c5e 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -1,10 +1,11 @@ import { EOL } from "os" import { Effect } from "effect" import { Provider } from "@/provider/provider" -import { ProviderID } from "../../provider/schema" + import { ModelsDev } from "@opencode-ai/core/models-dev" import { effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" +import { ProviderV2 } from "@opencode-ai/core/provider" export const ModelsCommand = effectCmd({ command: "models [provider]", @@ -33,7 +34,7 @@ export const ModelsCommand = effectCmd({ const provider = yield* Provider.Service const providers = yield* provider.list() - const print = (providerID: ProviderID, verbose?: boolean) => { + const print = (providerID: ProviderV2.ID, verbose?: boolean) => { const p = providers[providerID] const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b)) for (const [modelID, model] of sorted) { @@ -47,7 +48,7 @@ export const ModelsCommand = effectCmd({ } if (args.provider) { - const providerID = ProviderID.make(args.provider) + const providerID = ProviderV2.ID.make(args.provider) if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`) print(providerID, args.verbose) return @@ -61,6 +62,6 @@ export const ModelsCommand = effectCmd({ return a.localeCompare(b) }) - for (const providerID of ids) print(ProviderID.make(providerID), args.verbose) + for (const providerID of ids) print(ProviderV2.ID.make(providerID), args.verbose) }), }) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index d70fbe5bc..c10e7b7b9 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -669,12 +669,7 @@ export const layer = Layer.effect( } if (input.workspaceID === null) { - yield* sync.run(Session.Event.Updated, { - sessionID: input.sessionID, - info: { - workspaceID: null, - }, - }) + yield* session.setWorkspace({ sessionID: input.sessionID, workspaceID: undefined }) log.info("session warp complete", { workspaceID: input.workspaceID, @@ -695,12 +690,7 @@ export const layer = Layer.effect( const target = yield* WorkspaceAdapterRuntime.target(space) if (target.type === "local") { - yield* sync.run(Session.Event.Updated, { - sessionID: input.sessionID, - info: { - workspaceID: input.workspaceID, - }, - }) + yield* session.setWorkspace({ sessionID: input.sessionID, workspaceID: input.workspaceID }) log.info("session warp complete", { workspaceID: input.workspaceID, diff --git a/packages/opencode/src/event-v2-bridge.ts b/packages/opencode/src/event-v2-bridge.ts index 23923c504..3a29a6ebf 100644 --- a/packages/opencode/src/event-v2-bridge.ts +++ b/packages/opencode/src/event-v2-bridge.ts @@ -5,25 +5,12 @@ import { Bus as ProjectBus } from "@/bus" import { GlobalBus } from "@/bus/global" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { InstanceStore } from "@/project/instance-store" -import { SyncEvent } from "@/sync" import { EventV2 } from "@opencode-ai/core/event" -import { SessionProjector } from "@opencode-ai/core/session/projector" import "@opencode-ai/core/account" import "@opencode-ai/core/catalog" import "@opencode-ai/core/session/event" import { Context, Effect, Layer, Option, Stream } from "effect" -export function toSyncDefinition(definition: D) { - const result = { - type: definition.type, - version: definition.sync?.version, - aggregate: definition.sync?.aggregate, - schema: definition.data, - properties: definition.data, - } - return result as SyncEvent.Definition -} - export class Service extends Context.Service()("@opencode/EventV2Bridge") {} export const layer = Layer.effect( @@ -76,9 +63,7 @@ export const layer = Layer.effect( ) export const defaultLayer = layer.pipe( - Layer.provideMerge(SessionProjector.defaultLayer), Layer.provide(EventV2.defaultLayer), - Layer.provide(SyncEvent.defaultLayer), Layer.provide(ProjectBus.defaultLayer), ) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index a304fec54..3dfc1aafe 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -4,7 +4,7 @@ import { Auth } from "@/auth" import { InstanceState } from "@/effect/instance-state" import { optionalOmitUndefined } from "@opencode-ai/core/schema" import { Plugin } from "../plugin" -import { ProviderID } from "./schema" +import { ProviderV2 } from "@opencode-ai/core/provider" import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect" const When = Schema.Struct({ @@ -65,11 +65,11 @@ export const CallbackInput = Schema.Struct({ export type CallbackInput = Schema.Schema.Type export class OauthMissing extends Schema.TaggedErrorClass()("ProviderAuthOauthMissing", { - providerID: ProviderID, + providerID: ProviderV2.ID, }) {} export class OauthCodeMissing extends Schema.TaggedErrorClass()("ProviderAuthOauthCodeMissing", { - providerID: ProviderID, + providerID: ProviderV2.ID, }) {} export class OauthCallbackFailed extends Schema.TaggedErrorClass()( @@ -90,15 +90,15 @@ export interface Interface { readonly methods: () => Effect.Effect readonly authorize: ( input: { - providerID: ProviderID + providerID: ProviderV2.ID } & AuthorizeInput, ) => Effect.Effect - readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect + readonly callback: (input: { providerID: ProviderV2.ID } & CallbackInput) => Effect.Effect } interface State { - hooks: Record - pending: Map + hooks: Record + pending: Map } export class Service extends Context.Service()("@opencode/ProviderAuth") {} @@ -117,11 +117,11 @@ export const layer: Layer.Layer = hooks: Record.fromEntries( Arr.filterMap(plugins, (x) => x.auth?.provider !== undefined - ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const) + ? Result.succeed([ProviderV2.ID.make(x.auth.provider), x.auth] as const) : Result.failVoid, ), ), - pending: new Map(), + pending: new Map(), } }), ) @@ -160,7 +160,7 @@ export const layer: Layer.Layer = }) const authorize = Effect.fn("ProviderAuth.authorize")(function* ( - input: { providerID: ProviderID } & AuthorizeInput, + input: { providerID: ProviderV2.ID } & AuthorizeInput, ) { const { hooks, pending } = yield* InstanceState.get(state) const method = hooks[input.providerID].methods[input.method] @@ -184,7 +184,7 @@ export const layer: Layer.Layer = } }) - const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) { + const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderV2.ID } & CallbackInput) { const pending = (yield* InstanceState.get(state)).pending const match = pending.get(input.providerID) if (!match) return yield* new OauthMissing({ providerID: input.providerID }) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 7363b5ce5..b30ef5164 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -1,7 +1,7 @@ import { APICallError } from "ai" import { STATUS_CODES } from "http" import { iife } from "@/util/iife" -import type { ProviderID } from "./schema" +import type { ProviderV2 } from "@opencode-ai/core/provider" // Adapted from overflow detection patterns in: // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts @@ -45,7 +45,7 @@ function isOverflow(message: string) { return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message) } -function message(providerID: ProviderID, e: APICallError) { +function message(providerID: ProviderV2.ID, e: APICallError) { return iife(() => { const msg = e.message if (msg === "") { @@ -178,7 +178,7 @@ export type ParsedAPICallError = metadata?: Record } -export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError { +export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError { const m = message(input.providerID, input.error) const body = json(input.error.responseBody) if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 496a2f6d2..05bab8829 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -25,7 +25,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { isRecord } from "@/util/record" import { optionalOmitUndefined } from "@opencode-ai/core/schema" import * as ProviderTransform from "./transform" -import { ModelID, ProviderID } from "./schema" +import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelStatus } from "./model-status" import { RuntimeFlags } from "@/effect/runtime-flags" @@ -653,8 +653,8 @@ function custom(dep: CustomDep): Record { for (const m of result.models) { if (!input.models[m.id]) { models[m.id] = { - id: ModelID.make(m.id), - providerID: ProviderID.make("gitlab"), + id: ProviderV2.ModelID.make(m.id), + providerID: ProviderV2.ID.make("gitlab"), name: `Agent Platform (${m.name})`, family: "", api: { @@ -918,8 +918,8 @@ const ProviderLimit = Schema.Struct({ }) export const Model = Schema.Struct({ - id: ModelID, - providerID: ProviderID, + id: ProviderV2.ModelID, + providerID: ProviderV2.ID, api: ProviderApiInfo, name: Schema.String, family: optionalOmitUndefined(Schema.String), @@ -935,7 +935,7 @@ export const Model = Schema.Struct({ export type Model = Types.DeepMutable> export const Info = Schema.Struct({ - id: ProviderID, + id: ProviderV2.ID, name: Schema.String, source: Schema.Literals(["env", "config", "custom", "api"]), env: Schema.Array(Schema.String), @@ -975,8 +975,8 @@ export function defaultModelIDs()("ProviderModelNotFoundError", { - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, suggestions: Schema.optional(Schema.Array(Schema.String)), cause: Schema.optional(Schema.Defect), }) { @@ -986,7 +986,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass()("ProviderInitError", { - providerID: ProviderID, + providerID: ProviderV2.ID, cause: Schema.optional(Schema.Defect), }) { static isInstance(input: unknown): input is InitError { @@ -1001,7 +1001,7 @@ export class NoProvidersError extends Schema.TaggedErrorClass( } export class NoModelsError extends Schema.TaggedErrorClass()("ProviderNoModelsError", { - providerID: ProviderID, + providerID: ProviderV2.ID, }) { static isInstance(input: unknown): input is NoModelsError { return input instanceof NoModelsError @@ -1012,22 +1012,22 @@ export type DefaultModelError = ModelNotFoundError | NoProvidersError | NoModels export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModelsError export interface Interface { - readonly list: () => Effect.Effect> - readonly getProvider: (providerID: ProviderID) => Effect.Effect - readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect + readonly list: () => Effect.Effect> + readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect + readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect readonly getLanguage: (model: Model) => Effect.Effect readonly closest: ( - providerID: ProviderID, + providerID: ProviderV2.ID, query: string[], - ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> - readonly getSmallModel: (providerID: ProviderID) => Effect.Effect - readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }, DefaultModelError> + ) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined> + readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect + readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError> } interface State { models: Map - providers: Record - catalog: Record + providers: Record + catalog: Record sdk: Map modelLoaders: Record varsLoaders: Record @@ -1072,8 +1072,8 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] { function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const base: Model = { - id: ModelID.make(model.id), - providerID: ProviderID.make(provider.id), + id: ProviderV2.ModelID.make(model.id), + providerID: ProviderV2.ID.make(provider.id), name: model.name, family: model.family, api: { @@ -1130,7 +1130,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { const base = fromModelsDevModel(provider, model) models[id] = { ...base, - id: ModelID.make(id), + id: ProviderV2.ModelID.make(id), name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`, cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost, options: opts.provider?.body @@ -1146,7 +1146,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { } } return { - id: ProviderID.make(provider.id), + id: ProviderV2.ID.make(provider.id), source: "custom", name: provider.name, env: [...(provider.env ?? [])], @@ -1165,7 +1165,7 @@ function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels }) } -function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) { +function modelSuggestions(provider: Info | undefined, modelID: ProviderV2.ModelID, enableExperimentalModels: boolean) { const available = suggestionModelIDs(provider, enableExperimentalModels) const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target) if (fuzzy.length) return fuzzy @@ -1207,7 +1207,7 @@ export const layer = Layer.effect( const catalog = mapValues(modelsDev, fromModelsDevProvider) const database = mapValues(catalog, toPublicInfo) - const providers: Record = {} as Record + const providers: Record = {} as Record const languages = new Map() const modelLoaders: { [providerID: string]: CustomModelLoader @@ -1228,7 +1228,7 @@ export const layer = Layer.effect( log.info("init") - function mergeProvider(providerID: ProviderID, provider: Partial) { + function mergeProvider(providerID: ProviderV2.ID, provider: Partial) { const existing = providers[providerID] if (existing) { // @ts-expect-error @@ -1249,7 +1249,7 @@ export const layer = Layer.effect( const disabled = new Set(cfg.disabled_providers ?? []) const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null - function isProviderAllowed(providerID: ProviderID): boolean { + function isProviderAllowed(providerID: ProviderV2.ID): boolean { if (enabled && !enabled.has(providerID)) return false if (disabled.has(providerID)) return false return true @@ -1260,7 +1260,7 @@ export const layer = Layer.effect( const models = p?.models if (!p || !models) continue - const providerID = ProviderID.make(p.id) + const providerID = ProviderV2.ID.make(p.id) if (disabled.has(providerID)) continue const provider = database[providerID] @@ -1274,7 +1274,7 @@ export const layer = Layer.effect( id, { ...model, - id: ModelID.make(id), + id: ProviderV2.ModelID.make(id), providerID, }, ]), @@ -1286,7 +1286,7 @@ export const layer = Layer.effect( for (const [providerID, provider] of configProviders) { const existing = database[providerID] const parsed: Info = { - id: ProviderID.make(providerID), + id: ProviderV2.ID.make(providerID), name: provider.name ?? existing?.name ?? providerID, env: provider.env ?? existing?.env ?? [], options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), @@ -1309,7 +1309,7 @@ export const layer = Layer.effect( return existingModel?.name ?? modelID }) const parsedModel: Model = { - id: ModelID.make(modelID), + id: ProviderV2.ModelID.make(modelID), api: { id: apiID, npm: apiNpm, @@ -1317,7 +1317,7 @@ export const layer = Layer.effect( }, status: model.status ?? existingModel?.status ?? "active", name, - providerID: ProviderID.make(providerID), + providerID: ProviderV2.ID.make(providerID), capabilities: { temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, @@ -1379,7 +1379,7 @@ export const layer = Layer.effect( // load env const envs = yield* env.all() for (const [id, provider] of Object.entries(database)) { - const providerID = ProviderID.make(id) + const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue const apiKey = provider.env.map((item) => envs[item]).find(Boolean) if (!apiKey) continue @@ -1392,7 +1392,7 @@ export const layer = Layer.effect( // load apikeys const auths = yield* auth.all().pipe(Effect.orDie) for (const [id, provider] of Object.entries(auths)) { - const providerID = ProviderID.make(id) + const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue if (provider.type === "api") { mergeProvider(providerID, { @@ -1405,7 +1405,7 @@ export const layer = Layer.effect( // plugin auth loader - database now has entries for config providers for (const plugin of plugins) { if (!plugin.auth) continue - const providerID = ProviderID.make(plugin.auth.provider) + const providerID = ProviderV2.ID.make(plugin.auth.provider) if (disabled.has(providerID)) continue const stored = yield* auth.get(providerID).pipe(Effect.orDie) @@ -1424,7 +1424,7 @@ export const layer = Layer.effect( } for (const [id, fn] of Object.entries(custom(dep))) { - const providerID = ProviderID.make(id) + const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue const data = database[providerID] if (!data) { @@ -1444,7 +1444,7 @@ export const layer = Layer.effect( // load config - re-apply with updated data for (const [id, provider] of configProviders) { - const providerID = ProviderID.make(id) + const providerID = ProviderV2.ID.make(id) const partial: Partial = { source: "config" } if (provider.env) partial.env = provider.env if (provider.name) partial.name = provider.name @@ -1452,7 +1452,7 @@ export const layer = Layer.effect( mergeProvider(providerID, partial) } - const gitlab = ProviderID.make("gitlab") + const gitlab = ProviderV2.ID.make("gitlab") if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { yield* Effect.promise(async () => { try { @@ -1469,7 +1469,7 @@ export const layer = Layer.effect( } for (const [id, provider] of Object.entries(providers)) { - const providerID = ProviderID.make(id) + const providerID = ProviderV2.ID.make(id) if (!isProviderAllowed(providerID)) { delete providers[providerID] continue @@ -1483,10 +1483,10 @@ export const layer = Layer.effect( // These chat aliases are invalid for the special handling in the // built-in providers below, but custom providers may support them. (modelID === "gpt-5-chat-latest" && - (providerID === ProviderID.openai || - providerID === ProviderID.githubCopilot || - providerID === ProviderID.openrouter)) || - (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") + (providerID === ProviderV2.ID.openai || + providerID === ProviderV2.ID.githubCopilot || + providerID === ProviderV2.ID.openrouter)) || + (providerID === ProviderV2.ID.openrouter && modelID === "openai/gpt-5-chat") ) delete provider.models[modelID] if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID] @@ -1687,11 +1687,11 @@ export const layer = Layer.effect( } } - const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) => + const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderV2.ID) => InstanceState.use(state, (s) => s.providers[providerID]), ) - const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) { + const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) { const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) { @@ -1741,7 +1741,7 @@ export const layer = Layer.effect( ) }) - const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) { + const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderV2.ID, query: string[]) { const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) return undefined @@ -1753,7 +1753,7 @@ export const layer = Layer.effect( return undefined }) - const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) { + const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderV2.ID) { const cfg = yield* config.get() if (cfg.small_model) { @@ -1783,7 +1783,7 @@ export const layer = Layer.effect( priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] } for (const item of priority) { - if (providerID === ProviderID.amazonBedrock) { + if (providerID === ProviderV2.ID.amazonBedrock) { const crossRegionPrefixes = ["global.", "us.", "eu."] const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) @@ -1817,16 +1817,16 @@ export const layer = Layer.effect( const s = yield* InstanceState.get(state) const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe( - Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => { + Effect.map((x): { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[] => { if (!isRecord(x) || !Array.isArray(x.recent)) return [] return x.recent.flatMap((item) => { if (!isRecord(item)) return [] if (typeof item.providerID !== "string") return [] if (typeof item.modelID !== "string") return [] - return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }] + return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ProviderV2.ModelID.make(item.modelID) }] }) }), - Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])), + Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[])), ) for (const entry of recent) { const provider = s.providers[entry.providerID] @@ -1874,8 +1874,8 @@ export function sort(models: T[]) { export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { - providerID: ProviderID.make(providerID), - modelID: ModelID.make(rest.join("/")), + providerID: ProviderV2.ID.make(providerID), + modelID: ProviderV2.ModelID.make(rest.join("/")), } } diff --git a/packages/opencode/src/provider/schema.ts b/packages/opencode/src/provider/schema.ts deleted file mode 100644 index db05b4784..000000000 --- a/packages/opencode/src/provider/schema.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Schema } from "effect" - -import { withStatics } from "@opencode-ai/core/schema" - -const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID")) - -export type ProviderID = typeof providerIdSchema.Type - -export const ProviderID = providerIdSchema.pipe( - withStatics((schema: typeof providerIdSchema) => ({ - // Well-known providers - opencode: schema.make("opencode"), - anthropic: schema.make("anthropic"), - openai: schema.make("openai"), - google: schema.make("google"), - googleVertex: schema.make("google-vertex"), - githubCopilot: schema.make("github-copilot"), - amazonBedrock: schema.make("amazon-bedrock"), - azure: schema.make("azure"), - openrouter: schema.make("openrouter"), - mistral: schema.make("mistral"), - gitlab: schema.make("gitlab"), - })), -) - -const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID")) - -export type ModelID = typeof modelIdSchema.Type - -export const ModelID = modelIdSchema diff --git a/packages/opencode/src/server/projectors.ts b/packages/opencode/src/server/projectors.ts index 7aa7551db..2ded2c2cd 100644 --- a/packages/opencode/src/server/projectors.ts +++ b/packages/opencode/src/server/projectors.ts @@ -1,26 +1,2 @@ -import sessionProjectors from "../session/projectors" -import { SyncEvent } from "@/sync" -import { Session } from "@/session/session" -import { SessionTable } from "@opencode-ai/core/session/sql" -import { Database } from "@/storage/db" -import { eq } from "drizzle-orm" - export function initProjectors() { - SyncEvent.init({ - projectors: sessionProjectors, - convertEvent: (type, data) => { - if (type === "session.updated") { - const id = (data as SyncEvent.Event["data"]).sessionID - const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - - if (!row) return data - - return { - sessionID: id, - info: Session.fromRow(row), - } - } - return data - }, - }) } diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts index 33e6a8e4a..49f43f015 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts @@ -1,11 +1,12 @@ import { Auth } from "@/auth" -import { ProviderID } from "@/provider/schema" + import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { described } from "./metadata" +import { ProviderV2 } from "@opencode-ai/core/provider" const AuthParams = Schema.Struct({ - providerID: ProviderID, + providerID: ProviderV2.ID, }) const LogQuery = Schema.Struct({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts index 4cda970e8..12adafded 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts @@ -1,6 +1,6 @@ import { AccountID, OrgID } from "@/account/schema" import { MCP } from "@/mcp" -import { ProviderID, ModelID } from "@/provider/schema" + import { Session } from "@/session/session" import { Worktree } from "@/worktree" import { NonNegativeInt } from "@opencode-ai/core/schema" @@ -15,6 +15,7 @@ import { } from "../middleware/workspace-routing" import { described } from "./metadata" import { QueryBoolean } from "./query" +import { ProviderV2 } from "@opencode-ai/core/provider" const ConsoleStateResponse = Schema.Struct({ consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), @@ -49,8 +50,8 @@ const ToolListItem = Schema.Struct({ const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" }) export const ToolListQuery = Schema.Struct({ ...WorkspaceRoutingQueryFields, - provider: ProviderID, - model: ModelID, + provider: ProviderV2.ID, + model: ProviderV2.ModelID, }) const WorktreeList = Schema.Array(Schema.String) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts index 0d8e49022..3a9ae0c6d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts @@ -1,12 +1,13 @@ import { ProviderAuth } from "@/provider/auth" import { Provider } from "@/provider/provider" -import { ProviderID } from "@/provider/schema" + import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing" import { described } from "./metadata" +import { ProviderV2 } from "@opencode-ai/core/provider" const root = "/provider" @@ -21,7 +22,7 @@ export class ProviderAuthApiError extends Schema.ErrorClass Effect.gen(function* () { const auth = yield* Auth.Service const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: { - params: { providerID: ProviderID } + params: { providerID: ProviderV2.ID } payload: Auth.Info }) { yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie) return true }) - const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) { + const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderV2.ID } }) { yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie) return true }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts index b9766ca97..e1377b6f7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts @@ -2,13 +2,14 @@ import { ProviderAuth } from "@/provider/auth" import { Config } from "@/config/config" import { ModelsDev } from "@opencode-ai/core/models-dev" import { Provider } from "@/provider/provider" -import { ProviderID } from "@/provider/schema" + import { mapValues } from "remeda" import { Effect, Schema } from "effect" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" import { ProviderAuthApiError } from "../groups/provider" +import { ProviderV2 } from "@opencode-ai/core/provider" function mapProviderAuthError(self: Effect.Effect) { return self.pipe( @@ -62,7 +63,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" }) const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: { - params: { providerID: ProviderID } + params: { providerID: ProviderV2.ID } payload: ProviderAuth.AuthorizeInput }) { return yield* mapProviderAuthError( @@ -75,7 +76,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" }) const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: { - params: { providerID: ProviderID } + params: { providerID: ProviderV2.ID } request: HttpServerRequest.HttpServerRequest }) { const body = yield* Effect.orDie(ctx.request.text) @@ -90,7 +91,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" }) const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: { - params: { providerID: ProviderID } + params: { providerID: ProviderV2.ID } payload: ProviderAuth.CallbackInput }) { yield* mapProviderAuthError( diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts index e05e03337..2a988753d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts @@ -21,6 +21,7 @@ const log = Log.create({ service: "server.sync" }) export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) => Effect.gen(function* () { const workspace = yield* Workspace.Service + const session = yield* Session.Service const scope = yield* Scope.Scope const sync = yield* SyncEvent.Service @@ -61,12 +62,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl const workspaceID = yield* InstanceState.workspaceID if (!workspaceID) return yield* new HttpApiError.BadRequest({}) - yield* sync.run(Session.Event.Updated, { - sessionID: ctx.payload.sessionID, - info: { - workspaceID, - }, - }) + yield* session.setWorkspace({ sessionID: ctx.payload.sessionID, workspaceID }) log.info("sync session stolen", { sessionID: ctx.payload.sessionID, diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 6ccc995c6..1b98134ee 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -46,6 +46,7 @@ import { Todo } from "@/session/todo" import { SessionShare } from "@/share/session" import { ShareNext } from "@/share/share-next" import { EventV2Bridge } from "@/event-v2-bridge" +import { Database } from "@opencode-ai/core/database/database" import { Skill } from "@/skill" import { Snapshot } from "@/snapshot" import { SyncEvent } from "@/sync" @@ -196,6 +197,7 @@ export function createRoutes( Auth.defaultLayer, Command.defaultLayer, Config.defaultLayer, + Database.defaultLayer, File.defaultLayer, FileWatcher.defaultLayer, Format.defaultLayer, diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index c1abb6cd2..e2d486949 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -12,7 +12,7 @@ import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" import { Config } from "@/config/config" import { NotFoundError } from "@/storage/storage" -import { ModelID, ProviderID } from "@/provider/schema" + import { Effect, Layer, Context, Schema } from "effect" import * as DateTime from "effect/DateTime" import { InstanceState } from "@/effect/instance-state" @@ -21,6 +21,7 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" import { SessionEvent } from "@opencode-ai/core/session/event" +import { ProviderV2 } from "@opencode-ai/core/provider" const log = Log.create({ service: "session.compaction" }) @@ -200,7 +201,7 @@ export interface Interface { readonly create: (input: { sessionID: SessionID agent: string - model: { providerID: ProviderID; modelID: ModelID } + model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } auto: boolean overflow?: boolean }) => Effect.Effect @@ -585,7 +586,7 @@ export const layer = Layer.effect( const create = Effect.fn("SessionCompaction.create")(function* (input: { sessionID: SessionID agent: string - model: { providerID: ProviderID; modelID: ModelID } + model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID } auto: boolean overflow?: boolean }) { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 47e37443d..98ca7699f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,5 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { SessionID, MessageID, PartID } from "./schema" +import { SessionLegacy } from "@opencode-ai/core/session/legacy" +import { ProviderV2 } from "@opencode-ai/core/provider" import { APIError, AbortedError, @@ -14,14 +16,11 @@ import { SubtaskPart, User, WithParts, - type ModelID, - type ProviderID, type ToolPart, } from "@opencode-ai/core/session/legacy" import { NamedError } from "@opencode-ai/core/util/error" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" -import { SyncEvent } from "../sync" import { Database } from "@opencode-ai/core/database/database" import { NotFoundError } from "@/storage/storage" import { and } from "drizzle-orm" @@ -56,47 +55,10 @@ function truncateToolOutput(text: string, maxChars?: number) { return `${text.slice(0, maxChars)}\n[Tool output truncated for compaction: omitted ${omitted} chars]` } -const UpdatedEventSchema = Schema.Struct({ - sessionID: SessionID, - info: Info, -}) - -const RemovedEventSchema = Schema.Struct({ - sessionID: SessionID, - messageID: MessageID, -}) - -const PartUpdatedEventSchema = Schema.Struct({ - sessionID: SessionID, - part: Part, - time: Schema.Number, -}) - -const PartRemovedEventSchema = Schema.Struct({ - sessionID: SessionID, - messageID: MessageID, - partID: PartID, -}) - export const Event = { - Updated: SyncEvent.define({ - type: "message.updated", - version: 1, - aggregate: "sessionID", - schema: UpdatedEventSchema, - }), - Removed: SyncEvent.define({ - type: "message.removed", - version: 1, - aggregate: "sessionID", - schema: RemovedEventSchema, - }), - PartUpdated: SyncEvent.define({ - type: "message.part.updated", - version: 1, - aggregate: "sessionID", - schema: PartUpdatedEventSchema, - }), + Updated: BusEvent.define("message.updated", SessionLegacy.Event.MessageUpdated.data), + Removed: BusEvent.define("message.removed", SessionLegacy.Event.MessageRemoved.data), + PartUpdated: BusEvent.define("message.part.updated", SessionLegacy.Event.PartUpdated.data), PartDelta: BusEvent.define( "message.part.delta", Schema.Struct({ @@ -107,12 +69,7 @@ export const Event = { delta: Schema.String, }), ), - PartRemoved: SyncEvent.define({ - type: "message.part.removed", - version: 1, - aggregate: "sessionID", - schema: PartRemovedEventSchema, - }), + PartRemoved: BusEvent.define("message.part.removed", SessionLegacy.Event.PartRemoved.data), } const Cursor = Schema.Struct({ @@ -656,7 +613,7 @@ export function latest(msgs: WithParts[]) { export function fromError( e: unknown, - ctx: { providerID: ProviderID; aborted?: boolean }, + ctx: { providerID: ProviderV2.ID; aborted?: boolean }, ): NonNullable { switch (true) { case e instanceof DOMException && e.name === "AbortError": diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 39c842f94..e5332992f 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,9 +1,10 @@ import { Schema } from "effect" import { SessionID } from "./schema" -import { ModelID, ProviderID } from "../provider/schema" + import { NonNegativeInt } from "@opencode-ai/core/schema" import { MessageError } from "./message-error" import { AuthError, OutputLengthError } from "./message-error" +import { ProviderV2 } from "@opencode-ai/core/provider" export { AuthError, OutputLengthError } from "./message-error" export const ToolCall = Schema.Struct({ @@ -119,8 +120,8 @@ export const Info = Schema.Struct({ assistant: Schema.optional( Schema.Struct({ system: Schema.Array(Schema.String), - modelID: ModelID, - providerID: ProviderID, + modelID: ProviderV2.ModelID, + providerID: ProviderV2.ID, path: Schema.Struct({ cwd: Schema.String, root: Schema.String, diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts deleted file mode 100644 index a845b1e70..000000000 --- a/packages/opencode/src/session/projectors.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { NotFoundError } from "@/storage/storage" -import { SessionLegacy } from "@opencode-ai/core/session/legacy" -import { eq } from "drizzle-orm" -import { and } from "drizzle-orm" -import { sql } from "drizzle-orm" -import type { TxOrDb } from "@/storage/db" -import { SyncEvent } from "@/sync" -import * as Session from "./session" -import { MessageV2 } from "./message-v2" -import { SessionTable, MessageTable, PartTable } from "@opencode-ai/core/session/sql" -import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql" -import { Log } from "@opencode-ai/core/util/log" - -const log = Log.create({ service: "session.projector" }) - -function foreign(err: unknown) { - if (typeof err !== "object" || err === null) return false - if ("code" in err && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") return true - return "message" in err && typeof err.message === "string" && err.message.includes("FOREIGN KEY constraint failed") -} - -export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial | null } : T - -type Usage = Pick - -function usage(part: SessionLegacy.Part | unknown): Usage | undefined { - if (typeof part !== "object" || part === null) return undefined - const value = part as Record - if (value.type !== "step-finish") return undefined - if (!("cost" in value) || !("tokens" in value)) return undefined - return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] } -} - -function applyUsage(db: TxOrDb, sessionID: Session.Info["id"], value: Usage, sign = 1) { - db.update(SessionTable) - .set({ - cost: sql`${SessionTable.cost} + ${value.cost * sign}`, - tokens_input: sql`${SessionTable.tokens_input} + ${value.tokens.input * sign}`, - tokens_output: sql`${SessionTable.tokens_output} + ${value.tokens.output * sign}`, - tokens_reasoning: sql`${SessionTable.tokens_reasoning} + ${value.tokens.reasoning * sign}`, - tokens_cache_read: sql`${SessionTable.tokens_cache_read} + ${value.tokens.cache.read * sign}`, - tokens_cache_write: sql`${SessionTable.tokens_cache_write} + ${value.tokens.cache.write * sign}`, - time_updated: sql`${SessionTable.time_updated}`, - }) - .where(eq(SessionTable.id, sessionID)) - .run() -} - -function grab( - obj: T, - field1: K1, - cb?: (val: NonNullable) => X, -): X | undefined { - if (obj == undefined || !(field1 in obj)) return undefined - - const val = obj[field1] - if (val && typeof val === "object" && cb) { - return cb(val) - } - if (val === undefined) { - throw new Error( - "Session update failure: pass `null` to clear a field instead of `undefined`: " + JSON.stringify(obj), - ) - } - return val as X | undefined -} - -export function toPartialRow(info: DeepPartial) { - const obj = { - id: grab(info, "id"), - project_id: grab(info, "projectID"), - workspace_id: grab(info, "workspaceID"), - parent_id: grab(info, "parentID"), - slug: grab(info, "slug"), - directory: grab(info, "directory"), - path: grab(info, "path"), - title: grab(info, "title"), - version: grab(info, "version"), - share_url: grab(info, "share", (v) => grab(v, "url")), - summary_additions: grab(info, "summary", (v) => grab(v, "additions")), - summary_deletions: grab(info, "summary", (v) => grab(v, "deletions")), - summary_files: grab(info, "summary", (v) => grab(v, "files")), - summary_diffs: grab(info, "summary", (v) => grab(v, "diffs")), - cost: grab(info, "cost"), - tokens_input: grab(info, "tokens", (v) => grab(v, "input")), - tokens_output: grab(info, "tokens", (v) => grab(v, "output")), - tokens_reasoning: grab(info, "tokens", (v) => grab(v, "reasoning")), - tokens_cache_read: grab(info, "tokens", (v) => grab(v, "cache", (cache) => grab(cache, "read"))), - tokens_cache_write: grab(info, "tokens", (v) => grab(v, "cache", (cache) => grab(cache, "write"))), - revert: grab(info, "revert"), - permission: grab(info, "permission"), - time_created: grab(info, "time", (v) => grab(v, "created")), - time_updated: grab(info, "time", (v) => grab(v, "updated")), - time_compacting: grab(info, "time", (v) => grab(v, "compacting")), - time_archived: grab(info, "time", (v) => grab(v, "archived")), - } - - return Object.fromEntries(Object.entries(obj).filter(([_, val]) => val !== undefined)) -} - -export default [ - SyncEvent.project(Session.Event.Created, (db, data) => { - db.insert(SessionTable) - .values(Session.toRow(data.info as Session.Info)) - .run() - - if (data.info.workspaceID) { - db.update(WorkspaceTable).set({ time_used: Date.now() }).where(eq(WorkspaceTable.id, data.info.workspaceID)).run() - } - }), - - SyncEvent.project(Session.Event.Updated, (db, data) => { - const info = data.info - const row = db - .update(SessionTable) - .set({ time_updated: sql`${SessionTable.time_updated}`, ...toPartialRow(info as Session.Patch) }) - .where(eq(SessionTable.id, data.sessionID)) - .returning() - .get() - if (!row) throw new NotFoundError({ message: `Session not found: ${data.sessionID}` }) - }), - - SyncEvent.project(Session.Event.Deleted, (db, data) => { - db.delete(SessionTable).where(eq(SessionTable.id, data.sessionID)).run() - }), - - SyncEvent.project(MessageV2.Event.Updated, (db, data) => { - const time_created = data.info.time.created - const { id, sessionID, ...rest } = data.info - - try { - db.insert(MessageTable) - .values({ - id, - session_id: sessionID, - time_created, - data: rest, - }) - .onConflictDoUpdate({ target: MessageTable.id, set: { data: rest } }) - .run() - } catch (err) { - if (!foreign(err)) throw err - log.warn("ignored late message update", { messageID: id, sessionID }) - } - }), - - SyncEvent.project(MessageV2.Event.Removed, (db, data) => { - for (const row of db - .select() - .from(PartTable) - .where(and(eq(PartTable.message_id, data.messageID), eq(PartTable.session_id, data.sessionID))) - .all()) { - const previous = usage(row.data) - if (previous) applyUsage(db, data.sessionID, previous, -1) - } - db.delete(MessageTable) - .where(and(eq(MessageTable.id, data.messageID), eq(MessageTable.session_id, data.sessionID))) - .run() - }), - - SyncEvent.project(MessageV2.Event.PartRemoved, (db, data) => { - const row = db - .select() - .from(PartTable) - .where(and(eq(PartTable.id, data.partID), eq(PartTable.session_id, data.sessionID))) - .get() - const previous = row && usage(row.data) - if (previous) applyUsage(db, data.sessionID, previous, -1) - - db.delete(PartTable) - .where(and(eq(PartTable.id, data.partID), eq(PartTable.session_id, data.sessionID))) - .run() - }), - - SyncEvent.project(MessageV2.Event.PartUpdated, (db, data) => { - const { id, messageID, sessionID, ...rest } = data.part - const row = db.select().from(PartTable).where(eq(PartTable.id, id)).get() - - try { - db.insert(PartTable) - .values({ - id, - message_id: messageID, - session_id: sessionID, - time_created: data.time, - data: rest, - }) - .onConflictDoUpdate({ target: PartTable.id, set: { data: rest } }) - .run() - const previous = row && usage(row.data) - const next = usage(data.part) - if (previous) applyUsage(db, row.session_id, previous, -1) - if (next) applyUsage(db, sessionID, next) - } catch (err) { - if (!foreign(err)) throw err - log.warn("ignored late part update", { partID: id, messageID, sessionID }) - } - }), -] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9941d6969..041438c50 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -8,7 +8,7 @@ import { SessionRevert } from "./revert" import * as Session from "./session" import { Agent } from "../agent/agent" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../provider/schema" + import { type Tool as AITool, tool, jsonSchema } from "ai" import type { JSONSchema7 } from "@ai-sdk/provider" import { SessionCompaction } from "./compaction" @@ -239,8 +239,8 @@ export const layer = Layer.effect( const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: { session: Session.Info history: SessionLegacy.WithParts[] - providerID: ProviderID - modelID: ModelID + providerID: ProviderV2.ID + modelID: ProviderV2.ModelID }) { if (input.session.parentID) return if (!Session.isDefaultTitle(input.session.title)) return @@ -651,8 +651,8 @@ export const layer = Layer.effect( }) const getModel = Effect.fn("SessionPrompt.getModel")(function* ( - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, sessionID: SessionID, ) { const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit) @@ -679,8 +679,8 @@ export const layer = Layer.effect( .pipe(Effect.orDie) if (current?.model) { return { - providerID: ProviderID.make(current.model.providerID), - modelID: ModelID.make(current.model.id), + providerID: ProviderV2.ID.make(current.model.providerID), + modelID: ProviderV2.ModelID.make(current.model.id), ...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}), } } @@ -1666,8 +1666,8 @@ export const defaultLayer = Layer.suspend(() => ), ) const ModelRef = Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + providerID: ProviderV2.ID, + modelID: ProviderV2.ModelID, }) export const PromptInput = Schema.Struct({ diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 457438171..21096063e 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -3,7 +3,6 @@ import { SessionLegacy } from "@opencode-ai/core/session/legacy" import { Bus } from "../bus" import { Snapshot } from "../snapshot" import { Storage } from "@/storage/storage" -import { SyncEvent } from "../sync" import * as Log from "@opencode-ai/core/util/log" import * as Session from "./session" import { MessageV2 } from "./message-v2" @@ -37,7 +36,6 @@ export const layer = Layer.effect( const bus = yield* Bus.Service const summary = yield* SessionSummary.Service const state = yield* SessionRunState.Service - const sync = yield* SyncEvent.Service const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) { yield* state.assertNotBusy(input.sessionID) @@ -121,10 +119,7 @@ export const layer = Layer.effect( remove.push(msg) } for (const msg of remove) { - yield* sync.run(MessageV2.Event.Removed, { - sessionID, - messageID: msg.info.id, - }) + yield* sessions.removeMessage({ sessionID, messageID: msg.info.id }) } if (session.revert.partID && target) { const partID = session.revert.partID @@ -133,11 +128,7 @@ export const layer = Layer.effect( const removeParts = target.parts.slice(idx) target.parts = target.parts.slice(0, idx) for (const part of removeParts) { - yield* sync.run(MessageV2.Event.PartRemoved, { - sessionID, - messageID: target.info.id, - partID: part.id, - }) + yield* sessions.removePart({ sessionID, messageID: target.info.id, partID: part.id }) } } } @@ -156,7 +147,6 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Storage.defaultLayer), Layer.provide(Bus.layer), Layer.provide(SessionSummary.defaultLayer), - Layer.provide(SyncEvent.defaultLayer), ), ) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index a5a397632..7c7d57be6 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -10,6 +10,8 @@ import type { ProviderMetadata, Usage } from "@opencode-ai/llm" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Database } from "@opencode-ai/core/database/database" import { makeRuntime } from "@opencode-ai/core/effect/runtime" +import { EventV2 } from "@opencode-ai/core/event" +import { SessionV2 } from "@opencode-ai/core/session" import { NotFoundError } from "@/storage/storage" import { eq } from "drizzle-orm" @@ -21,7 +23,6 @@ import { like } from "drizzle-orm" import { inArray } from "drizzle-orm" import { lt } from "drizzle-orm" import { or } from "drizzle-orm" -import { SyncEvent } from "../sync" import type { SQL } from "drizzle-orm" import { PartTable, SessionTable } from "@opencode-ai/core/session/sql" import { ProjectTable } from "@opencode-ai/core/project/sql" @@ -34,14 +35,14 @@ import { Snapshot } from "@/snapshot" import { ProjectV2 } from "@opencode-ai/core/project" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { SessionID, MessageID, PartID } from "./schema" -import { ModelID, ProviderID } from "@/provider/schema" import type { Provider } from "@/provider/provider" import { Permission } from "@/permission" import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Option, Context, Schema, Types } from "effect" -import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" +import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { RuntimeFlags } from "@/effect/runtime-flags" +import { ProviderV2 } from "@opencode-ai/core/provider" const log = Log.create({ service: "session" }) const runtime = makeRuntime(Database.Service, Database.defaultLayer) @@ -85,8 +86,8 @@ export function fromRow(row: SessionRow): Info { agent: row.agent ?? undefined, model: row.model ? { - id: ModelID.make(row.model.id), - providerID: ProviderID.make(row.model.providerID), + id: ProviderV2.ModelID.make(row.model.id), + providerID: ProviderV2.ID.make(row.model.providerID), variant: row.model.variant, } : undefined, @@ -114,6 +115,13 @@ export function fromRow(row: SessionRow): Info { } } +function eventLocation(info: Pick) { + return { + directory: AbsolutePath.make(info.directory), + workspaceID: info.workspaceID, + } +} + export function toRow(info: Info) { return { id: info.id, @@ -203,8 +211,8 @@ const Revert = Schema.Struct({ }) const Model = Schema.Struct({ - id: ModelID, - providerID: ProviderID, + id: ProviderV2.ModelID, + providerID: ProviderV2.ID, variant: optionalOmitUndefined(Schema.String), }) @@ -334,25 +342,9 @@ const UpdatedEventSchema = Schema.Struct({ }) export const Event = { - Created: SyncEvent.define({ - type: "session.created", - version: 1, - aggregate: "sessionID", - schema: CreatedEventSchema, - }), - Updated: SyncEvent.define({ - type: "session.updated", - version: 1, - aggregate: "sessionID", - schema: UpdatedEventSchema, - busSchema: CreatedEventSchema, - }), - Deleted: SyncEvent.define({ - type: "session.deleted", - version: 1, - aggregate: "sessionID", - schema: CreatedEventSchema, - }), + Created: BusEvent.define("session.created", SessionLegacy.Event.Created.data), + Updated: BusEvent.define("session.updated", SessionLegacy.Event.Updated.data), + Deleted: BusEvent.define("session.deleted", SessionLegacy.Event.Deleted.data), Diff: BusEvent.define( "session.diff", Schema.Struct({ @@ -474,6 +466,8 @@ export interface Interface { }) => Effect.Effect readonly clearRevert: (sessionID: SessionID) => Effect.Effect readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect + readonly setShare: (input: { sessionID: SessionID; share: Info["share"] }) => Effect.Effect + readonly setWorkspace: (input: { sessionID: SessionID; workspaceID: Info["workspaceID"] }) => Effect.Effect readonly diff: (sessionID: SessionID) => Effect.Effect readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect readonly children: (parentID: SessionID) => Effect.Effect @@ -505,12 +499,18 @@ export class Service extends Context.Service()("@opencode/Se export const use = serviceUse(Service) -export type Patch = Types.DeepMutable["data"]["info"]> +export type Patch = Omit, "time" | "share" | "summary" | "revert" | "permission"> & { + time?: Partial + share?: Partial> | null + summary?: Info["summary"] | null + revert?: Info["revert"] | null + permission?: Info["permission"] | null +} export const layer: Layer.Layer< Service, never, - BackgroundJob.Service | Bus.Service | Storage.Service | SyncEvent.Service | RuntimeFlags.Service | Database.Service + BackgroundJob.Service | Bus.Service | Storage.Service | RuntimeFlags.Service | Database.Service | EventV2.Service > = Layer.effect( Service, Effect.gen(function* () { @@ -518,8 +518,8 @@ export const layer: Layer.Layer< const database = yield* Database.Service const background = yield* BackgroundJob.Service const bus = yield* Bus.Service + const events = yield* EventV2.Service const storage = yield* Storage.Service - const sync = yield* SyncEvent.Service const flags = yield* RuntimeFlags.Service const createNext = Effect.fn("Session.createNext")(function* (input: { @@ -556,15 +556,12 @@ export const layer: Layer.Layer< } log.info("created", result) - yield* sync.run(Event.Created, { sessionID: result.id, info: result }) + yield* events.publish(SessionLegacy.Event.Created, { sessionID: result.id, info: result }, { location: eventLocation(result) }) if (!flags.experimentalWorkspaces) { // This only exist for backwards compatibility. We should not be // manually publishing this event; it is a sync event now - yield* bus.publish(Event.Updated, { - sessionID: result.id, - info: result, - }) + yield* bus.publish(Event.Updated, { sessionID: result.id, info: result }) } return result @@ -609,8 +606,8 @@ export const layer: Layer.Layer< yield* remove(child.id) } - yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance }) - yield* sync.remove(sessionID) + yield* events.publish(SessionLegacy.Event.Deleted, { sessionID, info: session }, { location: eventLocation(session) }) + yield* events.remove(sessionID) } catch (e) { log.error(e) } @@ -618,17 +615,27 @@ export const layer: Layer.Layer< const updateMessage = (msg: T): Effect.Effect => Effect.gen(function* () { - yield* sync.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg }) + const session = yield* get(msg.sessionID) + yield* events.publish( + SessionLegacy.Event.MessageUpdated, + { sessionID: msg.sessionID, info: msg }, + { location: eventLocation(session) }, + ) return msg }).pipe(Effect.withSpan("Session.updateMessage")) const updatePart = (part: T): Effect.Effect => Effect.gen(function* () { - yield* sync.run(MessageV2.Event.PartUpdated, { - sessionID: part.sessionID, - part: structuredClone(part), - time: Date.now(), - }) + const session = yield* get(part.sessionID) + yield* events.publish( + SessionLegacy.Event.PartUpdated, + { + sessionID: part.sessionID, + part: structuredClone(part), + time: Date.now(), + }, + { location: eventLocation(session) }, + ) return part }).pipe(Effect.withSpan("Session.updatePart")) @@ -718,25 +725,40 @@ export const layer: Layer.Layer< return session }) - const patch = (sessionID: SessionID, info: Patch) => sync.run(Event.Updated, { sessionID, info }) + const patch = (sessionID: SessionID, info: Patch) => + Effect.gen(function* () { + const current = yield* get(sessionID) + const next = { + ...current, + ...info, + time: info.time ? { ...current.time, ...info.time } : current.time, + share: info.share === null ? undefined : info.share ? { ...current.share, ...info.share } : current.share, + summary: info.summary === null ? undefined : (info.summary ?? current.summary), + revert: info.revert === null ? undefined : (info.revert ?? current.revert), + permission: info.permission === null ? undefined : (info.permission ?? current.permission), + } as Info + yield* events.publish(SessionLegacy.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) }) + }) const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) { - yield* patch(sessionID, { time: { updated: Date.now() } }) + yield* patch(sessionID, { time: { updated: Date.now() } }).pipe(Effect.orDie) }) const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) { - yield* patch(input.sessionID, { title: input.title }) + yield* patch(input.sessionID, { title: input.title }).pipe(Effect.orDie) }) const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) { - yield* patch(input.sessionID, { time: { archived: input.time } }) + yield* patch(input.sessionID, { time: { archived: input.time } }).pipe(Effect.orDie) }) const setPermission = Effect.fn("Session.setPermission")(function* (input: { sessionID: SessionID permission: Permission.Ruleset }) { - yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }) + yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }).pipe( + Effect.orDie, + ) }) const setRevert = Effect.fn("Session.setRevert")(function* (input: { @@ -744,18 +766,31 @@ export const layer: Layer.Layer< revert: Info["revert"] summary: Info["summary"] }) { - yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert }) + yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert }).pipe( + Effect.orDie, + ) }) const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) { - yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }) + yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }).pipe(Effect.orDie) }) const setSummary = Effect.fn("Session.setSummary")(function* (input: { sessionID: SessionID summary: Info["summary"] }) { - yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }) + yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }).pipe(Effect.orDie) + }) + + const setShare = Effect.fn("Session.setShare")(function* (input: { sessionID: SessionID; share: Info["share"] }) { + yield* patch(input.sessionID, { share: input.share ?? null, time: { updated: Date.now() } }).pipe(Effect.orDie) + }) + + const setWorkspace = Effect.fn("Session.setWorkspace")(function* (input: { + sessionID: SessionID + workspaceID: Info["workspaceID"] + }) { + yield* patch(input.sessionID, { workspaceID: input.workspaceID, time: { updated: Date.now() } }).pipe(Effect.orDie) }) const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) { @@ -793,10 +828,15 @@ export const layer: Layer.Layer< sessionID: SessionID messageID: MessageID }) { - yield* sync.run(MessageV2.Event.Removed, { - sessionID: input.sessionID, - messageID: input.messageID, - }) + const session = yield* get(input.sessionID) + yield* events.publish( + SessionLegacy.Event.MessageRemoved, + { + sessionID: input.sessionID, + messageID: input.messageID, + }, + { location: eventLocation(session) }, + ) return input.messageID }) @@ -805,11 +845,16 @@ export const layer: Layer.Layer< messageID: MessageID partID: PartID }) { - yield* sync.run(MessageV2.Event.PartRemoved, { - sessionID: input.sessionID, - messageID: input.messageID, - partID: input.partID, - }) + const session = yield* get(input.sessionID) + yield* events.publish( + SessionLegacy.Event.PartRemoved, + { + sessionID: input.sessionID, + messageID: input.messageID, + partID: input.partID, + }, + { location: eventLocation(session) }, + ) return input.partID }) @@ -854,6 +899,8 @@ export const layer: Layer.Layer< setRevert, clearRevert, setSummary, + setShare, + setWorkspace, diff, messages, children, @@ -873,8 +920,9 @@ export const defaultLayer = layer.pipe( Layer.provide(BackgroundJob.defaultLayer), Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer), - Layer.provide(SyncEvent.defaultLayer), Layer.provide(Database.defaultLayer), + Layer.provide(EventV2.defaultLayer), + Layer.provide(SessionV2.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer), ) diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 8305f23c5..20ffb60e1 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -8,7 +8,7 @@ import { Tool } from "@/tool/tool" import { ToolJsonSchema } from "@/tool/json-schema" import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" -import { ModelID } from "@/provider/schema" + import { Plugin } from "@/plugin" import type { TaskPromptOps } from "@/tool/task" import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" @@ -19,6 +19,7 @@ import { SessionProcessor } from "./processor" import { PartID } from "./schema" import * as Log from "@opencode-ai/core/util/log" import { EffectBridge } from "@/effect/bridge" +import { ProviderV2 } from "@opencode-ai/core/provider" const log = Log.create({ service: "session.tools" }) @@ -74,7 +75,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { }) for (const item of yield* registry.tools({ - modelID: ModelID.make(input.model.api.id), + modelID: ProviderV2.ModelID.make(input.model.api.id), providerID: input.model.providerID, agent: input.agent, })) { diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index a13b6c9de..b27bc728a 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -1,6 +1,5 @@ import { Session } from "@/session/session" import { SessionID } from "@/session/schema" -import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" import { Config } from "@/config/config" import { RuntimeFlags } from "@/effect/runtime-flags" @@ -21,20 +20,19 @@ export const layer = Layer.effect( const session = yield* Session.Service const shareNext = yield* ShareNext.Service const scope = yield* Scope.Scope - const sync = yield* SyncEvent.Service const flags = yield* RuntimeFlags.Service const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) { const conf = yield* cfg.get() if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration") const result = yield* shareNext.create(sessionID) - yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } }) + yield* session.setShare({ sessionID, share: { url: result.url } }) return result }) const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) { yield* shareNext.remove(sessionID) - yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } }) + yield* session.setShare({ sessionID, share: undefined }) }) const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) { @@ -54,7 +52,6 @@ export const defaultLayer = layer.pipe( Layer.provide(ShareNext.defaultLayer), Layer.provide(Session.defaultLayer), Layer.provide(Config.defaultLayer), - Layer.provide(SyncEvent.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer), ) diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index aea7d9871..6b17f80dc 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -6,7 +6,7 @@ import { Account } from "@/account/account" import { Bus } from "@/bus" import { InstanceState } from "@/effect/instance-state" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "@/provider/schema" + import { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" @@ -15,6 +15,7 @@ import { eq } from "drizzle-orm" import { Config } from "@/config/config" import * as Log from "@opencode-ai/core/util/log" import { SessionShareTable } from "@opencode-ai/core/share/sql" +import { ProviderV2 } from "@opencode-ai/core/provider" const log = Log.create({ service: "share-next" }) const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" @@ -290,7 +291,7 @@ export const layer = Layer.effect( .map((item) => [`${item.providerID}/${item.modelID}`, item] as const), ).values(), ), - (item) => provider.getModel(ProviderID.make(item.providerID), ModelID.make(item.modelID)), + (item) => provider.getModel(ProviderV2.ID.make(item.providerID), ProviderV2.ModelID.make(item.modelID)), { concurrency: 8 }, ) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 71eba12bd..ad95a0e20 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -22,7 +22,7 @@ import { Schema } from "effect" import z from "zod" import { Plugin } from "../plugin" import { Provider } from "@/provider/provider" -import { ProviderID, type ModelID } from "../provider/schema" + import { WebSearchTool } from "./websearch" import { RepoCloneTool } from "./repo_clone" import { RepoOverviewTool } from "./repo_overview" @@ -56,11 +56,12 @@ import { Reference } from "@/reference/reference" import { BackgroundJob } from "@/background/job" import { SessionStatus } from "@/session/status" import { RuntimeFlags } from "@/effect/runtime-flags" +import { ProviderV2 } from "@opencode-ai/core/provider" const log = Log.create({ service: "tool.registry" }) -export function webSearchEnabled(providerID: ProviderID, flags = { exa: false, parallel: false }) { - return providerID === ProviderID.opencode || flags.exa || flags.parallel +export function webSearchEnabled(providerID: ProviderV2.ID, flags = { exa: false, parallel: false }) { + return providerID === ProviderV2.ID.opencode || flags.exa || flags.parallel } type TaskDef = Tool.InferDef @@ -77,7 +78,7 @@ export interface Interface { readonly ids: () => Effect.Effect readonly all: () => Effect.Effect readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }> - readonly tools: (model: { providerID: ProviderID; modelID: ModelID; agent: Agent.Info }) => Effect.Effect + readonly tools: (model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID; agent: Agent.Info }) => Effect.Effect } export class Service extends Context.Service()("@opencode/ToolRegistry") {} diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 066bf6394..34b6160d8 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -341,10 +341,6 @@ function sessionSequenceOwner(sessionID: SessionID) { )?.ownerID } -function sessionUpdatedType() { - return SyncEvent.versionedType(SessionNs.Event.Updated.type, SessionNs.Event.Updated.version) -} - describe("workspace schemas and exports", () => { test("keeps the historical event type names", () => { expect(Workspace.Event.Ready.type).toBe("workspace.ready") @@ -984,7 +980,7 @@ describe("workspace CRUD", () => { id: `evt_${unique("warp-source-history")}`, aggregate_id: historySessionID!, seq: historyNextSeq, - type: sessionUpdatedType(), + type: "session.updated.1", data: { sessionID: historySessionID!, info: { title: "from source history" } }, }, ]) @@ -1035,12 +1031,12 @@ describe("workspace CRUD", () => { { aggregateID: session.id, seq: 0, - type: SyncEvent.versionedType(SessionNs.Event.Created.type, SessionNs.Event.Created.version), + type: "session.created.1", }, { aggregateID: session.id, seq: historyNextSeq, - type: sessionUpdatedType(), + type: "session.updated.1", }, ], }) @@ -1348,7 +1344,7 @@ describe("workspace sync state", () => { id: `evt_${unique("history")}`, aggregate_id: historySessionID!, seq: historyNextSeq, - type: sessionUpdatedType(), + type: "session.updated.1", data: { sessionID: historySessionID!, info: { title: "from history" } }, }, ]), @@ -1494,7 +1490,7 @@ describe("workspace sync state", () => { id: `evt_${unique("sse")}`, aggregateID: sseSessionID!, seq: sseNextSeq, - type: sessionUpdatedType(), + type: "session.updated.1", data: { sessionID: sseSessionID!, info: { title: "from sse" } }, }, }, diff --git a/packages/opencode/test/fake/provider.ts b/packages/opencode/test/fake/provider.ts index 5f8f7a330..e90bde29e 100644 --- a/packages/opencode/test/fake/provider.ts +++ b/packages/opencode/test/fake/provider.ts @@ -1,11 +1,11 @@ import { Effect, Layer } from "effect" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../../src/provider/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" export namespace ProviderTest { export function model(override: Partial = {}): Provider.Model { - const id = override.id ?? ModelID.make("gpt-5.2") - const providerID = override.providerID ?? ProviderID.make("openai") + const id = override.id ?? ProviderV2.ModelID.make("gpt-5.2") + const providerID = override.providerID ?? ProviderV2.ID.make("openai") return { id, providerID, diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index c10957996..7daba16c2 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -5,7 +5,7 @@ import { Effect, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture" import { ProviderAuth } from "@/provider/auth" -import { ProviderID } from "../../src/provider/schema" + import { Plugin } from "@/plugin" import { RuntimeFlags } from "@/effect/runtime-flags" import { Auth } from "@/auth" @@ -13,6 +13,7 @@ import { Bus } from "@/bus" import { TestConfig } from "../fixture/config" import { testEffect } from "../lib/effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { ProviderV2 } from "@opencode-ai/core/provider" const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer)) @@ -77,11 +78,11 @@ describe("plugin.auth-override", () => { .methods() .pipe(Effect.provide(layer(plain, [])), provideInstance(plain)) - const copilot = methods[ProviderID.make("github-copilot")] + const copilot = methods[ProviderV2.ID.make("github-copilot")] expect(copilot).toBeDefined() expect(copilot.length).toBe(1) expect(copilot[0].label).toBe("Test Override Auth") - expect(plainMethods[ProviderID.make("github-copilot")][0].label).not.toBe("Test Override Auth") + expect(plainMethods[ProviderV2.ID.make("github-copilot")][0].label).not.toBe("Test Override Auth") }), { git: true }, 30000, diff --git a/packages/opencode/test/plugin/trigger.test.ts b/packages/opencode/test/plugin/trigger.test.ts index 3716bc3ac..6714d7247 100644 --- a/packages/opencode/test/plugin/trigger.test.ts +++ b/packages/opencode/test/plugin/trigger.test.ts @@ -11,12 +11,13 @@ import { Config } from "../../src/config/config" import { Env } from "../../src/env" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin/index" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" +import { ProviderV2 } from "@opencode-ai/core/provider" const configLayer = Config.layer.pipe( Layer.provide(EffectFlock.defaultLayer), @@ -74,8 +75,8 @@ const triggerSystemTransform = Effect.fn("PluginTriggerTest.triggerSystemTransfo systemHook, { model: { - providerID: ProviderID.anthropic, - modelID: ModelID.make("claude-sonnet-4-6"), + providerID: ProviderV2.ID.anthropic, + modelID: ProviderV2.ModelID.make("claude-sonnet-4-6"), }, }, out, diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 763b724b6..084d28dd4 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -6,9 +6,10 @@ import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import { Env } from "../../src/env" import { Provider } from "@/provider/provider" -import { ProviderID } from "../../src/provider/schema" + import { disposeAllInstances } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer)) @@ -62,8 +63,8 @@ it.instance( yield* set("AWS_REGION", "us-east-1") yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") }), { config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } }, ) @@ -73,8 +74,8 @@ it.instance("Bedrock: falls back to AWS_REGION env var when no config region", ( yield* set("AWS_REGION", "eu-west-1") yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") }), ) @@ -87,8 +88,8 @@ it.instance( yield* set("AWS_ACCESS_KEY_ID", "") yield* set("AWS_BEARER_TOKEN_BEDROCK", "") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") }), { config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } }, ) @@ -100,8 +101,8 @@ it.instance( yield* set("AWS_PROFILE", "default") yield* set("AWS_ACCESS_KEY_ID", "test-key-id") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1") + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1") }), { config: { @@ -116,8 +117,8 @@ it.instance( Effect.gen(function* () { yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe( + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.endpoint).toBe( "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com", ) }), @@ -141,8 +142,8 @@ it.instance( yield* set("AWS_PROFILE", "") yield* set("AWS_ACCESS_KEY_ID", "") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1") + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1") }), { config: { provider: { "amazon-bedrock": { options: { region: "us-east-1" } } } } }, ) @@ -157,8 +158,8 @@ it.instance( Effect.gen(function* () { yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }), { config: { @@ -178,8 +179,8 @@ it.instance( Effect.gen(function* () { yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }), { config: { @@ -199,8 +200,8 @@ it.instance( Effect.gen(function* () { yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }), { config: { @@ -220,8 +221,8 @@ it.instance( Effect.gen(function* () { yield* set("AWS_PROFILE", "default") const providers = yield* list - expect(providers[ProviderID.amazonBedrock]).toBeDefined() - expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() + expect(providers[ProviderV2.ID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }), { config: { diff --git a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts index 0c692c50c..cf18e842f 100644 --- a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts +++ b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts @@ -13,7 +13,7 @@ import { createAiGateway } from "ai-gateway-provider" import { createUnified } from "ai-gateway-provider/providers/unified" import { ProviderTransform } from "@/provider/transform" import type * as Provider from "@/provider/provider" -import { ModelID, ProviderID } from "@/provider/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" type Captured = { url: string; outerBody: unknown } type ProviderOptions = Record> @@ -56,8 +56,8 @@ afterEach(() => { }) const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ({ - id: ModelID.make(`cloudflare-ai-gateway/${apiId}`), - providerID: ProviderID.make("cloudflare-ai-gateway"), + id: ProviderV2.ModelID.make(`cloudflare-ai-gateway/${apiId}`), + providerID: ProviderV2.ID.make("cloudflare-ai-gateway"), name: apiId, api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: "ai-gateway-provider" }, capabilities: { diff --git a/packages/opencode/test/provider/digitalocean.test.ts b/packages/opencode/test/provider/digitalocean.test.ts index 665c792de..59c3f8da7 100644 --- a/packages/opencode/test/provider/digitalocean.test.ts +++ b/packages/opencode/test/provider/digitalocean.test.ts @@ -1,10 +1,11 @@ import { expect } from "bun:test" import { Provider } from "../../src/provider/provider" -import { ProviderID } from "../../src/provider/schema" + import { Effect } from "effect" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" -const DIGITALOCEAN = ProviderID.make("digitalocean") +const DIGITALOCEAN = ProviderV2.ID.make("digitalocean") const it = testEffect(Provider.defaultLayer) const withEnv = (values: Record, effect: Effect.Effect) => diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 4ac62cf69..563fa0255 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -6,7 +6,7 @@ export {} // import { test, expect, describe } from "bun:test" // import path from "path" -// import { ProviderID, ModelID } from "../../src/provider/schema" +// import { ProviderV2 } from "@opencode-ai/core/provider" // import { tmpdir, withTestInstance } from "../fixture/fixture" // import { Provider } from "@/provider/provider" // import { Env } from "../../src/env" diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8cf93e22d..f6b7fbe52 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -13,11 +13,12 @@ import { Config } from "@/config/config" import { Env } from "../../src/env" import { Plugin } from "../../src/plugin/index" import { Provider } from "@/provider/provider" -import { ProviderID, ModelID } from "../../src/provider/schema" + import { RuntimeFlags } from "@/effect/runtime-flags" import { Filesystem } from "@/util/filesystem" import { InstanceLayer } from "@/project/instance-layer" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const originalEnv = new Map() @@ -68,7 +69,7 @@ const providerLayer = (flags: Partial = {}) => const list = Provider.use.list() const paid = (providers: Record }>) => { - const item = providers[ProviderID.make("opencode")] + const item = providers[ProviderV2.ID.make("opencode")] expect(item).toBeDefined() return Object.values(item.models).filter((model) => model.cost.input > 0).length } @@ -104,11 +105,11 @@ it.instance("provider loaded from env variable", () => Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() // Provider should retain its connection source even if custom loaders // merge additional options. - expect(providers[ProviderID.anthropic].source).toBe("env") - expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].source).toBe("env") + expect(providers[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined() }), ) @@ -116,7 +117,7 @@ it.instance( "provider loaded from config with apiKey option", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() }), { config: { provider: { anthropic: { options: { apiKey: "config-api-key" } } } } }, ) @@ -126,7 +127,7 @@ it.instance( Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeUndefined() + expect(providers[ProviderV2.ID.anthropic]).toBeUndefined() }), { config: { disabled_providers: ["anthropic"] } }, ) @@ -137,8 +138,8 @@ it.instance( yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") yield* setProcessEnv("OPENAI_API_KEY", "test-openai-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - expect(providers[ProviderID.openai]).toBeUndefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.openai]).toBeUndefined() }), { config: { enabled_providers: ["anthropic"] } }, ) @@ -148,8 +149,8 @@ it.instance( Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - const models = Object.keys(providers[ProviderID.anthropic].models) + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderV2.ID.anthropic].models) expect(models).toContain("claude-sonnet-4-20250514") expect(models.length).toBe(1) }), @@ -161,8 +162,8 @@ it.instance( Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - const models = Object.keys(providers[ProviderID.anthropic].models) + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderV2.ID.anthropic].models) expect(models).not.toContain("claude-sonnet-4-20250514") }), { config: { provider: { anthropic: { blacklist: ["claude-sonnet-4-20250514"] } } } }, @@ -173,9 +174,9 @@ it.instance( Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined() - expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias") + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].models["my-alias"]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].models["my-alias"].name).toBe("My Custom Alias") }), { config: { @@ -190,9 +191,9 @@ it.instance( "custom provider with npm package", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("custom-provider")]).toBeDefined() - expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider") - expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-provider")]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-provider")].name).toBe("Custom Provider") + expect(providers[ProviderV2.ID.make("custom-provider")].models["custom-model"]).toBeDefined() }), { config: { @@ -220,8 +221,8 @@ it.instance( "filters alpha provider models by default", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined() - expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeUndefined() + expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeUndefined() }), { config: alphaProviderConfig }, ) @@ -230,8 +231,8 @@ experimentalModels.instance( "includes alpha provider models when experimental models are enabled", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined() - expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeDefined() }), { config: alphaProviderConfig }, ) @@ -240,11 +241,11 @@ it.instance( "custom DeepSeek openai-compatible model defaults interleaved reasoning field", Effect.gen(function* () { const providers = yield* list - const provider = providers[ProviderID.make("custom-provider")] + const provider = providers[ProviderV2.ID.make("custom-provider")] expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" }) expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" }) expect(provider.models["custom-model"].capabilities.interleaved).toBe(false) - expect(providers[ProviderID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe( + expect(providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe( false, ) }), @@ -279,10 +280,10 @@ it.instance( Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "env-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() // Config options should be merged - expect(providers[ProviderID.anthropic].options.timeout).toBe(60000) - expect(providers[ProviderID.anthropic].options.chunkTimeout).toBe(15000) + expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(60000) + expect(providers[ProviderV2.ID.anthropic].options.chunkTimeout).toBe(15000) }), { config: { provider: { anthropic: { options: { timeout: 60000, chunkTimeout: 15000 } } } } }, ) @@ -291,7 +292,7 @@ it.instance("getModel returns model for valid provider/model", () => Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const provider = yield* Provider.Service - const model = yield* provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514")) + const model = yield* provider.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514")) expect(model).toBeDefined() expect(String(model.providerID)).toBe("anthropic") expect(String(model.id)).toBe("claude-sonnet-4-20250514") @@ -303,7 +304,7 @@ it.instance("getModel returns model for valid provider/model", () => it.instance("getModel throws ModelNotFoundError for invalid model", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const exit = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("nonexistent-model")).pipe(Effect.exit) + const exit = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model")).pipe(Effect.exit) expect(exit._tag).toBe("Failure") }), ) @@ -311,7 +312,7 @@ it.instance("getModel throws ModelNotFoundError for invalid model", () => it.instance("getModel throws ModelNotFoundError for invalid provider", () => Effect.gen(function* () { const exit = yield* Provider.use - .getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model")) + .getModel(ProviderV2.ID.make("nonexistent-provider"), ProviderV2.ModelID.make("some-model")) .pipe(Effect.exit) expect(exit._tag).toBe("Failure") }), @@ -365,8 +366,8 @@ it.instance( "provider with baseURL from config", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("custom-openai")]).toBeDefined() - expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1") + expect(providers[ProviderV2.ID.make("custom-openai")]).toBeDefined() + expect(providers[ProviderV2.ID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1") }), { config: { @@ -387,7 +388,7 @@ it.instance( "model cost defaults to zero when not specified", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("test-provider")].models["test-model"] + const model = providers[ProviderV2.ID.make("test-provider")].models["test-model"] expect(model.cost.input).toBe(0) expect(model.cost.output).toBe(0) expect(model.cost.cache.read).toBe(0) @@ -412,7 +413,7 @@ it.instance( "model options are merged from existing model", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.options.customOption).toBe("custom-value") }), { @@ -431,7 +432,7 @@ it.instance( "provider removed when all models filtered out", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeUndefined() + expect(providers[ProviderV2.ID.anthropic]).toBeUndefined() }), { config: { provider: { anthropic: { options: { apiKey: "test-api-key" }, whitelist: ["nonexistent-model"] } } } }, ) @@ -439,7 +440,7 @@ it.instance( it.instance("closest finds model by partial match", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const result = yield* Provider.use.closest(ProviderID.anthropic, ["sonnet-4"]) + const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["sonnet-4"]) expect(result).toBeDefined() expect(String(result?.providerID)).toBe("anthropic") expect(String(result?.modelID)).toContain("sonnet-4") @@ -448,7 +449,7 @@ it.instance("closest finds model by partial match", () => it.instance("closest returns undefined for nonexistent provider", () => Effect.gen(function* () { - const result = yield* Provider.use.closest(ProviderID.make("nonexistent"), ["model"]) + const result = yield* Provider.use.closest(ProviderV2.ID.make("nonexistent"), ["model"]) expect(result).toBeUndefined() }), ) @@ -458,9 +459,9 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].models["my-sonnet"]).toBeDefined() - const model = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("my-sonnet")) + const model = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("my-sonnet")) expect(model).toBeDefined() expect(String(model.id)).toBe("my-sonnet") expect(model.name).toBe("My Sonnet Alias") @@ -481,7 +482,7 @@ it.instance( Effect.gen(function* () { const providers = yield* list // api field is stored on model.api.url, used by getSDK to set baseURL - expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1") + expect(providers[ProviderV2.ID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1") }), { config: { @@ -503,7 +504,7 @@ it.instance( "explicit baseURL overrides api field", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1") + expect(providers[ProviderV2.ID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1") }), { config: { @@ -526,7 +527,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.name).toBe("Custom Name for Sonnet") expect(model.capabilities.toolcall).toBe(true) expect(model.capabilities.attachment).toBe(true) @@ -544,7 +545,7 @@ it.instance( Effect.gen(function* () { yield* set("OPENAI_API_KEY", "test-openai-key") const providers = yield* list - expect(providers[ProviderID.openai]).toBeUndefined() + expect(providers[ProviderV2.ID.openai]).toBeUndefined() }), { config: { disabled_providers: ["openai"] } }, ) @@ -565,8 +566,8 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - const models = Object.keys(providers[ProviderID.anthropic].models) + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderV2.ID.anthropic].models) expect(models).toContain("claude-sonnet-4-20250514") expect(models).not.toContain("claude-opus-4-20250514") expect(models.length).toBe(1) @@ -587,7 +588,7 @@ it.instance( "model modalities default correctly", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("test-provider")].models["test-model"] + const model = providers[ProviderV2.ID.make("test-provider")].models["test-model"] expect(model.capabilities.input.text).toBe(true) expect(model.capabilities.output.text).toBe(true) }), @@ -610,7 +611,7 @@ it.instance( "model with custom cost values", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("test-provider")].models["test-model"] + const model = providers[ProviderV2.ID.make("test-provider")].models["test-model"] expect(model.cost.input).toBe(5) expect(model.cost.output).toBe(15) expect(model.cost.cache.read).toBe(2.5) @@ -641,7 +642,7 @@ it.instance( it.instance("getSmallModel returns appropriate small model", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const model = yield* Provider.use.getSmallModel(ProviderID.anthropic) + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic) expect(model).toBeDefined() expect(model?.id).toContain("haiku") }), @@ -651,7 +652,7 @@ it.instance( "getSmallModel respects config small_model override", Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const model = yield* Provider.use.getSmallModel(ProviderID.anthropic) + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic) expect(model).toBeDefined() expect(String(model?.providerID)).toBe("anthropic") expect(String(model?.id)).toBe("claude-sonnet-4-20250514") @@ -663,7 +664,7 @@ it.instance( "getSmallModel ignores invalid config small_model", Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const model = yield* Provider.use.getSmallModel(ProviderID.anthropic) + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic) expect(model).toBeUndefined() }), { config: { small_model: "anthropic/not-a-real-model" } }, @@ -690,10 +691,10 @@ it.instance( yield* set("ANTHROPIC_API_KEY", "test-anthropic-key") yield* set("OPENAI_API_KEY", "test-openai-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - expect(providers[ProviderID.openai]).toBeDefined() - expect(providers[ProviderID.anthropic].options.timeout).toBe(30000) - expect(providers[ProviderID.openai].options.timeout).toBe(60000) + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.openai]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(30000) + expect(providers[ProviderV2.ID.openai].options.timeout).toBe(60000) }), { config: { @@ -709,9 +710,9 @@ it.instance( "provider with custom npm package", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("local-llm")]).toBeDefined() - expect(providers[ProviderID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible") - expect(providers[ProviderID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1") + expect(providers[ProviderV2.ID.make("local-llm")]).toBeDefined() + expect(providers[ProviderV2.ID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible") + expect(providers[ProviderV2.ID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1") }), { config: { @@ -735,7 +736,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet") + expect(providers[ProviderV2.ID.anthropic].models["sonnet"].name).toBe("sonnet") }), { config: { @@ -753,9 +754,9 @@ it.instance( Effect.gen(function* () { yield* set("MULTI_ENV_KEY_1", "test-key") const providers = yield* list - expect(providers[ProviderID.make("multi-env")]).toBeDefined() + expect(providers[ProviderV2.ID.make("multi-env")]).toBeDefined() // When multiple env options exist, key should NOT be auto-set - expect(providers[ProviderID.make("multi-env")].key).toBeUndefined() + expect(providers[ProviderV2.ID.make("multi-env")].key).toBeUndefined() }), { config: { @@ -777,9 +778,9 @@ it.instance( Effect.gen(function* () { yield* set("SINGLE_ENV_KEY", "my-api-key") const providers = yield* list - expect(providers[ProviderID.make("single-env")]).toBeDefined() + expect(providers[ProviderV2.ID.make("single-env")]).toBeDefined() // Single env option should auto-set key - expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key") + expect(providers[ProviderV2.ID.make("single-env")].key).toBe("my-api-key") }), { config: { @@ -801,7 +802,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.cost.input).toBe(999) expect(model.cost.output).toBe(888) }), @@ -820,9 +821,9 @@ it.instance( "completely new provider not in database can be configured", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("brand-new-provider")]).toBeDefined() - expect(providers[ProviderID.make("brand-new-provider")].name).toBe("Brand New") - const model = providers[ProviderID.make("brand-new-provider")].models["new-model"] + expect(providers[ProviderV2.ID.make("brand-new-provider")]).toBeDefined() + expect(providers[ProviderV2.ID.make("brand-new-provider")].name).toBe("Brand New") + const model = providers[ProviderV2.ID.make("brand-new-provider")].models["new-model"] expect(model.capabilities.reasoning).toBe(true) expect(model.capabilities.attachment).toBe(true) expect(model.capabilities.input.image).toBe(true) @@ -861,11 +862,11 @@ it.instance( yield* set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google") const providers = yield* list // anthropic: in enabled, not in disabled = allowed - expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() // openai: in enabled, but also in disabled = NOT allowed - expect(providers[ProviderID.openai]).toBeUndefined() + expect(providers[ProviderV2.ID.openai]).toBeUndefined() // google: not in enabled = NOT allowed (even though not disabled) - expect(providers[ProviderID.google]).toBeUndefined() + expect(providers[ProviderV2.ID.google]).toBeUndefined() }), { // enabled_providers takes precedence — only these are considered @@ -878,7 +879,7 @@ it.instance( "model with tool_call false", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false) + expect(providers[ProviderV2.ID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false) }), { config: { @@ -899,7 +900,7 @@ it.instance( "model defaults tool_call to true when not specified", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true) + expect(providers[ProviderV2.ID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true) }), { config: { @@ -920,7 +921,7 @@ it.instance( "model headers are preserved", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("headers-provider")].models["model"] + const model = providers[ProviderV2.ID.make("headers-provider")].models["model"] expect(model.headers).toEqual({ "X-Custom-Header": "custom-value", Authorization: "Bearer special-token", @@ -955,7 +956,7 @@ it.instance( yield* set("FALLBACK_KEY", "fallback-api-key") const providers = yield* list // Provider should load because fallback env var is set - expect(providers[ProviderID.make("fallback-env")]).toBeDefined() + expect(providers[ProviderV2.ID.make("fallback-env")]).toBeDefined() }), { config: { @@ -975,8 +976,8 @@ it.instance( it.instance("getModel returns consistent results", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const model1 = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514")) - const model2 = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514")) + const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514")) + const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514")) expect(model1.providerID).toEqual(model2.providerID) expect(model1.id).toEqual(model2.id) expect(model1).toEqual(model2) @@ -987,7 +988,7 @@ it.instance( "provider name defaults to id when not in database", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id") + expect(providers[ProviderV2.ID.make("my-custom-id")].name).toBe("my-custom-id") }), { config: { @@ -1006,7 +1007,7 @@ it.instance( it.instance("ModelNotFoundError includes suggestions for typos", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const error = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")).pipe(Effect.flip) + const error = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4")).pipe(Effect.flip) expect(error.suggestions).toBeDefined() expect((error.suggestions ?? []).length).toBeGreaterThan(0) }), @@ -1016,7 +1017,7 @@ it.instance("ModelNotFoundError for provider includes suggestions", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const error = yield* Provider.use - .getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) + .getModel(ProviderV2.ID.make("antropic"), ProviderV2.ModelID.make("claude-sonnet-4")) .pipe(Effect.flip) expect(error.suggestions).toBeDefined() expect(error.suggestions).toContain("anthropic") @@ -1027,7 +1028,7 @@ it.instance("ModelNotFoundError suggests catalog models for unloaded providers", Effect.gen(function* () { yield* remove("OPENCODE_API_KEY") const error = yield* Provider.use - .getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model")) + .getModel(ProviderV2.ID.opencode, ProviderV2.ModelID.make("claude-haiku-fake-model")) .pipe(Effect.flip) if (!Provider.ModelNotFoundError.isInstance(error)) throw error expect(error.suggestions ?? []).toContain("claude-haiku-4-5") @@ -1036,7 +1037,7 @@ it.instance("ModelNotFoundError suggests catalog models for unloaded providers", it.instance("getProvider returns undefined for nonexistent provider", () => Effect.gen(function* () { - const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderID.make("nonexistent"))) + const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderV2.ID.make("nonexistent"))) expect(provider).toBeUndefined() }), ) @@ -1044,7 +1045,7 @@ it.instance("getProvider returns undefined for nonexistent provider", () => it.instance("getProvider returns provider info", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const provider = yield* Provider.use.getProvider(ProviderID.anthropic) + const provider = yield* Provider.use.getProvider(ProviderV2.ID.anthropic) expect(provider).toBeDefined() expect(String(provider?.id)).toBe("anthropic") }), @@ -1053,7 +1054,7 @@ it.instance("getProvider returns provider info", () => it.instance("closest returns undefined when no partial match found", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const result = yield* Provider.use.closest(ProviderID.anthropic, ["nonexistent-xyz-model"]) + const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["nonexistent-xyz-model"]) expect(result).toBeUndefined() }), ) @@ -1062,7 +1063,7 @@ it.instance("closest checks multiple query terms in order", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") // First term won't match, second will - const result = yield* Provider.use.closest(ProviderID.anthropic, ["nonexistent", "haiku"]) + const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["nonexistent", "haiku"]) expect(result).toBeDefined() expect(result?.modelID).toContain("haiku") }), @@ -1072,7 +1073,7 @@ it.instance( "model limit defaults to zero when not specified", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("no-limit")].models["model"] + const model = providers[ProviderV2.ID.make("no-limit")].models["model"] expect(model.limit.context).toBe(0) expect(model.limit.output).toBe(0) }), @@ -1097,10 +1098,10 @@ it.instance( yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list // Custom options should be merged - expect(providers[ProviderID.anthropic].options.timeout).toBe(30000) - expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value") + expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(30000) + expect(providers[ProviderV2.ID.anthropic].options.headers["X-Custom"]).toBe("custom-value") // anthropic custom loader adds its own headers, they should coexist - expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined() + expect(providers[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined() }), { config: { @@ -1113,7 +1114,7 @@ it.instance( "hosted nvidia provider adds billing origin header", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({ + expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", @@ -1126,7 +1127,7 @@ it.instance( "custom nvidia baseURL adds billing origin header", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({ + expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", @@ -1139,7 +1140,7 @@ it.instance( "explicit nvidia billing origin header is preserved", Effect.gen(function* () { const providers = yield* list - expect(providers[ProviderID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin") + expect(providers[ProviderV2.ID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin") }), { config: { @@ -1161,7 +1162,7 @@ it.instance( Effect.gen(function* () { yield* set("OPENAI_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.openai].models["my-custom-model"] + const model = providers[ProviderV2.ID.openai].models["my-custom-model"] expect(model).toBeDefined() expect(model.api.npm).toBe("@ai-sdk/openai") }), @@ -1187,15 +1188,15 @@ it.instance( Effect.gen(function* () { yield* set("OPENROUTER_API_KEY", "test-api-key") const providers = yield* list - expect(providers[ProviderID.openrouter]).toBeDefined() + expect(providers[ProviderV2.ID.openrouter]).toBeDefined() // New model not in database should inherit api.url from provider - const intellect = providers[ProviderID.openrouter].models["prime-intellect/intellect-3"] + const intellect = providers[ProviderV2.ID.openrouter].models["prime-intellect/intellect-3"] expect(intellect).toBeDefined() expect(intellect.api.url).toBe("https://openrouter.ai/api/v1") // Another new model should also inherit api.url - const deepseek = providers[ProviderID.openrouter].models["deepseek/deepseek-r1-0528"] + const deepseek = providers[ProviderV2.ID.openrouter].models["deepseek/deepseek-r1-0528"] expect(deepseek).toBeDefined() expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1") expect(deepseek.name).toBe("DeepSeek R1") @@ -1308,7 +1309,7 @@ it.instance("model variants are generated for reasoning models", () => yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list // Claude sonnet 4 has reasoning capability - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.capabilities.reasoning).toBe(true) expect(model.variants).toBeDefined() expect(Object.keys(model.variants!).length).toBeGreaterThan(0) @@ -1320,7 +1321,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants).toBeDefined() expect(model.variants!["high"]).toBeUndefined() // max variant should still exist @@ -1342,7 +1343,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["high"]).toBeDefined() expect(model.variants!["high"].thinking.budgetTokens).toBe(20000) }), @@ -1366,7 +1367,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["max"]).toBeDefined() expect(model.variants!["max"].disabled).toBeUndefined() expect(model.variants!["max"].customField).toBe("test") @@ -1391,7 +1392,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants).toBeDefined() expect(Object.keys(model.variants!).length).toBe(0) }), @@ -1415,7 +1416,7 @@ it.instance( Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] + const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["high"]).toBeDefined() // Should have both the generated thinking config and the custom option expect(model.variants!["high"].thinking).toBeDefined() @@ -1439,7 +1440,7 @@ it.instance( Effect.gen(function* () { yield* set("OPENAI_API_KEY", "test-api-key") const providers = yield* list - const model = providers[ProviderID.openai].models["gpt-5"] + const model = providers[ProviderV2.ID.openai].models["gpt-5"] expect(model.variants).toBeDefined() expect(model.variants!["high"]).toBeUndefined() // Other variants should still exist @@ -1456,7 +1457,7 @@ it.instance( "custom model with variants enabled and disabled", Effect.gen(function* () { const providers = yield* list - const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"] + const model = providers[ProviderV2.ID.make("custom-reasoning")].models["reasoning-model"] expect(model.variants).toBeDefined() // Enabled variants should exist expect(model.variants!["low"]).toBeDefined() @@ -1506,8 +1507,8 @@ it.instance( Effect.gen(function* () { yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") const providers = yield* list - expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined() - expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1") + expect(providers[ProviderV2.ID.make("vertex-proxy")]).toBeDefined() + expect(providers[ProviderV2.ID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1") }), { config: { @@ -1534,7 +1535,7 @@ it.instance( Effect.gen(function* () { yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") const providers = yield* list - const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"] + const model = providers[ProviderV2.ID.make("vertex-openai")].models["gpt-4"] expect(model).toBeDefined() expect(model.api.npm).toBe("@ai-sdk/openai-compatible") }), @@ -1563,7 +1564,7 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio yield* set("GOOGLE_CLOUD_PROJECT", "test-project") yield* set("VERTEX_LOCATION", "eu") const provider = yield* Provider.Service - const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default")) + const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default")) const language = yield* provider.getLanguage(model) expect(languageBaseURL(language)).toBe( "https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models", @@ -1577,8 +1578,8 @@ it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-re yield* set("VERTEX_LOCATION", "us") const provider = yield* Provider.Service const model = yield* provider.getModel( - ProviderID.make("google-vertex-anthropic"), - ModelID.make("claude-sonnet-4-6@default"), + ProviderV2.ID.make("google-vertex-anthropic"), + ProviderV2.ModelID.make("claude-sonnet-4-6@default"), ) const language = yield* provider.getLanguage(model) expect(languageBaseURL(language)).toBe( @@ -1592,7 +1593,7 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => yield* set("GOOGLE_CLOUD_PROJECT", "test-project") yield* set("VERTEX_LOCATION", "europe-west1") const provider = yield* Provider.Service - const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default")) + const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default")) const language = yield* provider.getLanguage(model) expect(languageBaseURL(language)).toBe( "https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models", @@ -1606,7 +1607,7 @@ it.instance("cloudflare-ai-gateway loads with env variables", () => yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway") yield* set("CLOUDFLARE_API_TOKEN", "test-token") const providers = yield* list - expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined() + expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")]).toBeDefined() }), ) @@ -1617,8 +1618,8 @@ it.instance( yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway") yield* set("CLOUDFLARE_API_TOKEN", "test-token") const providers = yield* list - expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined() - expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({ + expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")]).toBeDefined() + expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")].options.metadata).toEqual({ invoked_by: "test", project: "opencode", }) @@ -1681,14 +1682,14 @@ it.effect("plugin config providers persist after instance dispose", () => }).pipe(provideInstanceEffect(dir)) const first = yield* loadAndList - expect(first[ProviderID.make("demo")]).toBeDefined() - expect(first[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined() + expect(first[ProviderV2.ID.make("demo")]).toBeDefined() + expect(first[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined() yield* Effect.promise(() => disposeAllInstances()) const second = yield* loadAndList - expect(second[ProviderID.make("demo")]).toBeDefined() - expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined() + expect(second[ProviderV2.ID.make("demo")]).toBeDefined() + expect(second[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined() }).pipe(provideMultiInstance), ) @@ -1721,8 +1722,8 @@ it.instance( yield* set("ANTHROPIC_API_KEY", "test-anthropic-key") yield* set("OPENAI_API_KEY", "test-openai-key") const providers = yield* list - expect(providers[ProviderID.anthropic]).toBeDefined() - expect(providers[ProviderID.openai]).toBeUndefined() + expect(providers[ProviderV2.ID.anthropic]).toBeDefined() + expect(providers[ProviderV2.ID.openai]).toBeUndefined() }), ) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2bce15856..385053491 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { ProviderTransform } from "@/provider/transform" -import { ModelID, ProviderID } from "../../src/provider/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" describe("ProviderTransform.options - setCacheKey", () => { const sessionID = "test-session-123" @@ -1089,8 +1089,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { const result = ProviderTransform.message( msgs, { - id: ModelID.make("deepseek/deepseek-chat"), - providerID: ProviderID.make("deepseek"), + id: ProviderV2.ModelID.make("deepseek/deepseek-chat"), + providerID: ProviderV2.ID.make("deepseek"), api: { id: "deepseek-chat", url: "https://api.deepseek.com", @@ -1151,8 +1151,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { const result = ProviderTransform.message( msgs, { - id: ModelID.make("openai/gpt-4"), - providerID: ProviderID.make("openai"), + id: ProviderV2.ModelID.make("openai/gpt-4"), + providerID: ProviderV2.ID.make("openai"), api: { id: "gpt-4", url: "https://api.openai.com", diff --git a/packages/opencode/test/server/httpapi-event-diagnostics.test.ts b/packages/opencode/test/server/httpapi-event-diagnostics.test.ts index 66bd0bced..553369e2b 100644 --- a/packages/opencode/test/server/httpapi-event-diagnostics.test.ts +++ b/packages/opencode/test/server/httpapi-event-diagnostics.test.ts @@ -60,7 +60,7 @@ const publishConnected = Bus.use.publish(ServerEvent.Connected, {}) const publishPartUpdated = (partID: ReturnType) => { const sessionID = SessionID.make(`ses_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`) - return SyncEvent.use.run(MessageV2.Event.PartUpdated, { + return Bus.use.publish(MessageV2.Event.PartUpdated, { sessionID, part: { id: partID, sessionID, messageID: MessageID.ascending(), type: "text", text: "diag" }, time: Date.now(), diff --git a/packages/opencode/test/server/httpapi-exercise/runner.ts b/packages/opencode/test/server/httpapi-exercise/runner.ts index 16eb9a88f..db0f0b217 100644 --- a/packages/opencode/test/server/httpapi-exercise/runner.ts +++ b/packages/opencode/test/server/httpapi-exercise/runner.ts @@ -3,13 +3,14 @@ import { SessionLegacy } from "@opencode-ai/core/session/legacy" import { Cause, Duration, Effect } from "effect" import { TestLLMServer } from "../../lib/llm-server" import type { Config } from "../../../src/config/config" -import { ModelID, ProviderID } from "../../../src/provider/schema" + import type { MessageV2 } from "../../../src/session/message-v2" import { MessageID, PartID } from "../../../src/session/schema" import { call, callAuthProbe } from "./backend" import { original } from "./environment" import { runtime } from "./runtime" import type { ActiveScenario, Options, ProjectOptions, Result, Scenario, ScenarioContext, SeededContext } from "./types" +import { ProviderV2 } from "@opencode-ai/core/provider" export function runScenario(options: Options) { return (scenario: Scenario) => { @@ -148,8 +149,8 @@ function withContext( time: { created: Date.now() }, agent: "build", model: { - providerID: ProviderID.opencode, - modelID: ModelID.make("test"), + providerID: ProviderV2.ID.opencode, + modelID: ProviderV2.ModelID.make("test"), }, } const part: SessionLegacy.TextPart = { diff --git a/packages/opencode/test/server/httpapi-schema-error-body.test.ts b/packages/opencode/test/server/httpapi-schema-error-body.test.ts index 007a5c5a2..744ce8a68 100644 --- a/packages/opencode/test/server/httpapi-schema-error-body.test.ts +++ b/packages/opencode/test/server/httpapi-schema-error-body.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect } from "effect" import { eq } from "drizzle-orm" import * as Database from "@/storage/db" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { Server } from "../../src/server/server" import { Session } from "@/session/session" import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" @@ -12,6 +12,7 @@ import { PartTable } from "@opencode-ai/core/session/sql" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const it = testEffect(Session.defaultLayer) @@ -28,7 +29,7 @@ const seedCorruptStepFinishPart = Effect.gen(function* () { role: "user", sessionID: info.id, agent: "build", - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("test") }, time: { created: Date.now() }, }) const partID = PartID.ascending() diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index 22f3d3038..644dcfa01 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -15,7 +15,7 @@ import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server" import { Server } from "../../src/server/server" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" -import { ModelID, ProviderID } from "../../src/provider/schema" + import type { Config } from "@/config/config" import { Session as SessionNs } from "@/session/session" import { errorMessage } from "../../src/util/error" @@ -25,6 +25,7 @@ import { resetDatabase } from "../fixture/db" import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture" import { awaitWithTimeout, testEffect } from "../lib/effect" import { testProviderConfig } from "../lib/test-provider" +import { ProviderV2 } from "@opencode-ai/core/provider" const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) const it = testEffect( @@ -311,7 +312,7 @@ function seedMessage(directory: string, sessionID: string) { role: "user", time: { created: Date.now() }, agent: "test", - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("test") }, tools: {}, } satisfies SessionLegacy.User) const part = yield* svc.updatePart({ diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index af87e6a42..5e58d436a 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -8,7 +8,7 @@ import { registerAdapter } from "../../src/control-plane/adapters" import type { WorkspaceAdapter } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { PermissionID } from "../../src/permission/schema" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { InstanceBootstrap } from "../../src/project/bootstrap" import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project/bootstrap-service" import { InstanceStore } from "../../src/project/instance-store" @@ -65,7 +65,7 @@ function createTextMessage(sessionID: SessionIDType, text: string) { role: "user", sessionID, agent: "build", - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("test") }, time: { created: Date.now() }, }) const part = yield* svc.updatePart({ diff --git a/packages/opencode/test/server/negative-tokens-regression.test.ts b/packages/opencode/test/server/negative-tokens-regression.test.ts index 7cfa6da6d..4098f98d8 100644 --- a/packages/opencode/test/server/negative-tokens-regression.test.ts +++ b/packages/opencode/test/server/negative-tokens-regression.test.ts @@ -8,7 +8,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { eq } from "drizzle-orm" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { Server } from "../../src/server/server" import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" import { Session } from "@/session/session" @@ -18,6 +18,7 @@ import { PartTable } from "@opencode-ai/core/session/sql" import { resetDatabase } from "../fixture/db" import { TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const it = testEffect(Session.defaultLayer) @@ -30,7 +31,7 @@ function seedNegativeTokenSession() { role: "user", sessionID: info.id, agent: "build", - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("test") }, time: { created: Date.now() }, }) const partID = PartID.ascending() diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index f2487fc20..1498bbd5c 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,6 +1,8 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { Session as SessionNs } from "@/session/session" import * as Log from "@opencode-ai/core/util/log" import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture" @@ -24,6 +26,8 @@ const it = testEffect( Layer.provide(Storage.defaultLayer), Layer.provide(SyncEvent.defaultLayer), Layer.provide(Database.defaultLayer), + Layer.provide(EventV2.defaultLayer), + Layer.provide(SessionProjector.defaultLayer), Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: false })), Layer.provide(BackgroundJob.defaultLayer), ), diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index fbe205f64..28538e2aa 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -4,19 +4,20 @@ import { Effect } from "effect" import { Server } from "../../src/server/server" import { Session as SessionNs } from "@/session/session" import { MessageV2 } from "../../src/session/message-v2" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { MessageID, PartID, type SessionID } from "../../src/session/schema" import * as Log from "@opencode-ai/core/util/log" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) const it = testEffect(SessionNs.defaultLayer) const model = { - providerID: ProviderID.make("test"), - modelID: ModelID.make("test"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test"), } afterEach(async () => { diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index edd67f43f..3b381d18c 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -21,7 +21,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { SessionV2 } from "@opencode-ai/core/session" -import { ModelID, ProviderID } from "../../src/provider/schema" + import type { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" import { Snapshot } from "../../src/snapshot" @@ -33,6 +33,7 @@ import { SyncEvent } from "@/sync" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" import { LLMEvent, Usage } from "@opencode-ai/llm" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) @@ -46,8 +47,8 @@ const summary = Layer.succeed( ) const ref = { - providerID: ProviderID.make("test"), - modelID: ModelID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test-model"), } const usage = (input: ConstructorParameters[0]) => new Usage(input) diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index a47af8b1a..d4ef4f634 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -6,7 +6,7 @@ import { FetchHttpClient } from "effect/unstable/http" import { NodeFileSystem } from "@effect/platform-node" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { Instruction } from "../../src/session/instruction" import type { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" @@ -15,6 +15,7 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { TestConfig } from "../fixture/config" +import { ProviderV2 } from "@opencode-ai/core/provider" const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer)) @@ -75,8 +76,8 @@ function loaded(filepath: string): SessionLegacy.WithParts[] { time: { created: 0 }, agent: "build", model: { - providerID: ProviderID.make("anthropic"), - modelID: ModelID.make("claude-sonnet-4-20250514"), + providerID: ProviderV2.ID.make("anthropic"), + modelID: ProviderV2.ModelID.make("claude-sonnet-4-20250514"), }, }, parts: [ diff --git a/packages/opencode/test/session/llm-native-recorded.test.ts b/packages/opencode/test/session/llm-native-recorded.test.ts index 97906c0d2..decf758d8 100644 --- a/packages/opencode/test/session/llm-native-recorded.test.ts +++ b/packages/opencode/test/session/llm-native-recorded.test.ts @@ -13,7 +13,7 @@ import { Auth } from "@/auth" import { Config } from "@/config/config" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "@/provider/schema" + import { Filesystem } from "@/util/filesystem" import { LLMEvent, LLMResponse } from "@opencode-ai/llm" import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" @@ -25,6 +25,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, SessionID } from "../../src/session/schema" import { TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings") @@ -41,7 +42,7 @@ const replayOpenAIOAuth = { type RecordedScenario = { readonly id: string readonly name: string - readonly providerID: ProviderID + readonly providerID: ProviderV2.ID readonly modelID: string readonly cassette: string readonly protocol: string @@ -88,7 +89,7 @@ function decodeRecordOpenAIOAuth() { } const providerConfig = (input: { - readonly providerID: ProviderID + readonly providerID: ProviderV2.ID readonly name: string readonly env: string[] readonly npm: string @@ -113,7 +114,7 @@ const RECORDED_SCENARIOS = [ { id: "openai-api-key", name: "OpenAI API key", - providerID: ProviderID.openai, + providerID: ProviderV2.ID.openai, modelID: "gpt-4.1-mini", cassette: "session/native-openai-tool-loop", protocol: "openai-responses", @@ -121,7 +122,7 @@ const RECORDED_SCENARIOS = [ canRecord: () => Boolean(envValue("OPENCODE_RECORD_OPENAI_API_KEY", "OPENAI_API_KEY")), config: (model) => providerConfig({ - providerID: ProviderID.openai, + providerID: ProviderV2.ID.openai, name: "OpenAI", env: ["OPENAI_API_KEY"], npm: "@ai-sdk/openai", @@ -136,7 +137,7 @@ const RECORDED_SCENARIOS = [ { id: "openai-oauth", name: "OpenAI OAuth", - providerID: ProviderID.openai, + providerID: ProviderV2.ID.openai, modelID: "gpt-5.5", cassette: "session/native-openai-oauth-tool-loop", protocol: "openai-responses", @@ -147,7 +148,7 @@ const RECORDED_SCENARIOS = [ stableID: "openai-oauth", config: (model) => providerConfig({ - providerID: ProviderID.openai, + providerID: ProviderV2.ID.openai, name: "OpenAI", env: ["OPENAI_API_KEY"], npm: "@ai-sdk/openai", @@ -159,7 +160,7 @@ const RECORDED_SCENARIOS = [ { id: "opencode-proxy", name: "OpenCode proxy", - providerID: ProviderID.opencode, + providerID: ProviderV2.ID.opencode, modelID: "gpt-5.2-codex", cassette: "session/native-zen-tool-loop", protocol: "openai-responses", @@ -167,7 +168,7 @@ const RECORDED_SCENARIOS = [ canRecord: () => Boolean(process.env.OPENCODE_RECORD_CONSOLE_TOKEN && process.env.OPENCODE_RECORD_ZEN_ORG_ID), config: (model) => providerConfig({ - providerID: ProviderID.opencode, + providerID: ProviderV2.ID.opencode, name: "OpenCode Zen", env: ["OPENCODE_CONSOLE_TOKEN"], npm: "@ai-sdk/openai-compatible", @@ -182,7 +183,7 @@ const RECORDED_SCENARIOS = [ { id: "anthropic-api-key", name: "Anthropic API key", - providerID: ProviderID.anthropic, + providerID: ProviderV2.ID.anthropic, modelID: "claude-haiku-4-5-20251001", cassette: "session/native-anthropic-tool-loop", protocol: "anthropic-messages", @@ -190,7 +191,7 @@ const RECORDED_SCENARIOS = [ canRecord: () => Boolean(envValue("OPENCODE_RECORD_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY")), config: (model) => providerConfig({ - providerID: ProviderID.anthropic, + providerID: ProviderV2.ID.anthropic, name: "Anthropic", env: ["ANTHROPIC_API_KEY"], npm: "@ai-sdk/anthropic", @@ -372,7 +373,7 @@ const driveToolLoop = (scenario: RecordedScenario) => const stableID = scenario.stableID ?? scenario.providerID const sessionID = SessionID.make(`session-recorded-${stableID}-loop`) - const modelID = ModelID.make(model.id) + const modelID = ProviderV2.ModelID.make(model.id) const agent = { name: "test", mode: "primary", diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 076d4c9f7..29c25d1ad 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -6,13 +6,14 @@ import { Effect, Layer, Stream } from "effect" import { LLMNative } from "@/session/llm/native-request" import { LLMNativeRuntime } from "@/session/llm/native-runtime" import type { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "@/provider/schema" + import { OAUTH_DUMMY_KEY } from "@/auth" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const baseModel: Provider.Model = { - id: ModelID.make("gpt-5-mini"), - providerID: ProviderID.make("openai"), + id: ProviderV2.ModelID.make("gpt-5-mini"), + providerID: ProviderV2.ID.make("openai"), api: { id: "gpt-5-mini", url: "https://api.openai.com/v1", @@ -62,7 +63,7 @@ const baseModel: Provider.Model = { } const providerInfo: Provider.Info = { - id: ProviderID.make("openai"), + id: ProviderV2.ID.make("openai"), name: "OpenAI", source: "config", env: ["OPENAI_API_KEY"], @@ -354,7 +355,7 @@ describe("session.llm-native.request", () => { const compatible = LLMNative.model({ model: { ...baseModel, - providerID: ProviderID.make("opencode"), + providerID: ProviderV2.ID.make("opencode"), api: { ...baseModel.api, url: "https://ai.example.test/v1", npm: "@ai-sdk/openai-compatible" }, }, apiKey: "test-key", @@ -388,8 +389,8 @@ describe("session.llm-native.request", () => { }) expect( LLMNativeRuntime.status({ - model: { ...baseModel, providerID: ProviderID.make("opencode") }, - provider: { ...providerInfo, id: ProviderID.make("opencode") }, + model: { ...baseModel, providerID: ProviderV2.ID.make("opencode") }, + provider: { ...providerInfo, id: ProviderV2.ID.make("opencode") }, auth: undefined, }), ).toMatchObject({ @@ -400,10 +401,10 @@ describe("session.llm-native.request", () => { LLMNativeRuntime.status({ model: { ...baseModel, - providerID: ProviderID.make("opencode"), + providerID: ProviderV2.ID.make("opencode"), api: { ...baseModel.api, npm: "@ai-sdk/openai-compatible" }, }, - provider: { ...providerInfo, id: ProviderID.make("opencode") }, + provider: { ...providerInfo, id: ProviderV2.ID.make("opencode") }, auth: undefined, }), ).toMatchObject({ @@ -412,8 +413,8 @@ describe("session.llm-native.request", () => { }) expect( LLMNativeRuntime.status({ - model: { ...baseModel, providerID: ProviderID.make("google") }, - provider: { ...providerInfo, id: ProviderID.make("google") }, + model: { ...baseModel, providerID: ProviderV2.ID.make("google") }, + provider: { ...providerInfo, id: ProviderV2.ID.make("google") }, auth: undefined, }), ).toEqual({ type: "unsupported", reason: "provider is not openai, opencode, or anthropic" }) @@ -454,12 +455,12 @@ describe("session.llm-native.request", () => { LLMNativeRuntime.status({ model: { ...baseModel, - providerID: ProviderID.make("anthropic"), + providerID: ProviderV2.ID.make("anthropic"), api: { ...baseModel.api, npm: "@ai-sdk/anthropic", url: "https://api.anthropic.com/v1" }, }, provider: { ...providerInfo, - id: ProviderID.make("anthropic"), + id: ProviderV2.ID.make("anthropic"), name: "Anthropic", env: ["ANTHROPIC_API_KEY"], options: { apiKey: "test-anthropic-key" }, @@ -472,10 +473,10 @@ describe("session.llm-native.request", () => { test("prefers console provider api key over stored opencode auth", () => { expect( LLMNativeRuntime.status({ - model: { ...baseModel, providerID: ProviderID.make("opencode") }, + model: { ...baseModel, providerID: ProviderV2.ID.make("opencode") }, provider: { ...providerInfo, - id: ProviderID.make("opencode"), + id: ProviderV2.ID.make("opencode"), options: { apiKey: "console-token" }, key: "zen-token", }, diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 941129610..892700302 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -14,7 +14,7 @@ import { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" import { ModelsDev } from "@opencode-ai/core/models-dev" import { Plugin } from "@/plugin" -import { ProviderID, ModelID } from "../../src/provider/schema" + import { testEffect } from "../lib/effect" import type { Agent } from "../../src/agent/agent" import { MessageV2 } from "../../src/session/message-v2" @@ -23,6 +23,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { Permission } from "@/permission" import { LLMAISDK } from "@/session/llm/ai-sdk" import { Session as SessionNs } from "@/session/session" +import { ProviderV2 } from "@opencode-ai/core/provider" type ConfigModel = NonNullable[string]["models"]>[string] @@ -713,8 +714,8 @@ describe("session.llm.stream", () => { ) const resolved = yield* Provider.use.getModel( - ProviderID.make(vivgridFixture.providerID), - ModelID.make(fixture.model.id), + ProviderV2.ID.make(vivgridFixture.providerID), + ProviderV2.ModelID.make(fixture.model.id), ) const sessionID = SessionID.make("session-test-1") const agent = { @@ -732,7 +733,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" }, + model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" }, } satisfies SessionLegacy.User yield* drain({ @@ -787,8 +788,8 @@ describe("session.llm.stream", () => { const pending = waitStreamingRequest("/chat/completions") const resolved = yield* Provider.use.getModel( - ProviderID.make(alibabaQwenFixture.providerID), - ModelID.make(fixture.model.id), + ProviderV2.ID.make(alibabaQwenFixture.providerID), + ProviderV2.ModelID.make(fixture.model.id), ) const sessionID = SessionID.make("session-test-service-abort") const agent = { @@ -803,7 +804,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, } satisfies SessionLegacy.User const fiber = yield* drain({ @@ -855,8 +856,8 @@ describe("session.llm.stream", () => { ) const resolved = yield* Provider.use.getModel( - ProviderID.make(alibabaQwenFixture.providerID), - ModelID.make(fixture.model.id), + ProviderV2.ID.make(alibabaQwenFixture.providerID), + ProviderV2.ModelID.make(fixture.model.id), ) const sessionID = SessionID.make("session-test-tools") const agent = { @@ -872,7 +873,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, tools: { question: true }, } satisfies SessionLegacy.User @@ -959,7 +960,7 @@ describe("session.llm.stream", () => { ] const request = waitRequest("/responses", createEventResponse(responseChunks, true)) - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-2") const agent = { name: "test", @@ -975,7 +976,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" }, } satisfies SessionLegacy.User yield* drain({ @@ -1064,7 +1065,7 @@ describe("session.llm.stream", () => { }), ) - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-flag-off") const agent = { name: "test", @@ -1089,7 +1090,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" }, } satisfies SessionLegacy.User, sessionID, model: resolved, @@ -1134,7 +1135,7 @@ describe("session.llm.stream", () => { ] const request = waitRequest("/responses", createEventResponse(chunks, true)) - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native") const agent = { name: "test", @@ -1151,7 +1152,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" }, } satisfies SessionLegacy.User, sessionID, model: resolved, @@ -1218,7 +1219,7 @@ describe("session.llm.stream", () => { }), ) - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-injected-tool") const agent = { name: "test", @@ -1234,7 +1235,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id }, } satisfies SessionLegacy.User, sessionID, model: resolved, @@ -1306,7 +1307,7 @@ describe("session.llm.stream", () => { const request = waitRequest("/responses", createEventResponse(chunks, true)) let executed: unknown - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-tool") const agent = { name: "test", @@ -1322,7 +1323,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id }, } satisfies SessionLegacy.User, sessionID, model: resolved, @@ -1432,7 +1433,7 @@ describe("session.llm.stream", () => { ), ).toString("base64")}` - const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-data-url") const agent = { name: "test", @@ -1447,7 +1448,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id }, } satisfies SessionLegacy.User yield* drain({ @@ -1520,8 +1521,8 @@ describe("session.llm.stream", () => { const request = waitRequest("/messages", createEventResponse(chunks)) const resolved = yield* Provider.use.getModel( - ProviderID.make(minimaxFixture.providerID), - ModelID.make(model.id), + ProviderV2.ID.make(minimaxFixture.providerID), + ProviderV2.ModelID.make(model.id), ) const sessionID = SessionID.make("session-test-3") const agent = { @@ -1539,7 +1540,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") }, + model: { providerID: ProviderV2.ID.make("minimax"), modelID: ProviderV2.ModelID.make("MiniMax-M2.5") }, } satisfies SessionLegacy.User yield* drain({ @@ -1616,7 +1617,7 @@ describe("session.llm.stream", () => { ] const request = waitRequest("/messages", createEventResponse(chunks)) - const resolved = yield* Provider.use.getModel(ProviderID.make("anthropic"), ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-anthropic-tools") const agent = { name: "test", @@ -1630,7 +1631,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" }, + model: { providerID: ProviderV2.ID.make("anthropic"), modelID: resolved.id, variant: "max" }, } satisfies SessionLegacy.User const input = [ @@ -1815,7 +1816,7 @@ describe("session.llm.stream", () => { ] const request = waitRequest(pathSuffix, createEventResponse(chunks)) - const resolved = yield* Provider.use.getModel(ProviderID.make(geminiFixture.providerID), ModelID.make(model.id)) + const resolved = yield* Provider.use.getModel(ProviderV2.ID.make(geminiFixture.providerID), ProviderV2.ModelID.make(model.id)) const sessionID = SessionID.make("session-test-4") const agent = { name: "test", @@ -1832,7 +1833,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(geminiFixture.providerID), modelID: resolved.id }, + model: { providerID: ProviderV2.ID.make(geminiFixture.providerID), modelID: resolved.id }, } satisfies SessionLegacy.User yield* drain({ diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index dfbff389c..09d19cda3 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -4,14 +4,15 @@ import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" import { ProviderTransform } from "@/provider/transform" import type { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { SessionID, MessageID, PartID } from "../../src/session/schema" import { Question } from "../../src/question" +import { ProviderV2 } from "@opencode-ai/core/provider" const sessionID = SessionID.make("session") -const providerID = ProviderID.make("test") +const providerID = ProviderV2.ID.make("test") const model: Provider.Model = { - id: ModelID.make("test-model"), + id: ProviderV2.ModelID.make("test-model"), providerID, api: { id: "test-model", @@ -66,7 +67,7 @@ function userInfo(id: string): SessionLegacy.User { role: "user", time: { created: 0 }, agent: "user", - model: { providerID, modelID: ModelID.make("test") }, + model: { providerID, modelID: ProviderV2.ModelID.make("test") }, tools: {}, mode: "", } as unknown as SessionLegacy.User @@ -412,8 +413,8 @@ describe("session.message-v2.toModelMessage", () => { test("preserves jpeg tool-result media for anthropic models", async () => { const anthropicModel: Provider.Model = { ...model, - id: ModelID.make("anthropic/claude-opus-4-7"), - providerID: ProviderID.make("anthropic"), + id: ProviderV2.ModelID.make("anthropic/claude-opus-4-7"), + providerID: ProviderV2.ID.make("anthropic"), api: { id: "claude-opus-4-7-20250805", url: "https://api.anthropic.com", @@ -495,8 +496,8 @@ describe("session.message-v2.toModelMessage", () => { test("moves bedrock pdf tool-result media into a separate user message", async () => { const bedrockModel: Provider.Model = { ...model, - id: ModelID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"), - providerID: ProviderID.make("amazon-bedrock"), + id: ProviderV2.ModelID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"), + providerID: ProviderV2.ID.make("amazon-bedrock"), api: { id: "anthropic.claude-sonnet-4-6", url: "https://bedrock-runtime.us-east-1.amazonaws.com", @@ -1041,8 +1042,8 @@ describe("session.message-v2.toModelMessage", () => { const assistantID = "m-assistant" const openrouterModel: Provider.Model = { ...model, - id: ModelID.make("deepseek/deepseek-v4-pro"), - providerID: ProviderID.make("openrouter"), + id: ProviderV2.ModelID.make("deepseek/deepseek-v4-pro"), + providerID: ProviderV2.ID.make("openrouter"), api: { id: "deepseek/deepseek-v4-pro", url: "https://openrouter.ai/api/v1", diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index 81b4d9ffb..5da80ea3e 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -5,10 +5,11 @@ import { Effect, Layer, Option } from "effect" import { Session as SessionNs } from "@/session/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { NotFoundError } from "@/storage/storage" import * as Log from "@opencode-ai/core/util/log" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) @@ -97,8 +98,8 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* ( role: "assistant", time: { created: Date.now() }, parentID, - modelID: ModelID.make("test"), - providerID: ProviderID.make("test"), + modelID: ProviderV2.ModelID.make("test"), + providerID: ProviderV2.ID.make("test"), mode: "", agent: "default", path: { cwd: "/", root: "/" }, diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index c4f1fe1d0..4375c6aaa 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -14,7 +14,7 @@ import { Image } from "@/image/image" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { Session } from "@/session/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" @@ -31,6 +31,7 @@ import { raw, reply, TestLLMServer } from "../lib/llm-server" import { SyncEvent } from "@/sync" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) @@ -44,8 +45,8 @@ const summary = Layer.succeed( ) const ref = { - providerID: ProviderID.make("test"), - modelID: ModelID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test-model"), } const cfg = { diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 8f32a73f7..89573053f 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -20,7 +20,7 @@ import { Provider as ProviderSvc } from "@/provider/provider" import { Env } from "../../src/env" import { Git } from "../../src/git" import { Image } from "../../src/image/image" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" import { Session } from "@/session/session" @@ -57,6 +57,7 @@ import { reply, TestLLMServer } from "../lib/llm-server" import { SyncEvent } from "@/sync" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) @@ -70,8 +71,8 @@ const summary = Layer.succeed( ) const ref = { - providerID: ProviderID.make("test"), - modelID: ModelID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test-model"), } function withSh(fx: () => Effect.Effect) { @@ -727,8 +728,8 @@ it.instance("failed subtask preserves metadata on error tool state", () => expect(tool.state.metadata).toBeDefined() expect(tool.state.metadata?.sessionId).toBeDefined() expect(tool.state.metadata?.model).toEqual({ - providerID: ProviderID.make("test"), - modelID: ModelID.make("missing-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("missing-model"), }) }), ) @@ -2172,7 +2173,7 @@ noLLMServer.instance( const other = yield* prompt.prompt({ sessionID: session.id, agent: "build", - model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") }, + model: { providerID: ProviderV2.ID.make("opencode"), modelID: ProviderV2.ModelID.make("kimi-k2.5-free") }, noReply: true, parts: [{ type: "text", text: "hello" }], }) @@ -2187,8 +2188,8 @@ noLLMServer.instance( }) if (match.info.role !== "user") throw new Error("expected user message") expect(match.info.model).toEqual({ - providerID: ProviderID.make("test"), - modelID: ModelID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test-model"), variant: "xhigh", }) expect(match.info.model.variant).toBe("xhigh") diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 98305b904..2300e4ad4 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -7,13 +7,14 @@ import { Effect, Layer, Schedule, Schema } from "effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" -import { ProviderID } from "../../src/provider/schema" + import { SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" -const providerID = ProviderID.make("test") +const providerID = ProviderV2.ID.make("test") const retryProvider = "test" const it = testEffect(Layer.mergeAll(SessionStatus.defaultLayer, CrossSpawnSpawner.defaultLayer)) @@ -390,7 +391,7 @@ describe("session.message-v2.fromError", () => { responseBody: '{"error":"boom"}', isRetryable: false, }) - const result = MessageV2.fromError(error, { providerID: ProviderID.make("openai") }) + const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("openai") }) if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError") expect(result.data.isRetryable).toBe(true) }) @@ -409,7 +410,7 @@ describe("session.message-v2.fromError", () => { }, }), }, - { providerID: ProviderID.make("openai") }, + { providerID: ProviderV2.ID.make("openai") }, ) expect(SessionLegacy.APIError.isInstance(result)).toBe(true) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index ab17cc6cd..0df791096 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import path from "path" import { Effect, Layer } from "effect" import { Session } from "@/session/session" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { SessionRevert } from "../../src/session/revert" import { MessageV2 } from "../../src/session/message-v2" import { Snapshot } from "../../src/snapshot" @@ -13,6 +13,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" void Log.init({ print: false }) @@ -32,7 +33,7 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "de role: "user" as const, sessionID, agent, - model: { providerID: ProviderID.make("openai"), modelID: ModelID.make("gpt-4") }, + model: { providerID: ProviderV2.ID.make("openai"), modelID: ProviderV2.ModelID.make("gpt-4") }, time: { created: Date.now() }, }) }) @@ -48,8 +49,8 @@ const assistant = Effect.fn("test.assistant")(function* (sessionID: SessionID, p path: { cwd: dir, root: dir }, cost: 0, tokens: { output: 0, input: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - modelID: ModelID.make("gpt-4"), - providerID: ProviderID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), parentID, time: { created: Date.now() }, finish: "end_turn", @@ -115,8 +116,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: ProviderID.make("openai"), - modelID: ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -148,8 +149,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: ModelID.make("gpt-4"), - providerID: ProviderID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), parentID: userMsg1.id, time: { created: Date.now(), @@ -172,8 +173,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: ProviderID.make("openai"), - modelID: ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -205,8 +206,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: ModelID.make("gpt-4"), - providerID: ProviderID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), parentID: userMsg2.id, time: { created: Date.now(), @@ -277,8 +278,8 @@ describe("revert + compact workflow", () => { sessionID, agent: "default", model: { - providerID: ProviderID.make("openai"), - modelID: ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), }, time: { created: Date.now(), @@ -310,8 +311,8 @@ describe("revert + compact workflow", () => { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: ModelID.make("gpt-4"), - providerID: ProviderID.make("openai"), + modelID: ProviderV2.ModelID.make("gpt-4"), + providerID: ProviderV2.ID.make("openai"), parentID: userMsg.id, time: { created: Date.now(), diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 8566470d4..f78eb5fb8 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -1,6 +1,8 @@ import { describe, expect } from "bun:test" import { SessionLegacy } from "@opencode-ai/core/session/legacy" import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { Deferred, Effect, Exit, Layer } from "effect" import { Session as SessionNs } from "@/session/session" import { GlobalBus, type GlobalEvent } from "../../src/bus/global" @@ -25,6 +27,8 @@ const it = testEffect( Layer.provide(Storage.defaultLayer), Layer.provide(SyncEvent.defaultLayer), Layer.provide(Database.defaultLayer), + Layer.provide(EventV2.defaultLayer), + Layer.provide(SessionProjector.defaultLayer), Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: false })), Layer.provide(BackgroundJob.defaultLayer), ), diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 7f60a94f9..f60f24edf 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -31,10 +31,11 @@ import * as Truncate from "@/tool/truncate" import { InstanceState } from "@/effect/instance-state" import { Reference } from "@/reference/reference" import { RepositoryCache } from "@/reference/repository-cache" -import { ProviderID, ModelID } from "@/provider/schema" + import { ToolJsonSchema } from "@/tool/json-schema" import { MessageID, SessionID } from "@/session/schema" import { RuntimeFlags } from "@/effect/runtime-flags" +import { ProviderV2 } from "@opencode-ai/core/provider" const node = CrossSpawnSpawner.defaultLayer const configLayer = TestConfig.layer({ @@ -148,8 +149,8 @@ describe("tool.registry", () => { const build = yield* agent.get("build") if (!build) throw new Error("build agent not found") const task = (yield* registry.tools({ - providerID: ProviderID.opencode, - modelID: ModelID.make("test"), + providerID: ProviderV2.ID.opencode, + modelID: ProviderV2.ModelID.make("test"), agent: build, })).find((tool) => tool.id === "task") @@ -335,8 +336,8 @@ describe("tool.registry", () => { const agents = yield* Agent.Service const promptTools = yield* registry.tools({ - providerID: ProviderID.opencode, - modelID: ModelID.make("test"), + providerID: ProviderV2.ID.opencode, + modelID: ProviderV2.ModelID.make("test"), agent: yield* agents.defaultInfo(), }) const promptTool = promptTools.find((tool) => tool.id === "sql") diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index a12729e36..e59ea5986 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -13,21 +13,22 @@ import type { SessionPrompt } from "../../src/session/prompt" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionRunState } from "@/session/run-state" import { SessionStatus } from "@/session/status" -import { ModelID, ProviderID } from "../../src/provider/schema" + import { TaskTool, type TaskPromptOps } from "../../src/tool/task" import { Truncate } from "@/tool/truncate" import { ToolRegistry } from "@/tool/registry" import { RuntimeFlags } from "@/effect/runtime-flags" import { disposeAllInstances } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" afterEach(async () => { await disposeAllInstances() }) const ref = { - providerID: ProviderID.make("test"), - modelID: ModelID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + modelID: ProviderV2.ModelID.make("test-model"), } const layer = (flags: Partial = {}) => diff --git a/packages/opencode/test/tool/websearch.test.ts b/packages/opencode/test/tool/websearch.test.ts index b8edc2dc2..349606dec 100644 --- a/packages/opencode/test/tool/websearch.test.ts +++ b/packages/opencode/test/tool/websearch.test.ts @@ -2,9 +2,10 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { parseResponse } from "../../src/tool/mcp-websearch" import { selectWebSearchProvider, webSearchModelName, webSearchProviderLabel } from "../../src/tool/websearch" -import { ProviderID } from "../../src/provider/schema" + import { webSearchEnabled } from "../../src/tool/registry" import { it } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" const SESSION_ID = "ses_0196aabbccddeeff001122334455" @@ -37,10 +38,10 @@ describe("websearch provider", () => { }) test("is only enabled for opencode or explicit websearch provider flags", () => { - expect(webSearchEnabled(ProviderID.opencode, { exa: false, parallel: false })).toBe(true) - expect(webSearchEnabled(ProviderID.openai, { exa: false, parallel: false })).toBe(false) - expect(webSearchEnabled(ProviderID.openai, { exa: true, parallel: false })).toBe(true) - expect(webSearchEnabled(ProviderID.openai, { exa: false, parallel: true })).toBe(true) + expect(webSearchEnabled(ProviderV2.ID.opencode, { exa: false, parallel: false })).toBe(true) + expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: false })).toBe(false) + expect(webSearchEnabled(ProviderV2.ID.openai, { exa: true, parallel: false })).toBe(true) + expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: true })).toBe(true) }) test("uses branded labels", () => {