mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
resolveOmniRoutePluginOptions() auto-prefixes providerId with "opencode-"
(commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate accepts it as a
registered provider id. That prefixed value was being reused for the
OmniRoute-server-facing identifiers too: mapRawModelToModelV2's id/providerID,
mapComboToModelV2's providerID, and the dynamic provider hook's combo catalog
keys. OmniRoute's server has no "opencode-<x>" provider alias, so every
dispatched model failed credential lookup with "No credentials for
opencode-omniroute" / "No active credentials for provider:
opencode-omniroute".
Add a second, unprefixed omnirouteProviderId field and thread it through the
four dynamic-hook call sites that emit server-facing identifiers, while
leaving the OC-gate-prefixed providerId in place for AuthHook.provider,
provider registration (hook.id), and the static-catalog path (which OC
strips before dispatch, per the existing static-block comments).
This commit is contained in:
committed by
GitHub
parent
ec553dd9c0
commit
9c1db94c74
@@ -23,7 +23,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -238,9 +238,26 @@ function trimLeadingDashes(value: string): string {
|
||||
*/
|
||||
export function resolveOmniRoutePluginOptions(
|
||||
opts?: OmniRoutePluginOptions
|
||||
): Required<Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">> &
|
||||
Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
|
||||
): Required<
|
||||
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">
|
||||
> & {
|
||||
/**
|
||||
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
|
||||
* `providerId` above is auto-prefixed with "opencode-" ONLY to satisfy OC
|
||||
* 1.17.8+'s native-adapter gate ({openai, anthropic, opencode*}) — that
|
||||
* prefixed value is OC-internal and must be used ONLY for AuthHook.provider
|
||||
* and provider-registration keys (the OC config-hook top-level
|
||||
* `provider.<id>` block). `omnirouteProviderId` MUST be used everywhere an
|
||||
* identifier reaches or represents something OmniRoute's own server parses
|
||||
* (model `id` prefix, `ModelV2.providerID`, combo catalog keys in the
|
||||
* dynamic provider hook) — OmniRoute's `parseModel()` has no alias for
|
||||
* "opencode-<x>", so a prefixed id there is unrecoverable and credential
|
||||
* lookup fails with "No credentials for opencode-<x>".
|
||||
*/
|
||||
omnirouteProviderId: string;
|
||||
} & Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
|
||||
const rawProviderId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
|
||||
const omnirouteProviderId = trimLeadingOpencodePrefix(rawProviderId);
|
||||
// OC 1.17.8+ native-adapter gate rejects providerID not in
|
||||
// {openai, anthropic, opencode*}. Silently prefix so existing
|
||||
// configs (providerId: "omniroute") keep working.
|
||||
@@ -258,6 +275,7 @@ export function resolveOmniRoutePluginOptions(
|
||||
: DEFAULT_MODEL_CACHE_TTL_MS;
|
||||
return {
|
||||
providerId,
|
||||
omnirouteProviderId,
|
||||
displayName,
|
||||
modelCacheTtl,
|
||||
baseURL: opts?.baseURL,
|
||||
@@ -265,6 +283,18 @@ export function resolveOmniRoutePluginOptions(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a leading "opencode-" prefix (added only for the OC native-adapter
|
||||
* gate — see `resolveOmniRoutePluginOptions`) so the returned id is safe to
|
||||
* embed in anything OmniRoute's own server parses. A user-supplied
|
||||
* `providerId: "opencode-omniroute"` (already prefixed) resolves to the same
|
||||
* unprefixed "omniroute" as the default, matching `providerId`'s own
|
||||
* idempotent-prefix handling above.
|
||||
*/
|
||||
function trimLeadingOpencodePrefix(rawProviderId: string): string {
|
||||
return rawProviderId.startsWith("opencode-") ? rawProviderId.slice("opencode-".length) : rawProviderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict parse of raw plugin options (as received from opencode.json or a
|
||||
* direct factory call) into the validated `OmniRoutePluginOptions` shape.
|
||||
@@ -2661,7 +2691,8 @@ export function createOmniRouteProviderHook(
|
||||
if (canonicalDedup.has(entry.id)) continue;
|
||||
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
|
||||
const model = mapRawModelToModelV2(entry, {
|
||||
providerId: resolved.providerId,
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
providerId: resolved.omnirouteProviderId,
|
||||
baseURL,
|
||||
apiFormat: resolved.features?.apiFormat,
|
||||
});
|
||||
@@ -2826,7 +2857,8 @@ export function createOmniRouteProviderHook(
|
||||
const mapped = mapComboToModelV2(
|
||||
combo,
|
||||
memberEntries,
|
||||
resolved.providerId,
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
resolved.omnirouteProviderId,
|
||||
baseURL,
|
||||
features.apiFormat
|
||||
);
|
||||
@@ -2845,7 +2877,8 @@ export function createOmniRouteProviderHook(
|
||||
}
|
||||
}
|
||||
|
||||
const comboKey = buildComboKey(combo, usedComboKeys, resolved.providerId);
|
||||
// #6859: server-facing key — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
const comboKey = buildComboKey(combo, usedComboKeys, resolved.omnirouteProviderId);
|
||||
|
||||
// Collision policy: combos win. Warn ONCE per (cacheKey, comboKey)
|
||||
// when overwriting a same-key raw model so the operator can spot
|
||||
@@ -2947,7 +2980,8 @@ export function createOmniRouteProviderHook(
|
||||
},
|
||||
status: "active",
|
||||
release_date: "",
|
||||
providerID: resolved.providerId,
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
providerID: resolved.omnirouteProviderId,
|
||||
options: {},
|
||||
headers: {},
|
||||
};
|
||||
|
||||
@@ -447,14 +447,14 @@ test("models() returns combo entries merged into the map", async () => {
|
||||
|
||||
// 3 raw models + 1 combo = 4 entries
|
||||
assert.equal(Object.keys(out).length, 4);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
||||
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
|
||||
assert.ok(out["opencode-omniroute/claude-tier"]);
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
assert.ok(out["omniroute/claude-secondary"]);
|
||||
assert.ok(out["omniroute/gemini-3-flash"]);
|
||||
assert.ok(out["omniroute/claude-tier"]);
|
||||
|
||||
const combo = out["opencode-omniroute/claude-tier"];
|
||||
const combo = out["omniroute/claude-tier"];
|
||||
assert.equal(combo.name, "Claude Tier");
|
||||
assert.equal(combo.providerID, "opencode-omniroute");
|
||||
assert.equal(combo.providerID, "omniroute");
|
||||
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
|
||||
assert.equal(combo.limit.context, 100_000);
|
||||
assert.equal(combo.capabilities.reasoning, false);
|
||||
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["opencode-omniroute/phantom-combo"]);
|
||||
assert.ok(out["omniroute/phantom-combo"]);
|
||||
// With zero resolvable members, LCD = all-false (defensive posture).
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
|
||||
assert.equal(out["omniroute/phantom-combo"].capabilities.toolcall, false);
|
||||
assert.equal(out["omniroute/phantom-combo"].capabilities.reasoning, false);
|
||||
assert.equal(out["omniroute/phantom-combo"].limit.context, 0);
|
||||
});
|
||||
|
||||
test("models(): hidden combos are excluded from the map", async () => {
|
||||
@@ -505,8 +505,8 @@ test("models(): hidden combos are excluded from the map", async () => {
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["opencode-omniroute/visible"]);
|
||||
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
|
||||
assert.ok(out["omniroute/visible"]);
|
||||
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
|
||||
});
|
||||
|
||||
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
|
||||
@@ -530,8 +530,8 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
|
||||
});
|
||||
|
||||
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
|
||||
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
|
||||
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
|
||||
assert.ok(out["omniroute/claude-primary"], "combo surfaces under prefixed key");
|
||||
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
|
||||
|
||||
// No collision warning fires — dedup makes keys disjoint.
|
||||
const collisionWarns = warnings.filter((w) => {
|
||||
@@ -565,8 +565,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
|
||||
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
// First combo gets the bare slug; second gets disambiguated.
|
||||
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
|
||||
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
|
||||
assert.ok(out["omniroute/claude"], "first combo at prefixed slug");
|
||||
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
|
||||
});
|
||||
|
||||
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
|
||||
@@ -583,8 +583,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
|
||||
|
||||
// Catalog includes the models but NOT any combo entries.
|
||||
assert.equal(Object.keys(out).length, 2);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
assert.ok(out["omniroute/claude-secondary"]);
|
||||
|
||||
// Soft-fail warning surfaced.
|
||||
const softFail = warnings.find((w) => {
|
||||
@@ -609,7 +609,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
|
||||
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
|
||||
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
|
||||
assert.ok(second["opencode-omniroute/claude-tier"]);
|
||||
assert.ok(second["omniroute/claude-tier"]);
|
||||
});
|
||||
|
||||
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
|
||||
@@ -701,7 +701,7 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
const masterLight = out["opencode-omniroute/master-light"];
|
||||
const masterLight = out["omniroute/master-light"];
|
||||
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
|
||||
assert.equal(
|
||||
masterLight.limit.context,
|
||||
|
||||
@@ -376,7 +376,8 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 1, "enrichment fetcher called once");
|
||||
const m = out["opencode-omniroute/claude-sonnet-4-6"];
|
||||
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId.
|
||||
const m = out["omniroute/claude-sonnet-4-6"];
|
||||
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
|
||||
assert.equal(m.cost.input, 3, "enrichment pricing applied");
|
||||
assert.equal(m.cost.output, 15);
|
||||
@@ -402,7 +403,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
|
||||
assert.equal(
|
||||
out["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
out["omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id preserved"
|
||||
);
|
||||
@@ -463,7 +464,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 1, "compression metadata fetcher called");
|
||||
const combo = out["opencode-omniroute/claude-primary"];
|
||||
const combo = out["omniroute/claude-primary"];
|
||||
assert.ok(combo, "combo entry present");
|
||||
assert.match(
|
||||
combo.name,
|
||||
|
||||
99
@omniroute/opencode-plugin/tests/provider-id-routing.test.ts
Normal file
99
@omniroute/opencode-plugin/tests/provider-id-routing.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Regression test for #6859.
|
||||
*
|
||||
* `resolveOmniRoutePluginOptions()` auto-prefixes `providerId` with
|
||||
* `"opencode-"` (commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate
|
||||
* accepts it as an OC-registered provider id. That prefixed value must stay
|
||||
* OC-internal (AuthHook.provider / provider registration keys) — it must
|
||||
* NEVER leak into the identifiers OmniRoute's own server parses to resolve
|
||||
* credentials (`mapRawModelToModelV2`'s `id`/`providerID`,
|
||||
* `mapComboToModelV2`'s `providerID`, and the dynamic-hook catalog keys).
|
||||
*
|
||||
* OmniRoute's server-side `parseModel()` (open-sse/services/model.ts) splits
|
||||
* a dispatched model string on `/` to recover the provider name and look up
|
||||
* credentials. If the plugin embeds the OC-gate-prefixed id in that string,
|
||||
* the server looks up credentials for a provider named "opencode-omniroute"
|
||||
* (which never exists in `src/shared/constants/providers.ts`) instead of
|
||||
* "omniroute" — producing the exact "No credentials for opencode-omniroute" /
|
||||
* "No active credentials for provider: opencode-omniroute" errors reported
|
||||
* in #6859.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createOmniRouteProviderHook,
|
||||
mapRawModelToModelV2,
|
||||
resolveOmniRoutePluginOptions,
|
||||
} from "../src/index.js";
|
||||
|
||||
/**
|
||||
* Minimal stand-in for OmniRoute's own `parseModel()` (open-sse/services/
|
||||
* model.ts), which splits a dispatched `<providerID>/<modelID>` string on the
|
||||
* FIRST "/" to recover the provider name used for credential lookup. Kept
|
||||
* local (rather than cross-importing the real module) so this package's
|
||||
* self-contained test suite (`cd @omniroute/opencode-plugin && npm test`)
|
||||
* doesn't depend on the root repo's `@/*` path-alias resolution.
|
||||
*/
|
||||
function splitProviderFromDispatchedModel(modelStr: string): string {
|
||||
const idx = modelStr.indexOf("/");
|
||||
return idx === -1 ? modelStr : modelStr.slice(0, idx);
|
||||
}
|
||||
|
||||
const apiAuth = (key: string) => ({ type: "api" as const, key });
|
||||
|
||||
test("#6859: server-facing model id/providerID must resolve to the unprefixed provider name", () => {
|
||||
const resolved = resolveOmniRoutePluginOptions();
|
||||
|
||||
// The OC-gate-compatible id stays prefixed — it is legitimate for
|
||||
// AuthHook.provider / provider registration.
|
||||
assert.equal(resolved.providerId, "opencode-omniroute");
|
||||
|
||||
// A second, unprefixed id must be exposed for anything that reaches
|
||||
// OmniRoute's own server (model id prefix, ModelV2.providerID, combo keys).
|
||||
assert.equal(
|
||||
resolved.omnirouteProviderId,
|
||||
"omniroute",
|
||||
"resolveOmniRoutePluginOptions() must expose an unprefixed omnirouteProviderId"
|
||||
);
|
||||
|
||||
// A bare raw /v1/models entry (no existing "/" in its id — the common
|
||||
// case for OmniRoute's catalog) mapped with the server-facing id.
|
||||
const model = mapRawModelToModelV2(
|
||||
{ id: "claude-opus-4-7" },
|
||||
{ providerId: resolved.omnirouteProviderId, baseURL: "http://localhost:20128" }
|
||||
);
|
||||
|
||||
assert.equal(model.providerID, "omniroute");
|
||||
assert.equal(model.id, "omniroute/claude-opus-4-7");
|
||||
|
||||
// OpenCode dispatches back to OmniRoute using `providerID/modelKey`
|
||||
// (matches the issue's own repro: `-m opencode-omniroute/oc/big-pickle`).
|
||||
const dispatchedModelString = `${model.providerID}/claude-opus-4-7`;
|
||||
const parsedProvider = splitProviderFromDispatchedModel(dispatchedModelString);
|
||||
|
||||
assert.equal(
|
||||
parsedProvider,
|
||||
"omniroute",
|
||||
`server-side provider split resolved '${parsedProvider}', expected 'omniroute' — ` +
|
||||
`credentials lookup would fail for an OC-gate-prefixed provider id`
|
||||
);
|
||||
});
|
||||
|
||||
test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID never carry the OC-gate prefix", async () => {
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{
|
||||
fetcher: async () => [{ id: "claude-opus-4-7" }],
|
||||
combosFetcher: async () => [],
|
||||
}
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-test") as never });
|
||||
const model = out["omniroute/claude-opus-4-7"];
|
||||
assert.ok(model, "catalog keyed under the unprefixed provider name");
|
||||
assert.equal(model.providerID, "omniroute");
|
||||
assert.ok(
|
||||
!model.providerID.startsWith("opencode-"),
|
||||
"the OC-gate prefix must never leak into ModelV2.providerID"
|
||||
);
|
||||
});
|
||||
@@ -101,7 +101,10 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
|
||||
assert.equal(fetcher.callCount(), 1);
|
||||
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
|
||||
assert.equal(Object.keys(out).length, 3);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
|
||||
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
|
||||
// that prefix must never leak into anything OmniRoute's server parses.
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
});
|
||||
|
||||
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
|
||||
@@ -152,13 +155,17 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
|
||||
const claude = out["opencode-omniroute/claude-primary"];
|
||||
// #6859: dynamic-hook catalog keys/ids/providerID use the unprefixed
|
||||
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
|
||||
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
|
||||
// anything OmniRoute's own server parses for credential lookup.
|
||||
const claude = out["omniroute/claude-primary"];
|
||||
assert.ok(claude, "claude-primary present");
|
||||
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
|
||||
// static-catalog reader resolves `(providerID, modelID)` from the key.
|
||||
assert.equal(claude.id, "opencode-omniroute/claude-primary");
|
||||
assert.equal(claude.id, "omniroute/claude-primary");
|
||||
assert.equal(claude.name, "claude-primary");
|
||||
assert.equal(claude.providerID, "opencode-omniroute");
|
||||
assert.equal(claude.providerID, "omniroute");
|
||||
assert.equal(claude.api.id, "openai-compatible");
|
||||
assert.equal(claude.api.url, "https://or.example.com/v1");
|
||||
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");
|
||||
|
||||
1
changelog.d/fixes/6859-plugin-provider-id.md
Normal file
1
changelog.d/fixes/6859-plugin-provider-id.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-<x>` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859).
|
||||
Reference in New Issue
Block a user