pi-codebuddy-sdk 0.4.29 → 0.4.31
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 +9 -2
- package/package.json +1 -1
- package/src/cli-runtime.ts +70 -0
- package/src/config.ts +5 -0
- package/src/index.ts +27 -5
- package/src/learned-windows.ts +120 -0
- package/src/models.ts +34 -5
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Restart `pi` if it was already running.
|
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
23
|
-
- **`codebuddy` on your `PATH`** — the extension spawns the same CLI you use standalone. If `which codebuddy` fails, either add it to `PATH` or set `pathToCodebuddyCode` in `codebuddy-sdk.json` (see [Configuration](#configuration)).
|
|
23
|
+
- **`codebuddy` on your `PATH`** — the extension spawns the same CLI you use standalone. If `which codebuddy` fails, either add it to `PATH` or set `pathToCodebuddyCode` in `codebuddy-sdk.json` (see [Configuration](#configuration)). That option selects the CLI entry point only; the JavaScript runtime used to run it comes from the host, and a Bun-compiled host without a `bun` on `PATH` needs `provider.executable` (see below).
|
|
24
24
|
- **CodeBuddy auth already working** — if `codebuddy` works in your terminal, you do not need extra setup for this extension.
|
|
25
25
|
|
|
26
26
|
## Quick start (already using CodeBuddy)
|
|
@@ -101,7 +101,8 @@ File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json` in a project.
|
|
|
101
101
|
| `askCodebuddy.allowFullMode` | `true` | Allow write-capable delegation mode |
|
|
102
102
|
| `provider.appendSystemPrompt` | `true` | Use Pi's system prompt instead of CodeBuddy's default identity |
|
|
103
103
|
| `provider.strictMcpConfig` | `true` | Use only Pi-bridged MCP tools |
|
|
104
|
-
| `provider.pathToCodebuddyCode` | auto | Path to `codebuddy` when it is **not** on `PATH
|
|
104
|
+
| `provider.pathToCodebuddyCode` | auto | Path to the `codebuddy` **CLI entry point** when it is **not** on `PATH`; does not change the JS runtime |
|
|
105
|
+
| `provider.executable` | host runtime | Runtime used to spawn the CLI (e.g. `"node"`). Set it when the host is Bun-compiled (no `bun` on `PATH`); auto-detected otherwise |
|
|
105
106
|
| `provider.contextWindow` | estimated | Override context window (tokens) for **all** models (e.g. `300000`) |
|
|
106
107
|
| `provider.maxTokens` | estimated | Override max output tokens for **all** models (e.g. `32768`) |
|
|
107
108
|
| `provider.modelOverrides` | — | Per-model overrides, keyed by model id or substring (see below) |
|
|
@@ -122,6 +123,12 @@ key wins and beats the global `contextWindow` / `maxTokens`:
|
|
|
122
123
|
|
|
123
124
|
Supported per-model keys: `contextWindow`, `maxTokens`, `reasoning`, `images`.
|
|
124
125
|
|
|
126
|
+
Window values the backend reports for a model are remembered per model id in
|
|
127
|
+
`~/.pi/agent/codebuddy-sdk-learned-windows.json`, so the next start registers the
|
|
128
|
+
last served values instead of the name estimate. Precedence: `modelOverrides` >
|
|
129
|
+
learned > served metadata > estimate. Delete that file to start from estimates
|
|
130
|
+
again (ids the backend no longer serves are dropped automatically).
|
|
131
|
+
|
|
125
132
|
## Privacy
|
|
126
133
|
|
|
127
134
|
- The extension does **not** send conversation data to this repository or any third-party telemetry endpoint.
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick a runtime the host can actually spawn for the CodeBuddy CLI subprocess.
|
|
3
|
+
*
|
|
4
|
+
* The Agent SDK resolves the child runtime from the *host* runtime
|
|
5
|
+
* (`@tencent-ai/agent-sdk/lib/transport/process-transport.js`, `resolveRuntime()`):
|
|
6
|
+
* when `process.versions.bun` is set it always spawns `bun`, and `node` is only
|
|
7
|
+
* the final default. Bun-compiled hosts — `omp`, or any single-file Bun binary —
|
|
8
|
+
* ship no `bun` executable on `PATH`, so every spawn fails with
|
|
9
|
+
* `Executable not found in $PATH: "bun"` and neither model discovery nor a turn
|
|
10
|
+
* can run.
|
|
11
|
+
*
|
|
12
|
+
* When the host is Bun and `bun` is not resolvable, hand the SDK an explicit
|
|
13
|
+
* `executable` (its branch 2) pointing at `node`. `provider.executable` in
|
|
14
|
+
* `codebuddy-sdk.json` overrides the automatic choice.
|
|
15
|
+
*/
|
|
16
|
+
import { accessSync, constants } from "node:fs";
|
|
17
|
+
import { delimiter, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface RuntimeHost {
|
|
20
|
+
/** `process.versions` */
|
|
21
|
+
versions?: Record<string, string | undefined>;
|
|
22
|
+
/** `process.env.PATH` */
|
|
23
|
+
path?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function currentHost(): RuntimeHost {
|
|
27
|
+
return { versions: process.versions as Record<string, string | undefined>, path: process.env.PATH };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when `name` is a runnable file in one of the `PATH` entries. */
|
|
31
|
+
export function isOnPath(name: string, pathValue: string | undefined): boolean {
|
|
32
|
+
if (!name || !pathValue) return false;
|
|
33
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
34
|
+
if (!dir) continue;
|
|
35
|
+
try {
|
|
36
|
+
accessSync(join(dir, name), constants.X_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
// Not here — keep looking.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The runtime the CLI subprocess needs, or `undefined` when the SDK's own choice
|
|
47
|
+
* works (a node/deno host, or a Bun host that has a `bun` on `PATH`).
|
|
48
|
+
*/
|
|
49
|
+
export function resolveCliRuntime(host: RuntimeHost = currentHost()): string | undefined {
|
|
50
|
+
if (!host.versions?.bun) return undefined;
|
|
51
|
+
if (isOnPath("bun", host.path)) return undefined;
|
|
52
|
+
return isOnPath("node", host.path) ? "node" : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return `args` with `options.executable` filled in when the host cannot spawn
|
|
57
|
+
* the SDK's default runtime. An explicit `executable` — on the call or in the
|
|
58
|
+
* config — always wins.
|
|
59
|
+
*/
|
|
60
|
+
export function withCliRuntime<T extends { options?: { executable?: string } }>(
|
|
61
|
+
args: T,
|
|
62
|
+
configuredExecutable?: string,
|
|
63
|
+
host: RuntimeHost = currentHost(),
|
|
64
|
+
): T {
|
|
65
|
+
const options = args?.options;
|
|
66
|
+
if (options?.executable) return args;
|
|
67
|
+
const executable = configuredExecutable?.trim() || resolveCliRuntime(host);
|
|
68
|
+
if (!executable) return args;
|
|
69
|
+
return { ...args, options: { ...options, executable } };
|
|
70
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface Config {
|
|
|
27
27
|
settingSources?: SettingSource[];
|
|
28
28
|
strictMcpConfig?: boolean;
|
|
29
29
|
pathToCodebuddyCode?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Runtime used to spawn the CodeBuddy CLI (e.g. `"node"`). Defaults to the
|
|
32
|
+
* host runtime; set this when a Bun-compiled host has no `bun` on `PATH`.
|
|
33
|
+
*/
|
|
34
|
+
executable?: string;
|
|
30
35
|
/** Override the estimated context window (tokens) for ALL models. */
|
|
31
36
|
contextWindow?: number;
|
|
32
37
|
/** Override the estimated max output tokens for ALL models. */
|
package/src/index.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { appendFileSync, mkdirSync, realpathSync, statSync } from "fs";
|
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { dirname, join } from "path";
|
|
11
11
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
12
|
-
import { buildModels, codebuddyModelId, FALLBACK_MODELS, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
12
|
+
import { buildModels, codebuddyModelId, FALLBACK_MODELS, matchesAnyOverrideKey, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
13
13
|
import { readModelsCache, writeModelsCache } from "./models-cache.js";
|
|
14
|
+
import { applyLearnedWindows, pruneLearnedWindows, readLearnedWindows, recordServedWindows, writeLearnedWindows } from "./learned-windows.js";
|
|
14
15
|
import { clearOwnedStream, providerRegistration } from "./provider-registration.js";
|
|
15
16
|
import { syncServedWindow } from "./served-window.js";
|
|
17
|
+
import { withCliRuntime } from "./cli-runtime.js";
|
|
16
18
|
import { readTranscript } from "./transcript-compat.js";
|
|
17
19
|
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, buildCodebuddySystemPrompt } from "./skills.js";
|
|
18
20
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
@@ -165,7 +167,7 @@ function installTransportTeardownGuard(sdkQuery: unknown): void {
|
|
|
165
167
|
|
|
166
168
|
/** Create an SDK query and immediately arm the transport teardown guard. */
|
|
167
169
|
function startQuery(args: Parameters<typeof query>[0]): ReturnType<typeof query> {
|
|
168
|
-
const q = query(args);
|
|
170
|
+
const q = query(withCliRuntime(args, providerSettings.executable));
|
|
169
171
|
try {
|
|
170
172
|
installTransportTeardownGuard(q);
|
|
171
173
|
} catch (err) {
|
|
@@ -853,10 +855,18 @@ function buildMcpServers(tools: Tool[], queryCtx: QueryContext): Record<string,
|
|
|
853
855
|
// --- Usage helpers ---
|
|
854
856
|
|
|
855
857
|
function updateUsage(output: AssistantMessage, usage: Record<string, number | undefined>, model: Model<any>): void {
|
|
856
|
-
|
|
858
|
+
// CodeBuddy reports OpenAI/DeepSeek-style usage: input_tokens covers the whole prompt and
|
|
859
|
+
// cache_read_input_tokens is the cached slice of that same prompt. pi's Usage type is
|
|
860
|
+
// Anthropic-style, where input holds only the uncached remainder, because pi derives
|
|
861
|
+
// promptTokens = input + cacheRead + cacheWrite (core/cache-stats). Storing the total in
|
|
862
|
+
// the input field therefore counts the cached prompt twice and makes pi report bogus
|
|
863
|
+
// 'Cache miss: <n> tokens re-billed' notices. Mirrors pi-ai's openai-completions adapter.
|
|
864
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
865
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
866
|
+
if (usage.input_tokens != null) output.usage.input = Math.max(0, usage.input_tokens - cacheReadTokens - cacheWriteTokens);
|
|
857
867
|
if (usage.output_tokens != null) output.usage.output = usage.output_tokens;
|
|
858
|
-
if (usage.cache_read_input_tokens != null) output.usage.cacheRead =
|
|
859
|
-
if (usage.cache_creation_input_tokens != null) output.usage.cacheWrite =
|
|
868
|
+
if (usage.cache_read_input_tokens != null) output.usage.cacheRead = cacheReadTokens;
|
|
869
|
+
if (usage.cache_creation_input_tokens != null) output.usage.cacheWrite = cacheWriteTokens;
|
|
860
870
|
// CodeBuddy may report reasoning/thinking tokens separately, while pi's Usage type does not model that field.
|
|
861
871
|
const reasoning = usage.reasoning_tokens ?? usage.thinking_tokens;
|
|
862
872
|
if (reasoning != null) (output.usage as typeof output.usage & { reasoning?: number }).reasoning = reasoning;
|
|
@@ -888,6 +898,12 @@ function syncServedContextWindow(label: string, message: CbMessage, model: Model
|
|
|
888
898
|
for (const change of changes) {
|
|
889
899
|
debug(`${label}: ${change.id} ${change.field} ${change.from} -> ${change.to}`);
|
|
890
900
|
}
|
|
901
|
+
// Remember the served values so the next process starts from them instead of
|
|
902
|
+
// re-registering the estimate (this backend reports no window metadata).
|
|
903
|
+
if (changes.length) {
|
|
904
|
+
const learned = readLearnedWindows();
|
|
905
|
+
if (recordServedWindows(learned, changes)) writeLearnedWindows(learned);
|
|
906
|
+
}
|
|
891
907
|
}
|
|
892
908
|
|
|
893
909
|
// --- Effort level mapping ---
|
|
@@ -1727,6 +1743,12 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
|
1727
1743
|
providerSettings.modelOverrides ? undefined : providerSettings,
|
|
1728
1744
|
providerSettings.modelOverrides,
|
|
1729
1745
|
);
|
|
1746
|
+
// Re-apply windows learned in earlier processes: they beat the name
|
|
1747
|
+
// estimate and the served metadata, but never a user override. Ids the
|
|
1748
|
+
// backend no longer serves are dropped from the store.
|
|
1749
|
+
const learned = pruneLearnedWindows(readLearnedWindows(), MODELS.map((m) => m.id));
|
|
1750
|
+
MODELS = applyLearnedWindows(MODELS, learned, (id) => matchesAnyOverrideKey(id, providerSettings.modelOverrides));
|
|
1751
|
+
writeLearnedWindows(learned);
|
|
1730
1752
|
// Persist the successful discovery globally so a later session
|
|
1731
1753
|
// resume (module reload) can recover the real model list even if
|
|
1732
1754
|
// discovery fails that time (e.g. SDK subprocess gate is busy).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Served context windows, remembered across processes.
|
|
3
|
+
*
|
|
4
|
+
* This backend reports no window metadata, so every model is registered from a
|
|
5
|
+
* name estimate. The served value only arrives per turn, inside `modelUsage`;
|
|
6
|
+
* `syncServedWindow` applies it to the catalog entry and the live model, but
|
|
7
|
+
* only in memory. Without persistence that knowledge dies with the process:
|
|
8
|
+
* the next `discoverModels` re-registers the estimate and rewrites the model
|
|
9
|
+
* cache, so each restart shows the guess again for every model the user has not
|
|
10
|
+
* overridden by hand.
|
|
11
|
+
*
|
|
12
|
+
* Keep the last served value per model id on disk and re-apply it after
|
|
13
|
+
* `buildModels()`. Precedence: user `modelOverrides` > learned > SDK metadata >
|
|
14
|
+
* name estimate.
|
|
15
|
+
*/
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { dirname, join } from "path";
|
|
18
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
19
|
+
import type { WindowSyncChange } from "./served-window.js";
|
|
20
|
+
|
|
21
|
+
export interface LearnedWindow {
|
|
22
|
+
contextWindow?: number;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LearnedWindows = Record<string, LearnedWindow>;
|
|
27
|
+
|
|
28
|
+
/** In-process cache so repeated reads in one session do not touch the disk. */
|
|
29
|
+
export const LEARNED_WINDOWS_KEY = Symbol.for("codebuddy-sdk:learnedWindows");
|
|
30
|
+
|
|
31
|
+
const defaultStoreFile = () => join(homedir(), ".pi", "agent", "codebuddy-sdk-learned-windows.json");
|
|
32
|
+
|
|
33
|
+
// Injectable for tests.
|
|
34
|
+
const storeFile = () => process.env.CODEBUDDY_SDK_LEARNED_WINDOWS_PATH || defaultStoreFile();
|
|
35
|
+
|
|
36
|
+
function sanitize(value: unknown): LearnedWindow | undefined {
|
|
37
|
+
if (!value || typeof value !== "object") return undefined;
|
|
38
|
+
const raw = value as LearnedWindow;
|
|
39
|
+
const entry: LearnedWindow = {};
|
|
40
|
+
if (Number.isSafeInteger(raw.contextWindow) && (raw.contextWindow as number) > 0) entry.contextWindow = raw.contextWindow;
|
|
41
|
+
if (Number.isSafeInteger(raw.maxTokens) && (raw.maxTokens as number) > 0) entry.maxTokens = raw.maxTokens;
|
|
42
|
+
return Object.keys(entry).length ? entry : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readLearnedWindows(): LearnedWindows {
|
|
46
|
+
const inProcess = (globalThis as Record<symbol, unknown>)[LEARNED_WINDOWS_KEY] as LearnedWindows | undefined;
|
|
47
|
+
if (inProcess) return inProcess;
|
|
48
|
+
try {
|
|
49
|
+
const parsed: unknown = JSON.parse(readFileSync(storeFile(), "utf8"));
|
|
50
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
51
|
+
const map: LearnedWindows = {};
|
|
52
|
+
for (const [id, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
53
|
+
const entry = sanitize(value);
|
|
54
|
+
if (entry) map[id] = entry;
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// No store yet, or unreadable — the caller falls back to estimates.
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function writeLearnedWindows(map: LearnedWindows): void {
|
|
65
|
+
(globalThis as Record<symbol, unknown>)[LEARNED_WINDOWS_KEY] = map;
|
|
66
|
+
try {
|
|
67
|
+
const path = storeFile();
|
|
68
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
69
|
+
const tmp = `${path}.tmp`;
|
|
70
|
+
const sorted = Object.fromEntries(Object.entries(map).sort(([a], [b]) => a.localeCompare(b)));
|
|
71
|
+
writeFileSync(tmp, JSON.stringify(sorted));
|
|
72
|
+
renameSync(tmp, path);
|
|
73
|
+
} catch {
|
|
74
|
+
// Store is best-effort; the next turn re-learns the value.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Fold served-window changes into `map`. Returns true when something moved. */
|
|
79
|
+
export function recordServedWindows(map: LearnedWindows, changes: readonly WindowSyncChange[]): boolean {
|
|
80
|
+
let touched = false;
|
|
81
|
+
for (const change of changes) {
|
|
82
|
+
if (!Number.isSafeInteger(change.to) || change.to <= 0) continue;
|
|
83
|
+
const entry = (map[change.id] ??= {});
|
|
84
|
+
if (entry[change.field] === change.to) continue;
|
|
85
|
+
entry[change.field] = change.to;
|
|
86
|
+
touched = true;
|
|
87
|
+
}
|
|
88
|
+
return touched;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Drop ids the backend no longer serves. */
|
|
92
|
+
export function pruneLearnedWindows(map: LearnedWindows, ids: Iterable<string>): LearnedWindows {
|
|
93
|
+
const keep = new Set(ids);
|
|
94
|
+
const out: LearnedWindows = {};
|
|
95
|
+
for (const [id, value] of Object.entries(map)) {
|
|
96
|
+
if (keep.has(id)) out[id] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Overwrite estimated windows with learned ones. Matching is by exact id on
|
|
103
|
+
* purpose — override keys use substring matching, learned ids must not leak into
|
|
104
|
+
* neighbouring models. `skip` keeps user overrides authoritative.
|
|
105
|
+
*/
|
|
106
|
+
export function applyLearnedWindows<T extends { id: string; contextWindow: number; maxTokens: number }>(
|
|
107
|
+
models: readonly T[],
|
|
108
|
+
learned: LearnedWindows,
|
|
109
|
+
skip?: (id: string) => boolean,
|
|
110
|
+
): T[] {
|
|
111
|
+
return models.map((model) => {
|
|
112
|
+
const entry = learned[model.id];
|
|
113
|
+
if (!entry || skip?.(model.id)) return model;
|
|
114
|
+
return {
|
|
115
|
+
...model,
|
|
116
|
+
contextWindow: entry.contextWindow ?? model.contextWindow,
|
|
117
|
+
maxTokens: entry.maxTokens ?? model.maxTokens,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -24,12 +24,28 @@ type SdkModelMeta = {
|
|
|
24
24
|
contextWindow?: { supportedLengths?: ReadonlyArray<number>; defaultLength?: number };
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// Capabilities are inferred from the model id: this backend returns `{id, name}`
|
|
28
|
+
// only, so there is no metadata to read (unlike the window, capabilities cannot
|
|
29
|
+
// be learned from `modelUsage` either). `modelOverrides.reasoning` / `.images`
|
|
30
|
+
// still win over every hint below.
|
|
31
|
+
//
|
|
32
|
+
// The families cover the CodeBuddy catalog, whose entries are reasoning-only and
|
|
33
|
+
// image-capable apart from the two text-only models called out in
|
|
34
|
+
// `TEXT_ONLY_MODELS`. Reasoning hints stay broad (`glm`, `deepseek`, …) and image
|
|
35
|
+
// hints are version-scoped so an unverified model keeps the conservative default.
|
|
36
|
+
const REASONING_HINTS = /claude|gemini|gpt-5|glm|hy[34]|deepseek|kimi|minimax|step|space/i;
|
|
37
|
+
const IMAGE_HINTS = /claude|gemini|gpt|glm-5[.v]|hy[34]|deepseek-v4|kimi-k[23]|minimax-m[23]|step|space/i;
|
|
38
|
+
// Verified text-only: the CodeBuddy catalog marks both `supportsImages: false`,
|
|
39
|
+
// and a 1x1 PNG probe came back with "the current model doesn't support image
|
|
40
|
+
// reading" from each.
|
|
41
|
+
const TEXT_ONLY_MODELS = new Set(["glm-5.1", "glm-5.2"]);
|
|
42
|
+
|
|
27
43
|
function detectThinking(id: string): boolean {
|
|
28
|
-
return
|
|
44
|
+
return REASONING_HINTS.test(id);
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
function detectImages(id: string): boolean {
|
|
32
|
-
return
|
|
48
|
+
return !TEXT_ONLY_MODELS.has(id.toLowerCase()) && IMAGE_HINTS.test(id);
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
function estimateContext(id: string): number {
|
|
@@ -87,6 +103,21 @@ export type ModelOverrides = {
|
|
|
87
103
|
* (matched by exact id first, then case-insensitive substring, longest key
|
|
88
104
|
* wins) and beats the global defaults.
|
|
89
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* True when a `modelOverrides` key selects `id`: exact match, or a
|
|
108
|
+
* case-insensitive substring.
|
|
109
|
+
*/
|
|
110
|
+
export function overrideKeyMatches(id: string, key: string): boolean {
|
|
111
|
+
const k = key.toLowerCase();
|
|
112
|
+
return id === key || (k.length > 0 && id.toLowerCase().includes(k));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** True when any `modelOverrides` key selects `id`. */
|
|
116
|
+
export function matchesAnyOverrideKey(id: string, perModel?: Record<string, unknown>): boolean {
|
|
117
|
+
if (!perModel) return false;
|
|
118
|
+
return Object.keys(perModel).some((key) => overrideKeyMatches(id, key));
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
export function buildModels(
|
|
91
122
|
models: PiModel[],
|
|
92
123
|
globalOverrides?: ModelOverrides,
|
|
@@ -94,10 +125,8 @@ export function buildModels(
|
|
|
94
125
|
): PiModel[] {
|
|
95
126
|
const sortedKeys = Object.keys(perModel ?? {}).sort((a, b) => b.length - a.length);
|
|
96
127
|
const match = (id: string): ModelOverrides | undefined => {
|
|
97
|
-
const lower = id.toLowerCase();
|
|
98
128
|
for (const key of sortedKeys) {
|
|
99
|
-
|
|
100
|
-
if (id === key || (lower.includes(k) && k.length > 0)) return perModel![key];
|
|
129
|
+
if (overrideKeyMatches(id, key)) return perModel![key];
|
|
101
130
|
}
|
|
102
131
|
return undefined;
|
|
103
132
|
};
|