Skip to content

Commit 0e162e9

Browse files
grenadeclaude
andcommitted
feat(provider): add built-in helexa provider with dynamic model discovery
helexa is a self-hosted, distributed mesh that is not in the public models.dev catalog and whose model lists, limits, and cost change frequently. Add a first-class built-in provider so users never hand-edit opencode.json model tables: - Seed a minimal helexa provider into the catalog so setting HELEXA_ENDPOINT (or a provider baseURL) alone activates it — no opencode.json provider stanza needed. Rides @ai-sdk/openai-compatible. - Add a custom() loader whose discoverModels() fetches helexa's live /v1/models at the resolved endpoint and maps per-model token limits and cost onto opencode models, so context %, compaction headroom, and spend work with zero per-model config. Mapping verified against a live cortex gateway's /v1/models. - Require an explicit endpoint (baseURL option or HELEXA_ENDPOINT); no hardcoded default until a canonical public router exists. - Generalize the model-discovery invocation, which was hardcoded to gitlab, to run for any provider that registers a discoverModels loader. Refs helexa#61. Builds on helexa#60 (error envelope), anomalyco#48 (usage), anomalyco#62 (catalog limit/cost). Co-Authored-By: Claude <[email protected]>
1 parent 5d0f866 commit 0e162e9

1 file changed

Lines changed: 105 additions & 6 deletions

File tree

‎packages/opencode/src/provider/provider.ts‎

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,83 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
713713
},
714714
}
715715
}),
716+
helexa: Effect.fnUntraced(function* (input: Info) {
717+
const auth = yield* dep.auth(input.id)
718+
const env = yield* dep.env()
719+
const apiKey =
720+
auth?.type === "api" ? auth.key : auth?.type === "oauth" ? auth.access : env["HELEXA_API_KEY"]
721+
// Endpoint resolution: the provider `baseURL` option (config) or the
722+
// HELEXA_ENDPOINT env var. There is no hardcoded default yet — no
723+
// canonical public router exists — so helexa activates only when an
724+
// endpoint is explicitly configured. A router default can be added here
725+
// once a canonical gateway URL exists (helexa#61).
726+
const baseURL = (input.options?.baseURL as string | undefined) ?? env["HELEXA_ENDPOINT"]
727+
return {
728+
// Activate only with an explicit endpoint; without one there is nowhere
729+
// to discover from. An API key alone is insufficient.
730+
autoload: !!baseURL,
731+
options: baseURL ? { baseURL, apiKey } : {},
732+
// Discover the live catalog from the resolved endpoint. helexa serves
733+
// per-model limits/cost (helexa#62); we map them onto opencode models so
734+
// context %, compaction headroom, and spend all work with zero config.
735+
async discoverModels(): Promise<Record<string, Model>> {
736+
if (!baseURL) return {}
737+
const modelsURL = new URL("models", baseURL.endsWith("/") ? baseURL : baseURL + "/")
738+
try {
739+
const res = await fetch(modelsURL, {
740+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
741+
})
742+
if (!res.ok) return {}
743+
const body = (await res.json()) as any
744+
const list: any[] = Array.isArray(body) ? body : (body?.data ?? body?.models ?? [])
745+
const models: Record<string, Model> = {}
746+
for (const m of list) {
747+
const id: string | undefined = m?.id
748+
if (!id) continue
749+
const caps: string[] = Array.isArray(m?.capabilities) ? m.capabilities : []
750+
const vision = caps.includes("vision")
751+
models[ModelV2.ID.make(id)] = {
752+
id: ModelV2.ID.make(id),
753+
providerID: ProviderV2.ID.make("helexa"),
754+
name: typeof m?.name === "string" ? m.name : id,
755+
family: "",
756+
api: { id, url: baseURL, npm: "@ai-sdk/openai-compatible" },
757+
status: "active",
758+
headers: {},
759+
options: {},
760+
cost: {
761+
input: m?.cost?.input ?? 0,
762+
output: m?.cost?.output ?? 0,
763+
cache: {
764+
read: m?.cost?.cache_read ?? 0,
765+
write: m?.cost?.cache_write ?? 0,
766+
},
767+
},
768+
limit: {
769+
context: m?.limit?.context ?? 0,
770+
input: m?.limit?.input,
771+
output: m?.limit?.output ?? 0,
772+
},
773+
capabilities: {
774+
temperature: true,
775+
reasoning: !!m?.reasoning,
776+
attachment: vision,
777+
toolcall: m?.tool_call ?? true,
778+
input: { text: true, audio: false, image: vision, video: false, pdf: false },
779+
output: { text: true, audio: false, image: false, video: false, pdf: false },
780+
interleaved: false,
781+
},
782+
release_date: "",
783+
variants: {},
784+
}
785+
}
786+
return models
787+
} catch (e) {
788+
return {}
789+
}
790+
},
791+
}
792+
}),
716793
"cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
717794
// When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
718795
// skip the account ID check because the URL is already fully specified.
@@ -1297,7 +1374,25 @@ export const layer = Layer.effect(
12971374
Effect.gen(function* () {
12981375
const bridge = yield* EffectBridge.make()
12991376
const cfg = yield* config.get()
1300-
const modelsDev = yield* modelsDevSvc.get()
1377+
// Built-in helexa provider seed. helexa is a self-hosted, distributed
1378+
// mesh and is not published to the public models.dev catalog, so seed a
1379+
// minimal provider entry here so that setting HELEXA_ENDPOINT (or a
1380+
// provider baseURL) alone activates it — no opencode.json provider stanza
1381+
// needed. The endpoint is resolved at load time by the custom() loader,
1382+
// which also discovers the model list (per-model limits/cost). `api` is
1383+
// left empty: there is no canonical default endpoint yet (helexa#61).
1384+
// A real models.dev entry, if one is ever added, takes precedence.
1385+
const modelsDev: Record<string, ModelsDev.Provider> = {
1386+
helexa: {
1387+
id: "helexa",
1388+
name: "Helexa",
1389+
env: ["HELEXA_API_KEY"],
1390+
npm: "@ai-sdk/openai-compatible",
1391+
api: "",
1392+
models: {},
1393+
},
1394+
...(yield* modelsDevSvc.get()),
1395+
}
13011396
const catalog = mapValues(modelsDev, fromModelsDevProvider)
13021397
const database = mapValues(catalog, toPublicInfo)
13031398

@@ -1543,14 +1638,18 @@ export const layer = Layer.effect(
15431638
mergeProvider(providerID, partial)
15441639
}
15451640

1546-
const gitlab = ProviderV2.ID.make("gitlab")
1547-
if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
1641+
// Run model discovery for any provider that registered a discoverModels
1642+
// loader (gitlab, helexa, ...). Discovered models are additive — they
1643+
// never clobber models already present from models.dev or config.
1644+
for (const id of Object.keys(discoveryLoaders)) {
1645+
const providerID = ProviderV2.ID.make(id)
1646+
if (!providers[providerID] || !isProviderAllowed(providerID)) continue
15481647
yield* Effect.promise(async () => {
15491648
try {
1550-
const discovered = await discoveryLoaders[gitlab]()
1649+
const discovered = await discoveryLoaders[providerID]()
15511650
for (const [modelID, model] of Object.entries(discovered)) {
1552-
if (!providers[gitlab].models[modelID]) {
1553-
providers[gitlab].models[modelID] = model
1651+
if (!providers[providerID].models[modelID]) {
1652+
providers[providerID].models[modelID] = model
15541653
}
15551654
}
15561655
} catch (e) {}

0 commit comments

Comments
 (0)