Compare commits

..

2 Commits

Author SHA1 Message Date
opencode-agent[bot] 2017378db0 Fix beta integration 2026-08-16 05:16:58 +00:00
Dax Raad a48d44955e fix(cli): suppress compiled runtime warnings 2026-08-16 01:03:25 -04:00
5 changed files with 13 additions and 333 deletions
+1 -1
View File
@@ -41,8 +41,8 @@
"solid-js": "catalog:",
"tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10",
"web-tree-sitter": "0.25.10",
"uqr": "0.1.3",
"web-tree-sitter": "0.25.10",
"ws": "8.21.0"
},
"devDependencies": {
+12 -11
View File
@@ -102,6 +102,17 @@ for (const item of targets) {
const name = target.replace(binary, "cli")
const executablePath = await compileExecutable(item)
console.log(`building ${name}`)
const compileTarget = {
autoloadBunfig: false,
autoloadDotenv: false,
autoloadTsconfig: true,
autoloadPackageJson: true,
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
...(executablePath ? { executablePath } : {}),
outfile: path.join(outdir, name, "bin", binary),
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--no-warnings", "--"],
windows: {},
}
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
@@ -111,17 +122,7 @@ for (const item of targets) {
minify: true,
sourcemap: "inline",
splitting: true,
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
autoloadTsconfig: true,
autoloadPackageJson: true,
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
executablePath,
outfile: path.join(outdir, name, "bin", binary),
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
compile: compileTarget,
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_CLI_NAME: `'${binary}'`,
-2
View File
@@ -16,7 +16,6 @@ import { GroqPlugin } from "./provider/groq.js"
import { KiloPlugin } from "./provider/kilo.js"
import { LLMGatewayPlugin } from "./provider/llmgateway.js"
import { MistralPlugin } from "./provider/mistral.js"
import { ModalPlugin } from "./provider/modal.js"
import { NvidiaPlugin } from "./provider/nvidia.js"
import { OpenAIPlugin } from "./provider/openai.js"
import { SnowflakeCortexPlugin } from "./provider/snowflake-cortex.js"
@@ -50,7 +49,6 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
KiloPlugin,
LLMGatewayPlugin,
MistralPlugin,
ModalPlugin,
NvidiaPlugin,
OpencodePlugin,
SnowflakeCortexPlugin,
-176
View File
@@ -1,176 +0,0 @@
import { Money } from "@opencode-ai/schema/money"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Schema, Semaphore, Stream } from "effect"
import { Bus } from "../../bus.js"
import { Catalog } from "../../catalog.js"
import { Integration } from "../../integration.js"
import { Model } from "../../model.js"
import { Provider } from "../../provider.js"
const providerID = Provider.ID.make("modal")
const ReasoningOption = Schema.Struct({
type: Schema.Literal("effort"),
values: Schema.Array(Schema.NullOr(Schema.String)),
})
const Response = Schema.Struct({
data: Schema.Array(
Schema.Struct({
id: Schema.String,
base_model_id: Schema.optional(Schema.String),
hugging_face_id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
input_modalities: Schema.optional(Schema.Array(Schema.String)),
output_modalities: Schema.optional(Schema.Array(Schema.String)),
context_length: Schema.optional(Schema.Number),
max_output_length: Schema.optional(Schema.Number),
pricing: Schema.optional(
Schema.Struct({
prompt: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
completion: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
input_cache_read: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
}),
),
supported_features: Schema.optional(Schema.Array(Schema.String)),
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
interleaved: Schema.optional(
Schema.Union([
Schema.Boolean,
Schema.Struct({ field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]) }),
]),
),
}),
),
})
const decode = Schema.decodeUnknownSync(Response)
export const ModalPlugin = define({
id: "opencode.provider.modal",
effect: Effect.fn(function* (ctx) {
const bus = yield* Bus.Service
const catalog = yield* Catalog.Service
const loading = Semaphore.makeUnsafe(1)
let templates: Map<Model.ID, Model.Info> | undefined
let models: Map<Model.ID, Model.Info> | undefined
const load = Effect.fn("ModalPlugin.load")(function* () {
const existing =
templates ??
new Map(
(yield* catalog.model.all())
.filter((model) => model.providerID === providerID)
.map((model) => [model.id, model]),
)
templates = existing
const connection = yield* ctx.integration.connection.active("modal")
const credential = connection
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
: undefined
const provider = yield* catalog.provider.get(providerID)
const baseURL = typeof provider?.settings?.baseURL === "string" ? provider.settings.baseURL : undefined
if (credential?.type !== "key" || !baseURL) {
models = new Map()
return
}
models = yield* Effect.tryPromise({
try: () => discover(baseURL, credential.key, existing),
catch: (cause) => cause,
}).pipe(
Effect.catch((cause) =>
Effect.logWarning("failed to sync Modal models", { cause }).pipe(Effect.as(new Map<Model.ID, Model.Info>())),
),
)
})
yield* ctx.catalog.transform((draft) => {
if (!models) return
const provider = draft.provider.get(providerID)
if (!provider) return
for (const id of provider.models.keys()) {
if (!models.has(Model.ID.make(id))) draft.model.remove(providerID, Model.ID.make(id))
}
for (const [id, model] of models) {
draft.model.update(providerID, id, (item) => Object.assign(item, structuredClone(model)))
}
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("modal")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
)
yield* refresh().pipe(Effect.forkScoped)
}),
})
async function discover(baseURL: string, apiKey: string, templates: ReadonlyMap<Model.ID, Model.Info>) {
const response = await fetch(`${baseURL.replace(/\/+$/, "")}/models`, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(3_000),
})
if (!response.ok) throw new Error(`Failed to fetch Modal models: ${response.status}`)
return new Map(
decode(await response.json()).data.map((item) => {
const id = Model.ID.make(item.id)
const template = templates.get(Model.ID.make(item.base_model_id ?? item.hugging_face_id ?? item.id))
return [id, build(id, item, baseURL, template)]
}),
)
}
function build(id: Model.ID, item: (typeof Response.Type)["data"][number], baseURL: string, template?: Model.Info) {
const fallback: Model.Info = template ?? Model.Info.make(Model.Info.default(providerID, id))
const baseCost = fallback.cost[0]
const variants = item.reasoning_options?.flatMap((option) =>
option.values.map((value) => {
const effort = value ?? "none"
return {
id: Model.VariantID.make(effort),
settings: { reasoningEffort: effort },
}
}),
)
return Model.Info.make({
...structuredClone(fallback),
id,
modelID: id,
providerID,
name: item.name ?? fallback.name,
compatibility: Model.compatibility(item.interleaved) ?? fallback.compatibility,
package: fallback.package ?? Provider.aisdk("@ai-sdk/openai-compatible"),
settings: Provider.mergeOverlay(fallback.settings, { baseURL }),
capabilities: {
tools: item.supported_features?.includes("tools") ?? fallback.capabilities.tools,
input: item.input_modalities ? [...item.input_modalities] : [...fallback.capabilities.input],
output: item.output_modalities ? [...item.output_modalities] : [...fallback.capabilities.output],
},
variants: variants ?? [...fallback.variants],
cost: [
{
input: price(item.pricing?.prompt, baseCost?.input ?? Money.USDPerMillionTokens.zero),
output: price(item.pricing?.completion, baseCost?.output ?? Money.USDPerMillionTokens.zero),
cache: {
read: price(item.pricing?.input_cache_read, baseCost?.cache.read ?? Money.USDPerMillionTokens.zero),
write: baseCost?.cache.write ?? Money.USDPerMillionTokens.zero,
},
},
],
limit: {
context: item.context_length ?? fallback.limit.context,
input: fallback.limit.input,
output: item.max_output_length ?? fallback.limit.output,
},
status: fallback.status,
enabled: fallback.enabled,
})
}
function price(value: string | number | undefined, fallback: number) {
if (value === undefined) return Money.USDPerMillionTokens.make(fallback)
const parsed = Number(value)
return Money.USDPerMillionTokens.make(Number.isFinite(parsed) ? parsed * 1_000_000 : fallback)
}
@@ -1,143 +0,0 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Model } from "@opencode-ai/core/model"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { ModalPlugin } from "@opencode-ai/core/plugin/provider/modal"
import { Provider } from "@opencode-ai/core/provider"
import { State } from "@opencode-ai/core/state"
import { Money } from "@opencode-ai/schema/money"
import { expect } from "bun:test"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const providerID = Provider.ID.make("modal")
const integrationID = Integration.ID.make("modal")
const baseModelID = Model.ID.make("thinkingmachines/Inkling-NVFP4")
const runtimeModelID = Model.ID.make("workspace--inkling.us-west.modal.direct")
function eventually<A>(
effect: Effect.Effect<A>,
predicate: (value: A) => boolean,
remaining = 1000,
): Effect.Effect<A, Error> {
return Effect.gen(function* () {
const value = yield* effect
if (predicate(value)) return value
if (remaining === 0) return yield* Effect.fail(new Error("Timed out waiting for value"))
yield* Effect.promise(() => Bun.sleep(1))
return yield* eventually(effect, predicate, remaining - 1)
})
}
const setup = Effect.fn(function* (baseURL: string, key?: string) {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
yield* integrations.transform((draft) => {
draft.method.update({ integrationID, method: { type: "key" } })
})
if (key) yield* integrations.connection.key({ integrationID, key })
yield* State.batch(
Effect.gen(function* () {
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
provider.name = "Modal"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL }
provider.integrationID = integrationID
})
draft.model.update(providerID, baseModelID, (model) => {
model.name = "Inkling"
model.family = Model.Family.make("ling")
model.compatibility = { reasoningField: "reasoning_content" }
model.capabilities = { tools: true, input: ["text", "image", "audio"], output: ["text"] }
model.variants = [{ id: Model.VariantID.make("fallback"), settings: { reasoningEffort: "fallback" } }]
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(4),
cache: {
read: Money.USDPerMillionTokens.make(0.2),
write: Money.USDPerMillionTokens.zero,
},
},
]
model.limit = { context: 128_000, output: 8_192 }
model.time = { released: Date.parse("2026-07-15") }
})
})
yield* ModalPlugin.effect(yield* PluginHost.make(yield* Plugin.Service))
}),
)
})
it.live("discovers Modal workspace models", () =>
Effect.gen(function* () {
const requests: Array<{ authorization: string | null; path: string }> = []
using server = Bun.serve({
port: 0,
fetch(request) {
requests.push({ authorization: request.headers.get("authorization"), path: new URL(request.url).pathname })
return Response.json({
data: [
{
id: runtimeModelID,
base_model_id: baseModelID,
name: "Thinking Machines: Inkling",
input_modalities: ["text", "image", "audio"],
output_modalities: ["text"],
context_length: 1_048_576,
max_output_length: 262_144,
pricing: { prompt: "0.0000012", completion: "0.000005", input_cache_read: "0.00000027" },
supported_features: ["tools", "reasoning"],
reasoning_options: [{ type: "effort", values: ["none", "low", "high"] }],
interleaved: { field: "reasoning_content" },
},
],
})
},
})
yield* setup(`${server.url}v1`, "test-token")
const models = yield* eventually(
(yield* Catalog.Service).model
.all()
.pipe(Effect.map((models) => models.filter((model) => model.providerID === providerID))),
(models) => models.some((model) => model.id === runtimeModelID),
)
expect(requests).toEqual([{ authorization: "Bearer test-token", path: "/v1/models" }])
expect(models).toHaveLength(1)
expect(models[0]).toMatchObject({
id: runtimeModelID,
modelID: runtimeModelID,
name: "Thinking Machines: Inkling",
family: "ling",
compatibility: { reasoningField: "reasoning_content" },
settings: { baseURL: `${server.url}v1` },
capabilities: { tools: true, input: ["text", "image", "audio"], output: ["text"] },
variants: [
{ id: "none", settings: { reasoningEffort: "none" } },
{ id: "low", settings: { reasoningEffort: "low" } },
{ id: "high", settings: { reasoningEffort: "high" } },
],
cost: [{ input: 1.2, output: 5, cache: { read: 0.27, write: 0 } }],
limit: { context: 1_048_576, output: 262_144 },
})
}),
)
it.live("hides static Modal models when discovery fails", () =>
Effect.gen(function* () {
using server = Bun.serve({ port: 0, fetch: () => new Response(null, { status: 503 }) })
yield* setup(`${server.url}v1`, "test-token")
const models = yield* eventually(
(yield* Catalog.Service).model
.all()
.pipe(Effect.map((models) => models.filter((model) => model.providerID === providerID))),
(models) => models.length === 0,
)
expect(models).toEqual([])
}),
)