File size: 6,375 Bytes
4c76b0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Shared upstream model contract tests keep capability flags aligned across bundled catalogs.
import fs from "node:fs";
import path from "node:path";
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
import { describe, expect, it } from "vitest";
import { listGitTrackedFiles } from "../../test-utils/repo-files.js";

const repoRoot = path.resolve(import.meta.dirname, "../../..");
const MANIFEST_BASENAME = "openclaw.plugin.json";
const CODE_MODE_TIER_LITERAL = /codeMode:\s*"(?:preferred|capable)"/;
// Minimax resolves cost per provider surface, so its rows cannot live in one
// manifest catalog and remain invisible to the manifest scan below.
const UNCONVERTED_SOURCE_CATALOG_PLUGINS = ["minimax"];

type CatalogEntry = {
  /** `provider/model` ref used in failure output. */
  ref: string;
  /** Shared-model group: the normalized upstream model id, else the row's own. */
  groupKey: string;
  /** Declared upstream ref, present only on non-canonical rows. */
  upstreamModel?: string;
  /** Declared code-mode tier; absent rows fall back to the implicit `capable` tier. */
  codeMode?: string;
};

function listBundledPluginFiles(): string[] {
  const files = listGitTrackedFiles({ repoRoot, pathspecs: "extensions" });
  if (!files) {
    throw new Error("unable to list bundled plugin files from git");
  }
  return files;
}

function readBundledManifests(): Array<Record<string, unknown>> {
  return listBundledPluginFiles()
    .filter((file) => {
      const segments = file.split("/");
      return segments.length === 3 && segments[2] === MANIFEST_BASENAME;
    })
    .map(
      (file) =>
        JSON.parse(fs.readFileSync(path.join(repoRoot, file), "utf8")) as Record<string, unknown>,
    );
}

/**
 * Reduces a catalog model id to the vendor's own name for the weights.
 * Aggregators republish first-party models under a namespaced id and varying
 * case (`moonshotai/kimi-k3`, `zai-org/GLM-5.2`), so dropping one leading
 * namespace segment groups those with the first-party row automatically instead
 * of waiting for someone to declare `upstreamModel` on each one.
 */
function normalizeSharedModelId(modelId: string): string {
  const separator = modelId.indexOf("/");
  return (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase();
}

function collectCatalogEntries(): CatalogEntry[] {
  const entries: CatalogEntry[] = [];
  for (const manifest of readBundledManifests()) {
    const providers = readRecord(readRecord(manifest.modelCatalog)?.providers) ?? {};
    // CLI backends run their own harness, so catalog code-mode tiers never reach them.
    const cliBackends = new Set(
      Array.isArray(manifest.cliBackends) ? (manifest.cliBackends as string[]) : [],
    );
    for (const [providerId, provider] of Object.entries(providers)) {
      if (cliBackends.has(providerId)) {
        continue;
      }
      const models = readRecord(provider)?.models;
      if (!Array.isArray(models)) {
        continue;
      }
      for (const rawModel of models) {
        const model = readRecord(rawModel);
        const id = typeof model?.id === "string" ? model.id : undefined;
        if (!id) {
          continue;
        }
        const upstreamModel =
          typeof model?.upstreamModel === "string" ? model.upstreamModel : undefined;
        const codeMode = readRecord(model?.compat)?.codeMode;
        entries.push({
          ref: `${providerId}/${id}`,
          groupKey: normalizeSharedModelId(
            upstreamModel ? upstreamModel.slice(upstreamModel.indexOf("/") + 1) : id,
          ),
          ...(upstreamModel ? { upstreamModel } : {}),
          ...(typeof codeMode === "string" ? { codeMode } : {}),
        });
      }
    }
  }
  return entries;
}

function groupBySharedModel(entries: CatalogEntry[]): Map<string, CatalogEntry[]> {
  const groups = new Map<string, CatalogEntry[]>();
  for (const entry of entries) {
    const group = groups.get(entry.groupKey);
    if (group) {
      group.push(entry);
      continue;
    }
    groups.set(entry.groupKey, [entry]);
  }
  return groups;
}

describe("bundled shared upstream model catalogs", () => {
  // Read lazily: the non-isolated contract runner shares one process, so manifest
  // IO belongs inside the tests rather than in collection.
  let cached: CatalogEntry[] | undefined;
  const readEntries = (): CatalogEntry[] => (cached ??= collectCatalogEntries());

  it("resolves every declared upstream model ref to another bundled catalog row", () => {
    const entries = readEntries();
    const refs = new Set(entries.map((entry) => entry.ref));
    const unresolved = entries
      .filter((entry) => entry.upstreamModel)
      .filter((entry) => entry.upstreamModel === entry.ref || !refs.has(entry.upstreamModel ?? ""))
      .map((entry) => `${entry.ref} -> ${entry.upstreamModel}`)
      .toSorted();

    expect(unresolved).toEqual([]);
  });

  it("declares the code-mode tier on every catalog row that ships a shared model", () => {
    // `compat.codeMode` gates `tools.codeMode: "auto"` per model, so one catalog
    // flagging a model `preferred` while a sibling catalog for the same upstream
    // model stays silent makes the identical model behave differently by provider.
    const silent: string[] = [];
    for (const [groupKey, group] of groupBySharedModel(readEntries())) {
      if (group.length < 2 || !group.some((entry) => entry.codeMode)) {
        continue;
      }
      const declared = group
        .filter((entry) => entry.codeMode)
        .map((entry) => `${entry.ref}=${entry.codeMode}`)
        .toSorted()
        .join(", ");
      for (const entry of group.filter((candidate) => !candidate.codeMode)) {
        silent.push(`${entry.ref} (shared model ${groupKey}; siblings declare ${declared})`);
      }
    }

    expect(silent.toSorted()).toEqual([]);
  });

  it("keeps code-mode tiers out of plugin source so shared-model checks can see them", () => {
    const plugins = listBundledPluginFiles()
      .filter((file) => file.endsWith(".ts") && !file.includes(".test."))
      .filter((file) =>
        CODE_MODE_TIER_LITERAL.test(fs.readFileSync(path.join(repoRoot, file), "utf8")),
      )
      .map((file) => file.split("/")[1] as string);

    expect([...new Set(plugins)].toSorted()).toEqual(UNCONVERTED_SOURCE_CATALOG_PLUGINS);
  });
});