fix: bundle mac model catalog
This commit is contained in:
@@ -2,14 +2,28 @@ import Foundation
|
|||||||
import JavaScriptCore
|
import JavaScriptCore
|
||||||
|
|
||||||
enum ModelCatalogLoader {
|
enum ModelCatalogLoader {
|
||||||
static let defaultPath: String = FileManager().homeDirectoryForCurrentUser
|
static var defaultPath: String { self.resolveDefaultPath() }
|
||||||
.appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path
|
|
||||||
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
||||||
|
private nonisolated static let appSupportDir: URL = {
|
||||||
|
let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
return base.appendingPathComponent("Clawdbot", isDirectory: true)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static var cachePath: URL {
|
||||||
|
self.appSupportDir.appendingPathComponent("model-catalog/models.generated.js", isDirectory: false)
|
||||||
|
}
|
||||||
|
|
||||||
static func load(from path: String) async throws -> [ModelChoice] {
|
static func load(from path: String) async throws -> [ModelChoice] {
|
||||||
let expanded = (path as NSString).expandingTildeInPath
|
let expanded = (path as NSString).expandingTildeInPath
|
||||||
self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)")
|
guard let resolved = self.resolvePath(preferred: expanded) else {
|
||||||
let source = try String(contentsOfFile: expanded, encoding: .utf8)
|
self.logger.error("model catalog load failed: file not found")
|
||||||
|
throw NSError(
|
||||||
|
domain: "ModelCatalogLoader",
|
||||||
|
code: 1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "Model catalog file not found"])
|
||||||
|
}
|
||||||
|
self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: resolved.path).lastPathComponent)")
|
||||||
|
let source = try String(contentsOfFile: resolved.path, encoding: .utf8)
|
||||||
let sanitized = self.sanitize(source: source)
|
let sanitized = self.sanitize(source: source)
|
||||||
|
|
||||||
let ctx = JSContext()
|
let ctx = JSContext()
|
||||||
@@ -45,9 +59,82 @@ enum ModelCatalogLoader {
|
|||||||
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
|
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
|
||||||
}
|
}
|
||||||
self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)")
|
self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)")
|
||||||
|
if resolved.shouldCache {
|
||||||
|
self.cacheCatalog(sourcePath: resolved.path)
|
||||||
|
}
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func resolveDefaultPath() -> String {
|
||||||
|
let cache = self.cachePath.path
|
||||||
|
if FileManager().isReadableFile(atPath: cache) { return cache }
|
||||||
|
if let bundlePath = self.bundleCatalogPath() { return bundlePath }
|
||||||
|
if let nodePath = self.nodeModulesCatalogPath() { return nodePath }
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resolvePath(preferred: String) -> (path: String, shouldCache: Bool)? {
|
||||||
|
if FileManager().isReadableFile(atPath: preferred) {
|
||||||
|
return (preferred, preferred != self.cachePath.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let bundlePath = self.bundleCatalogPath(), bundlePath != preferred {
|
||||||
|
self.logger.warning("model catalog path missing; falling back to bundled catalog")
|
||||||
|
return (bundlePath, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = self.cachePath.path
|
||||||
|
if cache != preferred, FileManager().isReadableFile(atPath: cache) {
|
||||||
|
self.logger.warning("model catalog path missing; falling back to cached catalog")
|
||||||
|
return (cache, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let nodePath = self.nodeModulesCatalogPath(), nodePath != preferred {
|
||||||
|
self.logger.warning("model catalog path missing; falling back to node_modules catalog")
|
||||||
|
return (nodePath, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func bundleCatalogPath() -> String? {
|
||||||
|
guard let url = Bundle.main.url(forResource: "models.generated", withExtension: "js") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return url.path
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func nodeModulesCatalogPath() -> String? {
|
||||||
|
let roots = [
|
||||||
|
URL(fileURLWithPath: CommandResolver.projectRootPath()),
|
||||||
|
URL(fileURLWithPath: FileManager().currentDirectoryPath),
|
||||||
|
]
|
||||||
|
for root in roots {
|
||||||
|
let candidate = root
|
||||||
|
.appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js")
|
||||||
|
if FileManager().isReadableFile(atPath: candidate.path) {
|
||||||
|
return candidate.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func cacheCatalog(sourcePath: String) {
|
||||||
|
let destination = self.cachePath
|
||||||
|
do {
|
||||||
|
try FileManager().createDirectory(
|
||||||
|
at: destination.deletingLastPathComponent(),
|
||||||
|
withIntermediateDirectories: true)
|
||||||
|
if FileManager().fileExists(atPath: destination.path) {
|
||||||
|
try FileManager().removeItem(at: destination)
|
||||||
|
}
|
||||||
|
try FileManager().copyItem(atPath: sourcePath, toPath: destination.path)
|
||||||
|
self.logger.debug("model catalog cached file=\(destination.lastPathComponent)")
|
||||||
|
} catch {
|
||||||
|
self.logger.warning("model catalog cache failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func sanitize(source: String) -> String {
|
private static func sanitize(source: String) -> String {
|
||||||
guard let exportRange = source.range(of: "export const MODELS"),
|
guard let exportRange = source.range(of: "export const MODELS"),
|
||||||
let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"),
|
let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"),
|
||||||
|
|||||||
@@ -236,9 +236,6 @@
|
|||||||
"@sinclair/typebox": "0.34.47",
|
"@sinclair/typebox": "0.34.47",
|
||||||
"hono": "4.11.4",
|
"hono": "4.11.4",
|
||||||
"tar": "7.5.4"
|
"tar": "7.5.4"
|
||||||
},
|
|
||||||
"patchedDependencies": {
|
|
||||||
"@mariozechner/pi-ai@0.49.2": "patches/@mariozechner__pi-ai@0.49.2.patch"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vitest": {
|
"vitest": {
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
diff --git a/dist/providers/anthropic.js b/dist/providers/anthropic.js
|
|
||||||
index 1cba2f1365812fd2f88993009c9cc06e9c348279..664dd6d8b400ec523fb735480741b9ad64f9a68c 100644
|
|
||||||
--- a/dist/providers/anthropic.js
|
|
||||||
+++ b/dist/providers/anthropic.js
|
|
||||||
@@ -298,10 +298,11 @@ function createClient(model, apiKey, interleavedThinking) {
|
|
||||||
});
|
|
||||||
return { client, isOAuthToken: true };
|
|
||||||
}
|
|
||||||
+ const apiBetaFeatures = ["extended-cache-ttl-2025-04-11", ...betaFeatures];
|
|
||||||
const defaultHeaders = {
|
|
||||||
accept: "application/json",
|
|
||||||
"anthropic-dangerous-direct-browser-access": "true",
|
|
||||||
- "anthropic-beta": betaFeatures.join(","),
|
|
||||||
+ "anthropic-beta": apiBetaFeatures.join(","),
|
|
||||||
...(model.headers || {}),
|
|
||||||
};
|
|
||||||
const client = new Anthropic({
|
|
||||||
@@ -313,9 +314,11 @@ function createClient(model, apiKey, interleavedThinking) {
|
|
||||||
return { client, isOAuthToken: false };
|
|
||||||
}
|
|
||||||
function buildParams(model, context, isOAuthToken, options) {
|
|
||||||
+ const cacheControlTtl = !isOAuthToken ? (options?.cacheControlTtl ?? "1h") : undefined;
|
|
||||||
+ const cacheControl = cacheControlTtl ? { type: "ephemeral", ttl: cacheControlTtl } : { type: "ephemeral" };
|
|
||||||
const params = {
|
|
||||||
model: model.id,
|
|
||||||
- messages: convertMessages(context.messages, model, isOAuthToken),
|
|
||||||
+ messages: convertMessages(context.messages, model, isOAuthToken, cacheControl),
|
|
||||||
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
||||||
stream: true,
|
|
||||||
};
|
|
||||||
@@ -325,18 +328,14 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
||||||
- cache_control: {
|
|
||||||
- type: "ephemeral",
|
|
||||||
- },
|
|
||||||
+ cache_control: cacheControl,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (context.systemPrompt) {
|
|
||||||
params.system.push({
|
|
||||||
type: "text",
|
|
||||||
text: sanitizeSurrogates(context.systemPrompt),
|
|
||||||
- cache_control: {
|
|
||||||
- type: "ephemeral",
|
|
||||||
- },
|
|
||||||
+ cache_control: cacheControl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -346,9 +345,7 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: sanitizeSurrogates(context.systemPrompt),
|
|
||||||
- cache_control: {
|
|
||||||
- type: "ephemeral",
|
|
||||||
- },
|
|
||||||
+ cache_control: cacheControl,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -378,7 +375,7 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
||||||
function normalizeToolCallId(id) {
|
|
||||||
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
||||||
}
|
|
||||||
-function convertMessages(messages, model, isOAuthToken) {
|
|
||||||
+function convertMessages(messages, model, isOAuthToken, cacheControl) {
|
|
||||||
const params = [];
|
|
||||||
// Transform messages for cross-provider compatibility
|
|
||||||
const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
|
|
||||||
@@ -514,7 +511,7 @@ function convertMessages(messages, model, isOAuthToken) {
|
|
||||||
const lastBlock = lastMessage.content[lastMessage.content.length - 1];
|
|
||||||
if (lastBlock &&
|
|
||||||
(lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result")) {
|
|
||||||
- lastBlock.cache_control = { type: "ephemeral" };
|
|
||||||
+ lastBlock.cache_control = cacheControl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/dist/providers/openai-completions.js b/dist/providers/openai-completions.js
|
|
||||||
index ee5c88d8e280ceeff45ed075f2c7357d40005578..89daad7b0e53753e094028291226d32da9446440 100644
|
|
||||||
--- a/dist/providers/openai-completions.js
|
|
||||||
+++ b/dist/providers/openai-completions.js
|
|
||||||
@@ -305,7 +305,7 @@ function createClient(model, context, apiKey) {
|
|
||||||
function buildParams(model, context, options) {
|
|
||||||
const compat = getCompat(model);
|
|
||||||
const messages = convertMessages(model, context, compat);
|
|
||||||
- maybeAddOpenRouterAnthropicCacheControl(model, messages);
|
|
||||||
+ maybeAddOpenRouterAnthropicCacheControl(model, messages, options?.cacheControlTtl);
|
|
||||||
const params = {
|
|
||||||
model: model.id,
|
|
||||||
messages,
|
|
||||||
@@ -349,9 +349,10 @@ function buildParams(model, context, options) {
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
-function maybeAddOpenRouterAnthropicCacheControl(model, messages) {
|
|
||||||
+function maybeAddOpenRouterAnthropicCacheControl(model, messages, cacheControlTtl) {
|
|
||||||
if (model.provider !== "openrouter" || !model.id.startsWith("anthropic/"))
|
|
||||||
return;
|
|
||||||
+ const cacheControl = cacheControlTtl ? { type: "ephemeral", ttl: cacheControlTtl } : { type: "ephemeral" };
|
|
||||||
// Anthropic-style caching requires cache_control on a text part. Add a breakpoint
|
|
||||||
// on the last user/assistant message (walking backwards until we find text content).
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
@@ -361,7 +362,7 @@ function maybeAddOpenRouterAnthropicCacheControl(model, messages) {
|
|
||||||
const content = msg.content;
|
|
||||||
if (typeof content === "string") {
|
|
||||||
msg.content = [
|
|
||||||
- Object.assign({ type: "text", text: content }, { cache_control: { type: "ephemeral" } }),
|
|
||||||
+ Object.assign({ type: "text", text: content }, { cache_control: cacheControl }),
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -371,7 +372,7 @@ function maybeAddOpenRouterAnthropicCacheControl(model, messages) {
|
|
||||||
for (let j = content.length - 1; j >= 0; j--) {
|
|
||||||
const part = content[j];
|
|
||||||
if (part?.type === "text") {
|
|
||||||
- Object.assign(part, { cache_control: { type: "ephemeral" } });
|
|
||||||
+ Object.assign(part, { cache_control: cacheControl });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/dist/stream.js b/dist/stream.js
|
|
||||||
index d23fdd9f226a949fac4f2c7160af76f7f5fe71d1..3500f074bd88b85f4c7dd9bf42279f80fdf264d1 100644
|
|
||||||
--- a/dist/stream.js
|
|
||||||
+++ b/dist/stream.js
|
|
||||||
@@ -146,6 +146,7 @@ function mapOptionsForApi(model, options, apiKey) {
|
|
||||||
signal: options?.signal,
|
|
||||||
apiKey: apiKey || options?.apiKey,
|
|
||||||
sessionId: options?.sessionId,
|
|
||||||
+ cacheControlTtl: options?.cacheControlTtl,
|
|
||||||
};
|
|
||||||
// Helper to clamp xhigh to high for providers that don't support it
|
|
||||||
const clampReasoning = (effort) => (effort === "xhigh" ? "high" : effort);
|
|
||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -9,11 +9,6 @@ overrides:
|
|||||||
hono: 4.11.4
|
hono: 4.11.4
|
||||||
tar: 7.5.4
|
tar: 7.5.4
|
||||||
|
|
||||||
patchedDependencies:
|
|
||||||
'@mariozechner/pi-ai@0.49.2':
|
|
||||||
hash: 4ae0a92a4b2c74703711e2a62b745ca8af6a9948ea7fa923097e875c76354d7e
|
|
||||||
path: patches/@mariozechner__pi-ai@0.49.2.patch
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -44,7 +39,7 @@ importers:
|
|||||||
version: 0.49.2(ws@8.19.0)(zod@4.3.5)
|
version: 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: 0.49.2
|
specifier: 0.49.2
|
||||||
version: 0.49.2(patch_hash=4ae0a92a4b2c74703711e2a62b745ca8af6a9948ea7fa923097e875c76354d7e)(ws@8.19.0)(zod@4.3.5)
|
version: 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: 0.49.2
|
specifier: 0.49.2
|
||||||
version: 0.49.2(ws@8.19.0)(zod@4.3.5)
|
version: 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
@@ -6265,7 +6260,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.49.2(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-agent-core@0.49.2(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.49.2(patch_hash=4ae0a92a4b2c74703711e2a62b745ca8af6a9948ea7fa923097e875c76354d7e)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.49.2
|
'@mariozechner/pi-tui': 0.49.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -6276,7 +6271,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.49.2(patch_hash=4ae0a92a4b2c74703711e2a62b745ca8af6a9948ea7fa923097e875c76354d7e)(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-ai@0.49.2(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
||||||
'@aws-sdk/client-bedrock-runtime': 3.971.0
|
'@aws-sdk/client-bedrock-runtime': 3.971.0
|
||||||
@@ -6303,7 +6298,7 @@ snapshots:
|
|||||||
'@mariozechner/clipboard': 0.3.0
|
'@mariozechner/clipboard': 0.3.0
|
||||||
'@mariozechner/jiti': 2.6.5
|
'@mariozechner/jiti': 2.6.5
|
||||||
'@mariozechner/pi-agent-core': 0.49.2(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-agent-core': 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai': 0.49.2(patch_hash=4ae0a92a4b2c74703711e2a62b745ca8af6a9948ea7fa923097e875c76354d7e)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.49.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.49.2
|
'@mariozechner/pi-tui': 0.49.2
|
||||||
'@silvia-odwyer/photon-node': 0.3.4
|
'@silvia-odwyer/photon-node': 0.3.4
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
|
|||||||
@@ -207,6 +207,15 @@ echo "📦 Copying device model resources"
|
|||||||
rm -rf "$APP_ROOT/Contents/Resources/DeviceModels"
|
rm -rf "$APP_ROOT/Contents/Resources/DeviceModels"
|
||||||
cp -R "$ROOT_DIR/apps/macos/Sources/Clawdbot/Resources/DeviceModels" "$APP_ROOT/Contents/Resources/DeviceModels"
|
cp -R "$ROOT_DIR/apps/macos/Sources/Clawdbot/Resources/DeviceModels" "$APP_ROOT/Contents/Resources/DeviceModels"
|
||||||
|
|
||||||
|
echo "📦 Copying model catalog"
|
||||||
|
MODEL_CATALOG_SRC="$ROOT_DIR/node_modules/@mariozechner/pi-ai/dist/models.generated.js"
|
||||||
|
MODEL_CATALOG_DEST="$APP_ROOT/Contents/Resources/models.generated.js"
|
||||||
|
if [ -f "$MODEL_CATALOG_SRC" ]; then
|
||||||
|
cp "$MODEL_CATALOG_SRC" "$MODEL_CATALOG_DEST"
|
||||||
|
else
|
||||||
|
echo "WARN: model catalog missing at $MODEL_CATALOG_SRC (continuing)" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
echo "📦 Copying ClawdbotKit resources"
|
echo "📦 Copying ClawdbotKit resources"
|
||||||
CLAWDBOTKIT_BUNDLE="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG/ClawdbotKit_ClawdbotKit.bundle"
|
CLAWDBOTKIT_BUNDLE="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG/ClawdbotKit_ClawdbotKit.bundle"
|
||||||
if [ -d "$CLAWDBOTKIT_BUNDLE" ]; then
|
if [ -d "$CLAWDBOTKIT_BUNDLE" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user