hob-bot 1.0.4 → 1.1.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 +75 -5
- package/README.zh-CN.md +75 -5
- package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
- package/dist/web/assets/index-B-8ISOr1.js +267 -0
- package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
- package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
- package/dist/web/index.html +2 -2
- package/dist/web/mock/desktop.jpg +0 -0
- package/dist/web/mock/photo.jpg +0 -0
- package/dist/web/mock/photo2.jpg +0 -0
- package/dist/web/mock/sunset.html +90 -0
- package/package.json +7 -1
- package/server/src/actions.ts +16 -56
- package/server/src/assistant-skills.ts +201 -0
- package/server/src/boot.ts +1 -0
- package/server/src/claude-status.ts +47 -0
- package/server/src/config.ts +3 -1
- package/server/src/delegations.ts +21 -11
- package/server/src/desktop-control.ts +152 -0
- package/server/src/desktop-events.ts +63 -0
- package/server/src/desktop-public.ts +43 -0
- package/server/src/desktop-tools.ts +161 -0
- package/server/src/desktop.ts +76 -0
- package/server/src/embedding.ts +52 -0
- package/server/src/files.ts +63 -43
- package/server/src/http.ts +325 -13
- package/server/src/jobs.ts +280 -41
- package/server/src/knowledge-index.ts +123 -0
- package/server/src/knowledge-tools.ts +94 -0
- package/server/src/knowledge.ts +451 -0
- package/server/src/lore-entry.ts +10 -0
- package/server/src/main.ts +6 -0
- package/server/src/mcp.ts +745 -0
- package/server/src/memory.ts +11 -4
- package/server/src/now.ts +33 -0
- package/server/src/overview.ts +1 -1
- package/server/src/pages.ts +1 -1
- package/server/src/pi.ts +92 -16
- package/server/src/prompt.ts +46 -156
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +39 -5
- package/server/src/router-activity.ts +4 -0
- package/server/src/router-tools.ts +132 -38
- package/server/src/router.ts +153 -17
- package/server/src/routines.ts +14 -4
- package/server/src/runtime.ts +15 -0
- package/server/src/settings.ts +43 -1
- package/server/src/store.ts +4 -3
- package/server/src/transcript.ts +57 -4
- package/server/src/worker-tools.ts +14 -12
- package/shared/knowledge.ts +92 -0
- package/shared/mcp.ts +91 -0
- package/shared/skills.ts +18 -0
- package/shared/types.ts +103 -20
- package/vendor/claude-bridge/HOB-PATCH.md +10 -0
- package/vendor/claude-bridge/LICENSE +21 -0
- package/vendor/claude-bridge/package.json +62 -0
- package/vendor/claude-bridge/src/agents-md.ts +14 -0
- package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
- package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
- package/vendor/claude-bridge/src/attachments.ts +135 -0
- package/vendor/claude-bridge/src/config.ts +105 -0
- package/vendor/claude-bridge/src/convert.ts +222 -0
- package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
- package/vendor/claude-bridge/src/index.ts +2503 -0
- package/vendor/claude-bridge/src/log-paths.ts +19 -0
- package/vendor/claude-bridge/src/mcp-server.ts +169 -0
- package/vendor/claude-bridge/src/models.ts +164 -0
- package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
- package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
- package/vendor/claude-bridge/src/query-state.ts +118 -0
- package/vendor/claude-bridge/src/rate-limit.ts +48 -0
- package/vendor/claude-bridge/src/session-cwd.ts +38 -0
- package/vendor/claude-bridge/src/session-verify.ts +38 -0
- package/vendor/claude-bridge/src/skills.ts +20 -0
- package/vendor/claude-bridge/src/transcript.ts +86 -0
- package/vendor/claude-bridge/src/usage.ts +53 -0
- package/vendor/lore-pi-extension/package.json +1 -1
- package/vendor/lore-pi-extension/tools.ts +8 -5
- package/dist/web/assets/index-BCGEZvgg.css +0 -1
- package/dist/web/assets/index-BqkIAlk9.js +0 -258
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge's debug and diagnostics logs live in pi's agent dir, so a layout
|
|
3
|
+
* that sets PI_CODING_AGENT_DIR keeps them beside pi's other state instead of a
|
|
4
|
+
* fixed ~/.pi/agent. The debug log alone stays overridable, because the test
|
|
5
|
+
* harness and the probes redirect it to a throwaway path.
|
|
6
|
+
*
|
|
7
|
+
* Paths resolve per call rather than at import time, so tests can swap the
|
|
8
|
+
* environment mid-process and see the new paths immediately.
|
|
9
|
+
*/
|
|
10
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
export function debugLogPath(): string {
|
|
14
|
+
return process.env.CLAUDE_BRIDGE_DEBUG_PATH || join(getAgentDir(), "claude-bridge.log");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function diagLogPath(): string {
|
|
18
|
+
return join(getAgentDir(), "claude-bridge-diag.log");
|
|
19
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// In-process MCP server that exposes pi tools to Claude Code.
|
|
2
|
+
//
|
|
3
|
+
// Pi declares tool parameters as TypeBox objects, which are already JSON
|
|
4
|
+
// Schema at runtime — the same thing MCP puts on the wire. This serves them
|
|
5
|
+
// verbatim instead of going through the SDK's `createSdkMcpServer`, which only
|
|
6
|
+
// accepts Zod and therefore forces a JSON Schema → Zod → JSON Schema round
|
|
7
|
+
// trip. That round trip is lossy below the top level: nested objects collapse
|
|
8
|
+
// to open records and `anyOf`/`const` vanish, so Claude saw only the first
|
|
9
|
+
// level of any tool with a nested schema — including the builtin `edit`.
|
|
10
|
+
// The one exception is a top-level combinator, flattened below because Claude
|
|
11
|
+
// Code drops a tool whose schema has one.
|
|
12
|
+
//
|
|
13
|
+
// Handlers go on the underlying protocol server rather than through
|
|
14
|
+
// `McpServer.registerTool`, which is the Zod-only path. Skipping registerTool
|
|
15
|
+
// also skips its argument validation, which is what we want: pi validates and
|
|
16
|
+
// executes tools itself, and the arguments MCP sees are discarded. A rejection
|
|
17
|
+
// there would only prevent the handler from running, stranding the call.
|
|
18
|
+
//
|
|
19
|
+
// This rests on the Agent SDK treating what we hand it as an opaque JSON-RPC
|
|
20
|
+
// endpoint: `connectSdkMcpServer` in sdk.mjs calls `instance.connect(transport)`
|
|
21
|
+
// and nothing else, so none of McpServer's higher-level machinery is required.
|
|
22
|
+
// The `McpServer` wrapper is kept only because the SDK's `mcpServers` option is
|
|
23
|
+
// typed against that class. If this breaks after an SDK update, check whether
|
|
24
|
+
// the SDK began inspecting the instance — reading registered tools, or expecting
|
|
25
|
+
// tools/list_changed notifications we never send.
|
|
26
|
+
|
|
27
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
29
|
+
import type { McpResult } from "./extract-tool-results.js";
|
|
30
|
+
|
|
31
|
+
// Claude Code stamps every tools/call with the id of the tool_use block it came
|
|
32
|
+
// from. That is the only reliable way to pair a call with its result: call order
|
|
33
|
+
// is not guaranteed to match the order the tool_use blocks were emitted, so
|
|
34
|
+
// counting calls mispairs results as soon as the two diverge.
|
|
35
|
+
//
|
|
36
|
+
// This is a Claude Code extension, not part of the MCP spec — CC sets it in
|
|
37
|
+
// `src/services/mcp/client.ts` (see reference-code/claude-code-rip). If CC ever
|
|
38
|
+
// stops sending it, every tool call fails with the error below rather than
|
|
39
|
+
// silently pairing results to the wrong call, which is the intended tradeoff.
|
|
40
|
+
const TOOL_USE_ID_META = "claudecode/toolUseId";
|
|
41
|
+
|
|
42
|
+
export interface McpToolDef {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
inputSchema: unknown;
|
|
46
|
+
handler: (toolCallId: string) => Promise<McpResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MCP requires an object schema. Pi types tool parameters as any TypeBox schema,
|
|
50
|
+
// so a scalar or array one typechecks but cannot go on the wire — that is a bug
|
|
51
|
+
// in the tool, and reporting it at startup names the culprit. Degrading it to
|
|
52
|
+
// "takes no arguments" instead would surface much later as Claude calling the
|
|
53
|
+
// tool with no arguments and pi's own validation rejecting them.
|
|
54
|
+
function assertObjectSchema(tool: McpToolDef): void {
|
|
55
|
+
const schema = tool.inputSchema as Record<string, unknown> | undefined;
|
|
56
|
+
if (!schema || schema.type !== "object") {
|
|
57
|
+
throw new Error(`${tool.name}: MCP tool parameters must be an object schema, got ${JSON.stringify(schema)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Claude Code drops an MCP tool outright when its input schema has a top-level
|
|
62
|
+
// anyOf/oneOf/allOf ("its input schema uses top-level anyOf, which the Anthropic
|
|
63
|
+
// API does not accept"; CC's own normalizer for this is gated off for our
|
|
64
|
+
// server). The tool is still in our served-name map, so a call Claude makes to it
|
|
65
|
+
// anyway is forwarded to pi and runs there while CC answers "No such tool
|
|
66
|
+
// available" and retries under a fresh id — the tool runs twice, or the retry's
|
|
67
|
+
// handler waits forever. Advertise a flat object instead, built the way CC's
|
|
68
|
+
// normalizer does: root properties win over branch ones, root `required` stays,
|
|
69
|
+
// and allOf requirements join it (anyOf/oneOf ones hold only for one branch).
|
|
70
|
+
// Like CC, branch `$ref`s into the root's `$defs`/`definitions` are followed,
|
|
71
|
+
// and the lost constraint is restated as a note for the tool description, so
|
|
72
|
+
// Claude's first call is likely to pass pi's validation against the full schema.
|
|
73
|
+
const ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"] as const;
|
|
74
|
+
|
|
75
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveLocalRef(branch: unknown, root: Record<string, unknown>): unknown {
|
|
80
|
+
if (!isRecord(branch) || typeof branch.$ref !== "string") return branch;
|
|
81
|
+
const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(branch.$ref);
|
|
82
|
+
const defs = match && root[match[1]];
|
|
83
|
+
const target = match && isRecord(defs) ? defs[match[2]] : undefined;
|
|
84
|
+
return isRecord(target) ? target : branch;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A branch is named by its required keys, else by all of its keys.
|
|
88
|
+
function branchGroup(branch: unknown): string | null {
|
|
89
|
+
if (!isRecord(branch)) return null;
|
|
90
|
+
const { required, properties } = branch;
|
|
91
|
+
if (Array.isArray(required) && required.length > 0 && required.every((key) => typeof key === "string")) return required.join(", ");
|
|
92
|
+
const keys = isRecord(properties) ? Object.keys(properties) : [];
|
|
93
|
+
return keys.length > 0 ? keys.join(", ") : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// CC's wording, so the tool reads the same as one CC normalized itself.
|
|
97
|
+
function combinatorNote(combinators: readonly string[], branchesOf: (key: string) => unknown[]): string {
|
|
98
|
+
if (!combinators.includes("anyOf") && !combinators.includes("oneOf")) {
|
|
99
|
+
return "Input constraint: all listed parameters apply together (flattened from a JSON Schema allOf).";
|
|
100
|
+
}
|
|
101
|
+
const key = combinators.includes("oneOf") ? "oneOf" : "anyOf";
|
|
102
|
+
const rule = key === "oneOf" ? "Provide parameters for exactly one of" : "Provide parameters for at least one of";
|
|
103
|
+
const groups = [...new Set(branchesOf(key).map(branchGroup).filter((group) => group !== null))];
|
|
104
|
+
if (groups.length === 0) return `Input constraint: ${rule} the documented parameter groups (flattened from a JSON Schema ${key}).`;
|
|
105
|
+
return `Input constraint: ${rule}: ${groups.map((group) => `(${group})`).join(" or ")}.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function flattenRootCombinators(schema: Record<string, unknown>): { schema: Record<string, unknown>; note?: string } {
|
|
109
|
+
const combinators = ROOT_COMBINATORS.filter((key) => key in schema);
|
|
110
|
+
if (combinators.length === 0) return { schema };
|
|
111
|
+
|
|
112
|
+
const { anyOf: _anyOf, oneOf: _oneOf, allOf: _allOf, ...rest } = schema;
|
|
113
|
+
const branchesOf = (key: string) => {
|
|
114
|
+
const branches = schema[key];
|
|
115
|
+
return Array.isArray(branches) ? branches.map((branch) => resolveLocalRef(branch, schema)) : [];
|
|
116
|
+
};
|
|
117
|
+
const properties: Record<string, unknown> = {};
|
|
118
|
+
const required: string[] = [];
|
|
119
|
+
const addProperties = (source: unknown) => {
|
|
120
|
+
if (!isRecord(source) || !isRecord(source.properties)) return;
|
|
121
|
+
for (const [key, value] of Object.entries(source.properties)) if (!(key in properties)) properties[key] = value;
|
|
122
|
+
};
|
|
123
|
+
const addRequired = (source: unknown) => {
|
|
124
|
+
if (!isRecord(source) || !Array.isArray(source.required)) return;
|
|
125
|
+
for (const key of source.required) if (typeof key === "string" && !required.includes(key)) required.push(key);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
addProperties(schema);
|
|
129
|
+
for (const key of combinators) for (const branch of branchesOf(key)) addProperties(branch);
|
|
130
|
+
addRequired(schema);
|
|
131
|
+
for (const branch of branchesOf("allOf")) addRequired(branch);
|
|
132
|
+
|
|
133
|
+
const flat: Record<string, unknown> = { ...rest, type: "object", properties };
|
|
134
|
+
if (required.length > 0) flat.required = required;
|
|
135
|
+
else delete flat.required;
|
|
136
|
+
return { schema: flat, note: combinatorNote(combinators, branchesOf) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createToolServer(name: string, tools: McpToolDef[]) {
|
|
140
|
+
const server = new McpServer({ name, version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
141
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
142
|
+
for (const tool of tools) assertObjectSchema(tool);
|
|
143
|
+
|
|
144
|
+
server.server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
145
|
+
tools: tools.map((tool) => {
|
|
146
|
+
const { schema, note } = flattenRootCombinators(tool.inputSchema as Record<string, unknown>);
|
|
147
|
+
return {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: [note, tool.description].filter(Boolean).join("\n\n"),
|
|
150
|
+
inputSchema: schema,
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
156
|
+
const tool = byName.get(request.params.name);
|
|
157
|
+
if (!tool) throw new Error(`Unknown tool: ${request.params.name}`);
|
|
158
|
+
const toolCallId = request.params._meta?.[TOOL_USE_ID_META];
|
|
159
|
+
if (typeof toolCallId !== "string") {
|
|
160
|
+
throw new Error(`${tool.name}: tools/call is missing _meta["${TOOL_USE_ID_META}"] — cannot pair the result with its tool call`);
|
|
161
|
+
}
|
|
162
|
+
// Narrowed deliberately: McpResult also carries `toolCallId`, which is our
|
|
163
|
+
// own bookkeeping for pairing and not part of MCP's CallToolResult.
|
|
164
|
+
const { content, isError } = await tool.handler(toolCallId);
|
|
165
|
+
return { content, isError };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return { type: "sdk" as const, name, instance: server };
|
|
169
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Model selection + display-order policy for the model picker. The picker is
|
|
2
|
+
// driven by pi-ai's anthropic catalog: models appear (and disappear) with it,
|
|
3
|
+
// no per-model code here. Extracted from index.ts so tests can import without
|
|
4
|
+
// activating the extension.
|
|
5
|
+
// `resolveModel` resolves family shortcuts (opus/sonnet/fable) to the newest
|
|
6
|
+
// matching id regardless of sort order; sort order only drives picker display.
|
|
7
|
+
|
|
8
|
+
const TWO_HUNDRED_K_CONTEXT = 200_000;
|
|
9
|
+
const ONE_M_CONTEXT = 1_000_000;
|
|
10
|
+
|
|
11
|
+
// pi-ai ships dated snapshot ids (claude-opus-4-5-20251101, ...) alongside the
|
|
12
|
+
// bare ids. They are never exposed - and must not steal first-partial-match
|
|
13
|
+
// shortcuts like "opus-4-5" from the bare id.
|
|
14
|
+
function isDatedAlias(id: string): boolean {
|
|
15
|
+
return /-20\d{6}$/.test(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Family tiers for display order: flagship families first; unknown families
|
|
19
|
+
// sink below all known ones.
|
|
20
|
+
const FAMILY_ORDER = ["fable", "opus", "sonnet", "haiku"];
|
|
21
|
+
|
|
22
|
+
// Project pi-ai's model entries down to the fields pi's registerProvider expects,
|
|
23
|
+
// newest generation first. Context-dependent display labels are applied after
|
|
24
|
+
// plan/long-context config is known.
|
|
25
|
+
// Version rank of a claude id, e.g. claude-opus-4-7 → ["opus", 4, 7]. Shared by
|
|
26
|
+
// the display sort and resolveModel's newest-first partial tiebreak.
|
|
27
|
+
function versionRank(id: string): { family: string; tuple: [number, number] } {
|
|
28
|
+
const [, family, major, minor] = id.split("-");
|
|
29
|
+
return { family, tuple: [Number(major) || 0, Number(minor) || 0] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildModels<T extends { id: string; [key: string]: any }>(piAiModels: T[]) {
|
|
33
|
+
return piAiModels
|
|
34
|
+
.filter((m) => typeof m.id === "string" && !isDatedAlias(m.id))
|
|
35
|
+
.sort((a, b) => {
|
|
36
|
+
const fa = FAMILY_ORDER.indexOf(versionRank(a.id).family);
|
|
37
|
+
const fb = FAMILY_ORDER.indexOf(versionRank(b.id).family);
|
|
38
|
+
const ta = fa === -1 ? FAMILY_ORDER.length : fa;
|
|
39
|
+
const tb = fb === -1 ? FAMILY_ORDER.length : fb;
|
|
40
|
+
if (ta !== tb) return ta - tb;
|
|
41
|
+
const ra = versionRank(a.id).tuple;
|
|
42
|
+
const rb = versionRank(b.id).tuple;
|
|
43
|
+
if (ra[0] !== rb[0]) return rb[0] - ra[0];
|
|
44
|
+
if (ra[1] !== rb[1]) return rb[1] - ra[1];
|
|
45
|
+
return a.id.localeCompare(b.id);
|
|
46
|
+
})
|
|
47
|
+
// Forward thinkingLevelMap so pi-ai's per-model overrides (e.g. opus-4-8
|
|
48
|
+
// mapping xhigh→xhigh and max→max) are visible to the effort lookup.
|
|
49
|
+
.map(({ id, name, reasoning, input, contextWindow, maxTokens, thinkingLevelMap }) => ({
|
|
50
|
+
id,
|
|
51
|
+
name,
|
|
52
|
+
reasoning, input, contextWindow, maxTokens,
|
|
53
|
+
thinkingLevelMap,
|
|
54
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type LongContextSettings = {
|
|
59
|
+
plan: "pro" | "max";
|
|
60
|
+
longContextExtraUsage: boolean;
|
|
61
|
+
// Model ids whose declared 1M context Claude Code turned out not to serve;
|
|
62
|
+
// forces bare id at 200K without a code change.
|
|
63
|
+
forceTwoHundredK?: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ClaudeCodeRuntimeModel = {
|
|
67
|
+
cliModelId: string;
|
|
68
|
+
contextWindow: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Measured Claude Agent SDK behavior - see diag/CONTEXT-SIZE.md:
|
|
72
|
+
// - The `[1m]` suffix is the only reliable way to request 1M context through
|
|
73
|
+
// the SDK; bare ids serve 200K.
|
|
74
|
+
// - An unentitled `[1m]` id is rejected outright (400/429), failing every turn
|
|
75
|
+
// — worse than serving 200K, so the default is bare id at 200K and only
|
|
76
|
+
// measured-good ids get `[1m]`.
|
|
77
|
+
// - The registered contextWindow must match the window the bridge actually
|
|
78
|
+
// requests, or pi's status bar and compaction threshold misreport.
|
|
79
|
+
// [1m] ids verified to serve 1M on every plan (sonnet-5-5 measured on Pro with
|
|
80
|
+
// and without Extra Usage; opus-5-5 on Max per its run notes). A new model
|
|
81
|
+
// serves 200K until someone measures it (diag/context-size.mjs) and adds it
|
|
82
|
+
// here. Known exception unrelated to long context: fable-5 is not included
|
|
83
|
+
// with Pro account without Extra Usage.
|
|
84
|
+
const MEASURED_ONE_M = new Set([
|
|
85
|
+
"claude-fable-5",
|
|
86
|
+
"claude-fable-5-1",
|
|
87
|
+
"claude-opus-5-5",
|
|
88
|
+
"claude-opus-5",
|
|
89
|
+
"claude-opus-4-8",
|
|
90
|
+
"claude-opus-4-7",
|
|
91
|
+
"claude-sonnet-5",
|
|
92
|
+
"claude-sonnet-5-5",
|
|
93
|
+
"claude-haiku-5-5",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// Measured exceptions: pi-ai declares 1M and the [1m] id works, but only when
|
|
97
|
+
// the plan allows it.
|
|
98
|
+
const PLAN_GATED_ONE_M: Record<string, (settings: LongContextSettings) => boolean> = {
|
|
99
|
+
// [1m] measured 1M on Max plan / extra usage; 429 on Pro without it.
|
|
100
|
+
"claude-opus-4-6": (settings) => settings.plan === "max" || settings.longContextExtraUsage,
|
|
101
|
+
// [1m] measured 1M with extra usage only.
|
|
102
|
+
"claude-sonnet-4-6": (settings) => settings.longContextExtraUsage,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function resolveClaudeCodeRuntimeModel(
|
|
106
|
+
model: { id: string },
|
|
107
|
+
settings: LongContextSettings,
|
|
108
|
+
): ClaudeCodeRuntimeModel {
|
|
109
|
+
const modelId = model.id;
|
|
110
|
+
if (settings.forceTwoHundredK?.includes(modelId)) {
|
|
111
|
+
return { cliModelId: modelId, contextWindow: TWO_HUNDRED_K_CONTEXT };
|
|
112
|
+
}
|
|
113
|
+
if (MEASURED_ONE_M.has(modelId)) {
|
|
114
|
+
return { cliModelId: `${modelId}[1m]`, contextWindow: ONE_M_CONTEXT };
|
|
115
|
+
}
|
|
116
|
+
const planGate = PLAN_GATED_ONE_M[modelId];
|
|
117
|
+
if (planGate) {
|
|
118
|
+
const useOneM = planGate(settings);
|
|
119
|
+
return {
|
|
120
|
+
cliModelId: useOneM ? `${modelId}[1m]` : modelId,
|
|
121
|
+
contextWindow: useOneM ? ONE_M_CONTEXT : TWO_HUNDRED_K_CONTEXT,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// No measured row: bare id at 200K, the safe default (see diag/CONTEXT-SIZE.md).
|
|
125
|
+
return { cliModelId: modelId, contextWindow: TWO_HUNDRED_K_CONTEXT };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function claudeCodeModelId(model: { id: string }, settings: LongContextSettings): string {
|
|
129
|
+
return resolveClaudeCodeRuntimeModel(model, settings).cliModelId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
|
|
133
|
+
const lower = input.toLowerCase();
|
|
134
|
+
// Exact first, then partial (mirrors pi's tryMatchModel ordering), so a
|
|
135
|
+
// longer newer id containing the input (claude-fable-5-1 vs "claude-fable-5")
|
|
136
|
+
// cannot shadow the exact match.
|
|
137
|
+
return models.find((m) => m.id === lower)
|
|
138
|
+
?? newestPartialMatch(models.filter((m) => m.id.includes(lower)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Newest match by version rank — independent of registration order.
|
|
142
|
+
function newestPartialMatch<T extends { id: string }>(candidates: T[]): T | undefined {
|
|
143
|
+
if (candidates.length === 0) return undefined;
|
|
144
|
+
return candidates.reduce((best, m) => {
|
|
145
|
+
const [vb, vbest] = [versionRank(m.id).tuple, versionRank(best.id).tuple];
|
|
146
|
+
const newer = vb[0] !== vbest[0] ? vb[0] > vbest[0] : vb[1] > vbest[1];
|
|
147
|
+
return newer ? m : best;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Produce the model metadata registered with pi. The registered contextWindow must
|
|
152
|
+
// match the window the bridge actually requests from Claude Code, or pi's status
|
|
153
|
+
// bar and auto-compaction threshold will misreport. The runtime policy is based
|
|
154
|
+
// on measured SDK behavior - see diag/CONTEXT-SIZE.md
|
|
155
|
+
export function applyLongContext<T extends { id: string; name: string; contextWindow?: number | null }>(
|
|
156
|
+
models: T[],
|
|
157
|
+
settings: LongContextSettings,
|
|
158
|
+
): T[] {
|
|
159
|
+
return models.map((m) => {
|
|
160
|
+
const { contextWindow } = resolveClaudeCodeRuntimeModel(m, settings);
|
|
161
|
+
const name = contextWindow > TWO_HUNDRED_K_CONTEXT && !/\b1M\b/i.test(m.name) ? `${m.name} 1M` : m.name;
|
|
162
|
+
return contextWindow === m.contextWindow && name === m.name ? m : { ...m, contextWindow, name };
|
|
163
|
+
});
|
|
164
|
+
}
|