refactor(opencode): migrate session events to core
This commit is contained in:
@@ -103,6 +103,7 @@ export function definitions() {
|
||||
export interface PublishOptions {
|
||||
readonly id?: ID
|
||||
readonly metadata?: Record<string, unknown>
|
||||
readonly location?: Location.Ref
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
@@ -236,7 +237,7 @@ export const layer = Layer.effect(
|
||||
|
||||
function publish<D extends Definition>(definition: D, data: Data<D>, 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 } : {}),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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<typeof rest>
|
||||
}
|
||||
|
||||
function partData(part: typeof SessionLegacy.Event.PartUpdated.Type["data"]["part"]): typeof PartTable.$inferInsert.data {
|
||||
const { id: _, messageID: __, sessionID: ___, ...rest } = part
|
||||
return rest as DeepMutable<typeof rest>
|
||||
}
|
||||
|
||||
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))
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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<number | null> {
|
||||
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<string, unknown> }>,
|
||||
): 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<T extends { name: string }>(providers: T[]): T[] {
|
||||
|
||||
function modelVariantsFromProviders(
|
||||
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
|
||||
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<string, { variants?: Record<string, any> }> }>,
|
||||
): { 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>
|
||||
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())
|
||||
|
||||
@@ -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<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
|
||||
type BusDefinition = BusEvent.Definition<string, Schema.Top> | EventV2.Definition<string, Schema.Top>
|
||||
type BusSchema<D extends BusDefinition> = D extends { data: infer S extends Schema.Top }
|
||||
? S
|
||||
: D extends { properties: infer S extends Schema.Top }
|
||||
? S
|
||||
: never
|
||||
type BusProperties<D extends BusDefinition> = Schema.Schema.Type<BusSchema<D>>
|
||||
|
||||
export const InstanceDisposed = BusEvent.define(
|
||||
"server.instance.disposed",
|
||||
@@ -21,7 +28,7 @@ export const InstanceDisposed = BusEvent.define(
|
||||
}),
|
||||
)
|
||||
|
||||
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
||||
type Payload<D extends BusDefinition = BusDefinition> = {
|
||||
id: string
|
||||
type: D["type"]
|
||||
properties: BusProperties<D>
|
||||
@@ -33,7 +40,7 @@ type State = {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly publish: <D extends BusEvent.Definition>(
|
||||
readonly publish: <D extends BusDefinition>(
|
||||
def: D,
|
||||
properties: BusProperties<D>,
|
||||
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: <D extends BusEvent.Definition>(
|
||||
readonly subscribe: <D extends BusDefinition>(
|
||||
def: D,
|
||||
) => Effect.Effect<Stream.Stream<Payload<D>>, never, Scope.Scope>
|
||||
readonly subscribeAll: () => Effect.Effect<Stream.Stream<Payload>, never, Scope.Scope>
|
||||
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
||||
readonly subscribeCallback: <D extends BusDefinition>(
|
||||
def: D,
|
||||
callback: (event: Payload<D>) => unknown,
|
||||
) => Effect.Effect<() => void>
|
||||
@@ -86,7 +93,7 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
function getOrCreate<D extends BusEvent.Definition>(state: State, def: D) {
|
||||
function getOrCreate<D extends BusDefinition>(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<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>, options?: { id?: string }) {
|
||||
function publish<D extends BusDefinition>(def: D, properties: BusProperties<D>, 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 = <D extends BusEvent.Definition>(
|
||||
const subscribe = <D extends BusDefinition>(
|
||||
def: D,
|
||||
): Effect.Effect<Stream.Stream<Payload<D>>, never, Scope.Scope> =>
|
||||
Effect.gen(function* () {
|
||||
@@ -169,7 +176,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* <D extends BusEvent.Definition>(
|
||||
const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* <D extends BusDefinition>(
|
||||
def: D,
|
||||
callback: (event: Payload<D>) => unknown,
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<D extends EventV2.Definition>(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<D["type"], D["data"], D["data"]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, EventV2.Interface>()("@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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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<typeof CallbackInput>
|
||||
|
||||
export class OauthMissing extends Schema.TaggedErrorClass<OauthMissing>()("ProviderAuthOauthMissing", {
|
||||
providerID: ProviderID,
|
||||
providerID: ProviderV2.ID,
|
||||
}) {}
|
||||
|
||||
export class OauthCodeMissing extends Schema.TaggedErrorClass<OauthCodeMissing>()("ProviderAuthOauthCodeMissing", {
|
||||
providerID: ProviderID,
|
||||
providerID: ProviderV2.ID,
|
||||
}) {}
|
||||
|
||||
export class OauthCallbackFailed extends Schema.TaggedErrorClass<OauthCallbackFailed>()(
|
||||
@@ -90,15 +90,15 @@ export interface Interface {
|
||||
readonly methods: () => Effect.Effect<Methods>
|
||||
readonly authorize: (
|
||||
input: {
|
||||
providerID: ProviderID
|
||||
providerID: ProviderV2.ID
|
||||
} & AuthorizeInput,
|
||||
) => Effect.Effect<Authorization | undefined, Error>
|
||||
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
|
||||
readonly callback: (input: { providerID: ProviderV2.ID } & CallbackInput) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
interface State {
|
||||
hooks: Record<ProviderID, Hook>
|
||||
pending: Map<ProviderID, AuthOAuthResult>
|
||||
hooks: Record<ProviderV2.ID, Hook>
|
||||
pending: Map<ProviderV2.ID, AuthOAuthResult>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
|
||||
@@ -117,11 +117,11 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
|
||||
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<ProviderID, AuthOAuthResult>(),
|
||||
pending: new Map<ProviderV2.ID, AuthOAuthResult>(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -160,7 +160,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
|
||||
})
|
||||
|
||||
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<Service, never, Auth.Service | Plugin.Service> =
|
||||
}
|
||||
})
|
||||
|
||||
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 })
|
||||
|
||||
@@ -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<string, string>
|
||||
}
|
||||
|
||||
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") {
|
||||
|
||||
@@ -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<string, CustomLoader> {
|
||||
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<Schema.Schema.Type<typeof Model>>
|
||||
|
||||
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<T extends { models: Record<string, { id: string
|
||||
}
|
||||
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("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<ModelNotFoundErr
|
||||
}
|
||||
|
||||
export class InitError extends Schema.TaggedErrorClass<InitError>()("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<NoProvidersError>(
|
||||
}
|
||||
|
||||
export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("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<Record<ProviderID, Info>>
|
||||
readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
|
||||
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
||||
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
|
||||
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
|
||||
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
||||
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
||||
readonly closest: (
|
||||
providerID: ProviderID,
|
||||
providerID: ProviderV2.ID,
|
||||
query: string[],
|
||||
) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
|
||||
readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
|
||||
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }, DefaultModelError>
|
||||
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
|
||||
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
|
||||
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
|
||||
}
|
||||
|
||||
interface State {
|
||||
models: Map<string, LanguageModelV3>
|
||||
providers: Record<ProviderID, Info>
|
||||
catalog: Record<ProviderID, Info>
|
||||
providers: Record<ProviderV2.ID, Info>
|
||||
catalog: Record<ProviderV2.ID, Info>
|
||||
sdk: Map<string, BundledSDK>
|
||||
modelLoaders: Record<string, CustomModelLoader>
|
||||
varsLoaders: Record<string, CustomVarsLoader>
|
||||
@@ -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<ProviderID, Info> = {} as Record<ProviderID, Info>
|
||||
const providers: Record<ProviderV2.ID, Info> = {} as Record<ProviderV2.ID, Info>
|
||||
const languages = new Map<string, LanguageModelV3>()
|
||||
const modelLoaders: {
|
||||
[providerID: string]: CustomModelLoader
|
||||
@@ -1228,7 +1228,7 @@ export const layer = Layer.effect(
|
||||
|
||||
log.info("init")
|
||||
|
||||
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
||||
function mergeProvider(providerID: ProviderV2.ID, provider: Partial<Info>) {
|
||||
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<Info> = { 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<T extends { id: string }>(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("/")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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<typeof Session.Event.Updated>["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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ProviderAuthApiError
|
||||
{
|
||||
name: ProviderAuthErrorName,
|
||||
data: Schema.Struct({
|
||||
providerID: Schema.optional(ProviderID),
|
||||
providerID: Schema.optional(ProviderV2.ID),
|
||||
field: Schema.optional(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
kind: Schema.optional(Schema.String),
|
||||
@@ -55,7 +56,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||
params: { providerID: ProviderID },
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.AuthorizeInput,
|
||||
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
||||
@@ -68,7 +69,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
||||
params: { providerID: ProviderID },
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.CallbackInput,
|
||||
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
|
||||
import { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const root = "/session"
|
||||
export const ListQuery = Schema.Struct({
|
||||
@@ -55,13 +56,13 @@ export const UpdatePayload = Schema.Struct({
|
||||
})
|
||||
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
|
||||
export const InitPayload = Schema.Struct({
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
providerID: ProviderV2.ID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
export const SummarizePayload = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
auto: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { RootHttpApi } from "../api"
|
||||
import { LogInput } from "../groups/control"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (handlers) =>
|
||||
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
|
||||
})
|
||||
|
||||
@@ -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<A, R>(self: Effect.Effect<A, ProviderAuth.Error, R>) {
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void>
|
||||
@@ -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
|
||||
}) {
|
||||
|
||||
@@ -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<Assistant["error"]> {
|
||||
switch (true) {
|
||||
case e instanceof DOMException && e.name === "AbortError":
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> | null } : T
|
||||
|
||||
type Usage = Pick<SessionLegacy.StepFinishPart, "cost" | "tokens">
|
||||
|
||||
function usage(part: SessionLegacy.Part | unknown): Usage | undefined {
|
||||
if (typeof part !== "object" || part === null) return undefined
|
||||
const value = part as Record<string, unknown>
|
||||
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<T extends object, K1 extends keyof T, X>(
|
||||
obj: T,
|
||||
field1: K1,
|
||||
cb?: (val: NonNullable<T[K1]>) => 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<Session.Info>) {
|
||||
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 })
|
||||
}
|
||||
}),
|
||||
]
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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<Info, "directory" | "workspaceID">) {
|
||||
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<void>
|
||||
readonly clearRevert: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect<void>
|
||||
readonly setShare: (input: { sessionID: SessionID; share: Info["share"] }) => Effect.Effect<void>
|
||||
readonly setWorkspace: (input: { sessionID: SessionID; workspaceID: Info["workspaceID"] }) => Effect.Effect<void>
|
||||
readonly diff: (sessionID: SessionID) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<SessionLegacy.WithParts[], NotFound>
|
||||
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
|
||||
@@ -505,12 +499,18 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export type Patch = Types.DeepMutable<SyncEvent.Event<typeof Event.Updated>["data"]["info"]>
|
||||
export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert" | "permission"> & {
|
||||
time?: Partial<Info["time"]>
|
||||
share?: Partial<NonNullable<Info["share"]>> | 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 = <T extends SessionLegacy.Info>(msg: T): Effect.Effect<T> =>
|
||||
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 = <T extends SessionLegacy.Part>(part: T): Effect.Effect<T> =>
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
})) {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
|
||||
|
||||
@@ -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<typeof TaskTool>
|
||||
@@ -77,7 +78,7 @@ export interface Interface {
|
||||
readonly ids: () => Effect.Effect<string[]>
|
||||
readonly all: () => Effect.Effect<Tool.Def[]>
|
||||
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
|
||||
readonly tools: (model: { providerID: ProviderID; modelID: ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
|
||||
readonly tools: (model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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> = {}): 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string, Record<string, JSONValue>>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, string | undefined>()
|
||||
|
||||
@@ -68,7 +69,7 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
const list = Provider.use.list()
|
||||
|
||||
const paid = (providers: Record<string, { models: Record<string, { cost: { input: number } }> }>) => {
|
||||
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()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -60,7 +60,7 @@ const publishConnected = Bus.use.publish(ServerEvent.Connected, {})
|
||||
|
||||
const publishPartUpdated = (partID: ReturnType<typeof PartID.ascending>) => {
|
||||
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(),
|
||||
|
||||
@@ -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<A, E>(
|
||||
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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<typeof Usage>[0]) => new Usage(input)
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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<NonNullable<Config.Info["provider"]>[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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "/" },
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<RuntimeFlags.Info> = {}) =>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user