mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Expand OpenCode/MiMo fingerprint accounts in combo builder (#6087). Integrated into release/v3.8.44.
This commit is contained in:
@@ -65,6 +65,7 @@ type ProviderConnectionLike = {
|
||||
lastTested?: string | null;
|
||||
updatedAt?: string | null;
|
||||
testStatus?: string | null;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type ProviderNodeLike = {
|
||||
@@ -223,6 +224,50 @@ function deriveConnectionStatus(connection: ProviderConnectionLike): BuilderConn
|
||||
return "active";
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a list of raw DB connection rows into `ComboBuilderConnectionOption` items.
|
||||
*
|
||||
* No-auth account providers (opencode, mimocode) pack multiple "accounts" as UUIDs in
|
||||
* `providerSpecificData.fingerprints` of a single DB row (#6087). This helper inflates
|
||||
* them: each fingerprint becomes a separate option labeled "Account N" with an id of
|
||||
* `${rowId}|fp|${fingerprint}` so the combo step can pin a specific account.
|
||||
* Rows without fingerprints are converted 1:1 via buildConnectionOption as before.
|
||||
*/
|
||||
export function expandConnectionOptions(
|
||||
connections: ProviderConnectionLike[]
|
||||
): ComboBuilderConnectionOption[] {
|
||||
const result: ComboBuilderConnectionOption[] = [];
|
||||
for (const connection of connections) {
|
||||
const fingerprints = Array.isArray(connection.providerSpecificData?.fingerprints)
|
||||
? (connection.providerSpecificData.fingerprints as unknown[]).filter(
|
||||
(fp): fp is string => typeof fp === "string" && fp.length > 0
|
||||
)
|
||||
: [];
|
||||
if (fingerprints.length > 0) {
|
||||
const baseStatus = deriveConnectionStatus(connection);
|
||||
const basePriority = typeof connection.priority === "number" ? connection.priority : 0;
|
||||
for (let i = 0; i < fingerprints.length; i++) {
|
||||
result.push({
|
||||
id: `${toStringOrNull(connection.id) || ""}|fp|${fingerprints[i]}`,
|
||||
label: `Account ${i + 1}`,
|
||||
type: toStringOrNull(connection.authType) || "unknown",
|
||||
status: baseStatus,
|
||||
priority: basePriority,
|
||||
isActive: connection.isActive !== false,
|
||||
defaultModel: toStringOrNull(connection.defaultModel),
|
||||
rateLimitedUntil: toNumberOrNull(connection.rateLimitedUntil),
|
||||
lastError: toStringOrNull(connection.lastError),
|
||||
lastTested: toStringOrNull(connection.lastTested),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const opt = buildConnectionOption(connection);
|
||||
if (opt) result.push(opt);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildConnectionOption(
|
||||
connection: ProviderConnectionLike
|
||||
): ComboBuilderConnectionOption | null {
|
||||
@@ -509,10 +554,9 @@ export async function getComboBuilderOptions(): Promise<ComboBuilderOptionsPaylo
|
||||
customModels
|
||||
);
|
||||
|
||||
const normalizedConnections = providerConnections
|
||||
.map((connection) => buildConnectionOption(connection))
|
||||
.filter((connection): connection is ComboBuilderConnectionOption => Boolean(connection))
|
||||
.sort(compareConnections);
|
||||
const normalizedConnections = expandConnectionOptions(providerConnections).sort(
|
||||
compareConnections
|
||||
);
|
||||
|
||||
const activeConnectionCount = normalizedConnections.filter(
|
||||
(connection) => connection.isActive
|
||||
|
||||
87
tests/unit/combo-builder-fingerprint-expansion.test.ts
Normal file
87
tests/unit/combo-builder-fingerprint-expansion.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Tests for combo builder fingerprint expansion (#6087).
|
||||
*
|
||||
* No-auth account providers (opencode, mimocode) pack multiple accounts as UUIDs inside
|
||||
* providerSpecificData.fingerprints of a SINGLE provider_connections row. The combo
|
||||
* builder must expand them into one selectable option per account.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { expandConnectionOptions } from "../../src/lib/combos/builderOptions.ts";
|
||||
|
||||
function makeConn(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "conn-1",
|
||||
provider: "opencode",
|
||||
authType: "noauth",
|
||||
name: "OpenCode Account 1",
|
||||
priority: 0,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("expandConnectionOptions — fingerprint expansion", () => {
|
||||
it("expands 3 fingerprints into 3 separate connection options", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ providerSpecificData: { fingerprints: ["fp-a", "fp-b", "fp-c"] } }),
|
||||
]);
|
||||
assert.equal(result.length, 3);
|
||||
assert.equal(result[0].label, "Account 1");
|
||||
assert.equal(result[1].label, "Account 2");
|
||||
assert.equal(result[2].label, "Account 3");
|
||||
});
|
||||
|
||||
it("encodes fingerprint into the connection id for pinning", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ id: "row-abc", providerSpecificData: { fingerprints: ["fp-x", "fp-y"] } }),
|
||||
]);
|
||||
assert.ok(result[0].id.includes("fp-x"), `id should contain fp-x, got: ${result[0].id}`);
|
||||
assert.ok(result[1].id.includes("fp-y"), `id should contain fp-y, got: ${result[1].id}`);
|
||||
assert.ok(result[0].id.includes("row-abc"));
|
||||
});
|
||||
|
||||
it("falls back to the row itself when there are no fingerprints", () => {
|
||||
const result = expandConnectionOptions([makeConn({ name: "OpenCode Account 1" })]);
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0].id, "conn-1");
|
||||
assert.equal(result[0].label, "OpenCode Account 1");
|
||||
});
|
||||
|
||||
it("treats an empty fingerprints array the same as no fingerprints", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ providerSpecificData: { fingerprints: [] } }),
|
||||
]);
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0].id, "conn-1");
|
||||
});
|
||||
|
||||
it("skips non-string entries in fingerprints array", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ providerSpecificData: { fingerprints: ["fp-1", null, 42, "fp-2"] } }),
|
||||
]);
|
||||
assert.equal(result.length, 2);
|
||||
assert.equal(result[0].label, "Account 1");
|
||||
assert.equal(result[1].label, "Account 2");
|
||||
});
|
||||
|
||||
it("expands fingerprints across multiple rows, accumulating all accounts", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ id: "row-1", providerSpecificData: { fingerprints: ["fp-1", "fp-2"] } }),
|
||||
makeConn({ id: "row-2", providerSpecificData: { fingerprints: ["fp-3"] } }),
|
||||
]);
|
||||
assert.equal(result.length, 3);
|
||||
const ids = result.map((r) => r.id);
|
||||
assert.ok(ids.some((id) => id.includes("fp-1")));
|
||||
assert.ok(ids.some((id) => id.includes("fp-2")));
|
||||
assert.ok(ids.some((id) => id.includes("fp-3")));
|
||||
});
|
||||
|
||||
it("inherits isActive from the parent connection row", () => {
|
||||
const result = expandConnectionOptions([
|
||||
makeConn({ isActive: false, providerSpecificData: { fingerprints: ["fp-inactive"] } }),
|
||||
]);
|
||||
assert.equal(result[0].isActive, false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user