Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions @omniroute/opencode-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,18 @@ export function resolveOmniRoutePluginOptions(
opts?: OmniRoutePluginOptions
): Required<Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">> &
Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
const providerId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
const rawProviderId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
// OC 1.17.8+ native-adapter gate rejects providerID not in
// {openai, anthropic, opencode*}. Silently prefix so existing
// configs (providerId: "omniroute") keep working.
const providerId = rawProviderId.startsWith("opencode-")
? rawProviderId
: `opencode-${rawProviderId}`;
const displayName =
opts?.displayName ??
(providerId === OMNIROUTE_PROVIDER_KEY ? "OmniRoute" : `OmniRoute (${providerId})`);
(providerId === `opencode-${OMNIROUTE_PROVIDER_KEY}`
? "OmniRoute"
: `OmniRoute (${providerId})`);
const modelCacheTtl =
typeof opts?.modelCacheTtl === "number" && opts.modelCacheTtl > 0
? opts.modelCacheTtl
Expand Down Expand Up @@ -4398,8 +4406,24 @@ export function createOmniRouteConfigHook(
authJson = undefined;
}

const entry = authJson?.[resolved.providerId] as AuthJsonApiEntry | undefined;
const apiKey = entry && entry.type === "api" && typeof entry.key === "string" ? entry.key : "";
// Try both prefixed (e.g. opencode-omniroute) and unprefixed (e.g. omniroute)
// keys so a user who ran `/connect omniroute` before the auto-prefix fix
// does not need to re-auth. Also handles dual-key for auth.json entries
// written by a newer OC dispatcher with the prefixed key.
const bareKey = resolved.providerId.startsWith("opencode-")
? resolved.providerId.slice("opencode-".length)
: resolved.providerId;
const lookupKeys = [resolved.providerId];
if (bareKey !== resolved.providerId) lookupKeys.push(bareKey);
let entry;
for (const k of lookupKeys) {
const e = authJson?.[k];
if (e?.type === "api" && typeof e.key === "string" && e.key.length > 0) {
entry = e;
break;
}
}
Comment on lines +4418 to +4425

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Declaring let entry; without an explicit type can trigger a TypeScript compilation error under strict compiler settings (such as noImplicitAny). We can make this more declarative, robust, and fully type-safe by using Array.prototype.find with an explicit type cast.

    const entry = lookupKeys
      .map((k) => authJson?.[k] as AuthJsonApiEntry | undefined)
      .find((e) => e?.type === "api" && typeof e.key === "string" && e.key.length > 0);

const apiKey = entry?.type === "api" && typeof entry.key === "string" ? entry.key : "";

if (!apiKey) {
// (c) no apiKey — silent no-op (with debug breadcrumb). The operator
Expand Down
6 changes: 3 additions & 3 deletions @omniroute/opencode-plugin/tests/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import { createOmniRouteAuthHook } from "../src/index.js";

test("createOmniRouteAuthHook: default providerId is 'omniroute'", () => {
const hook = createOmniRouteAuthHook();
assert.equal(hook.provider, "omniroute");
assert.equal(hook.provider, "opencode-omniroute");
});

test("createOmniRouteAuthHook: custom providerId binds to hook.provider (multi-instance)", () => {
const hook = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(hook.provider, "omniroute-preprod");
assert.equal(hook.provider, "opencode-omniroute-preprod");
});

test("createOmniRouteAuthHook: methods[0] is type 'api' with label including displayName", () => {
Expand All @@ -30,7 +30,7 @@ test("createOmniRouteAuthHook: methods[0] is type 'api' with label including dis
assert.equal(m.label, "OmniRoute API Key");

const custom = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(custom.methods[0].label, "OmniRoute (omniroute-preprod) API Key");
assert.equal(custom.methods[0].label, "OmniRoute (opencode-omniroute-preprod) API Key");
});

test("createOmniRouteAuthHook: prompts[0] uses key='apiKey' per @opencode-ai/plugin contract", () => {
Expand Down
40 changes: 20 additions & 20 deletions @omniroute/opencode-plugin/tests/combos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["omniroute/gemini-3-flash"]);
assert.ok(out["omniroute/claude-tier"]);
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"]);

const combo = out["omniroute/claude-tier"];
const combo = out["opencode-omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.providerID, "omniroute");
assert.equal(combo.providerID, "opencode-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);
Expand All @@ -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["omniroute/phantom-combo"]);
assert.ok(out["opencode-omniroute/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
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);
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);
});

test("models(): hidden combos are excluded from the map", async () => {
Expand All @@ -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["omniroute/visible"]);
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
assert.ok(out["opencode-omniroute/visible"]);
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
});

test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
Expand All @@ -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["omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");

// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
Expand Down Expand Up @@ -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["omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
});

test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
Expand All @@ -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["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);

// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
Expand All @@ -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["omniroute/claude-tier"]);
assert.ok(second["opencode-omniroute/claude-tier"]);
});

test("models(): combos refetched after TTL expiry (same key as models)", async () => {
Expand Down Expand Up @@ -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["omniroute/master-light"];
const masterLight = out["opencode-omniroute/master-light"];
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
assert.equal(
masterLight.limit.context,
Expand Down
Loading
Loading