tickmarkr 1.84.0 → 1.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +96 -0
- package/dist/adapters/catalog.js +176 -0
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +59 -1
- package/dist/adapters/fake.js +42 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +25 -26
- package/dist/adapters/registry.js +173 -110
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +36 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/status.js +202 -46
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +161 -12
- package/dist/config/config.d.ts +82 -5
- package/dist/config/config.js +253 -66
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +21 -3
- package/dist/drivers/herdr.js +344 -110
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +105 -10
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +35 -25
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1488 -330
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +358 -4
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +354 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const ModelWindowClaimSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
modelId: z.string().min(1),
|
|
5
|
+
window: z.number().int().positive(),
|
|
6
|
+
source: z.string().url(),
|
|
7
|
+
readDate: z.iso.date(),
|
|
8
|
+
})
|
|
9
|
+
.strict();
|
|
10
|
+
/**
|
|
11
|
+
* Validate cited model-window claims before production code can consume them.
|
|
12
|
+
* These values are vendor-published claims, not measurements made by tickmarkr.
|
|
13
|
+
*/
|
|
14
|
+
export function loadModelWindowClaims(input) {
|
|
15
|
+
const claims = z.array(ModelWindowClaimSchema).parse(input);
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const claim of claims) {
|
|
18
|
+
if (seen.has(claim.modelId))
|
|
19
|
+
throw new Error(`duplicate model id ${claim.modelId}`);
|
|
20
|
+
seen.add(claim.modelId);
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze(claims.map((claim) => Object.freeze(claim)));
|
|
23
|
+
}
|
|
24
|
+
const ANTHROPIC_SOURCE = "https://platform.claude.com/docs/en/about-claude/models/overview";
|
|
25
|
+
const OPENAI_SOURCE = "https://developers.openai.com/api/docs/models";
|
|
26
|
+
const OPENAI_GPT_55_SOURCE = "https://developers.openai.com/api/docs/models/gpt-5.5";
|
|
27
|
+
const CURSOR_SOURCE = "https://cursor.com/help/ai-features/max-mode";
|
|
28
|
+
const ZAI_SOURCE = "https://z.ai/blog/glm-5.2";
|
|
29
|
+
const XAI_SOURCE = "https://docs.x.ai/developers/models/grok-4.5";
|
|
30
|
+
const KIMI_SOURCE = "https://www.kimi.com/code/docs/en/kimi-code-cli/configuration/config-files.html";
|
|
31
|
+
const READ_DATE = "2026-08-05";
|
|
32
|
+
const VENDORED_MODEL_WINDOW_CLAIMS = [
|
|
33
|
+
{ modelId: "fable", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
|
|
34
|
+
{ modelId: "opus", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
|
|
35
|
+
{ modelId: "sonnet", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
|
|
36
|
+
{ modelId: "haiku", window: 200_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
|
|
37
|
+
{ modelId: "gpt-5.6-sol", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
|
|
38
|
+
{ modelId: "gpt-5.5", window: 1_050_000, source: OPENAI_GPT_55_SOURCE, readDate: READ_DATE },
|
|
39
|
+
{ modelId: "gpt-5.6-terra", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
|
|
40
|
+
{ modelId: "gpt-5.6-luna", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
|
|
41
|
+
{ modelId: "composer-2.5", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
|
|
42
|
+
{ modelId: "composer-2.5-fast", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
|
|
43
|
+
{ modelId: "zai-coding-plan/glm-5.2", window: 1_000_000, source: ZAI_SOURCE, readDate: READ_DATE },
|
|
44
|
+
{ modelId: "zai/glm-5.2", window: 1_000_000, source: ZAI_SOURCE, readDate: READ_DATE },
|
|
45
|
+
{ modelId: "grok-4.5", window: 500_000, source: XAI_SOURCE, readDate: READ_DATE },
|
|
46
|
+
{ modelId: "grok-composer-2.5-fast", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
|
|
47
|
+
{ modelId: "kimi-code/k3", window: 1_048_576, source: KIMI_SOURCE, readDate: READ_DATE },
|
|
48
|
+
{ modelId: "kimi-code/kimi-for-coding", window: 262_144, source: KIMI_SOURCE, readDate: READ_DATE },
|
|
49
|
+
{ modelId: "kimi-code/kimi-for-coding-highspeed", window: 262_144, source: KIMI_SOURCE, readDate: READ_DATE },
|
|
50
|
+
];
|
|
51
|
+
/** The closed, production-readable catalog of cited context-window claims. */
|
|
52
|
+
export const CITED_MODEL_WINDOWS = loadModelWindowClaims(VENDORED_MODEL_WINDOW_CLAIMS);
|
|
53
|
+
export function resolveModelWindowClaim(modelId, input = CITED_MODEL_WINDOWS) {
|
|
54
|
+
const claim = loadModelWindowClaims(input).find((entry) => entry.modelId === modelId);
|
|
55
|
+
return claim === undefined ? { status: "unknown", modelId } : { status: "declared", claim };
|
|
56
|
+
}
|
|
57
|
+
/** Fail when either side contains a model id absent from the other side. */
|
|
58
|
+
export function assertModelWindowClaimsMatchSeededModels(seededModelIds, input = CITED_MODEL_WINDOWS) {
|
|
59
|
+
const tableIds = new Set(loadModelWindowClaims(input).map((claim) => claim.modelId));
|
|
60
|
+
const seedIds = new Set(seededModelIds);
|
|
61
|
+
const tableOnly = [...tableIds].filter((id) => !seedIds.has(id)).sort();
|
|
62
|
+
const seedOnly = [...seedIds].filter((id) => !tableIds.has(id)).sort();
|
|
63
|
+
const differences = [
|
|
64
|
+
tableOnly.length ? `table-only model ids: ${tableOnly.join(", ")}` : "",
|
|
65
|
+
seedOnly.length ? `seed-only model ids: ${seedOnly.join(", ")}` : "",
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
if (differences.length)
|
|
68
|
+
throw new Error(differences.join("; "));
|
|
69
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { type Task } from "../graph/schema.js";
|
|
2
|
+
import { type VerdictUnparseableCause } from "../gates/verdict-cause.js";
|
|
2
3
|
import type { WorkerResult } from "./types.js";
|
|
3
4
|
export declare function buildTaskPrompt(task: Task, feedback?: string, nonce?: string): string;
|
|
4
5
|
export declare function writePrompt(dir: string, task: Task, attempt: number, feedback?: string, nonce?: string): string;
|
|
5
6
|
export declare function trailerPattern(nonce: string): string;
|
|
6
7
|
export declare const NO_TRAILER_SUMMARY = "worker produced no TICKMARKR_RESULT trailer";
|
|
7
8
|
export declare const UNPARSEABLE_TRAILER_SUMMARY = "unparseable TICKMARKR_RESULT trailer";
|
|
8
|
-
export
|
|
9
|
+
export type ClassifiedWorkerResult = WorkerResult & {
|
|
10
|
+
cause?: VerdictUnparseableCause;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseWorkerResult(raw: string, nonce: string): ClassifiedWorkerResult;
|
|
9
13
|
export type DeadChannelReason = "auth-required" | "setup-required" | "provider-outage" | "timeout";
|
|
10
14
|
export declare function classifyDeadChannel(result: WorkerResult): DeadChannelReason | undefined;
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
|
+
import { classifyVerdictCause } from "../gates/verdict-cause.js";
|
|
4
5
|
export function buildTaskPrompt(task, feedback = "", nonce = "") {
|
|
5
6
|
const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
6
7
|
return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
|
|
@@ -41,15 +42,22 @@ function trailerTokenPositions(raw, nonce) {
|
|
|
41
42
|
positions.push(i);
|
|
42
43
|
return positions;
|
|
43
44
|
}
|
|
45
|
+
// The worker protocol binds participation in the nonce-bearing TICKMARKR_RESULT token outside its
|
|
46
|
+
// JSON object, while judge/review/consult bind it inside the object. Parsing has already rejected every
|
|
47
|
+
// candidate by the time this runs, so canonicalize that token with the `ok` key this boundary requests;
|
|
48
|
+
// the shared classifier still owns the resulting cause and callers cannot infer one from token count.
|
|
49
|
+
function workerVerdictWitness(raw, nonce, positions) {
|
|
50
|
+
if (positions.length === 0)
|
|
51
|
+
return raw;
|
|
52
|
+
return `{"nonce":${JSON.stringify(nonce)},"ok":false,`;
|
|
53
|
+
}
|
|
44
54
|
// v1.65 T1: the parse boundary's own no-trailer sentinel summaries. classifyDeadChannel keys on
|
|
45
55
|
// these — a result carrying any other summary is a PARSED trailer, i.e. the worker speaking.
|
|
46
56
|
export const NO_TRAILER_SUMMARY = "worker produced no TICKMARKR_RESULT trailer";
|
|
47
57
|
export const UNPARSEABLE_TRAILER_SUMMARY = "unparseable TICKMARKR_RESULT trailer";
|
|
48
58
|
export function parseWorkerResult(raw, nonce) {
|
|
49
|
-
const fail = (summary) => ({ ok: false, summary, deviations: [], raw });
|
|
59
|
+
const fail = (summary, cause) => ({ ok: false, summary, deviations: [], raw, cause });
|
|
50
60
|
const positions = trailerTokenPositions(raw, nonce);
|
|
51
|
-
if (positions.length === 0)
|
|
52
|
-
return fail(NO_TRAILER_SUMMARY);
|
|
53
61
|
// TUIs echo the prompt template, redraw lines, and HARD-wrap the JSON with per-line margins
|
|
54
62
|
// (cursor does; recent-unwrapped can't rejoin hard newlines). Scan occurrences backward — last
|
|
55
63
|
// parseable wins — joining wrapped lines, stripping margin/box chrome, and growing the candidate
|
|
@@ -79,7 +87,8 @@ export function parseWorkerResult(raw, nonce) {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
|
-
|
|
90
|
+
const cause = classifyVerdictCause(workerVerdictWitness(raw, nonce, positions), nonce, "ok");
|
|
91
|
+
return fail(cause === "malformed-verdict" ? UNPARSEABLE_TRAILER_SUMMARY : NO_TRAILER_SUMMARY, cause);
|
|
83
92
|
}
|
|
84
93
|
// Signatures anchor distinctive CLI-error phrasing — never bare fragments or bare status-code
|
|
85
94
|
// numbers (the QUOTA_RE Pitfall-3 lesson): these run only over no-trailer output, but a stalled
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type CatalogEntriesOptions, type CliEntry } from "./catalog.js";
|
|
3
3
|
import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
|
|
4
|
-
export declare const CANDIDATE_CLI_CATALOG: readonly [
|
|
5
|
-
export declare
|
|
6
|
-
export declare function resolveShellBinary(bin: string, cwd?: string): {
|
|
4
|
+
export declare const CANDIDATE_CLI_CATALOG: readonly string[];
|
|
5
|
+
export declare function registeredAdapterBinaries(adapters?: WorkerAdapter[]): string[];
|
|
6
|
+
export declare function resolveShellBinary(bin: string, cwd?: string, pathEnv?: string): {
|
|
7
7
|
resolved?: string;
|
|
8
8
|
all: string[];
|
|
9
9
|
};
|
|
@@ -27,40 +27,39 @@ export declare function modelAliasLine(excluded: {
|
|
|
27
27
|
}[]): string;
|
|
28
28
|
export type CandidateCliDetection = {
|
|
29
29
|
binary: string;
|
|
30
|
-
|
|
30
|
+
path: string;
|
|
31
31
|
};
|
|
32
|
-
export declare function
|
|
33
|
-
export declare function
|
|
32
|
+
export declare function parseHerdrCliNames(help: string): string[];
|
|
33
|
+
export declare function herdrNominatedCliNames(cwd?: string, pathEnv?: string): string[];
|
|
34
|
+
export interface DetectCandidateCliOptions extends CatalogEntriesOptions {
|
|
35
|
+
cwd?: string;
|
|
34
36
|
pathEnv?: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
entries?: readonly CliEntry[];
|
|
38
|
+
adapters?: WorkerAdapter[];
|
|
39
|
+
resolveBinary?: (binary: string) => {
|
|
40
|
+
resolved?: string;
|
|
41
|
+
all: string[];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function detectCandidateClis(opts?: DetectCandidateCliOptions): CandidateCliDetection[];
|
|
45
|
+
export declare function formatCandidateCliRow({ binary, path }: CandidateCliDetection): string;
|
|
37
46
|
export declare function probeHelpText(bin: string): string | undefined;
|
|
38
47
|
export declare function missingDeclaredFlags(helpText: string, flags: string[]): string[];
|
|
39
48
|
export declare function flagDriftWarnings(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, helpOf?: (bin: string) => string | undefined): string[];
|
|
40
|
-
export declare function
|
|
49
|
+
export declare function adaptersFromCliEntries(entries: readonly CliEntry[]): WorkerAdapter[];
|
|
50
|
+
export interface AllAdaptersOptions {
|
|
41
51
|
fakeScriptPath?: string;
|
|
42
|
-
|
|
52
|
+
cliEntries?: readonly CliEntry[];
|
|
53
|
+
operatorYaml?: string | null;
|
|
54
|
+
operatorPath?: string | false;
|
|
55
|
+
}
|
|
56
|
+
export declare function allAdapters(opts?: AllAdaptersOptions): WorkerAdapter[];
|
|
43
57
|
export declare function getAdapter(id: string, adapters: WorkerAdapter[]): WorkerAdapter;
|
|
44
58
|
export declare function probeAll(adapters: WorkerAdapter[], opts?: {
|
|
45
59
|
cwd?: string;
|
|
46
60
|
}): Promise<Record<string, AuthHealth>>;
|
|
47
61
|
export type ProbeModelStatus = "ok" | "timeout" | "failed";
|
|
48
62
|
export type ProbeModelProgress = (adapter: string, model: string, status: ProbeModelStatus, durationMs: number) => void;
|
|
49
|
-
export interface AutoPreferDoc {
|
|
50
|
-
derivedAt: string;
|
|
51
|
-
[shape: string]: string[] | string;
|
|
52
|
-
}
|
|
53
|
-
export declare const pendingAutoPreferKey: unique symbol;
|
|
54
|
-
export declare const DOCTOR_ROUTING_STALE_MS: number;
|
|
55
|
-
export declare function deriveAutoPrefer(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, profile?: RoutingProfile): AutoPreferDoc;
|
|
56
|
-
export declare function readAutoPrefer(repoRoot: string): AutoPreferDoc | null;
|
|
57
|
-
export declare function routingPreferContext(repoRoot: string, cfg: TickmarkrConfig, opts?: {
|
|
58
|
-
globalDir?: string;
|
|
59
|
-
}): {
|
|
60
|
-
autoPrefer?: AutoPreferDoc;
|
|
61
|
-
doctorFresh: boolean;
|
|
62
|
-
overlayPreferShapes: ReadonlySet<string>;
|
|
63
|
-
};
|
|
64
63
|
export declare function probeModels(cfg: TickmarkrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
|
|
65
64
|
export declare function writeDoctor(repoRoot: string, health: Record<string, AuthHealth>): void;
|
|
66
65
|
export declare function readDoctor(repoRoot: string): Record<string, AuthHealth> | null;
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { overlayPreferShapes, TIER_RANK } from "../config/config.js";
|
|
6
5
|
import { modelLints, suggestOverlay } from "./model-lints.js";
|
|
7
6
|
import { HerdrDriver } from "../drivers/herdr.js";
|
|
8
7
|
import { tickmarkrDir, stateDirName } from "../graph/graph.js";
|
|
9
8
|
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../route/preference.js";
|
|
10
|
-
import { learnedScore } from "../route/profile.js";
|
|
11
|
-
import { marginalCostRank } from "../route/router.js";
|
|
12
9
|
import { sh } from "../run/git.js";
|
|
13
|
-
import { claudeCode } from "./claude-code.js";
|
|
14
|
-
import { codex } from "./codex.js";
|
|
15
|
-
import { cursorAgent } from "./cursor-agent.js";
|
|
16
10
|
import { FakeAdapter } from "./fake.js";
|
|
17
|
-
import {
|
|
18
|
-
import { kimi } from "./kimi.js";
|
|
19
|
-
import { opencode } from "./opencode.js";
|
|
20
|
-
import { pi } from "./pi.js";
|
|
11
|
+
import { parseWorkerResult } from "./prompt.js";
|
|
21
12
|
import { sealHerdrEnv } from "../drivers/subprocess.js";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
13
|
+
import { catalogEntries, isNativeCliDrive, projectCliEntries, SHIPPED_CLI_CATALOG, } from "./catalog.js";
|
|
14
|
+
import { channelKey, channelsFromConfig, modelAuthed, MODEL_ID_RE, QUOTA_RE, shq, } from "./types.js";
|
|
15
|
+
// Compatibility projection for callers/tests that only need shipped advisory names. The literal
|
|
16
|
+
// list is gone: both advisory and routable catalog views derive from SHIPPED_CLI_CATALOG. Native
|
|
17
|
+
// definitions (claudeCode, codex, cursorAgent, opencode, pi, grok, kimi) are owned by catalog.ts;
|
|
18
|
+
// this registry consumes their drive contracts instead of restating their registration order.
|
|
19
|
+
export const CANDIDATE_CLI_CATALOG = SHIPPED_CLI_CATALOG
|
|
20
|
+
.filter((entry) => entry.drive === undefined)
|
|
21
|
+
.map((entry) => entry.binary);
|
|
22
|
+
const SHIPPED_NATIVE_ADAPTERS = new Set(SHIPPED_CLI_CATALOG.flatMap((entry) => isNativeCliDrive(entry.drive) ? [entry.drive.adapter] : []));
|
|
23
|
+
const declarativeBinaries = new WeakMap();
|
|
24
|
+
// OBS-304: this was a hand-maintained mirror of the registered binaries, and the guard test that
|
|
25
|
+
// forbids catalog overlap checked against IT rather than against the adapters — so when kimi gained
|
|
26
|
+
// an adapter and nobody updated the copy, the guard kept passing on a collision it existed to catch.
|
|
27
|
+
// Derive it. A list that restates another list is the defect, not the entry that fell out of sync.
|
|
28
|
+
export function registeredAdapterBinaries(adapters = allAdapters()) {
|
|
29
|
+
return adapters.map((a) => a.hardcodedFlags?.binary ?? declarativeBinaries.get(a)).filter((b) => !!b);
|
|
30
|
+
}
|
|
27
31
|
// OBS-117: worker panes run commands through `bash -lc` (SubprocessDriver + sh()), not bare spawnSync(bin).
|
|
28
32
|
// Doctor must resolve adapter binaries the same way so a shadow install on PATH cannot disagree with dispatch.
|
|
29
33
|
function shellSpawnSync(cmd, cwd, timeoutMs = 10000) {
|
|
@@ -36,9 +40,24 @@ function shellSpawnSync(cmd, cwd, timeoutMs = 10000) {
|
|
|
36
40
|
});
|
|
37
41
|
return { code: r.status ?? 1, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim() };
|
|
38
42
|
}
|
|
39
|
-
export function resolveShellBinary(bin, cwd = process.cwd()) {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
export function resolveShellBinary(bin, cwd = process.cwd(), pathEnv) {
|
|
44
|
+
// An explicit PATH is a deterministic fixture/operator boundary. The default remains the same
|
|
45
|
+
// login-shell environment used by dispatched workers (OBS-117).
|
|
46
|
+
const pathPrefix = pathEnv === undefined ? "" : `export PATH=${shq(pathEnv)}; `;
|
|
47
|
+
const r = shellSpawnSync(`${pathPrefix}type -a ${shq(bin)} 2>/dev/null | sed -n 's/^.* is \\(.*\\)$/\\1/p'`, cwd);
|
|
48
|
+
const hits = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
49
|
+
// OBS-304: a duplicated PATH entry makes `type -a` emit the SAME path twice, which doctor then
|
|
50
|
+
// reported as "2 installs on PATH — …/claude, …/claude". Count distinct real files, so the real
|
|
51
|
+
// shadows (codex: homebrew + nvm) stop being buried in noise. realpath resolves symlinked twins.
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const all = hits.filter((p) => {
|
|
54
|
+
let key = p;
|
|
55
|
+
try {
|
|
56
|
+
key = realpathSync(p);
|
|
57
|
+
}
|
|
58
|
+
catch { /* unreadable or dangling — fall back to the literal path */ }
|
|
59
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
60
|
+
});
|
|
42
61
|
return { resolved: all[0], all };
|
|
43
62
|
}
|
|
44
63
|
export function probeVersionShell(bin, cwd = process.cwd()) {
|
|
@@ -101,39 +120,47 @@ export function modelAliasLine(excluded) {
|
|
|
101
120
|
const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s reported model list)`);
|
|
102
121
|
return `model alias: ${excluded.length} channel(s) invalid — ${parts.join(", ")}`;
|
|
103
122
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
// Herdr's supported-kind help is nomination data only. Parse the closed token list and discard
|
|
124
|
+
// everything else before any name can reach a shell resolver.
|
|
125
|
+
export function parseHerdrCliNames(help) {
|
|
126
|
+
const body = /\[possible values:\s*([^\]]+)\]/m.exec(help)?.[1] ?? "";
|
|
127
|
+
return [...new Set(body.split(",").map((name) => name.trim()).filter((name) => /^[a-z0-9-]+$/.test(name)))];
|
|
107
128
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
129
|
+
export function herdrNominatedCliNames(cwd = process.cwd(), pathEnv) {
|
|
130
|
+
// This asks a known driver binary for static help. It is deliberately NOT gated on
|
|
131
|
+
// HerdrDriver.available(), which reports HERDR_ENV rather than binary presence.
|
|
132
|
+
const { resolved } = resolveShellBinary("herdr", cwd, pathEnv);
|
|
133
|
+
if (!resolved)
|
|
134
|
+
return [];
|
|
135
|
+
const r = shellSpawnSync(`${shq(resolved)} agent start --help`, cwd);
|
|
136
|
+
return r.code === 0 ? parseHerdrCliNames(`${r.stdout}\n${r.stderr}`) : [];
|
|
119
137
|
}
|
|
120
138
|
export function detectCandidateClis(opts = {}) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
140
|
+
const entries = opts.entries ?? catalogEntries({
|
|
141
|
+
shipped: opts.shipped,
|
|
142
|
+
operatorYaml: opts.operatorYaml,
|
|
143
|
+
operatorPath: opts.operatorPath,
|
|
144
|
+
herdrNames: opts.herdrNames ?? herdrNominatedCliNames(cwd, opts.pathEnv),
|
|
145
|
+
});
|
|
146
|
+
const registered = new Set(registeredAdapterBinaries(opts.adapters ?? allAdapters({ cliEntries: entries })));
|
|
147
|
+
const advisory = projectCliEntries(entries).advisory.filter((entry) => !registered.has(entry.binary));
|
|
148
|
+
const resolveBinary = opts.resolveBinary
|
|
149
|
+
?? ((binary) => resolveShellBinary(binary, cwd, opts.pathEnv));
|
|
150
|
+
const discovered = [];
|
|
151
|
+
for (const entry of advisory) {
|
|
152
|
+
const { resolved } = resolveBinary(entry.binary);
|
|
153
|
+
if (resolved)
|
|
154
|
+
discovered.push({ binary: entry.binary, path: resolved });
|
|
127
155
|
}
|
|
128
|
-
return
|
|
156
|
+
return discovered;
|
|
129
157
|
}
|
|
130
|
-
export function formatCandidateCliRow({ binary,
|
|
131
|
-
|
|
132
|
-
return ` ! ${binary.padEnd(14)} detected: ${ver} (no tickmarkr adapter — not routable)`;
|
|
158
|
+
export function formatCandidateCliRow({ binary, path }) {
|
|
159
|
+
return ` ! ${binary.padEnd(14)} detected at ${path} (no drive contract — not routable)`;
|
|
133
160
|
}
|
|
134
|
-
// v1.65 T3:
|
|
135
|
-
//
|
|
136
|
-
//
|
|
161
|
+
// v1.65 T3: bounded, error/status-checked and fail-open. Unlike candidate detection above, this
|
|
162
|
+
// executes only registered adapter binaries with drive contracts. undefined = unavailable/broken,
|
|
163
|
+
// so no drift verdict piles onto the existing auth report.
|
|
137
164
|
export function probeHelpText(bin) {
|
|
138
165
|
try {
|
|
139
166
|
const r = spawnSync(bin, ["--help"], { encoding: "utf8", timeout: 10000 });
|
|
@@ -169,11 +196,103 @@ export function flagDriftWarnings(adapters, health, helpOf = probeHelpText) {
|
|
|
169
196
|
}
|
|
170
197
|
return out;
|
|
171
198
|
}
|
|
199
|
+
function renderDriveTemplate(template, promptFile, model) {
|
|
200
|
+
return template.replaceAll("{promptFile}", shq(promptFile)).replaceAll("{model}", shq(model));
|
|
201
|
+
}
|
|
202
|
+
function valueAtPath(value, path) {
|
|
203
|
+
if (!path)
|
|
204
|
+
return value;
|
|
205
|
+
return path.split(".").reduce((cursor, part) => {
|
|
206
|
+
if (!cursor || typeof cursor !== "object")
|
|
207
|
+
return undefined;
|
|
208
|
+
return cursor[part];
|
|
209
|
+
}, value);
|
|
210
|
+
}
|
|
211
|
+
function parseDeclaredModels(raw, parser, path) {
|
|
212
|
+
let values;
|
|
213
|
+
if (parser === "json") {
|
|
214
|
+
try {
|
|
215
|
+
const selected = valueAtPath(JSON.parse(raw), path);
|
|
216
|
+
values = Array.isArray(selected) ? selected : [];
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (parser === "pi-table") {
|
|
223
|
+
const lines = raw.trim().split("\n");
|
|
224
|
+
const header = lines.findIndex((line) => /^provider\s+model\b/i.test(line.trim()));
|
|
225
|
+
values = header === -1 ? [] : lines.slice(header + 1).map((line) => {
|
|
226
|
+
const [provider, model] = line.trim().split(/\s+/);
|
|
227
|
+
return provider && model ? `${provider}/${model}` : "";
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
values = raw.split("\n").map((line) => line.trim());
|
|
232
|
+
}
|
|
233
|
+
return [...new Set(values.filter((value) => typeof value === "string" && MODEL_ID_RE.test(value)))];
|
|
234
|
+
}
|
|
235
|
+
function declarativeAdapter(entry) {
|
|
236
|
+
const drive = entry.drive;
|
|
237
|
+
const adapter = {
|
|
238
|
+
id: entry.id,
|
|
239
|
+
vendor: entry.vendor,
|
|
240
|
+
probe: async () => {
|
|
241
|
+
const health = probeVersionShell(entry.binary);
|
|
242
|
+
if (!health.installed)
|
|
243
|
+
return health;
|
|
244
|
+
const version = health.version ?? "";
|
|
245
|
+
if (!new RegExp(entry.identity).test(version)) {
|
|
246
|
+
return {
|
|
247
|
+
installed: true,
|
|
248
|
+
authed: false,
|
|
249
|
+
version,
|
|
250
|
+
models: [],
|
|
251
|
+
note: `identity mismatch for ${entry.binary}`,
|
|
252
|
+
binaryPaths: health.binaryPaths,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return health;
|
|
256
|
+
},
|
|
257
|
+
channels: (cfg) => channelsFromConfig(entry.id, cfg),
|
|
258
|
+
headlessCommand: (promptFile, model) => renderDriveTemplate(drive.headless, promptFile, model),
|
|
259
|
+
interactiveCommand: (promptFile, model) => drive.interactive === null
|
|
260
|
+
? null
|
|
261
|
+
: renderDriveTemplate(drive.interactive, promptFile, model),
|
|
262
|
+
invoke(_task, _cwd, assignment, ctx) {
|
|
263
|
+
return { command: this.headlessCommand(ctx.promptFile, assignment.model) };
|
|
264
|
+
},
|
|
265
|
+
parse: parseWorkerResult,
|
|
266
|
+
...(drive.listModels ? {
|
|
267
|
+
listModels: async () => {
|
|
268
|
+
const { resolved } = resolveShellBinary(entry.binary);
|
|
269
|
+
if (!resolved)
|
|
270
|
+
return [];
|
|
271
|
+
const cmd = [shq(resolved), ...drive.listModels.argv.map(shq)].join(" ");
|
|
272
|
+
const result = shellSpawnSync(cmd, process.cwd(), 15000);
|
|
273
|
+
return result.code === 0
|
|
274
|
+
? parseDeclaredModels(result.stdout, drive.listModels.parser, drive.listModels.path)
|
|
275
|
+
: [];
|
|
276
|
+
},
|
|
277
|
+
} : {}),
|
|
278
|
+
};
|
|
279
|
+
declarativeBinaries.set(adapter, entry.binary);
|
|
280
|
+
return adapter;
|
|
281
|
+
}
|
|
282
|
+
export function adaptersFromCliEntries(entries) {
|
|
283
|
+
return projectCliEntries(entries).routable.map((entry) => {
|
|
284
|
+
if (isNativeCliDrive(entry.drive))
|
|
285
|
+
return entry.drive.adapter;
|
|
286
|
+
return declarativeAdapter(entry);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
172
289
|
export function allAdapters(opts = {}) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
290
|
+
const entries = opts.cliEntries ?? catalogEntries({
|
|
291
|
+
operatorYaml: opts.operatorYaml,
|
|
292
|
+
operatorPath: opts.operatorPath,
|
|
293
|
+
herdrNames: [],
|
|
294
|
+
});
|
|
295
|
+
const real = adaptersFromCliEntries(entries);
|
|
177
296
|
const fakePath = opts.fakeScriptPath ?? process.env.TICKMARKR_FAKE_SCRIPT;
|
|
178
297
|
return fakePath ? [new FakeAdapter(fakePath), ...real] : real;
|
|
179
298
|
}
|
|
@@ -233,58 +352,6 @@ function probeFailure(code, stdout, stderr, timedOut, timeoutMs = MODEL_PROBE_TI
|
|
|
233
352
|
? reasonTail(output) || `probe exited ${code}`
|
|
234
353
|
: undefined;
|
|
235
354
|
}
|
|
236
|
-
export const pendingAutoPreferKey = Symbol.for("tickmarkr.pendingAutoPrefer");
|
|
237
|
-
const median = (xs) => {
|
|
238
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
239
|
-
const m = Math.floor(s.length / 2);
|
|
240
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
241
|
-
};
|
|
242
|
-
// ponytail: 24h TTL matches plan.ts staleness — promote to config when an operator asks.
|
|
243
|
-
export const DOCTOR_ROUTING_STALE_MS = 24 * 60 * 60 * 1000;
|
|
244
|
-
export function deriveAutoPrefer(cfg, adapters, health, profile) {
|
|
245
|
-
const derivedAt = new Date().toISOString();
|
|
246
|
-
const out = { derivedAt };
|
|
247
|
-
// v1.52 T5: routing.floors is the only band authority — a map entry can no longer carry a tier.
|
|
248
|
-
for (const shape of Object.keys(cfg.routing.map)) {
|
|
249
|
-
const minTier = cfg.routing.floors[shape] ?? "cheap";
|
|
250
|
-
const ranked = [];
|
|
251
|
-
for (const a of adapters) {
|
|
252
|
-
const h = health[a.id];
|
|
253
|
-
if (!h?.installed || !h?.authed || typeof a.channels !== "function")
|
|
254
|
-
continue;
|
|
255
|
-
const qualifying = a.channels(cfg).filter((c) => TIER_RANK[c.tier] >= TIER_RANK[minTier] && h.modelAuth?.[c.model]?.authed === true);
|
|
256
|
-
if (!qualifying.length)
|
|
257
|
-
continue;
|
|
258
|
-
const durations = qualifying
|
|
259
|
-
.map((c) => h.modelAuth?.[c.model]?.durationMs)
|
|
260
|
-
.filter((d) => typeof d === "number");
|
|
261
|
-
ranked.push({
|
|
262
|
-
adapter: a.id,
|
|
263
|
-
mc: Math.min(...qualifying.map(marginalCostRank)),
|
|
264
|
-
dur: durations.length ? median(durations) : Number.POSITIVE_INFINITY,
|
|
265
|
-
learned: profile ? Math.max(...qualifying.map((c) => learnedScore(profile, shape, channelKey(c), c.channel))) : 0,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
ranked.sort((a, b) => a.mc - b.mc || a.dur - b.dur || b.learned - a.learned);
|
|
269
|
-
out[shape] = ranked.map((r) => r.adapter);
|
|
270
|
-
}
|
|
271
|
-
return out;
|
|
272
|
-
}
|
|
273
|
-
export function readAutoPrefer(repoRoot) {
|
|
274
|
-
const raw = readDoctorFile(repoRoot);
|
|
275
|
-
if (!raw || typeof raw.autoPrefer !== "object" || raw.autoPrefer === null)
|
|
276
|
-
return null;
|
|
277
|
-
return raw.autoPrefer;
|
|
278
|
-
}
|
|
279
|
-
export function routingPreferContext(repoRoot, cfg, opts = {}) {
|
|
280
|
-
const age = doctorAgeMs(repoRoot);
|
|
281
|
-
const doctorFresh = age !== null && age <= DOCTOR_ROUTING_STALE_MS;
|
|
282
|
-
return {
|
|
283
|
-
autoPrefer: doctorFresh ? readAutoPrefer(repoRoot) ?? undefined : undefined,
|
|
284
|
-
doctorFresh,
|
|
285
|
-
overlayPreferShapes: overlayPreferShapes(repoRoot, opts),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
355
|
const probeModelStatus = (v) => v.authed ? "ok" : v.reason?.includes("timed out") ? "timeout" : "failed";
|
|
289
356
|
// v1.21: one bounded, headless call per configured model; detected-but-unclassified models never enter this loop.
|
|
290
357
|
export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
|
|
@@ -296,7 +363,7 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
|
|
|
296
363
|
if (!h?.installed)
|
|
297
364
|
return;
|
|
298
365
|
// Tests inject FakeAdapter; never let an incidental default adapter spend a real token in the suite.
|
|
299
|
-
if (process.env.VITEST &&
|
|
366
|
+
if (process.env.VITEST && SHIPPED_NATIVE_ADAPTERS.has(a))
|
|
300
367
|
return;
|
|
301
368
|
const verdicts = {};
|
|
302
369
|
const priorModelAuth = priorHealth?.[a.id]?.modelAuth;
|
|
@@ -360,7 +427,6 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
|
|
|
360
427
|
}
|
|
361
428
|
h.modelAuth = verdicts;
|
|
362
429
|
}));
|
|
363
|
-
health[pendingAutoPreferKey] = deriveAutoPrefer(cfg, adapters, health);
|
|
364
430
|
}
|
|
365
431
|
// T2: caps concurrent probes per adapter at 2 — v1.33.5 regression, 4 concurrent codex exec in one
|
|
366
432
|
// repo made ALL 4 time out where sequential passed 2/4 (suspected CLI self-contention).
|
|
@@ -388,20 +454,17 @@ function readDoctorFile(repoRoot) {
|
|
|
388
454
|
}
|
|
389
455
|
export function writeDoctor(repoRoot, health) {
|
|
390
456
|
tickmarkrDir(repoRoot);
|
|
391
|
-
const pending = health[pendingAutoPreferKey];
|
|
392
|
-
const payload = { ...health };
|
|
393
|
-
delete payload[pendingAutoPreferKey];
|
|
394
|
-
if (pending)
|
|
395
|
-
payload.autoPrefer = pending;
|
|
396
457
|
const path = doctorPath(repoRoot);
|
|
397
458
|
const tmp = `${path}.tmp`;
|
|
398
|
-
writeFileSync(tmp, JSON.stringify(
|
|
459
|
+
writeFileSync(tmp, JSON.stringify(health, null, 2) + "\n");
|
|
399
460
|
renameSync(tmp, path);
|
|
400
461
|
}
|
|
401
462
|
export function readDoctor(repoRoot) {
|
|
402
463
|
const raw = readDoctorFile(repoRoot);
|
|
403
464
|
if (!raw)
|
|
404
465
|
return null;
|
|
466
|
+
// pre-v1.86 caches may still carry an `autoPrefer` key — strip it; the mechanism is deleted and
|
|
467
|
+
// the key is never read back into health.
|
|
405
468
|
const { autoPrefer: _, ...health } = raw;
|
|
406
469
|
return health;
|
|
407
470
|
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export interface InputBox {
|
|
|
86
86
|
fingerprint: string;
|
|
87
87
|
match?(paneText: string): boolean;
|
|
88
88
|
emptyMatch?(paneText: string): boolean;
|
|
89
|
+
occupiedMatch?(paneText: string): boolean;
|
|
89
90
|
launchCommand?(command: string): boolean;
|
|
90
91
|
readinessTimeoutMs?: number;
|
|
91
92
|
}
|
|
@@ -93,6 +94,8 @@ export declare function declareInputBox(adapterId: string, inputBox: InputBox):
|
|
|
93
94
|
export declare function declaredInputBoxForWorkerName(workerName: string): InputBox | undefined;
|
|
94
95
|
export declare function matchesInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
95
96
|
export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
97
|
+
export declare function matchesOccupiedInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
98
|
+
export declare function missingInputStateDeclarations(inputBox: InputBox | undefined): string[];
|
|
96
99
|
export interface WorkerAdapter {
|
|
97
100
|
id: string;
|
|
98
101
|
vendor: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -45,13 +45,46 @@ export function matchesInputBox(paneText, inputBox) {
|
|
|
45
45
|
export function matchesEmptyInputBox(paneText, inputBox) {
|
|
46
46
|
return inputBox.emptyMatch?.(paneText) === true;
|
|
47
47
|
}
|
|
48
|
+
// The OCCUPIED state: the adapter's own pin when it has one, else painted-and-not-empty, which two
|
|
49
|
+
// adapter declarations decide between them. Either way the answer comes from the adapter, never from
|
|
50
|
+
// where the prompt happens to sit in the transcript.
|
|
51
|
+
export function matchesOccupiedInputBox(paneText, inputBox) {
|
|
52
|
+
if (inputBox.occupiedMatch)
|
|
53
|
+
return inputBox.occupiedMatch(paneText) === true;
|
|
54
|
+
return matchesInputBox(paneText, inputBox) && !matchesEmptyInputBox(paneText, inputBox);
|
|
55
|
+
}
|
|
56
|
+
// v1.85 T5: which input-state declarations a typed delivery needs and this adapter has not made.
|
|
57
|
+
// Empty is always required (it is the only positive submission evidence); occupied needs either its
|
|
58
|
+
// own matcher or the `match` that derives it. A non-empty result is a fail-closed refusal, named.
|
|
59
|
+
export function missingInputStateDeclarations(inputBox) {
|
|
60
|
+
if (!inputBox)
|
|
61
|
+
return ["inputBox"];
|
|
62
|
+
const missing = [];
|
|
63
|
+
if (!inputBox.emptyMatch)
|
|
64
|
+
missing.push("emptyMatch");
|
|
65
|
+
if (!inputBox.occupiedMatch && !inputBox.match)
|
|
66
|
+
missing.push("occupiedMatch");
|
|
67
|
+
return missing;
|
|
68
|
+
}
|
|
48
69
|
export function channelsFromConfig(adapterId, cfg) {
|
|
49
70
|
const e = cfg.tiers[adapterId];
|
|
50
71
|
if (!e)
|
|
51
72
|
return [];
|
|
52
|
-
return Object.entries(e.models).
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
return Object.entries(e.models).flatMap(([model, tier]) => {
|
|
74
|
+
const override = e.modelOverrides?.[model];
|
|
75
|
+
const vendor = override?.vendor ?? e.vendor;
|
|
76
|
+
// Parsed configs guarantee this only occurs for invalid data, but direct callers must fail closed
|
|
77
|
+
// too: an undeclared vendor is not review diversity and provider prefixes are never provenance.
|
|
78
|
+
if (!vendor?.trim())
|
|
79
|
+
return [];
|
|
80
|
+
return [{
|
|
81
|
+
adapter: adapterId,
|
|
82
|
+
vendor,
|
|
83
|
+
model,
|
|
84
|
+
channel: override?.channel ?? e.channel,
|
|
85
|
+
tier,
|
|
86
|
+
}];
|
|
87
|
+
});
|
|
55
88
|
}
|
|
56
89
|
export function channelKey(c) {
|
|
57
90
|
return `${c.adapter}:${c.model}`;
|