mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 09:16:20 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 529106dae8 | |||
| a03882ff48 | |||
| 72244e9e09 |
@@ -375,6 +375,12 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
|
|||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
|
if (event.type === "provider.models.updated") {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
|||||||
import { ProviderTransform } from "./transform"
|
import { ProviderTransform } from "./transform"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { ModelStatus } from "./model-status"
|
import { ModelStatus } from "./model-status"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { ProviderError } from "./error"
|
import { ProviderError } from "./error"
|
||||||
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
|
|
||||||
const OPENAI_HEADER_TIMEOUT_DEFAULT = 10_000
|
const OPENAI_HEADER_TIMEOUT_DEFAULT = 10_000
|
||||||
|
|
||||||
@@ -1045,6 +1047,13 @@ export const ConfigProvidersResult = Schema.Struct({
|
|||||||
})
|
})
|
||||||
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
ModelsUpdated: EventV2.define({
|
||||||
|
type: "provider.models.updated",
|
||||||
|
schema: { providerID: ProviderV2.ID },
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
export function toPublicInfo(provider: Info): Info {
|
export function toPublicInfo(provider: Info): Info {
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
JSON.stringify(provider, (_, value) => {
|
JSON.stringify(provider, (_, value) => {
|
||||||
@@ -1279,6 +1288,7 @@ export const layer = Layer.effect(
|
|||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const modelsDevSvc = yield* ModelsDev.Service
|
const modelsDevSvc = yield* ModelsDev.Service
|
||||||
const runtimeFlags = yield* RuntimeFlags.Service
|
const runtimeFlags = yield* RuntimeFlags.Service
|
||||||
|
const events = yield* EventV2Bridge.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(() =>
|
const state = yield* InstanceState.make<State>(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -1334,35 +1344,15 @@ export const layer = Layer.effect(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const hook of plugins) {
|
const providerHooks = plugins.flatMap((hook) => {
|
||||||
const p = hook.provider
|
const provider = hook.provider
|
||||||
const models = p?.models
|
if (!provider?.models) return []
|
||||||
if (!p || !models) continue
|
const providerID = ProviderV2.ID.make(provider.id)
|
||||||
|
if (!isProviderAllowed(providerID) || !database[providerID]) return []
|
||||||
const providerID = ProviderV2.ID.make(p.id)
|
return [{ providerID, models: provider.models }]
|
||||||
if (disabled.has(providerID)) continue
|
})
|
||||||
|
|
||||||
const provider = database[providerID]
|
|
||||||
if (!provider) continue
|
|
||||||
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
||||||
|
|
||||||
provider.models = yield* Effect.promise(async () => {
|
|
||||||
const next = await models(toPublicInfo(provider), { auth: pluginAuth })
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(next).map(([id, model]) => [
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
...model,
|
|
||||||
id: ModelV2.ID.make(id),
|
|
||||||
providerID,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// extend database from config
|
// extend database from config
|
||||||
for (const [providerID, provider] of configProviders) {
|
function applyConfigProvider(providerID: string, provider: (typeof configProviders)[number][1]) {
|
||||||
const existing = database[providerID]
|
const existing = database[providerID]
|
||||||
const parsed: Info = {
|
const parsed: Info = {
|
||||||
id: ProviderV2.ID.make(providerID),
|
id: ProviderV2.ID.make(providerID),
|
||||||
@@ -1454,6 +1444,7 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
database[providerID] = parsed
|
database[providerID] = parsed
|
||||||
}
|
}
|
||||||
|
for (const [providerID, provider] of configProviders) applyConfigProvider(providerID, provider)
|
||||||
|
|
||||||
// load env
|
// load env
|
||||||
const envs = yield* env.all()
|
const envs = yield* env.all()
|
||||||
@@ -1544,13 +1535,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, provider] of Object.entries(providers)) {
|
function finalizeProvider(providerID: ProviderV2.ID, provider: Info) {
|
||||||
const providerID = ProviderV2.ID.make(id)
|
|
||||||
if (!isProviderAllowed(providerID)) {
|
|
||||||
delete providers[providerID]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const configProvider = cfg.provider?.[providerID]
|
const configProvider = cfg.provider?.[providerID]
|
||||||
|
|
||||||
for (const [modelID, model] of Object.entries(provider.models)) {
|
for (const [modelID, model] of Object.entries(provider.models)) {
|
||||||
@@ -1586,14 +1571,26 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(provider.models).length === 0) {
|
const refreshProviders = new Map(
|
||||||
|
[...new Set(providerHooks.map((hook) => hook.providerID))].flatMap((providerID) => {
|
||||||
|
const provider = providers[providerID]
|
||||||
|
return provider ? [[providerID, { ...provider, models: {} }] as const] : []
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [id, provider] of Object.entries(providers)) {
|
||||||
|
const providerID = ProviderV2.ID.make(id)
|
||||||
|
if (!isProviderAllowed(providerID)) {
|
||||||
delete providers[providerID]
|
delete providers[providerID]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
finalizeProvider(providerID, provider)
|
||||||
|
if (Object.keys(provider.models).length === 0) delete providers[providerID]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
models: languages,
|
models: languages,
|
||||||
providers,
|
providers,
|
||||||
catalog,
|
catalog,
|
||||||
@@ -1601,6 +1598,41 @@ export const layer = Layer.effect(
|
|||||||
modelLoaders,
|
modelLoaders,
|
||||||
varsLoaders,
|
varsLoaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield* Effect.forEach(providerHooks, ({ providerID, models }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const provider = database[providerID]
|
||||||
|
if (!provider) return
|
||||||
|
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
||||||
|
const next = yield* Effect.promise(() => models(toPublicInfo(provider), { auth: pluginAuth }))
|
||||||
|
provider.models = Object.fromEntries(
|
||||||
|
Object.entries(next).map(([id, model]) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
...model,
|
||||||
|
id: ModelV2.ID.make(id),
|
||||||
|
providerID,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
const configProvider = configProviders.find(([id]) => id === providerID)
|
||||||
|
if (configProvider) applyConfigProvider(...configProvider)
|
||||||
|
const current = providers[providerID] ?? refreshProviders.get(providerID)
|
||||||
|
if (!current) return
|
||||||
|
const nextProvider = { ...current, models: database[providerID].models }
|
||||||
|
finalizeProvider(providerID, nextProvider)
|
||||||
|
if (Object.keys(nextProvider.models).length === 0) delete providers[providerID]
|
||||||
|
else providers[providerID] = nextProvider
|
||||||
|
for (const key of languages.keys()) {
|
||||||
|
if (key.startsWith(`${providerID}/`)) languages.delete(key)
|
||||||
|
}
|
||||||
|
yield* events.publish(Event.ModelsUpdated, { providerID })
|
||||||
|
}).pipe(
|
||||||
|
Effect.catchCause((cause) => Effect.logWarning("Failed to refresh provider models", { providerID, cause })),
|
||||||
|
),
|
||||||
|
).pipe(Effect.forkScoped)
|
||||||
|
|
||||||
|
return result
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1928,6 +1960,7 @@ export const defaultLayer = Layer.suspend(() =>
|
|||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(ModelsDev.defaultLayer),
|
Layer.provide(ModelsDev.defaultLayer),
|
||||||
Layer.provide(RuntimeFlags.defaultLayer),
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
|
Layer.provide(EventV2Bridge.defaultLayer),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1957,6 +1990,7 @@ export const node = LayerNode.make(layer, [
|
|||||||
Plugin.node,
|
Plugin.node,
|
||||||
ModelsDev.node,
|
ModelsDev.node,
|
||||||
RuntimeFlags.node,
|
RuntimeFlags.node,
|
||||||
|
EventV2Bridge.node,
|
||||||
])
|
])
|
||||||
|
|
||||||
export * as Provider from "./provider"
|
export * as Provider from "./provider"
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Provider } from "../../src/provider/provider"
|
import { Provider } from "../../src/provider/provider"
|
||||||
|
|
||||||
import { Effect } from "effect"
|
import { Deferred, Effect } from "effect"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { GlobalBus, type GlobalEvent } from "@/bus/global"
|
||||||
|
|
||||||
const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
|
const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
|
||||||
const it = testEffect(Provider.defaultLayer)
|
const it = testEffect(Provider.defaultLayer)
|
||||||
|
|
||||||
|
const refreshedModels = Effect.fn("DigitalOceanTest.refreshedModels")(function* () {
|
||||||
|
const provider = yield* Provider.Service
|
||||||
|
const refreshed = yield* Deferred.make<void>()
|
||||||
|
const listener = (event: GlobalEvent) => {
|
||||||
|
if (event.payload.type !== "provider.models.updated" || event.payload.properties.providerID !== DIGITALOCEAN) return
|
||||||
|
Deferred.doneUnsafe(refreshed, Effect.void)
|
||||||
|
}
|
||||||
|
GlobalBus.on("event", listener)
|
||||||
|
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", listener)))
|
||||||
|
yield* provider.list()
|
||||||
|
yield* Deferred.await(refreshed)
|
||||||
|
return (yield* provider.list())[DIGITALOCEAN].models
|
||||||
|
})
|
||||||
|
|
||||||
const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
|
const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
@@ -73,9 +88,7 @@ it.instance(
|
|||||||
oauth_expires: String(Date.now() + 60 * 60 * 1000),
|
oauth_expires: String(Date.now() + 60 * 60 * 1000),
|
||||||
},
|
},
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const provider = yield* Provider.Service
|
const models = yield* refreshedModels()
|
||||||
const providers = yield* provider.list()
|
|
||||||
const models = providers[DIGITALOCEAN].models
|
|
||||||
expect(models["router:my-router"]).toBeDefined()
|
expect(models["router:my-router"]).toBeDefined()
|
||||||
expect(models["router:my-router"].api.id).toBe("router:my-router")
|
expect(models["router:my-router"].api.id).toBe("router:my-router")
|
||||||
expect(models["router:my-router"].api.url).toBe("https://inference.do-ai.run/v1")
|
expect(models["router:my-router"].api.url).toBe("https://inference.do-ai.run/v1")
|
||||||
@@ -97,9 +110,7 @@ it.instance(
|
|||||||
oauth_expires: "1",
|
oauth_expires: "1",
|
||||||
},
|
},
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const provider = yield* Provider.Service
|
const models = yield* refreshedModels()
|
||||||
const providers = yield* provider.list()
|
|
||||||
const models = providers[DIGITALOCEAN].models
|
|
||||||
expect(models["router:stale-router"]).toBeDefined()
|
expect(models["router:stale-router"]).toBeDefined()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { InstanceLayer } from "@/project/instance-layer"
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
|
|
||||||
const originalEnv = new Map<string, string | undefined>()
|
const originalEnv = new Map<string, string | undefined>()
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(ModelsDev.defaultLayer),
|
Layer.provide(ModelsDev.defaultLayer),
|
||||||
Layer.provide(RuntimeFlags.layer(flags)),
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
|
Layer.provide(EventV2Bridge.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const list = Provider.use.list()
|
const list = Provider.use.list()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { TestInstance } from "../fixture/fixture"
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
|
|
||||||
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
|
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
|
||||||
|
|
||||||
@@ -270,6 +271,7 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
|
|||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(ModelsDev.defaultLayer),
|
Layer.provide(ModelsDev.defaultLayer),
|
||||||
Layer.provide(RuntimeFlags.defaultLayer),
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
|
Layer.provide(EventV2Bridge.defaultLayer),
|
||||||
)
|
)
|
||||||
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
|
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
|
||||||
const metadata = {
|
const metadata = {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export type Event =
|
|||||||
| EventTuiSessionSelect2
|
| EventTuiSessionSelect2
|
||||||
| EventMcpToolsChanged
|
| EventMcpToolsChanged
|
||||||
| EventMcpBrowserOpenFailed
|
| EventMcpBrowserOpenFailed
|
||||||
|
| EventProviderModelsUpdated
|
||||||
| EventCommandExecuted
|
| EventCommandExecuted
|
||||||
| EventProjectDirectoriesUpdated
|
| EventProjectDirectoriesUpdated
|
||||||
| EventProjectUpdated
|
| EventProjectUpdated
|
||||||
@@ -1495,6 +1496,13 @@ export type GlobalEvent = {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
id: string
|
||||||
|
type: "provider.models.updated"
|
||||||
|
properties: {
|
||||||
|
providerID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "command.executed"
|
type: "command.executed"
|
||||||
@@ -5138,6 +5146,14 @@ export type EventMcpBrowserOpenFailed = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventProviderModelsUpdated = {
|
||||||
|
id: string
|
||||||
|
type: "provider.models.updated"
|
||||||
|
properties: {
|
||||||
|
providerID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventCommandExecuted = {
|
export type EventCommandExecuted = {
|
||||||
id: string
|
id: string
|
||||||
type: "command.executed"
|
type: "command.executed"
|
||||||
|
|||||||
@@ -159,11 +159,26 @@ export const {
|
|||||||
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshProviders(workspace: string | undefined) {
|
||||||
|
const [providers, providerList] = await Promise.all([
|
||||||
|
sdk.client.config.providers({ workspace }, { throwOnError: true }).then((x) => x.data),
|
||||||
|
sdk.client.provider.list({ workspace }, { throwOnError: true }).then((x) => x.data),
|
||||||
|
])
|
||||||
|
batch(() => {
|
||||||
|
setStore("provider", reconcile(providers.providers))
|
||||||
|
setStore("provider_default", reconcile(providers.default))
|
||||||
|
setStore("provider_next", reconcile(providerList))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
event.subscribe((event, { workspace }) => {
|
event.subscribe((event, { workspace }) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "server.instance.disposed":
|
case "server.instance.disposed":
|
||||||
void bootstrap()
|
void bootstrap()
|
||||||
break
|
break
|
||||||
|
case "provider.models.updated":
|
||||||
|
void refreshProviders(workspace)
|
||||||
|
break
|
||||||
case "permission.replied": {
|
case "permission.replied": {
|
||||||
const requests = store.permission[event.properties.sessionID]
|
const requests = store.permission[event.properties.sessionID]
|
||||||
if (!requests) break
|
if (!requests) break
|
||||||
|
|||||||
Reference in New Issue
Block a user