diff --git a/CHANGELOG.md b/CHANGELOG.md index 88deadb0..5a31cdcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ The format is based on Keep a Changelog. ## Unreleased +### Agents and API + +- updated the Pi integration for the current `earendil-works/pi` packages and + release feed, registered mere.run through Pi's native dynamic-provider API, + and added a reusable package under `integrations/pi`. +- made `/v1/models` self-describing with additive task, tool-call, reasoning, + modality, context/output-limit, and OpenAI-dialect metadata. Pi now exposes + only models whose running API lane supports tools, maps model-specific + thinking levels, and honors Muse reasoning effort. +- moved served-model capabilities into typed managed-model catalog profiles. + API request validation, `/v1/models`, agent eligibility, and Pi's offline + fallback now consume the same source of truth, with runtime limit settings + applied only when describing the active server. + ### Music - accelerated MiniMax Music 3 on Apple Silicon with a reachable 16,385-row diff --git a/README.md b/README.md index aec19a04..7bdaea92 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,10 @@ swift run mere.run model capabilities --recommended # Choose guided, bring-your-own-agent, or manual setup swift run mere.run setup +# Or install Pi and launch it against a self-describing local provider +swift run mere.run agent install-pi +swift run mere.run agent start + # Pull a Hugging Face-backed model into the local model store swift run mere.run model pull image-zimage-nano swift run mere.run model pull image-zimage-nano --preflight --json @@ -1015,6 +1019,11 @@ The public OSS build keeps local-first behavior by default and requires explicit model - `mere.run api serve` can bind to loopback without auth, but non-loopback hosts require `--api-key` or `MERERUN_API_KEY` - the OpenAI-compatible chat and embedding routes require `Content-Type: application/json`, support `--rate-limit-per-minute` for basic abuse control, decode the common OpenAI request shapes, and reject unsupported high-impact fields before generation +- `/v1/models` adds task, tool-call, reasoning, modality, limit, and + compatibility metadata. The bundled Pi provider consumes these fields and + exposes only tool-capable chat models instead of guessing from model names. + Those fields project from typed managed-model catalog profiles; aliases and + active runtime limit overrides are layered on when the server responds. - API LoRA adapters are operator-controlled with `--lora`; it accepts a verified installed adapter catalog id or a local path, while per-request LoRA paths are rejected diff --git a/Sources/MereRunCLI/Commands/APIServeCommand.swift b/Sources/MereRunCLI/Commands/APIServeCommand.swift index f9fc9996..c4010d5b 100644 --- a/Sources/MereRunCLI/Commands/APIServeCommand.swift +++ b/Sources/MereRunCLI/Commands/APIServeCommand.swift @@ -547,11 +547,27 @@ struct APIEngineCapabilities: Equatable, Sendable { var supportsLogprobs: Bool = false var supportsProviderThinkingControls: Bool = false - static let localText = APIEngineCapabilities() + static func catalog(_ profile: ManagedModelAPIProfile) -> APIEngineCapabilities { + APIEngineCapabilities( + supportsRawProxy: profile.supportsRawProxy, + supportsTools: profile.toolCall, + supportsToolChoice: profile.supportsToolChoice, + supportsDeveloperRole: profile.compatibility.supportsDeveloperRole, + supportsStructuredOutputs: profile.structuredOutput, + supportsReasoningEffort: profile.compatibility.supportsReasoningEffort, + supportsMaxCompletionTokens: profile.compatibility.maxTokensField == .maxCompletionTokens, + supportsUsageInStreaming: profile.compatibility.supportsUsageInStreaming, + supportsVisionContentParts: profile.inputModalities.contains(.image), + supportsStrictMode: profile.compatibility.supportsStrictMode, + supportsStopSequences: profile.supportsStopSequences, + supportsSeed: profile.supportsSeed, + supportsPenalties: profile.supportsPenalties, + supportsLogprobs: profile.supportsLogprobs, + supportsProviderThinkingControls: profile.supportsProviderThinkingControls + ) + } - static let localTextWithStopSequences = APIEngineCapabilities( - supportsStopSequences: true - ) + static let localText = APIEngineCapabilities() static let localTextWithStructuredJSON = APIEngineCapabilities( supportsStructuredOutputs: true @@ -562,56 +578,11 @@ struct APIEngineCapabilities: Equatable, Sendable { supportsToolChoice: true ) - static let localTextWithToolsAndStopSequences = APIEngineCapabilities( - supportsTools: true, - supportsToolChoice: true, - supportsStopSequences: true - ) - - static let localTextWithToolsAndStructuredJSON = APIEngineCapabilities( - supportsTools: true, - supportsToolChoice: true, - supportsStructuredOutputs: true - ) - static let localTextWithToolsAndVision = APIEngineCapabilities( supportsTools: true, supportsToolChoice: true, supportsVisionContentParts: true ) - - static let localTextWithToolsVisionAndReasoning = APIEngineCapabilities( - supportsTools: true, - supportsToolChoice: true, - supportsReasoningEffort: true, - supportsVisionContentParts: true - ) - - static let localTextWithToolsVisionAndStructuredJSON = APIEngineCapabilities( - supportsTools: true, - supportsToolChoice: true, - supportsStructuredOutputs: true, - supportsVisionContentParts: true, - supportsStrictMode: false - ) - - static let rawProxy = APIEngineCapabilities( - supportsRawProxy: true, - supportsTools: true, - supportsToolChoice: true, - supportsDeveloperRole: true, - supportsStructuredOutputs: true, - supportsReasoningEffort: true, - supportsMaxCompletionTokens: true, - supportsUsageInStreaming: true, - supportsVisionContentParts: true, - supportsStrictMode: false, - supportsStopSequences: true, - supportsSeed: true, - supportsPenalties: true, - supportsLogprobs: true, - supportsProviderThinkingControls: true - ) } struct APIHealthStatus: Codable, Equatable, Sendable { @@ -1241,6 +1212,78 @@ enum APIServerContract { ) } + static func chatModel( + id: String, + name: String, + profile: ManagedModelAPIProfile, + contextWindow: Int, + maximumOutputTokens: Int, + createdAt: Date = Date() + ) -> OpenAIModel { + let compatibility = profile.compatibility + let thinkingLevels = profile.thinkingLevels.isEmpty + ? nil + : profile.thinkingLevels.map(\.rawValue) + let thinkingLevelMap = profile.thinkingLevelMap.isEmpty + ? nil + : Dictionary(uniqueKeysWithValues: profile.thinkingLevelMap.map { + ($0.key.rawValue, $0.value.rawValue) + }) + + return OpenAIModel( + id: id, + object: "model", + created: Int(createdAt.timeIntervalSince1970), + owned_by: "mere.run", + name: name, + task: profile.task.rawValue, + reasoning: profile.reasoning, + thinking_levels: thinkingLevels, + tool_call: profile.toolCall, + structured_output: profile.structuredOutput, + modalities: OpenAIModelModalities( + input: profile.inputModalities.map(\.rawValue), + output: profile.outputModalities.map(\.rawValue) + ), + limit: OpenAIModelLimit(context: contextWindow, output: maximumOutputTokens), + openai_compat: OpenAIModelCompatibility( + supports_store: compatibility.supportsStore, + supports_developer_role: compatibility.supportsDeveloperRole, + supports_reasoning_effort: compatibility.supportsReasoningEffort, + supports_usage_in_streaming: compatibility.supportsUsageInStreaming, + supports_finish_reason: compatibility.supportsFinishReason, + max_tokens_field: compatibility.maxTokensField.rawValue, + supports_strict_mode: compatibility.supportsStrictMode, + thinking_format: compatibility.thinkingFormat?.rawValue, + thinking_level_map: thinkingLevelMap, + requires_reasoning_content_on_assistant_messages: compatibility + .requiresReasoningContentOnAssistantMessages + ) + ) + } + + static func companionModel( + id: String, + profile: ManagedModelAPIProfile, + createdAt: Date = Date() + ) -> OpenAIModel { + return OpenAIModel( + id: id, + object: "model", + created: Int(createdAt.timeIntervalSince1970), + owned_by: "mere.run", + name: id, + task: profile.task.rawValue, + reasoning: profile.reasoning, + tool_call: profile.toolCall, + structured_output: profile.structuredOutput, + modalities: OpenAIModelModalities( + input: profile.inputModalities.map(\.rawValue), + output: profile.outputModalities.map(\.rawValue) + ) + ) + } + static func embeddingTexts(from request: OpenAIEmbeddingRequest) throws -> [String] { guard !request.model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw APIRequestValidationError.invalidField("model", "must not be empty") @@ -2430,7 +2473,8 @@ enum APIServerContract { fallbackLoraPath: String?, contextSize: Int, capabilities: APIEngineCapabilities = .localText, - servedModelID: String? = nil + servedModelID: String? = nil, + apiProfile: ManagedModelAPIProfile? = nil ) throws -> ChatRequest { guard !openaiRequest.messages.isEmpty else { throw APIRequestValidationError.invalidField("messages", "must contain at least one message") @@ -2476,11 +2520,18 @@ enum APIServerContract { // R1-style lanes degenerate without reasoning; their published top_k // applies only when the client did not set explicit sampling. let laneModelID = servedModelID ?? "" + let resolvedAPIProfile = apiProfile + ?? servedModelID.flatMap { ManagedModelCatalog.apiProfile(for: $0) } let recommendedSampling = Q35Resources.recommendedSampling(forModelId: laneModelID) let isLaguna = LagunaResources.handles(modelSpec: laneModelID) let usesExplicitSampling = openaiRequest.temperature != nil || openaiRequest.top_p != nil || openaiRequest.min_p != nil + let reasoningEffort = try reasoningEffort( + from: openaiRequest.reasoning_effort, + capabilities: capabilities, + profile: resolvedAPIProfile + ) return ChatRequest( messages: messages, @@ -2499,7 +2550,10 @@ enum APIServerContract { minP: openaiRequest.min_p == nil && isLaguna ? LagunaResources.recommendedMinP : minP, - showThinking: requiresJSON ? false : Q35Resources.thinkingDefault(forModelId: laneModelID), + reasoningEffort: reasoningEffort, + showThinking: requiresJSON + ? false + : resolvedAPIProfile?.thinkingLevels == [.high], lora: lora, requiresJSON: requiresJSON, tools: tools, @@ -2802,6 +2856,28 @@ enum APIServerContract { return try validateMaxTokens(maxCompletionTokens ?? maxTokens, contextSize: contextSize) } + private static func reasoningEffort( + from rawValue: String?, + capabilities: APIEngineCapabilities, + profile: ManagedModelAPIProfile? + ) throws -> Double? { + guard let rawValue else { return nil } + guard !capabilities.supportsRawProxy else { return nil } + let normalized = rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + guard let level = ManagedModelThinkingLevel(rawValue: normalized), + let strength = profile?.reasoningEffortStrengths[level] else { + let supportedLevels = ManagedModelThinkingLevel.allCases + .filter { profile?.reasoningEffortStrengths[$0] != nil } + .map(\.rawValue) + .joined(separator: ", ") + throw APIRequestValidationError.invalidField( + "reasoning_effort", + "must be one of \(supportedLevels)" + ) + } + return strength + } + static func acceptsJSONContentType(_ rawValue: String?) -> Bool { guard let mediaType = rawValue? .split(separator: ";", maxSplits: 1, omittingEmptySubsequences: true) @@ -3810,7 +3886,7 @@ actor CodeGenServer { let includeLoopbackArtifactModels = APIVFXArtifactRoutePolicy.allows( remoteAddress: remoteAddress ) - var models = try await pool.modelsResponse() + var models = try await pool.modelsResponse(serverContextSize: contextSize) if !includeLoopbackArtifactModels { models.data.removeAll { APIVFXArtifactRoutePolicy.modelIDs.contains($0.id) } } @@ -3818,12 +3894,13 @@ actor CodeGenServer { includeLoopbackArtifactModels: includeLoopbackArtifactModels ) where !models.data.contains(where: { $0.id == modelID }) { + guard let profile = ManagedModelCatalog.apiProfile(for: modelID) else { + continue + } models.data.append( - OpenAIModel( + APIServerContract.companionModel( id: modelID, - object: "model", - created: Int(Date().timeIntervalSince1970), - owned_by: "mere.run" + profile: profile ) ) } diff --git a/Sources/MereRunCLI/Commands/AgentCommand.swift b/Sources/MereRunCLI/Commands/AgentCommand.swift index f8450133..15c3e4e0 100644 --- a/Sources/MereRunCLI/Commands/AgentCommand.swift +++ b/Sources/MereRunCLI/Commands/AgentCommand.swift @@ -240,13 +240,13 @@ struct AgentOnboard: AsyncParsableCommand { } let startable = MereRunAgentModelCatalog.fallbackStartableRecommendation(on: machine) - let selectedAgentID = startable?.id ?? AgentModelResources.qwen35NineBModelId + let selectedAgentID = startable?.id ?? Q35Resources.ornith9BModelId print("\nAgent readiness") if let startable { print(" Recommended setup agent: \(startable.id) (\(startable.displayName)).") print(" Start a guided session with: \(CLICommandDisplay.command("agent start --model \(selectedAgentID)"))") if startable.id == DeepseekV4FlashResources.defaultModelId { - print(" DeepSeek V4 Flash is the preferred 96 GB+ setup-agent tier; smaller Qwen agents are alternatives, not upgrades.") + print(" DeepSeek V4 Flash is the preferred 96 GB+ setup-agent tier; smaller tool-capable native agents are alternatives, not upgrades.") } } if let codeReport = reports.first(where: { $0.spec.id == CodeGenResources.defaultModelId }) { @@ -362,7 +362,12 @@ struct AgentOnboard: AsyncParsableCommand { guard let recommendation = SetupAgentRuntime.recommendation(forManagedModelID: normalized) else { throw ValidationError("Unsupported Pi provider model: \(modelID)") } - return SetupAgentRuntime.providerModel(for: recommendation) + guard recommendation.isStartableByMereRun else { + throw ValidationError( + "Pi requires a tool-capable chat model; \(modelID) is only available through the text-code API lane." + ) + } + return try SetupAgentRuntime.providerModel(for: recommendation) } } diff --git a/Sources/MereRunCLI/Commands/ModelCapabilitiesCommand.swift b/Sources/MereRunCLI/Commands/ModelCapabilitiesCommand.swift index e92a6945..c623cba2 100644 --- a/Sources/MereRunCLI/Commands/ModelCapabilitiesCommand.swift +++ b/Sources/MereRunCLI/Commands/ModelCapabilitiesCommand.swift @@ -86,7 +86,7 @@ struct ModelCapabilities: ParsableCommand { print(" \(CLICommandDisplay.command("agent start --model \(agent.id)"))") print(" \(agent.displayName): \(agent.summary)") if agent.id == DeepseekV4FlashResources.defaultModelId { - print(" note: DeepSeek V4 Flash is the preferred 96 GB+ setup-agent tier; smaller Qwen agents are lower-memory alternatives, not upgrades.") + print(" note: DeepSeek V4 Flash is the preferred 96 GB+ setup-agent tier; smaller tool-capable native agents are lower-memory alternatives, not upgrades.") } } diff --git a/Sources/MereRunCLI/Commands/SetupCommand.swift b/Sources/MereRunCLI/Commands/SetupCommand.swift index e94995ba..ccca53de 100644 --- a/Sources/MereRunCLI/Commands/SetupCommand.swift +++ b/Sources/MereRunCLI/Commands/SetupCommand.swift @@ -196,7 +196,7 @@ struct Setup: AsyncParsableCommand { switch agentModel { case .small: print(" Unavailable.") - print(" Qwen3.5 9B setup agent requires at least 16 GB unified memory.") + print(" Ornith 1.0 9B setup agent requires at least 16 GB unified memory.") case .tier: print(" Unavailable.") print(" No local agent tier is supported on this machine.") @@ -461,30 +461,26 @@ struct SetupAgentRuntime { } static func runtime(for recommendation: MereRunAgentModelRecommendation) throws -> SetupAgentRuntime { + guard recommendation.isStartableByMereRun else { + throw ValidationError( + "\(recommendation.displayName) cannot be used with Pi because its API lane does not support tool calls." + ) + } guard let modelID = recommendation.managedModelID, let spec = ManagedModelCatalog.spec(for: modelID) else { throw ValidationError("\(recommendation.displayName) is not managed by mere.run yet.") } - let engine: APIEngine - switch recommendation.servingEngine { - case .textCode: - engine = .textCode - case .textChatGemma4: - engine = .textChatGemma4 - case .textChatQ36: - engine = .textChatQ36 - case .textChatQ35: - engine = .textChatQ36 - case .deepseekV4Flash: - engine = .textChatDeepseekV4Flash - case .sourceConfigured: - throw ValidationError("\(recommendation.displayName) requires an external local model before it can be started.") + guard let runtimeServingEngine = spec.apiProfile?.servingEngine, + let engine = APIEngine(rawValue: runtimeServingEngine.rawValue) else { + throw ValidationError( + "\(recommendation.displayName) does not have a cataloged local serving engine." + ) } return SetupAgentRuntime( recommendation: recommendation, spec: spec, engine: engine, - providerModel: providerModel(for: recommendation) + providerModel: try providerModel(for: recommendation) ) } @@ -495,32 +491,19 @@ struct SetupAgentRuntime { return try runtime(for: recommendation) } - static func providerModel(for recommendation: MereRunAgentModelRecommendation) -> PiProviderModel { - // DeepSeek V4 Flash has a specific Pi compat profile (DSML thinking - // format, reasoning effort, etc.) documented in the ds4 README. - if recommendation.servingEngine == .deepseekV4Flash { - return .deepseekV4Flash + static func providerModel( + for recommendation: MereRunAgentModelRecommendation + ) throws -> PiProviderModel { + guard let modelID = recommendation.managedModelID, + let profile = ManagedModelCatalog.apiProfile(for: modelID) else { + throw ValidationError( + "\(recommendation.displayName) does not have a cataloged API profile." + ) } - return PiProviderModel( + return try PiProviderModel( id: recommendation.id, name: "\(recommendation.displayName) (mere.run)", - contextWindow: contextWindow(for: recommendation), - maxTokens: 4096 + profile: profile ) } - - private static func contextWindow(for recommendation: MereRunAgentModelRecommendation) -> Int { - switch recommendation.servingEngine { - case .textChatGemma4: - return Gemma4Resources.defaultContextLength - case .textChatQ36, .textChatQ35: - return Q35Resources.defaultContextLength - case .deepseekV4Flash: - return DeepseekV4FlashResources.defaultContextLength - case .sourceConfigured: - return 32768 - case .textCode: - return 32768 - } - } } diff --git a/Sources/MereRunCLI/Guides/agent-install-pi.md b/Sources/MereRunCLI/Guides/agent-install-pi.md index 898bc538..1a01325f 100644 --- a/Sources/MereRunCLI/Guides/agent-install-pi.md +++ b/Sources/MereRunCLI/Guides/agent-install-pi.md @@ -2,7 +2,10 @@ ## Purpose -Install the latest Pi coding-agent release so mere.run can launch a guided local setup agent. Auto-install uses the published macOS release assets; on Linux, install Pi separately and pass `--pi-path` or put `pi` on PATH. +Install the latest Pi coding-agent release from the current +`earendil-works/pi` upstream so mere.run can launch a guided local setup agent. +Auto-install uses the published macOS release assets; on Linux, install Pi +separately and pass `--pi-path` or put `pi` on PATH. ## Required Models @@ -23,7 +26,8 @@ mere.run agent install-pi --help - Run before `agent start` on macOS when Pi is not on PATH or not managed by mere.run. - Use `--force` when the installed Pi binary is corrupt or outdated. -- Pair with `agent onboard --configure-pi` to register the local provider. +- Pair with `agent onboard --configure-pi` to register a native Pi provider + that discovers tool-capable models from mere.run. ## Examples @@ -52,3 +56,4 @@ mere.run agent install-pi --force - https://github.com/sawfwair/mere-run/blob/main/Sources/MereRunCLI/Commands/AgentCommand.swift - https://github.com/sawfwair/mere-run/blob/main/Sources/MereRunCLI/Support/PiAgentIntegration.swift +- https://github.com/earendil-works/pi diff --git a/Sources/MereRunCLI/Guides/agent-onboard.md b/Sources/MereRunCLI/Guides/agent-onboard.md index 8b7c9288..d3a5b6ba 100644 --- a/Sources/MereRunCLI/Guides/agent-onboard.md +++ b/Sources/MereRunCLI/Guides/agent-onboard.md @@ -8,12 +8,10 @@ Summarize this Mac's model capabilities and optionally prepare the Pi coding-age No model is required to print readiness. Optional model pulls/configuration should use the recommended setup-agent tier; on 96 GB+ Apple Silicon Macs that -is `text-agent-deepseek-v4-flash`. Smaller Qwen agent models are lower-memory -or comparison alternatives. `text-code-north-mini` is available for native -GGUF coding-agent experiments, `text-agent-ornith-35b` is available for larger -native GGUF Ornith evals, and `text-agent-ornith-9b` is available for native -Qwen-family MLX/OptiQ coding-agent experiments. `text-agent-ornith-35b-mlx` -is the local converted native MLX Q4 lane for larger Ornith evals. +is `text-agent-deepseek-v4-flash`. Tool-capable native chat models such as +`text-agent-ornith-9b` are lower-memory alternatives. GGUF models served by +the `text-code` engine remain useful for direct coding experiments and evals, +but that API lane rejects tool calls and is not exposed to Pi. ## Install And Check @@ -38,10 +36,6 @@ mere.run agent onboard --help - Run plain `agent onboard` first; it is informational. - Use `--install-pi` before `agent start` if Pi is not already installed. - Use `--configure-pi --model ` when Pi should call a local mere.run API provider. -- For North Mini Code, pull `text-code-north-mini`, start `api serve --engine text-code`, - then use `--configure-pi --model text-code-north-mini --host --port `. -- For Ornith 35B, pull `text-agent-ornith-35b`, start `api serve --engine text-code --model text-agent-ornith-35b`, - then use `--configure-pi --model text-agent-ornith-35b --host --port `. - For Ornith 35B MLX, install `text-agent-ornith-35b-mlx`, start `api serve --engine text-chat-q36 --model text-agent-ornith-35b-mlx`, then use `--configure-pi --model text-agent-ornith-35b-mlx --host --port `. - For Ornith, pull `text-agent-ornith-9b`, start `api serve --engine text-chat-q36 --model text-agent-ornith-9b`, @@ -57,16 +51,6 @@ mere.run agent onboard mere.run agent onboard --install-pi --configure-pi --model text-agent-deepseek-v4-flash ``` -```bash -mere.run model pull text-code-north-mini -mere.run agent onboard --configure-pi --model text-code-north-mini --port 8080 -``` - -```bash -mere.run model pull text-agent-ornith-35b -mere.run agent onboard --configure-pi --model text-agent-ornith-35b --port 8080 -``` - ```bash mere.run model pull text-agent-ornith-9b mere.run agent onboard --configure-pi --model text-agent-ornith-9b --port 8080 @@ -81,7 +65,8 @@ mere.run agent onboard --configure-pi --model text-agent-ornith-9b --port 8080 ## Troubleshooting -- Provider model unsupported: choose a model printed by onboarding. +- Provider model unsupported: choose a tool-capable model printed as startable + by onboarding; `text-code` models cannot run Pi tools. - Pi install fails: rerun with network access and without `--quiet`. - No recommended downloads: use manual setup or a local model path. diff --git a/Sources/MereRunCLI/Guides/agent-start.md b/Sources/MereRunCLI/Guides/agent-start.md index 159de0b6..e5d33519 100644 --- a/Sources/MereRunCLI/Guides/agent-start.md +++ b/Sources/MereRunCLI/Guides/agent-start.md @@ -6,7 +6,10 @@ Start Pi against a local mere.run setup-agent API server. This is the guided "he ## Required Models -Use this machine's supported setup-agent tier. On 96 GB+ Apple Silicon Macs, `text-agent-deepseek-v4-flash` is the preferred managed setup agent. Smaller Qwen, North Mini Code, and Ornith 35B models are lower-memory or comparison alternatives, not upgrades from DeepSeek V4 Flash. On Linux, provide Pi with `--pi-path` or PATH; auto-install uses macOS release assets. +Use this machine's supported tool-capable setup-agent tier. On 96 GB+ Apple +Silicon Macs, `text-agent-deepseek-v4-flash` is the preferred managed setup +agent. On Linux, provide Pi with `--pi-path` or PATH; auto-install uses macOS +release assets. ## Install And Check @@ -33,12 +36,10 @@ mere.run status - Run `model capabilities --recommended` or `agent onboard` first and use the recommended setup-agent id. - Pull the selected model before `agent start`. - Use `--skip-server` only when you already started a compatible local API server. -- Use `agent start --model text-code-north-mini` to compare North Mini Code - through the native GGUF code runtime. -- Use `agent start --model text-agent-ornith-35b` to compare the larger Ornith - GGUF coding-agent target through the same native `text-code` runtime. - Use `agent start --model text-agent-ornith-35b-mlx` to compare the locally converted Ornith 35B MLX target through the native Qwen-family runtime. +- Use `text code` or `api serve --engine text-code` for GGUF coding models; + that API lane intentionally rejects Pi tool calls. - On Linux, provide an existing Pi binary with `--pi-path` or PATH before starting. - Run `status` when you need to confirm the local server and served model. - Keep the default prompt unless the user has a specific setup goal. @@ -57,13 +58,14 @@ mere.run agent start \ ## Iteration Tips -- Use DeepSeek V4 Flash on 96 GB+ Macs; start with a smaller Qwen agent only on lower-memory machines or when comparing behavior. +- Use DeepSeek V4 Flash on 96 GB+ Macs; start with a smaller tool-capable native + agent on lower-memory machines or when comparing behavior. - Check the server log path printed to stderr when startup hangs. - Re-run onboarding after model pulls or provider changes. ## Troubleshooting -- Model unsupported: choose a supported model from `agent onboard`. +- Model unsupported: choose a tool-capable startable model from `agent onboard`. - Model missing: run `mere.run model pull `. - Pi missing: run `mere.run agent install-pi` on macOS, or pass `--pi-path` / put `pi` on PATH on Linux. - Health check times out: verify host/port and local API logs. diff --git a/Sources/MereRunCLI/Guides/api-serve.md b/Sources/MereRunCLI/Guides/api-serve.md index f206ce5f..f5d7c2f7 100644 --- a/Sources/MereRunCLI/Guides/api-serve.md +++ b/Sources/MereRunCLI/Guides/api-serve.md @@ -133,7 +133,13 @@ download route. - Request `model` resolves by runtime alias, then curated catalog id, then the startup default from `--engine`/`--model`. - `/v1/models` returns installed API-capable chat catalog ids, aliases, and - installed native embedding, image, TTS, and ASR sidecar model ids. + installed native sidecars. Mere-specific additive fields describe each + model's task, tool-call and reasoning support, thinking levels, input/output + modalities, context/output limits, and OpenAI compatibility dialect so agent + harnesses do not have to guess from an id. +- Managed-model metadata comes from the typed catalog API profile. The running + server overlays aliases and configured context/output defaults; explicit + uncataloged model paths use a conservative profile for their serving engine. - The embedding, image/image-edit, TTS, and ASR endpoints each retain one bounded most-recently-used runtime. Their autonomous idle timers default to 300 seconds and re-read managed `pinned`/`ttlSeconds` settings while idle. Active diff --git a/Sources/MereRunCLI/Guides/setup.md b/Sources/MereRunCLI/Guides/setup.md index c96fc2e7..01ee9563 100644 --- a/Sources/MereRunCLI/Guides/setup.md +++ b/Sources/MereRunCLI/Guides/setup.md @@ -8,10 +8,10 @@ Choose a guided, bring-your-own-agent, or manual setup path for a new mere.run i No model is required to view the plan. Agent setup selects this machine's supported tier; on 96 GB+ machines that is `text-agent-deepseek-v4-flash`. -Smaller Qwen agent models are lower-memory or comparison alternatives, not -upgrades from DeepSeek V4 Flash. `text-code-north-mini` can be pulled for -native GGUF coding-agent experiments through the same `text-code` runtime; -`text-agent-ornith-35b` is the larger explicit Ornith GGUF eval target. +The small tier is the tool-capable native `text-agent-ornith-9b`. GGUF models +such as North Mini and Ornith 35B remain available for direct coding +experiments, but the `text-code` API lane rejects tool calls and cannot host +the Pi setup agent. ## Install And Check @@ -39,9 +39,7 @@ mere.run model capabilities --recommended - Use `--mode manual --dry-run` for docs or scripts. - Use agent mode only with a supported local runtime and model. Prefer `--agent-model tier` unless the user asks for a smaller comparison model. - On Linux, provide Pi with `--pi-path` or put `pi` on PATH; auto-install uses macOS release assets. -- Use `--agent-model small` for the smallest setup model, or pull - `text-code-north-mini` or `text-agent-ornith-35b` manually when comparing - coding models against Qwen. +- Use `--agent-model small` for the smallest tool-capable setup model. ## Examples diff --git a/Sources/MereRunCLI/Support/PiAgentIntegration.swift b/Sources/MereRunCLI/Support/PiAgentIntegration.swift index 29b012a1..9fbe029d 100644 --- a/Sources/MereRunCLI/Support/PiAgentIntegration.swift +++ b/Sources/MereRunCLI/Support/PiAgentIntegration.swift @@ -13,19 +13,35 @@ struct PiAgentInstallResult { let binaryURL: URL } +enum PiProviderModelError: LocalizedError { + case invalidCatalogProfile(String) + + var errorDescription: String? { + switch self { + case .invalidCatalogProfile(let modelID): + return "Catalog model '\(modelID)' does not define a complete chat API profile." + } + } +} + struct PiProviderModel { let id: String let name: String let contextWindow: Int let maxTokens: Int + /// Input modalities Pi can represent for this model. + let inputModalities: [String] /// Whether the model supports a separate "thinking" / reasoning channel. let reasoning: Bool + /// Whether the API/model pair can execute Pi's function tools. + let toolCall: Bool /// Provider-level OpenAI-compat flags. Matches the shape pi-coding-agent's /// provider catalog expects (see the ds4 README "For Pi" section). let supportsStore: Bool let supportsDeveloperRole: Bool let supportsReasoningEffort: Bool let supportsUsageInStreaming: Bool + let supportsFinishReason: Bool let supportsStrictMode: Bool /// "max_tokens" for legacy OpenAI servers, "max_completion_tokens" for newer. let maxTokensField: String @@ -34,75 +50,59 @@ struct PiProviderModel { /// DSML servers require the original `reasoning_content` to be sent back on /// follow-up assistant messages so the transcript matches what was sampled. let requiresReasoningContentOnAssistantMessages: Bool - /// Optional map from Pi thinking-level keys (off/minimal/low/medium/high/xhigh) - /// to the provider's native effort label. `nil` value in JS means the model - /// should be invoked without a reasoning_effort argument. + /// Optional map from Pi thinking-level keys to the provider's native effort + /// label. A nil value is rendered as null, which marks that level unsupported. let thinkingLevelMap: [(key: String, value: String?)]? init( id: String, name: String, - contextWindow: Int, - maxTokens: Int, - reasoning: Bool = false, - supportsStore: Bool = false, - supportsDeveloperRole: Bool = false, - supportsReasoningEffort: Bool = false, - supportsUsageInStreaming: Bool = false, - supportsStrictMode: Bool = false, - maxTokensField: String = "max_tokens", - thinkingFormat: String? = nil, - requiresReasoningContentOnAssistantMessages: Bool = false, - thinkingLevelMap: [(key: String, value: String?)]? = nil - ) { + profile: ManagedModelAPIProfile + ) throws { + guard profile.task == .chatCompletions, + let contextWindow = profile.contextWindow, + let maxTokens = profile.maximumOutputTokens else { + throw PiProviderModelError.invalidCatalogProfile(id) + } + let supportedThinkingLevels = Set(profile.thinkingLevels) + let thinkingLevelMap = profile.reasoning + ? ManagedModelThinkingLevel.allCases.compactMap { level -> (key: String, value: String?)? in + guard supportedThinkingLevels.contains(level) else { + return (level.rawValue, nil) + } + guard let mapped = profile.thinkingLevelMap[level] else { + return nil + } + return (level.rawValue, mapped.rawValue) + } + : nil + let compatibility = profile.compatibility self.id = id self.name = name self.contextWindow = contextWindow self.maxTokens = maxTokens - self.reasoning = reasoning - self.supportsStore = supportsStore - self.supportsDeveloperRole = supportsDeveloperRole - self.supportsReasoningEffort = supportsReasoningEffort - self.supportsUsageInStreaming = supportsUsageInStreaming - self.supportsStrictMode = supportsStrictMode - self.maxTokensField = maxTokensField - self.thinkingFormat = thinkingFormat - self.requiresReasoningContentOnAssistantMessages = requiresReasoningContentOnAssistantMessages + self.inputModalities = profile.inputModalities.compactMap { modality in + switch modality { + case .text, .image: + return modality.rawValue + case .audio, .video, .embedding, .geometry, .threeD: + return nil + } + } + self.reasoning = profile.reasoning + self.toolCall = profile.toolCall + self.supportsStore = compatibility.supportsStore + self.supportsDeveloperRole = compatibility.supportsDeveloperRole + self.supportsReasoningEffort = compatibility.supportsReasoningEffort + self.supportsUsageInStreaming = compatibility.supportsUsageInStreaming + self.supportsFinishReason = compatibility.supportsFinishReason + self.supportsStrictMode = compatibility.supportsStrictMode + self.maxTokensField = compatibility.maxTokensField.rawValue + self.thinkingFormat = compatibility.thinkingFormat?.rawValue + self.requiresReasoningContentOnAssistantMessages = compatibility + .requiresReasoningContentOnAssistantMessages self.thinkingLevelMap = thinkingLevelMap } - - static let qwen3CoderNext = PiProviderModel( - id: CodeGenResources.defaultModelId, - name: "Qwen3-Coder Next (mere.run)", - contextWindow: 32768, - maxTokens: 4096 - ) - - /// DeepSeek V4 Flash compat profile from the upstream ds4 README's - /// "For Pi, add a provider to ~/.pi/agent/models.json" section. - static let deepseekV4Flash = PiProviderModel( - id: DeepseekV4FlashResources.defaultModelId, - name: "DeepSeek V4 Flash (mere.run)", - contextWindow: DeepseekV4FlashResources.defaultContextLength, - maxTokens: DeepseekV4FlashResources.defaultContextLength, - reasoning: true, - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: true, - supportsUsageInStreaming: true, - supportsStrictMode: false, - maxTokensField: "max_tokens", - thinkingFormat: "deepseek", - requiresReasoningContentOnAssistantMessages: true, - thinkingLevelMap: [ - ("off", nil), - ("minimal", "low"), - ("low", "low"), - ("medium", "medium"), - ("high", "high"), - ("xhigh", "xhigh"), - ] - ) } enum PiAgentIntegration { @@ -221,7 +221,7 @@ enum PiAgentIntegration { static func writeLocalProviderExtension( host: String, port: Int, - model: PiProviderModel = .qwen3CoderNext, + model: PiProviderModel, apiKey: String = "mere-run", homeDirectory: URL? = nil, persistConfiguration: Bool = true, @@ -251,9 +251,8 @@ enum PiAgentIntegration { return extensionURL } - /// Render the pi-coding-agent extension TS for a single mere.run provider. - /// Mirrors the JSON provider catalog shape documented in the ds4 README - /// ("For Pi, add a provider to ~/.pi/agent/models.json"). + /// Render a current Pi extension with one offline fallback and live model + /// discovery from mere.run's self-describing `/v1/models` endpoint. static func renderPiProviderExtension( model: PiProviderModel, baseURL: String, @@ -264,6 +263,7 @@ enum PiAgentIntegration { providerCompat.append("supportsDeveloperRole: \(model.supportsDeveloperRole)") providerCompat.append("supportsReasoningEffort: \(model.supportsReasoningEffort)") providerCompat.append("supportsUsageInStreaming: \(model.supportsUsageInStreaming)") + providerCompat.append("supportsFinishReason: \(model.supportsFinishReason)") providerCompat.append("maxTokensField: \"\(model.maxTokensField)\"") providerCompat.append("supportsStrictMode: \(model.supportsStrictMode)") if let format = model.thinkingFormat { @@ -273,12 +273,15 @@ enum PiAgentIntegration { providerCompat.append("requiresReasoningContentOnAssistantMessages: true") } let compatBlock = providerCompat - .map { " \($0)" } + .map { " \($0)" } .joined(separator: ",\n") var modelLines: [String] = [] - modelLines.append("id: \"\(model.id)\"") - modelLines.append("name: \"\(model.name)\"") + modelLines.append("id: \(javascriptStringLiteral(model.id))") + modelLines.append("name: \(javascriptStringLiteral(model.name))") + modelLines.append("api: \"openai-completions\" as const") + modelLines.append("provider: \"mere-run\"") + modelLines.append("baseUrl") modelLines.append("reasoning: \(model.reasoning)") if let map = model.thinkingLevelMap, !map.isEmpty { let entries = map.map { entry -> String in @@ -289,7 +292,10 @@ enum PiAgentIntegration { }.joined(separator: ",\n") modelLines.append("thinkingLevelMap: {\n\(entries)\n }") } - modelLines.append("input: [\"text\"]") + let inputModalities = model.inputModalities + .map(javascriptStringLiteral) + .joined(separator: ", ") + modelLines.append("input: [\(inputModalities)] as Array<\"text\" | \"image\">") modelLines.append("contextWindow: \(model.contextWindow)") modelLines.append("maxTokens: \(model.maxTokens)") modelLines.append(""" @@ -301,31 +307,164 @@ enum PiAgentIntegration { } """) let modelBody = modelLines - .map { " \($0)" } + .map { " \($0)" } .joined(separator: ",\n") + let fallbackModel: String + if model.toolCall { + fallbackModel = [ + " {", + modelBody + ",", + " compat: {", + compatBlock, + " }", + " }", + ].joined(separator: "\n") + } else { + fallbackModel = "" + } + + let quotedBaseURL = javascriptStringLiteral(baseURL) + let quotedAPIKey = javascriptStringLiteral(apiKey) + return """ - import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + import { createProvider, openAICompletionsApi, type Model } from "@earendil-works/pi-ai/compat"; + import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + + type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; + + interface MereRunModel { + id: string; + name?: string; + task?: string; + reasoning?: boolean; + thinking_levels?: ThinkingLevel[]; + tool_call?: boolean; + modalities?: { input: string[]; output: string[] }; + limit?: { context: number; output: number }; + openai_compat?: { + supports_store: boolean; + supports_developer_role: boolean; + supports_reasoning_effort: boolean; + supports_usage_in_streaming: boolean; + supports_finish_reason: boolean; + max_tokens_field: "max_tokens" | "max_completion_tokens"; + supports_strict_mode: boolean; + thinking_format?: "deepseek"; + thinking_level_map?: Partial>; + requires_reasoning_content_on_assistant_messages: boolean; + }; + } - export default function(pi: ExtensionAPI) { - pi.registerProvider("mere-run", { - name: "mere.run Local", - baseUrl: "\(baseURL)", + const baseUrl = \(quotedBaseURL); + const apiKey = \(quotedAPIKey); + const thinkingLevels: ThinkingLevel[] = [ + "off", "minimal", "low", "medium", "high", "xhigh", "max" + ]; + const fallbackModels: ReturnType[] = [ + \(fallbackModel) + ]; + + function mapThinkingLevels(model: MereRunModel) { + if (!model.reasoning || !model.thinking_levels?.length) return undefined; + const supported = new Set(model.thinking_levels); + const overrides = model.openai_compat?.thinking_level_map ?? {}; + const result: Partial> = {}; + for (const level of thinkingLevels) { + if (!supported.has(level)) result[level] = null; + else if (overrides[level]) result[level] = overrides[level]; + } + return result; + } + + function mapModel(model: MereRunModel): Model<"openai-completions"> { + const compat = model.openai_compat; + return { + id: model.id, + name: model.name ?? model.id, api: "openai-completions", - apiKey: "\(apiKey)", - compat: { - \(compatBlock) - }, - models: [ - { - \(modelBody) - } - ] + provider: "mere-run", + baseUrl, + reasoning: model.reasoning ?? false, + thinkingLevelMap: mapThinkingLevels(model), + input: (model.modalities?.input ?? ["text"]) + .filter((value): value is "text" | "image" => value === "text" || value === "image"), + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.limit?.context ?? 32_768, + maxTokens: model.limit?.output ?? 4_096, + compat: compat ? { + supportsStore: compat.supports_store, + supportsDeveloperRole: compat.supports_developer_role, + supportsReasoningEffort: compat.supports_reasoning_effort, + supportsUsageInStreaming: compat.supports_usage_in_streaming, + supportsFinishReason: compat.supports_finish_reason, + maxTokensField: compat.max_tokens_field, + supportsStrictMode: compat.supports_strict_mode, + thinkingFormat: compat.thinking_format, + requiresReasoningContentOnAssistantMessages: + compat.requires_reasoning_content_on_assistant_messages + } : undefined + }; + } + + async function discoverModels( + credential: string, + signal: AbortSignal + ): Promise[]> { + const response = await fetch(`${baseUrl}/models`, { + headers: { Authorization: `Bearer ${credential}` }, + signal }); + if (!response.ok) throw new Error(`mere.run model discovery failed: HTTP ${response.status}`); + const payload = await response.json() as { data?: MereRunModel[] }; + return (payload.data ?? []) + .filter((entry) => entry.task === "chat.completions" && entry.tool_call === true) + .map(mapModel); + } + + export default async function(pi: ExtensionAPI) { + const initialModels = await discoverModels( + apiKey, + AbortSignal.timeout(2_000) + ).catch(() => fallbackModels); + pi.registerProvider(createProvider({ + id: "mere-run", + name: "mere.run Local", + baseUrl, + auth: { + apiKey: { + name: "mere.run local API key", + async resolve({ credential }) { + const key = credential?.key ?? apiKey; + return { auth: { apiKey: key }, source: "mere.run local" }; + } + } + }, + models: initialModels, + async fetchModels(context) { + const credential = context.credential?.type === "api_key" + ? context.credential.key ?? apiKey + : apiKey; + return discoverModels(credential, context.signal); + }, + api: openAICompletionsApi() + })); } """ } + private static func javascriptStringLiteral(_ value: String) -> String { + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + .replacingOccurrences(of: "\u{2028}", with: "\\u2028") + .replacingOccurrences(of: "\u{2029}", with: "\\u2029") + return "\"\(escaped)\"" + } + static func mereRunPiHomeDirectory() -> URL { MereRunModelPaths.applicationSupportBase .appendingPathComponent("agent", isDirectory: true) @@ -422,7 +561,7 @@ enum PiAgentIntegration { } private static func fetchLatestRelease() async throws -> GitHubRelease { - let url = URL(string: "https://api.github.com/repos/badlogic/pi-mono/releases/latest")! + let url = URL(string: "https://api.github.com/repos/earendil-works/pi/releases/latest")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(GitHubRelease.self, from: data) } diff --git a/Sources/MereRunCLI/Support/RuntimeModelPool.swift b/Sources/MereRunCLI/Support/RuntimeModelPool.swift index 121552b5..1810dedd 100644 --- a/Sources/MereRunCLI/Support/RuntimeModelPool.swift +++ b/Sources/MereRunCLI/Support/RuntimeModelPool.swift @@ -898,12 +898,12 @@ actor RuntimeModelPool { let settings: RuntimeModelSettings let spec: ManagedModelSpec? + var apiProfile: ManagedModelAPIProfile { + spec?.apiProfile ?? .runtimeFallback(for: engine) + } + var openAICompatibility: APIEngineCapabilities { - var capabilities = engine.openAICompatibility - if spec?.category == .visionChat { - capabilities.supportsVisionContentParts = true - } - return capabilities + .catalog(apiProfile) } } @@ -992,8 +992,35 @@ actor RuntimeModelPool { _ = try await loadModel(idOrAlias: defaultModelID) } - func modelsResponse(createdAt: Date = Date()) throws -> OpenAIModelsResponse { - APIServerContract.modelsResponse(modelIds: try listedOpenAIModelIDs(), createdAt: createdAt) + func modelsResponse( + serverContextSize: Int = 32_768, + createdAt: Date = Date() + ) throws -> OpenAIModelsResponse { + let models = try listedOpenAIModelIDs().map { listedID in + let resolved = try resolveModel(listedID, requireInstalled: false) + let profile = resolved.apiProfile + let configuredContextWindow = resolved.settings.maxContextTokens ?? serverContextSize + let contextWindow = min( + profile.contextWindow ?? configuredContextWindow, + configuredContextWindow + ) + let catalogOutputLimit = profile.maximumOutputTokens ?? contextWindow + let configuredOutputLimit = resolved.settings.maxTokens ?? catalogOutputLimit + let maximumOutputTokens = min( + min(catalogOutputLimit, configuredOutputLimit), + contextWindow + ) + let name = resolved.spec?.upstreamRepoId ?? resolved.id + return APIServerContract.chatModel( + id: listedID, + name: name, + profile: profile, + contextWindow: contextWindow, + maximumOutputTokens: maximumOutputTokens, + createdAt: createdAt + ) + } + return OpenAIModelsResponse(object: "list", data: models) } func status() async -> RuntimeModelPoolStatus { @@ -1192,7 +1219,8 @@ actor RuntimeModelPool { fallbackLoraPath: fallbackLoraPath, contextSize: contextSize, capabilities: capabilities, - servedModelID: resolved.id + servedModelID: resolved.id, + apiProfile: resolved.apiProfile ) chatRequest.kvCacheMode = resolved.settings.kvCacheMode let includeUsage = try APIServerContract.includeUsageInStreaming( @@ -2344,26 +2372,7 @@ enum RuntimeLoadedModel: Sendable { extension RuntimeServingEngine { var openAICompatibility: APIEngineCapabilities { - switch self { - case .textCode: - return .localTextWithStopSequences - case .textChatKlein: - return .localTextWithStructuredJSON - case .textChatGemma4: - return .localTextWithToolsAndStructuredJSON - case .textChatLaguna: - return .localTextWithToolsAndStopSequences - case .textChatQ36, .textChatQ35: - return .localTextWithToolsVisionAndStructuredJSON - case .textChatLFM2: - return .localTextWithTools - case .textChatDeepseekV4Flash: - return .rawProxy - case .textChatMuseGlimmer: - return .localTextWithToolsVisionAndReasoning - case .textChatNemotronH: - return .localTextWithToolsAndStopSequences - } + .catalog(.runtimeFallback(for: self)) } } diff --git a/Sources/MereRunCLI/Support/SetupAgentPrompt.swift b/Sources/MereRunCLI/Support/SetupAgentPrompt.swift index abda81b7..83999926 100644 --- a/Sources/MereRunCLI/Support/SetupAgentPrompt.swift +++ b/Sources/MereRunCLI/Support/SetupAgentPrompt.swift @@ -45,7 +45,7 @@ enum SetupAgentPrompt { Setup workflow rules: - If the selected setup-agent is recommended, do not suggest another setup/chat agent as an upgrade. - - On 96 GB+ Apple Silicon Macs, DeepSeek V4 Flash is the preferred premier setup-agent tier; smaller Qwen agents are alternatives for lower-memory workflows. + - On 96 GB+ Apple Silicon Macs, DeepSeek V4 Flash is the preferred premier setup-agent tier; smaller tool-capable native agents are alternatives for lower-memory workflows. - Treat the recommended setup-agent line as authoritative for agent/chat setup recommendations; the broader supported managed-model list is cross-modality coverage, not a ranked upgrade list. - Do not explore the repository to discover setup facts unless a listed command fails. - Do not run demo scripts, sample scripts, `demo.sh`, `scripts/check.sh`, `swift build`, or `swift test` for onboarding. diff --git a/Sources/MereRunCore/AgentModelResources.swift b/Sources/MereRunCore/AgentModelResources.swift index fe3b71ac..0dae926a 100644 --- a/Sources/MereRunCore/AgentModelResources.swift +++ b/Sources/MereRunCore/AgentModelResources.swift @@ -80,7 +80,12 @@ public struct MereRunAgentModelRecommendation: Hashable, Sendable { public let reason: String? public var isStartableByMereRun: Bool { - managedModelID != nil && !sourceConfigurationRequired + guard let managedModelID, + !sourceConfigurationRequired, + let profile = ManagedModelCatalog.apiProfile(for: managedModelID) else { + return false + } + return profile.task == .chatCompletions && profile.toolCall } public init( @@ -128,7 +133,7 @@ public enum MereRunAgentModelCatalog { on machine: MereRunMachineProfile = .current ) -> MereRunAgentModelRecommendation? { guard machine.unifiedMemoryGB >= 16 else { return nil } - return qwen35NineB() + return ornith9B() } public static func tierRecommendation( @@ -138,15 +143,12 @@ public enum MereRunAgentModelCatalog { if machine.unifiedMemoryGB >= 96 { return deepseekV4Flash() } - if machine.isLinux && machine.unifiedMemoryGB >= 64 { - return qwen3CoderNext() + if machine.isLinux && machine.unifiedMemoryGB >= 24 { + return q36Nano() } if machine.unifiedMemoryGB >= 64 { return gemma12B4Bit() } - if machine.isLinux && machine.unifiedMemoryGB >= 24 { - return q36Nano() - } if machine.unifiedMemoryGB >= 24 { return gemma12B4Bit() } @@ -169,15 +171,12 @@ public enum MereRunAgentModelCatalog { if machine.unifiedMemoryGB >= 96 { return deepseekV4Flash() } - if machine.isLinux && machine.unifiedMemoryGB >= 64 { - return qwen3CoderNext() + if machine.isLinux && machine.unifiedMemoryGB >= 24 { + return q36Nano() } if machine.unifiedMemoryGB >= 64 { return gemma12B4Bit() } - if machine.isLinux && machine.unifiedMemoryGB >= 24 { - return q36Nano() - } if machine.unifiedMemoryGB >= 24 { return gemma12B4Bit() } diff --git a/Sources/MereRunCore/CodeGen/OpenAITypes.swift b/Sources/MereRunCore/CodeGen/OpenAITypes.swift index 66852e50..0f887285 100644 --- a/Sources/MereRunCore/CodeGen/OpenAITypes.swift +++ b/Sources/MereRunCore/CodeGen/OpenAITypes.swift @@ -1049,12 +1049,101 @@ public struct OpenAIModel: Codable, Sendable { public var object: String public var created: Int public var owned_by: String + public var name: String? + public var task: String? + public var reasoning: Bool? + public var thinking_levels: [String]? + public var tool_call: Bool? + public var structured_output: Bool? + public var modalities: OpenAIModelModalities? + public var limit: OpenAIModelLimit? + public var openai_compat: OpenAIModelCompatibility? - public init(id: String, object: String, created: Int, owned_by: String) { + public init( + id: String, + object: String, + created: Int, + owned_by: String, + name: String? = nil, + task: String? = nil, + reasoning: Bool? = nil, + thinking_levels: [String]? = nil, + tool_call: Bool? = nil, + structured_output: Bool? = nil, + modalities: OpenAIModelModalities? = nil, + limit: OpenAIModelLimit? = nil, + openai_compat: OpenAIModelCompatibility? = nil + ) { self.id = id self.object = object self.created = created self.owned_by = owned_by + self.name = name + self.task = task + self.reasoning = reasoning + self.thinking_levels = thinking_levels + self.tool_call = tool_call + self.structured_output = structured_output + self.modalities = modalities + self.limit = limit + self.openai_compat = openai_compat + } +} + +public struct OpenAIModelModalities: Codable, Equatable, Sendable { + public var input: [String] + public var output: [String] + + public init(input: [String], output: [String]) { + self.input = input + self.output = output + } +} + +public struct OpenAIModelLimit: Codable, Equatable, Sendable { + public var context: Int + public var output: Int + + public init(context: Int, output: Int) { + self.context = context + self.output = output + } +} + +public struct OpenAIModelCompatibility: Codable, Equatable, Sendable { + public var supports_store: Bool + public var supports_developer_role: Bool + public var supports_reasoning_effort: Bool + public var supports_usage_in_streaming: Bool + public var supports_finish_reason: Bool + public var max_tokens_field: String + public var supports_strict_mode: Bool + public var thinking_format: String? + public var thinking_level_map: [String: String]? + public var requires_reasoning_content_on_assistant_messages: Bool + + public init( + supports_store: Bool, + supports_developer_role: Bool, + supports_reasoning_effort: Bool, + supports_usage_in_streaming: Bool, + supports_finish_reason: Bool, + max_tokens_field: String, + supports_strict_mode: Bool, + thinking_format: String? = nil, + thinking_level_map: [String: String]? = nil, + requires_reasoning_content_on_assistant_messages: Bool = false + ) { + self.supports_store = supports_store + self.supports_developer_role = supports_developer_role + self.supports_reasoning_effort = supports_reasoning_effort + self.supports_usage_in_streaming = supports_usage_in_streaming + self.supports_finish_reason = supports_finish_reason + self.max_tokens_field = max_tokens_field + self.supports_strict_mode = supports_strict_mode + self.thinking_format = thinking_format + self.thinking_level_map = thinking_level_map + self.requires_reasoning_content_on_assistant_messages = requires_reasoning_content_on_assistant_messages } } diff --git a/Sources/MereRunCore/ManagedModelCatalog.swift b/Sources/MereRunCore/ManagedModelCatalog.swift index 7a9c8aad..be5b32f6 100644 --- a/Sources/MereRunCore/ManagedModelCatalog.swift +++ b/Sources/MereRunCore/ManagedModelCatalog.swift @@ -26,6 +26,417 @@ public enum ManagedModelCategory: String, CaseIterable, Hashable, Sendable { case video = "video" } +public enum ManagedModelAPITask: String, Hashable, Sendable { + case chatCompletions = "chat.completions" + case imageGenerations = "images.generations" + case imageEdits = "images.edits" + case audioSpeech = "audio.speech" + case audioTranscriptions = "audio.transcriptions" + case embeddings + case visionGeometry = "vision.geometry" + case visionDepth = "vision.depth" + case visionImageTo3D = "vision.image_to_3d" +} + +public enum ManagedModelAPIModality: String, Hashable, Sendable { + case text + case image + case audio + case video + case embedding + case geometry + case threeD = "3d" +} + +public enum ManagedModelThinkingLevel: String, CaseIterable, Hashable, Sendable { + case off + case minimal + case low + case medium + case high + case xhigh + case max +} + +public enum ManagedModelMaxTokensField: String, Hashable, Sendable { + case maxTokens = "max_tokens" + case maxCompletionTokens = "max_completion_tokens" +} + +public enum ManagedModelThinkingFormat: String, Hashable, Sendable { + case deepseek +} + +public struct ManagedModelOpenAICompatibilityProfile: Hashable, Sendable { + public let supportsStore: Bool + public let supportsDeveloperRole: Bool + public let supportsReasoningEffort: Bool + public let supportsUsageInStreaming: Bool + public let supportsFinishReason: Bool + public let maxTokensField: ManagedModelMaxTokensField + public let supportsStrictMode: Bool + public let thinkingFormat: ManagedModelThinkingFormat? + public let requiresReasoningContentOnAssistantMessages: Bool + + public init( + supportsStore: Bool = false, + supportsDeveloperRole: Bool = true, + supportsReasoningEffort: Bool = false, + supportsUsageInStreaming: Bool = true, + supportsFinishReason: Bool = true, + maxTokensField: ManagedModelMaxTokensField = .maxCompletionTokens, + supportsStrictMode: Bool = false, + thinkingFormat: ManagedModelThinkingFormat? = nil, + requiresReasoningContentOnAssistantMessages: Bool = false + ) { + self.supportsStore = supportsStore + self.supportsDeveloperRole = supportsDeveloperRole + self.supportsReasoningEffort = supportsReasoningEffort + self.supportsUsageInStreaming = supportsUsageInStreaming + self.supportsFinishReason = supportsFinishReason + self.maxTokensField = maxTokensField + self.supportsStrictMode = supportsStrictMode + self.thinkingFormat = thinkingFormat + self.requiresReasoningContentOnAssistantMessages = requiresReasoningContentOnAssistantMessages + } +} + +/// The capabilities mere.run promises when it serves a managed model. +/// +/// This is deliberately catalog metadata rather than a client-specific model +/// definition. API discovery, request validation, and harness integrations all +/// project from the same profile, then apply runtime settings such as context +/// and output-token overrides. +public struct ManagedModelAPIProfile: Hashable, Sendable { + public let task: ManagedModelAPITask + public let servingEngine: RuntimeServingEngine? + public let inputModalities: [ManagedModelAPIModality] + public let outputModalities: [ManagedModelAPIModality] + public let contextWindow: Int? + public let maximumOutputTokens: Int? + public let thinkingLevels: [ManagedModelThinkingLevel] + public let thinkingLevelMap: [ManagedModelThinkingLevel: ManagedModelThinkingLevel] + public let reasoningEffortStrengths: [ManagedModelThinkingLevel: Double] + public let toolCall: Bool + public let structuredOutput: Bool + public let compatibility: ManagedModelOpenAICompatibilityProfile + public let supportsRawProxy: Bool + public let supportsToolChoice: Bool + public let supportsStopSequences: Bool + public let supportsSeed: Bool + public let supportsPenalties: Bool + public let supportsLogprobs: Bool + public let supportsProviderThinkingControls: Bool + + public var reasoning: Bool { + !thinkingLevels.isEmpty + } + + public init( + task: ManagedModelAPITask, + servingEngine: RuntimeServingEngine? = nil, + inputModalities: [ManagedModelAPIModality], + outputModalities: [ManagedModelAPIModality], + contextWindow: Int? = nil, + maximumOutputTokens: Int? = nil, + thinkingLevels: [ManagedModelThinkingLevel] = [], + thinkingLevelMap: [ManagedModelThinkingLevel: ManagedModelThinkingLevel] = [:], + reasoningEffortStrengths: [ManagedModelThinkingLevel: Double] = [:], + toolCall: Bool = false, + structuredOutput: Bool = false, + compatibility: ManagedModelOpenAICompatibilityProfile = .init(), + supportsRawProxy: Bool = false, + supportsToolChoice: Bool = false, + supportsStopSequences: Bool = false, + supportsSeed: Bool = false, + supportsPenalties: Bool = false, + supportsLogprobs: Bool = false, + supportsProviderThinkingControls: Bool = false + ) { + self.task = task + self.servingEngine = servingEngine + self.inputModalities = inputModalities + self.outputModalities = outputModalities + self.contextWindow = contextWindow + self.maximumOutputTokens = maximumOutputTokens + self.thinkingLevels = thinkingLevels + self.thinkingLevelMap = thinkingLevelMap + self.reasoningEffortStrengths = reasoningEffortStrengths + self.toolCall = toolCall + self.structuredOutput = structuredOutput + self.compatibility = compatibility + self.supportsRawProxy = supportsRawProxy + self.supportsToolChoice = supportsToolChoice + self.supportsStopSequences = supportsStopSequences + self.supportsSeed = supportsSeed + self.supportsPenalties = supportsPenalties + self.supportsLogprobs = supportsLogprobs + self.supportsProviderThinkingControls = supportsProviderThinkingControls + } +} + +public extension ManagedModelAPIProfile { + static func textCode( + contextWindow: Int = 32_768, + maximumOutputTokens: Int = 4_096 + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textCode, + contextWindow: contextWindow, + maximumOutputTokens: maximumOutputTokens, + supportsStopSequences: true + ) + } + + static func klein(contextWindow: Int = 32_768) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatKlein, + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + structuredOutput: true + ) + } + + static func gemma4( + inputModalities: [ManagedModelAPIModality] = [.text], + contextWindow: Int = Gemma4Resources.defaultContextLength + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatGemma4, + inputModalities: inputModalities, + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + toolCall: true, + structuredOutput: true + ) + } + + static func laguna(contextWindow: Int = LagunaResources.defaultContextLength) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatLaguna, + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + toolCall: true, + supportsStopSequences: true + ) + } + + static func q36( + contextWindow: Int, + fixedReasoning: Bool = false + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatQ36, + inputModalities: [.text, .image], + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + thinkingLevels: fixedReasoning ? [.high] : [], + toolCall: true, + structuredOutput: true + ) + } + + static func lfm2( + inputModalities: [ManagedModelAPIModality] = [.text], + contextWindow: Int = LFM2Resources.defaultContextLength + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatLFM2, + inputModalities: inputModalities, + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + toolCall: true + ) + } + + static func deepseekV4Flash( + contextWindow: Int = DeepseekV4FlashResources.defaultContextLength + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatDeepseekV4Flash, + inputModalities: [.text, .image], + contextWindow: contextWindow, + maximumOutputTokens: contextWindow, + thinkingLevels: [.off, .minimal, .low, .medium, .high, .xhigh], + thinkingLevelMap: [.minimal: .low], + toolCall: true, + compatibility: ManagedModelOpenAICompatibilityProfile( + supportsDeveloperRole: false, + supportsReasoningEffort: true, + maxTokensField: .maxTokens, + thinkingFormat: .deepseek, + requiresReasoningContentOnAssistantMessages: true + ), + supportsRawProxy: true, + supportsStopSequences: true, + supportsSeed: true, + supportsPenalties: true, + supportsLogprobs: true, + supportsProviderThinkingControls: true + ) + } + + static func museGlimmer( + contextWindow: Int = MuseGlimmerResources.defaultContextLength + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatMuseGlimmer, + inputModalities: [.text, .image], + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + thinkingLevels: [.minimal, .low, .medium, .high, .xhigh, .max], + thinkingLevelMap: [.minimal: .low, .max: .xhigh], + reasoningEffortStrengths: [ + .minimal: 0.1, + .low: 0.25, + .medium: 0.5, + .high: 0.8, + .xhigh: 1, + .max: 1, + ], + toolCall: true, + compatibility: ManagedModelOpenAICompatibilityProfile( + supportsReasoningEffort: true + ) + ) + } + + static func nemotronH( + contextWindow: Int = NemotronHResources.defaultContextLength + ) -> ManagedModelAPIProfile { + chat( + servingEngine: .textChatNemotronH, + contextWindow: contextWindow, + maximumOutputTokens: 4_096, + toolCall: true, + supportsStopSequences: true + ) + } + + static func runtimeFallback(for engine: RuntimeServingEngine) -> ManagedModelAPIProfile { + switch engine { + case .textCode: + return .textCode() + case .textChatKlein: + return .klein() + case .textChatGemma4: + return .gemma4() + case .textChatLaguna: + return .laguna() + case .textChatQ36, .textChatQ35: + return .q36(contextWindow: Q35Resources.defaultContextLength) + case .textChatLFM2: + return .lfm2() + case .textChatDeepseekV4Flash: + return .deepseekV4Flash() + case .textChatMuseGlimmer: + return .museGlimmer() + case .textChatNemotronH: + return .nemotronH() + } + } + + static func companion( + modelID: String, + category: ManagedModelCategory? + ) -> ManagedModelAPIProfile? { + if modelID == QwenImageEditRepository.modelId { + return ManagedModelAPIProfile( + task: .imageEdits, + inputModalities: [.text, .image], + outputModalities: [.image] + ) + } + switch category { + case .image: + return ManagedModelAPIProfile( + task: .imageGenerations, + inputModalities: [.text], + outputModalities: [.image] + ) + case .image3D: + return ManagedModelAPIProfile( + task: .visionImageTo3D, + inputModalities: [.image], + outputModalities: [.threeD] + ) + case .speechTTS: + return ManagedModelAPIProfile( + task: .audioSpeech, + inputModalities: [.text], + outputModalities: [.audio] + ) + case .speechASR: + return ManagedModelAPIProfile( + task: .audioTranscriptions, + inputModalities: [.audio], + outputModalities: [.text] + ) + case .textEmbed: + return ManagedModelAPIProfile( + task: .embeddings, + inputModalities: [.text], + outputModalities: [.embedding] + ) + case .visionGeometry: + return ManagedModelAPIProfile( + task: .visionGeometry, + inputModalities: [.image], + outputModalities: [.geometry] + ) + case .visionDepth: + return ManagedModelAPIProfile( + task: .visionDepth, + inputModalities: [.video], + outputModalities: [.video] + ) + default: + return nil + } + } + + private static func chat( + servingEngine: RuntimeServingEngine, + inputModalities: [ManagedModelAPIModality] = [.text], + contextWindow: Int, + maximumOutputTokens: Int, + thinkingLevels: [ManagedModelThinkingLevel] = [], + thinkingLevelMap: [ManagedModelThinkingLevel: ManagedModelThinkingLevel] = [:], + reasoningEffortStrengths: [ManagedModelThinkingLevel: Double] = [:], + toolCall: Bool = false, + structuredOutput: Bool = false, + compatibility: ManagedModelOpenAICompatibilityProfile = .init(), + supportsRawProxy: Bool = false, + supportsStopSequences: Bool = false, + supportsSeed: Bool = false, + supportsPenalties: Bool = false, + supportsLogprobs: Bool = false, + supportsProviderThinkingControls: Bool = false + ) -> ManagedModelAPIProfile { + ManagedModelAPIProfile( + task: .chatCompletions, + servingEngine: servingEngine, + inputModalities: inputModalities, + outputModalities: [.text], + contextWindow: contextWindow, + maximumOutputTokens: maximumOutputTokens, + thinkingLevels: thinkingLevels, + thinkingLevelMap: thinkingLevelMap, + reasoningEffortStrengths: reasoningEffortStrengths, + toolCall: toolCall, + structuredOutput: structuredOutput, + compatibility: compatibility, + supportsRawProxy: supportsRawProxy, + supportsToolChoice: toolCall, + supportsStopSequences: supportsStopSequences, + supportsSeed: supportsSeed, + supportsPenalties: supportsPenalties, + supportsLogprobs: supportsLogprobs, + supportsProviderThinkingControls: supportsProviderThinkingControls + ) + } +} + public enum ManagedModelInstallShape: Hashable, Sendable { case directoryRoot case singleFile(relativePath: String) @@ -176,6 +587,7 @@ public struct ManagedModelSpec: Hashable, Sendable { public let estimatedDownloadBytes: Int64? public let defaultCLICommands: [String] public let companionModelIDs: [String] + public let apiProfile: ManagedModelAPIProfile? public init( id: String, @@ -193,7 +605,8 @@ public struct ManagedModelSpec: Hashable, Sendable { resolutionFallbackIDs: [String] = [], estimatedDownloadBytes: Int64? = nil, defaultCLICommands: [String] = [], - companionModelIDs: [String] = [] + companionModelIDs: [String] = [], + apiProfile: ManagedModelAPIProfile? = nil ) { self.id = id self.category = category @@ -211,10 +624,19 @@ public struct ManagedModelSpec: Hashable, Sendable { self.estimatedDownloadBytes = estimatedDownloadBytes self.defaultCLICommands = defaultCLICommands self.companionModelIDs = companionModelIDs + self.apiProfile = apiProfile ?? ManagedModelAPIProfile.companion( + modelID: id, + category: category + ) } } public enum ManagedModelCatalog { + public static func apiProfile(for modelID: String) -> ManagedModelAPIProfile? { + spec(for: modelID)?.apiProfile + ?? ManagedModelAPIProfile.companion(modelID: modelID, category: nil) + } + private static let diffusersImageSnapshotPatterns = [ "LICENSE*", "README.md", @@ -1002,7 +1424,8 @@ public enum ManagedModelCatalog { installShape: .directoryRoot, validationKind: .hfTextChat, runtimeAutoDownloadAllowed: false, - defaultCLICommands: ["api serve"] + defaultCLICommands: ["api serve"], + apiProfile: .klein() ), ManagedModelSpec( id: "text-chat-psi-agent", @@ -1023,7 +1446,8 @@ public enum ManagedModelCatalog { validationKind: .gemma4, resolutionFallbackIDs: ["text-chat-gemma4-max", "text-chat-gemma4-nano"], estimatedDownloadBytes: 62_578_654_199, - defaultCLICommands: ["text chat", "text train-lora", "api serve"] + defaultCLICommands: ["text chat", "text train-lora", "api serve"], + apiProfile: .gemma4() ), ManagedModelSpec( id: Gemma4Resources.turboModelId, @@ -1037,7 +1461,8 @@ public enum ManagedModelCatalog { validationKind: .gemma4, runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: 31 * 1_073_741_824, - defaultCLICommands: ["text chat", "text train-lora", "api serve"] + defaultCLICommands: ["text chat", "text train-lora", "api serve"], + apiProfile: .gemma4() ), ManagedModelSpec( id: Gemma4Resources.twelveBModelId, @@ -1051,7 +1476,8 @@ public enum ManagedModelCatalog { validationKind: .gemma4, estimatedDownloadBytes: 25 * 1_073_741_824, defaultCLICommands: ["text chat", "text train-lora", "api serve"], - companionModelIDs: [Gemma4MTPResources.modelId] + companionModelIDs: [Gemma4MTPResources.modelId], + apiProfile: .gemma4() ), ManagedModelSpec( id: Gemma4Resources.twelveB4BitModelId, @@ -1067,7 +1493,8 @@ public enum ManagedModelCatalog { validationKind: .gemma4, estimatedDownloadBytes: 6_773_374_762, defaultCLICommands: ["text chat", "text train-lora", "api serve"], - companionModelIDs: [Gemma4MTPResources.modelId] + companionModelIDs: [Gemma4MTPResources.modelId], + apiProfile: .gemma4() ), ManagedModelSpec( id: Gemma4Resources.visionTwelveBModelId, @@ -1081,7 +1508,8 @@ public enum ManagedModelCatalog { validationKind: .gemma4Unified, estimatedDownloadBytes: 25 * 1_073_741_824, defaultCLICommands: ["api serve"], - companionModelIDs: [Gemma4MTPResources.modelId] + companionModelIDs: [Gemma4MTPResources.modelId], + apiProfile: .gemma4(inputModalities: [.text, .image]) ), ManagedModelSpec( id: "text-chat-gemma4-nano", @@ -1094,7 +1522,8 @@ public enum ManagedModelCatalog { upstreamRepoId: Gemma4Resources.nanoUpstreamModelId, validationKind: .gemma4, estimatedDownloadBytes: 16_024_791_983, - defaultCLICommands: ["text chat", "text train-lora", "api serve"] + defaultCLICommands: ["text chat", "text train-lora", "api serve"], + apiProfile: .gemma4() ), ManagedModelSpec( id: "text-chat-gemma4-max", @@ -1107,7 +1536,8 @@ public enum ManagedModelCatalog { upstreamRepoId: Gemma4Resources.maxUpstreamModelId, validationKind: .gemma4, estimatedDownloadBytes: 62_578_654_199, - defaultCLICommands: ["text chat", "text train-lora", "api serve"] + defaultCLICommands: ["text chat", "text train-lora", "api serve"], + apiProfile: .gemma4() ), ManagedModelSpec( id: LagunaResources.modelID, @@ -1124,7 +1554,8 @@ public enum ManagedModelCatalog { runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: LagunaResources.estimatedDownloadBytes, defaultCLICommands: ["text chat", "api serve", "model benchmark chat"], - companionModelIDs: [LagunaResources.dflashModelID] + companionModelIDs: [LagunaResources.dflashModelID], + apiProfile: .laguna() ), ManagedModelSpec( id: LagunaResources.xsModelID, @@ -1145,7 +1576,8 @@ public enum ManagedModelCatalog { "text train-lora", "api serve", "model benchmark chat", - ] + ], + apiProfile: .laguna() ), ManagedModelSpec( id: InklingResources.modelID, @@ -1187,7 +1619,8 @@ public enum ManagedModelCatalog { runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: MuseGlimmerResources.estimatedDownloadBytes, defaultCLICommands: ["text chat", "api serve"], - companionModelIDs: [MuseGlimmerResources.assistantModelId] + companionModelIDs: [MuseGlimmerResources.assistantModelId], + apiProfile: .museGlimmer() ), ManagedModelSpec( id: NemotronHResources.modelID, @@ -1204,7 +1637,8 @@ public enum ManagedModelCatalog { runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: NemotronHResources.estimatedDownloadBytes, defaultCLICommands: ["text chat", "api serve", "model benchmark chat"], - companionModelIDs: [NemotronHResources.dsparkModelID] + companionModelIDs: [NemotronHResources.dsparkModelID], + apiProfile: .nemotronH() ), ManagedModelSpec( id: Q35Resources.q36NanoModelId, @@ -1215,7 +1649,8 @@ public enum ManagedModelCatalog { upstreamRevision: Q35Resources.q36NanoUpstreamRevision, validationKind: .q35, estimatedDownloadBytes: 24 * 1_073_741_824, - defaultCLICommands: ["chat", "api serve"] + defaultCLICommands: ["chat", "api serve"], + apiProfile: .q36(contextWindow: Q35Resources.defaultContextLength) ), ManagedModelSpec( id: Q35Resources.bonsai27B1BitModelId, @@ -1226,7 +1661,11 @@ public enum ManagedModelCatalog { upstreamRevision: Q35Resources.bonsai27B1BitUpstreamRevision, validationKind: .q35, estimatedDownloadBytes: Q35Resources.bonsai27B1BitEstimatedDownloadBytes, - defaultCLICommands: ["text chat", "api serve", "model benchmark chat"] + defaultCLICommands: ["text chat", "api serve", "model benchmark chat"], + apiProfile: .q36( + contextWindow: Q35Resources.bonsai27B1BitContextLength, + fixedReasoning: true + ) ), ManagedModelSpec( id: Q35Resources.bonsai27B2BitModelId, @@ -1237,7 +1676,11 @@ public enum ManagedModelCatalog { upstreamRevision: Q35Resources.bonsai27B2BitUpstreamRevision, validationKind: .q35, estimatedDownloadBytes: Q35Resources.bonsai27B2BitEstimatedDownloadBytes, - defaultCLICommands: ["text chat", "api serve", "model benchmark chat"] + defaultCLICommands: ["text chat", "api serve", "model benchmark chat"], + apiProfile: .q36( + contextWindow: Q35Resources.bonsai27B2BitContextLength, + fixedReasoning: true + ) ), ManagedModelSpec( id: Q35Resources.ornith9BModelId, @@ -1248,7 +1691,11 @@ public enum ManagedModelCatalog { upstreamRevision: Q35Resources.ornith9BUpstreamRevision, validationKind: .q35, estimatedDownloadBytes: Q35Resources.ornith9BEstimatedDownloadBytes, - defaultCLICommands: ["chat", "api serve", "agent start"] + defaultCLICommands: ["chat", "api serve", "agent start"], + apiProfile: .q36( + contextWindow: Q35Resources.defaultContextLength, + fixedReasoning: true + ) ), ManagedModelSpec( id: Q35Resources.ornith35BMLXModelId, @@ -1259,7 +1706,11 @@ public enum ManagedModelCatalog { validationKind: .q35, runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: Q35Resources.ornith35BMLXEstimatedDownloadBytes, - defaultCLICommands: ["chat", "api serve", "agent start", "model benchmark code"] + defaultCLICommands: ["chat", "api serve", "agent start", "model benchmark code"], + apiProfile: .q36( + contextWindow: Q35Resources.defaultContextLength, + fixedReasoning: true + ) ), ManagedModelSpec( id: AgentModelResources.qwen35NineBModelId, @@ -1270,7 +1721,8 @@ public enum ManagedModelCatalog { upstreamRevision: AgentModelResources.qwen35NineBRevision, validationKind: .codegenGGUF, estimatedDownloadBytes: 5_680_522_464, - defaultCLICommands: ["api serve", "text code"] + defaultCLICommands: ["api serve", "text code"], + apiProfile: .textCode() ), ManagedModelSpec( id: NorthMiniCodeResources.modelId, @@ -1281,7 +1733,11 @@ public enum ManagedModelCatalog { upstreamRevision: NorthMiniCodeResources.upstreamRevision, validationKind: .codegenGGUF, estimatedDownloadBytes: NorthMiniCodeResources.estimatedDownloadBytes, - defaultCLICommands: ["text code", "api serve", "agent start"] + defaultCLICommands: ["text code", "api serve", "agent start"], + apiProfile: .textCode( + contextWindow: NorthMiniCodeResources.runtimeContextLength, + maximumOutputTokens: NorthMiniCodeResources.maxOutputTokens + ) ), ManagedModelSpec( id: Ornith35BCodeResources.modelId, @@ -1292,7 +1748,11 @@ public enum ManagedModelCatalog { upstreamRevision: Ornith35BCodeResources.upstreamRevision, validationKind: .codegenGGUF, estimatedDownloadBytes: Ornith35BCodeResources.estimatedDownloadBytes, - defaultCLICommands: ["text code", "api serve", "agent start"] + defaultCLICommands: ["text code", "api serve", "agent start"], + apiProfile: .textCode( + contextWindow: Ornith35BCodeResources.runtimeContextLength, + maximumOutputTokens: Ornith35BCodeResources.maxOutputTokens + ) ), ManagedModelSpec( // GGUF Qwen3.6-35B-A3B: the CUDA default chat model. Routes through @@ -1312,7 +1772,8 @@ public enum ManagedModelCatalog { upstreamRevision: "main", validationKind: .codegenGGUF, estimatedDownloadBytes: 22 * 1_073_741_824, - defaultCLICommands: ["text chat", "api serve"] + defaultCLICommands: ["text chat", "api serve"], + apiProfile: .textCode() ), ManagedModelSpec( id: DeepseekV4FlashResources.defaultModelId, @@ -1323,7 +1784,8 @@ public enum ManagedModelCatalog { upstreamRevision: DeepseekV4FlashResources.defaultRevision, validationKind: .deepseekV4FlashIMatrixGGUF, estimatedDownloadBytes: DeepseekV4FlashResources.defaultGGUFByteCount, - defaultCLICommands: ["api serve", "agent"] + defaultCLICommands: ["api serve", "agent"], + apiProfile: .deepseekV4Flash() ), ManagedModelSpec( id: LFM2Resources.defaultModelId, @@ -1346,7 +1808,8 @@ public enum ManagedModelCatalog { validationKind: .lfm2, runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: 10 * 1_073_741_824, - defaultCLICommands: ["text chat", "api serve"] + defaultCLICommands: ["text chat", "api serve"], + apiProfile: .lfm2() ), ManagedModelSpec( id: LFM2Resources.denseModelId, @@ -1369,7 +1832,8 @@ public enum ManagedModelCatalog { validationKind: .lfm2, runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: 1_601_108_788, - defaultCLICommands: ["text chat", "api serve"] + defaultCLICommands: ["text chat", "api serve"], + apiProfile: .lfm2() ), ManagedModelSpec( id: LFM2Resources.visionModelId, @@ -1392,7 +1856,8 @@ public enum ManagedModelCatalog { validationKind: .lfm2, runtimeAutoDownloadAllowed: false, estimatedDownloadBytes: LFM2Resources.visionEstimatedDownloadBytes, - defaultCLICommands: ["text chat", "api serve"] + defaultCLICommands: ["text chat", "api serve"], + apiProfile: .lfm2(inputModalities: [.text, .image]) ), ManagedModelSpec( id: "speech-tts-qwen3-nano", @@ -1524,7 +1989,8 @@ public enum ManagedModelCatalog { validationKind: .codegenGGUF, aliasKind: .codegenGGUF, estimatedDownloadBytes: 48_410_992_032, - defaultCLICommands: ["text code"] + defaultCLICommands: ["text code"], + apiProfile: .textCode() ), ManagedModelSpec( id: "text-embed-qwen3-0.6b", diff --git a/Sources/MereRunCore/RuntimeModelSettings.swift b/Sources/MereRunCore/RuntimeModelSettings.swift index 4032d37e..be153e18 100644 --- a/Sources/MereRunCore/RuntimeModelSettings.swift +++ b/Sources/MereRunCore/RuntimeModelSettings.swift @@ -410,28 +410,7 @@ public struct RuntimeModelSettingsStore { public extension ManagedModelSpec { var defaultRuntimeServingEngine: RuntimeServingEngine? { - switch validationKind { - case .codegenGGUF: - return .textCode - case .gemma4, .gemma4Unified: - return .textChatGemma4 - case .laguna: - return .textChatLaguna - case .q35: - return .textChatQ36 - case .lfm2: - return .textChatLFM2 - case .museGlimmer: - return .textChatMuseGlimmer - case .nemotronH: - return .textChatNemotronH - case .deepseekV4FlashIMatrixGGUF: - return .textChatDeepseekV4Flash - case .hfTextChat where id == ModelResolver.ModelID.mebot.rawValue: - return .textChatKlein - default: - return nil - } + apiProfile?.servingEngine } var isAPIServableRuntimeModel: Bool { diff --git a/Tests/MereRunCLITests/APIServeCommandTests.swift b/Tests/MereRunCLITests/APIServeCommandTests.swift index 6c455a24..acb9662d 100644 --- a/Tests/MereRunCLITests/APIServeCommandTests.swift +++ b/Tests/MereRunCLITests/APIServeCommandTests.swift @@ -519,6 +519,54 @@ final class APIServeCommandTests: XCTestCase { XCTAssertEqual(Set(response.data.map(\.owned_by)), Set(["mere.run"])) } + func testChatModelContractDescribesDeepSeekForHarnesses() throws { + let model = APIServerContract.chatModel( + id: DeepseekV4FlashResources.defaultModelId, + name: "DeepSeek V4 Flash", + profile: .deepseekV4Flash(), + contextWindow: 32_768, + maximumOutputTokens: 32_768, + createdAt: Date(timeIntervalSince1970: 123) + ) + + XCTAssertEqual(model.task, "chat.completions") + XCTAssertEqual(model.reasoning, true) + XCTAssertEqual(model.thinking_levels, ["off", "minimal", "low", "medium", "high", "xhigh"]) + XCTAssertEqual(model.tool_call, true) + XCTAssertEqual(model.modalities, OpenAIModelModalities(input: ["text", "image"], output: ["text"])) + XCTAssertEqual(model.limit, OpenAIModelLimit(context: 32_768, output: 32_768)) + XCTAssertEqual(model.openai_compat?.supports_developer_role, false) + XCTAssertEqual(model.openai_compat?.supports_reasoning_effort, true) + XCTAssertEqual(model.openai_compat?.supports_finish_reason, true) + XCTAssertEqual(model.openai_compat?.max_tokens_field, "max_tokens") + XCTAssertEqual(model.openai_compat?.thinking_format, "deepseek") + XCTAssertEqual(model.openai_compat?.thinking_level_map, ["minimal": "low"]) + XCTAssertEqual(model.openai_compat?.requires_reasoning_content_on_assistant_messages, true) + + let json = try XCTUnwrap(String(data: JSONEncoder().encode(model), encoding: .utf8)) + XCTAssertTrue(json.contains("\"tool_call\":true")) + XCTAssertTrue(json.contains("\"thinking_levels\"")) + XCTAssertTrue(json.contains("\"openai_compat\"")) + } + + func testCompanionModelContractLabelsNonChatTasks() throws { + let profile = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: ModelResolver.ModelID.qwen3Embedding.rawValue) + ) + let embedding = APIServerContract.companionModel( + id: ModelResolver.ModelID.qwen3Embedding.rawValue, + profile: profile, + createdAt: Date(timeIntervalSince1970: 123) + ) + + XCTAssertEqual(embedding.task, "embeddings") + XCTAssertEqual(embedding.tool_call, false) + XCTAssertEqual( + embedding.modalities, + OpenAIModelModalities(input: ["text"], output: ["embedding"]) + ) + } + func testEmbeddingRequestDecodesStringInputAndUnknownFields() throws { let data = """ { @@ -2590,6 +2638,24 @@ final class APIServeCommandTests: XCTestCase { } } + func testMuseReasoningEffortMapsToNativeStrength() throws { + let request = OpenAIChatRequest( + model: MuseGlimmerResources.modelId, + messages: [OpenAIChatMessage(role: "user", content: "hello")], + reasoning_effort: "high" + ) + + let chatRequest = try APIServerContract.chatRequest( + from: request, + fallbackLoraPath: nil, + contextSize: 4_096, + capabilities: RuntimeServingEngine.textChatMuseGlimmer.openAICompatibility, + servedModelID: MuseGlimmerResources.modelId + ) + + XCTAssertEqual(chatRequest.reasoningEffort, 0.8) + } + func testStreamingUsageOptionHonorsCapabilities() throws { let request = OpenAIChatRequest( model: "mererun-test-model", diff --git a/Tests/MereRunCLITests/RuntimeModelPoolTests.swift b/Tests/MereRunCLITests/RuntimeModelPoolTests.swift index f2b89420..49b26022 100644 --- a/Tests/MereRunCLITests/RuntimeModelPoolTests.swift +++ b/Tests/MereRunCLITests/RuntimeModelPoolTests.swift @@ -208,10 +208,49 @@ final class RuntimeModelPoolTests: XCTestCase { settingsStore: RuntimeModelSettingsStore(modelsDir: root) ) - let response = try await pool.modelsResponse(createdAt: Date(timeIntervalSince1970: 10)) + let response = try await pool.modelsResponse( + serverContextSize: 8_192, + createdAt: Date(timeIntervalSince1970: 10) + ) XCTAssertTrue(response.data.contains { $0.id == "custom.gguf" }) - XCTAssertEqual(response.data.first { $0.id == "custom.gguf" }?.created, 10) + let model = try XCTUnwrap(response.data.first { $0.id == "custom.gguf" }) + XCTAssertEqual(model.created, 10) + XCTAssertEqual(model.task, "chat.completions") + XCTAssertEqual(model.tool_call, false) + XCTAssertEqual(model.reasoning, false) + XCTAssertEqual(model.modalities, OpenAIModelModalities(input: ["text"], output: ["text"])) + XCTAssertEqual(model.limit, OpenAIModelLimit(context: 8_192, output: 4_096)) + } + + func testModelsResponseOverlaysRuntimeLimitsOnCatalogProfile() async throws { + let root = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let settingsStore = RuntimeModelSettingsStore(modelsDir: root) + try settingsStore.writeSettings( + RuntimeModelSettings(maxContextTokens: 8_192, maxTokens: 1_024), + for: Q35Resources.ornith9BModelId + ) + let pool = RuntimeModelPool( + defaultModelID: Q35Resources.ornith9BModelId, + defaultEngine: .textChatQ36, + startupModelPath: nil, + settingsStore: settingsStore + ) + + let response = try await pool.modelsResponse(serverContextSize: 32_768) + let model = try XCTUnwrap( + response.data.first { $0.id == Q35Resources.ornith9BModelId } + ) + + XCTAssertEqual(model.limit, OpenAIModelLimit(context: 8_192, output: 1_024)) + XCTAssertEqual(model.reasoning, true) + XCTAssertEqual(model.thinking_levels, ["high"]) + XCTAssertEqual(model.tool_call, true) + XCTAssertEqual( + model.modalities, + OpenAIModelModalities(input: ["text", "image"], output: ["text"]) + ) } func testStatusReportsGemma4PrefixKVCacheCapabilityWhenEnabled() async throws { diff --git a/Tests/MereRunCLITests/SetupCommandParsingTests.swift b/Tests/MereRunCLITests/SetupCommandParsingTests.swift index 33331529..14c3bea7 100644 --- a/Tests/MereRunCLITests/SetupCommandParsingTests.swift +++ b/Tests/MereRunCLITests/SetupCommandParsingTests.swift @@ -47,10 +47,12 @@ final class SetupCommandParsingTests: XCTestCase { ) ) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) - XCTAssertEqual(providerModel.id, AgentModelResources.qwen35NineBModelId) + XCTAssertEqual(providerModel.id, Q35Resources.ornith9BModelId) XCTAssertNotEqual(providerModel.id, CodeGenResources.defaultModelId) + XCTAssertTrue(providerModel.reasoning) + XCTAssertTrue(providerModel.toolCall) } func testDeepseekProviderMatchesServedContextAndStartupTimeout() throws { @@ -66,7 +68,7 @@ final class SetupCommandParsingTests: XCTestCase { ) let runtime = try SetupAgentRuntime.runtime(for: recommendation) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) XCTAssertEqual(providerModel.contextWindow, DeepseekV4FlashResources.defaultContextLength) XCTAssertEqual(providerModel.maxTokens, DeepseekV4FlashResources.defaultContextLength) @@ -89,13 +91,13 @@ final class SetupCommandParsingTests: XCTestCase { .first { $0.id == NorthMiniCodeResources.modelId } ) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) XCTAssertEqual(providerModel.id, NorthMiniCodeResources.modelId) XCTAssertEqual(providerModel.contextWindow, NorthMiniCodeResources.runtimeContextLength) XCTAssertEqual(providerModel.maxTokens, NorthMiniCodeResources.maxOutputTokens) XCTAssertFalse(providerModel.reasoning) - XCTAssertTrue(recommendation.isStartableByMereRun) + XCTAssertFalse(recommendation.isStartableByMereRun) XCTAssertEqual(recommendation.servingEngine, .textCode) } @@ -112,7 +114,7 @@ final class SetupCommandParsingTests: XCTestCase { ) let runtime = try SetupAgentRuntime.runtime(for: recommendation) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) XCTAssertEqual(runtime.engine, .textChatGemma4) XCTAssertEqual(providerModel.id, Gemma4Resources.twelveB4BitModelId) @@ -134,13 +136,13 @@ final class SetupCommandParsingTests: XCTestCase { .first { $0.id == Ornith35BCodeResources.modelId } ) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) XCTAssertEqual(providerModel.id, Ornith35BCodeResources.modelId) XCTAssertEqual(providerModel.contextWindow, Ornith35BCodeResources.runtimeContextLength) XCTAssertEqual(providerModel.maxTokens, Ornith35BCodeResources.maxOutputTokens) XCTAssertFalse(providerModel.reasoning) - XCTAssertTrue(recommendation.isStartableByMereRun) + XCTAssertFalse(recommendation.isStartableByMereRun) XCTAssertEqual(recommendation.servingEngine, .textCode) } @@ -158,11 +160,13 @@ final class SetupCommandParsingTests: XCTestCase { ) let runtime = try SetupAgentRuntime.runtime(for: recommendation) - let providerModel = SetupAgentRuntime.providerModel(for: recommendation) + let providerModel = try SetupAgentRuntime.providerModel(for: recommendation) XCTAssertEqual(runtime.engine, .textChatQ36) XCTAssertEqual(providerModel.id, Q35Resources.ornith9BModelId) XCTAssertEqual(providerModel.contextWindow, Q35Resources.defaultContextLength) + XCTAssertTrue(providerModel.reasoning) + XCTAssertTrue(providerModel.toolCall) XCTAssertTrue(recommendation.isStartableByMereRun) XCTAssertEqual(recommendation.servingEngine, .textChatQ35) } @@ -173,15 +177,17 @@ final class SetupCommandParsingTests: XCTestCase { defer { try? FileManager.default.removeItem(at: home) } + let profile = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: Q35Resources.ornith9BModelId) + ) let extensionURL = try PiAgentIntegration.writeLocalProviderExtension( host: "127.0.0.1", port: 8080, model: PiProviderModel( - id: AgentModelResources.qwen35NineBModelId, - name: "Qwen3.5 9B", - contextWindow: 32_768, - maxTokens: 4_096 + id: Q35Resources.ornith9BModelId, + name: "Ornith 9B", + profile: profile ), homeDirectory: home, persistConfiguration: false @@ -194,6 +200,15 @@ final class SetupCommandParsingTests: XCTestCase { .path ) XCTAssertTrue(FileManager.default.fileExists(atPath: extensionURL.path)) + let extensionSource = try String(contentsOf: extensionURL, encoding: .utf8) + XCTAssertTrue(extensionSource.contains("@earendil-works/pi-coding-agent")) + XCTAssertTrue(extensionSource.contains("createProvider")) + XCTAssertTrue(extensionSource.contains("openAICompletionsApi")) + XCTAssertTrue(extensionSource.contains("const initialModels = await discoverModels")) + XCTAssertTrue(extensionSource.contains("fetchModels")) + XCTAssertTrue(extensionSource.contains("entry.tool_call === true")) + XCTAssertTrue(extensionSource.contains("input: [\"text\", \"image\"]")) + XCTAssertTrue(extensionSource.contains("supportsFinishReason: true")) } func testAgentStartModelIsOptional() throws { @@ -293,7 +308,7 @@ final class SetupCommandParsingTests: XCTestCase { XCTAssertTrue(prompt.contains("Recommended setup-agent tier for this Mac")) XCTAssertTrue(prompt.contains("Selected setup-agent is recommended: true")) XCTAssertTrue(prompt.contains("DeepSeek V4 Flash is the preferred premier setup-agent tier")) - XCTAssertTrue(prompt.contains("smaller Qwen agents are alternatives")) + XCTAssertTrue(prompt.contains("smaller tool-capable native agents are alternatives")) XCTAssertTrue(prompt.contains("mere.run model capabilities --recommended")) XCTAssertTrue(prompt.contains("mere.run model list")) XCTAssertTrue(prompt.contains("Do not run demo scripts")) @@ -301,7 +316,7 @@ final class SetupCommandParsingTests: XCTestCase { XCTAssertTrue(prompt.contains("Never pass `--allow-unsupported`")) } - func testAgentStartPrefersInstalledStartableAgentModel() throws { + func testAgentStartDoesNotTreatTextOnlyModelAsPiStartable() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("mere-run-agent-start-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) @@ -324,7 +339,7 @@ final class SetupCommandParsingTests: XCTestCase { ) ) - XCTAssertEqual(recommendation?.id, AgentModelResources.qwen35NineBModelId) + XCTAssertNil(recommendation) } func testAgentStartPrefersInstalledDeepseekOverQwenCode() throws { diff --git a/Tests/MereRunCoreTests/ManagedModelCatalogTests.swift b/Tests/MereRunCoreTests/ManagedModelCatalogTests.swift index 3995a3f9..927d5864 100644 --- a/Tests/MereRunCoreTests/ManagedModelCatalogTests.swift +++ b/Tests/MereRunCoreTests/ManagedModelCatalogTests.swift @@ -42,6 +42,83 @@ final class ManagedModelCatalogTests: XCTestCase { XCTAssertNil(ModelResolver.ModelID(rawValue: "text-chat-q35-nano")) } + func testAPIServableModelsOwnCompleteCatalogProfiles() { + let expectedChatModels = ManagedModelCatalog.allSpecs.filter { + $0.defaultCLICommands.contains("api serve") + || $0.id == CodeGenResources.defaultModelId + } + + XCTAssertFalse(expectedChatModels.isEmpty) + for spec in expectedChatModels { + guard let profile = spec.apiProfile else { + XCTFail("API-served model \(spec.id) is missing a catalog API profile.") + continue + } + XCTAssertEqual(profile.task, .chatCompletions, spec.id) + XCTAssertNotNil(profile.servingEngine, spec.id) + XCTAssertGreaterThan(profile.contextWindow ?? 0, 0, spec.id) + XCTAssertGreaterThan(profile.maximumOutputTokens ?? 0, 0, spec.id) + } + } + + func testCatalogProfilesOwnHarnessSpecificCapabilities() throws { + let ornith = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: Q35Resources.ornith9BModelId) + ) + XCTAssertEqual(ornith.contextWindow, Q35Resources.defaultContextLength) + XCTAssertEqual(ornith.maximumOutputTokens, 4_096) + XCTAssertEqual(ornith.thinkingLevels, [.high]) + XCTAssertTrue(ornith.toolCall) + XCTAssertTrue(ornith.inputModalities.contains(.image)) + + let deepseek = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: DeepseekV4FlashResources.defaultModelId) + ) + XCTAssertEqual(deepseek.compatibility.maxTokensField, .maxTokens) + XCTAssertEqual(deepseek.compatibility.thinkingFormat, .deepseek) + XCTAssertEqual(deepseek.thinkingLevelMap, [.minimal: .low]) + XCTAssertFalse(deepseek.compatibility.supportsDeveloperRole) + XCTAssertTrue(deepseek.compatibility.supportsReasoningEffort) + XCTAssertTrue(deepseek.toolCall) + + let muse = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: MuseGlimmerResources.modelId) + ) + XCTAssertEqual(muse.reasoningEffortStrengths[.minimal], 0.1) + XCTAssertEqual(muse.reasoningEffortStrengths[.high], 0.8) + XCTAssertEqual(muse.reasoningEffortStrengths[.max], 1) + } + + func testQ35OCRModelsAreNotChatRuntimeModels() throws { + let full = try XCTUnwrap( + ManagedModelCatalog.spec(for: Q35Resources.infinityParser2ProModelId) + ) + let quantized = try XCTUnwrap( + ManagedModelCatalog.spec(for: Q35Resources.infinityParser2ProInt8ModelId) + ) + + XCTAssertNil(full.apiProfile) + XCTAssertNil(full.defaultRuntimeServingEngine) + XCTAssertNil(quantized.apiProfile) + XCTAssertNil(quantized.defaultRuntimeServingEngine) + } + + func testCompanionAPIProfilesComeFromCatalog() throws { + let embedding = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: ModelResolver.ModelID.qwen3Embedding.rawValue) + ) + XCTAssertEqual(embedding.task, .embeddings) + XCTAssertEqual(embedding.inputModalities, [.text]) + XCTAssertEqual(embedding.outputModalities, [.embedding]) + + let imageEdit = try XCTUnwrap( + ManagedModelCatalog.apiProfile(for: QwenImageEditRepository.modelId) + ) + XCTAssertEqual(imageEdit.task, .imageEdits) + XCTAssertEqual(imageEdit.inputModalities, [.text, .image]) + XCTAssertEqual(imageEdit.outputModalities, [.image]) + } + func testAllRuntimeAutoDownloadSpecsHaveManagedSource() { for spec in ManagedModelCatalog.allSpecs where spec.runtimeAutoDownloadAllowed { XCTAssertTrue( diff --git a/Tests/MereRunCoreTests/ManagedModelSupportTests.swift b/Tests/MereRunCoreTests/ManagedModelSupportTests.swift index 5f287515..0d31ed8a 100644 --- a/Tests/MereRunCoreTests/ManagedModelSupportTests.swift +++ b/Tests/MereRunCoreTests/ManagedModelSupportTests.swift @@ -267,7 +267,7 @@ final class ManagedModelSupportTests: XCTestCase { } - func testNorthMiniCodeIsSupportedOnThirtyTwoGBAndStartable() throws { + func testNorthMiniCodeIsSupportedOnThirtyTwoGBButNotPiStartable() throws { let spec = try XCTUnwrap(ManagedModelCatalog.spec(for: NorthMiniCodeResources.modelId)) let machine = MereRunMachineProfile( physicalMemoryBytes: 32 * 1_073_741_824, @@ -283,7 +283,7 @@ final class ManagedModelSupportTests: XCTestCase { ) XCTAssertTrue(report.isSupported) - XCTAssertTrue(recommendation.isStartableByMereRun) + XCTAssertFalse(recommendation.isStartableByMereRun) XCTAssertEqual(recommendation.servingEngine, .textCode) } @@ -348,7 +348,7 @@ final class ManagedModelSupportTests: XCTestCase { XCTAssertEqual(recommendation.servingEngine, .textChatQ35) } - func testOrnith35BIsSupportedOnSixtyFourGBAndStartableThroughTextCode() throws { + func testOrnith35BIsSupportedOnSixtyFourGBButNotPiStartable() throws { let spec = try XCTUnwrap(ManagedModelCatalog.spec(for: Ornith35BCodeResources.modelId)) let machine = MereRunMachineProfile( physicalMemoryBytes: 64 * 1_073_741_824, @@ -364,7 +364,7 @@ final class ManagedModelSupportTests: XCTestCase { ) XCTAssertTrue(report.isSupported) - XCTAssertTrue(recommendation.isStartableByMereRun) + XCTAssertFalse(recommendation.isStartableByMereRun) XCTAssertEqual(recommendation.servingEngine, .textCode) } @@ -504,7 +504,7 @@ final class ManagedModelSupportTests: XCTestCase { let recommendation = try XCTUnwrap(MereRunAgentModelCatalog.recommendation(for: .tier, on: machine)) - XCTAssertEqual(recommendation.id, AgentModelResources.qwen35NineBModelId) + XCTAssertEqual(recommendation.id, Q35Resources.ornith9BModelId) XCTAssertTrue(recommendation.isStartableByMereRun) } @@ -601,7 +601,7 @@ final class ManagedModelSupportTests: XCTestCase { XCTAssertNil(MereRunAgentModelCatalog.recommendation(for: .tier, on: machine)) } - func testAgentTierSelectsCoderOnLinuxWithEnoughMemory() throws { + func testAgentTierSelectsToolCapableQ36OnLinuxWithEnoughMemory() throws { let machine = MereRunMachineProfile( physicalMemoryBytes: 64 * 1_073_741_824, processorName: "Linux", @@ -611,7 +611,7 @@ final class ManagedModelSupportTests: XCTestCase { let recommendation = try XCTUnwrap(MereRunAgentModelCatalog.recommendation(for: .tier, on: machine)) - XCTAssertEqual(recommendation.id, CodeGenResources.defaultModelId) + XCTAssertEqual(recommendation.id, Q35Resources.q36NanoModelId) XCTAssertTrue(recommendation.isStartableByMereRun) } } diff --git a/docs/cli.md b/docs/cli.md index f9dcc020..64b851a9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2831,15 +2831,15 @@ swift run mere.run setup --mode manual Agent model choices: -- `small`: `text-agent-qwen35-9b`, a Qwen3.5 9B Q4 GGUF setup agent for 16 GB machines -- `tier`: the best supported local tier for this machine, currently 9B, Qwen3.6 nano, Qwen3-Coder Next, or DeepSeek V4 Flash on 96 GB+ machines +- `small`: `text-agent-ornith-9b`, a tool-capable native OptiQ setup agent for 16 GB machines +- `tier`: the best supported tool-capable local tier for this machine, currently Ornith 9B, Gemma 4, Qwen3.6 nano on Linux, or DeepSeek V4 Flash on 96 GB+ machines - `premier`: `text-agent-deepseek-v4-flash`, the preferred managed 96 GB+ setup-agent tier served by the bundled DS4 engine North Mini Code (`text-code-north-mini`) is available as a managed native GGUF coding model. It is pullable through `model pull`, can be served with -`api serve --engine text-code --model text-code-north-mini`, and can be started -through the Pi-backed `agent start` path like other `text-code` models once the -installed llama.cpp runtime supports the `cohere2moe` architecture. +`api serve --engine text-code --model text-code-north-mini`, and can be used +for direct code sessions and evals. The `text-code` API lane rejects tool calls, +so it is not exposed to Pi. Ornith (`text-agent-ornith-9b`) is available as an experimental native MLX/OptiQ coding-agent model. It uses the Qwen-family runtime, so serve it with @@ -2871,7 +2871,6 @@ swift run mere.run agent onboard swift run mere.run agent onboard --pull-recommended --accept-model-license swift run mere.run agent onboard --install-pi --configure-pi swift run mere.run agent onboard --configure-pi --model text-agent-deepseek-v4-flash -swift run mere.run agent onboard --configure-pi --model text-code-north-mini --port 8080 swift run mere.run agent onboard --configure-pi --model text-agent-ornith-9b --port 8080 ``` @@ -2893,7 +2892,7 @@ extension that the CLI will actually use. ### `mere.run agent install-pi` -Install the latest `badlogic/pi-mono` release asset for the current macOS +Install the latest `earendil-works/pi` release asset for the current macOS architecture into the mere.run application-support directory. ```bash @@ -2902,9 +2901,9 @@ swift run mere.run agent install-pi ### `mere.run agent start` -Start a local API server for a selected managed agent model and launch Pi -against the `mere-run` provider. GGUF code models use `--engine text-code`, -Qwen3.6 uses `--engine text-chat-q36`, and DeepSeek V4 Flash uses the DS4-backed +Start a local API server for a selected tool-capable managed agent model and +launch Pi against the native `mere-run` provider. Qwen-family models use a +native chat engine and DeepSeek V4 Flash uses the DS4-backed `--engine text-chat-deepseek-v4-flash`. If `--model` is omitted, `agent start` uses the best installed startable setup agent first, then a valid persisted Pi provider model, then the current machine's startable hardware tier. On 96 GB+ diff --git a/docs/getting-started.md b/docs/getting-started.md index 45649750..79362668 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -233,8 +233,8 @@ swift run mere.run setup The setup command offers a local Mere agent powered by Pi, a bring-your-own-agent handoff prompt for Claude/Codex, or manual commands. Use -`--mode agent --agent-model small` to select the Qwen3.5 9B GGUF setup agent -explicitly. On 96 GB+ Apple Silicon Macs, the hardware-tier and premier agent +`--mode agent --agent-model small` to select the tool-capable native Ornith 9B +setup agent explicitly. On 96 GB+ Apple Silicon Macs, the hardware-tier and premier agent path selects DeepSeek V4 Flash as the preferred setup agent. On Linux, install or provide Pi separately with `--pi-path` or PATH before using `--start`. diff --git a/docs/model-sources.md b/docs/model-sources.md index c07bb5e5..5af435c9 100644 --- a/docs/model-sources.md +++ b/docs/model-sources.md @@ -38,6 +38,13 @@ This is the authoritative public catalog list. It is kept in sync with from the runtime catalog used by `mere.run model list`, `mere.run model capabilities --all`, and `mere.run model pull`. +API-served catalog entries also own a typed profile for their task, serving +engine, input/output modalities, tool and structured-output support, reasoning +levels and mappings, context/output limits, and OpenAI compatibility behavior. +`/v1/models`, request validation, agent eligibility, and the Pi provider all +project from that profile. Runtime aliases and configured limits are applied as +an effective overlay; they are not a second capability catalog. + | Catalog category | Model ID | | --- | --- | @@ -262,9 +269,10 @@ existing installs and explicit local paths keep working: text-chat IDs listed here remain local-path-only until they have public Hugging Face sources. -`text-agent-qwen35-9b` is the low-memory setup-agent model. It uses the public +`text-agent-qwen35-9b` is a low-memory GGUF coding model. It uses the public Hugging Face source `unsloth/Qwen3.5-9B-GGUF` and selects -`Qwen3.5-9B-Q4_K_M.gguf`. +`Qwen3.5-9B-Q4_K_M.gguf`. Its `text-code` API lane does not expose tool calls; +the small Pi setup-agent tier instead uses `text-agent-ornith-9b`. `text-code-north-mini` installs the Unsloth GGUF quant of Cohere Labs' North Mini Code 1.0 at the pinned catalog revision. North Mini Code is a 30B total / diff --git a/docs/runtime/api-server.md b/docs/runtime/api-server.md index 66fd70b5..b9888ba8 100644 --- a/docs/runtime/api-server.md +++ b/docs/runtime/api-server.md @@ -427,9 +427,19 @@ load/unload/settings endpoint. Sidecars still appear in `/v1/models` and in the `sidecars` object returned by `/runtime/status`. `GET /v1/models` returns installed API-servable chat managed IDs, configured -aliases, and installed native sidecar model ids for embeddings, image, TTS, and -ASR. Missing catalog models fail with an OpenAI-style error that tells the user -to pull the model first. +aliases, and installed native sidecars. In addition to the standard OpenAI +fields, each entry can describe its `task`, `tool_call`, `reasoning`, +`thinking_levels`, input/output `modalities`, context/output `limit`, and +`openai_compat` dialect. Agent harnesses can therefore discover what the +running server actually supports instead of inferring capabilities from a +model name. Missing catalog models fail with an OpenAI-style error that tells +the user to pull the model first. + +For managed models, these capabilities come from the typed API profile attached +to the managed-model catalog entry. The runtime layer adds the request-facing +ID or alias and applies configured context/output defaults to the reported +limits. A server started from an explicit uncataloged path uses a conservative +profile for its selected serving engine. ## OpenAI chat compatibility diff --git a/docs/runtime/model-management.md b/docs/runtime/model-management.md index ca446c31..383694dc 100644 --- a/docs/runtime/model-management.md +++ b/docs/runtime/model-management.md @@ -256,12 +256,16 @@ adapter ID and its published four-step schedule automatically. Guided onboarding for the shared model store and first local agent. The command offers a Pi-powered Mere agent, a BYOA prompt for Claude/Codex, or manual -commands. The small local agent model is `text-agent-qwen35-9b`; hardware-tier -setup can select Qwen3.6 nano, Qwen3-Coder Next, or DeepSeek V4 Flash. On 96 GB+ +commands. The small local agent model is the tool-capable +`text-agent-ornith-9b`; hardware-tier setup can select Gemma 4, Qwen3.6 nano on +Linux, or DeepSeek V4 Flash. On 96 GB+ Apple Silicon Macs, `text-agent-deepseek-v4-flash` is the preferred managed -setup-agent tier; smaller Qwen agent models are alternatives, not upgrades. +setup-agent tier; smaller tool-capable native agents are alternatives, not +upgrades. `text-code-north-mini` can be pulled, inspected, and run through the native -GGUF code runtime for coding-agent comparisons against `text-code-qwen3`. +GGUF code runtime for direct coding comparisons against `text-code-qwen3`. +The `text-code` API lane rejects tool calls, so these models are not Pi setup +agents. `text-agent-ornith-9b` can be pulled, inspected, and run through the native Qwen-family MLX/OptiQ runtime for coding-agent comparisons. `text-agent-ornith-35b-mlx` is a local-only converted MLX Q4 Ornith target; it diff --git a/integrations/pi/README.md b/integrations/pi/README.md new file mode 100644 index 00000000..50a1fea1 --- /dev/null +++ b/integrations/pi/README.md @@ -0,0 +1,30 @@ +# mere.run for Pi + +This Pi package registers a native `mere-run` provider and discovers compatible local models from the running mere.run API server. It reads the server's model task, tool-call support, reasoning levels, input modalities, context/output limits, and OpenAI compatibility flags instead of guessing from a model ID. + +## Use it + +Start mere.run first: + +```bash +mere.run api serve --host 127.0.0.1 --port 8080 +``` + +Install this package from a local checkout: + +```bash +pi install /path/to/mere-run/integrations/pi +pi --provider mere-run +``` + +The package defaults to `http://127.0.0.1:8080/v1`. Override the endpoint or API key when needed: + +```bash +MERERUN_BASE_URL=http://127.0.0.1:9090/v1 \ +MERERUN_API_KEY=secret \ +pi --provider mere-run +``` + +Only `chat.completions` models that report `tool_call: true` are exposed to Pi. Image, audio, embedding, geometry, and text-only chat models remain available through the mere.run API but are not presented as coding-agent models. + +`mere.run agent start` remains the managed one-command path. It installs the current Pi release, starts the local server, writes an isolated provider extension with a selected-model fallback, and launches Pi. diff --git a/integrations/pi/extensions/mere-run.ts b/integrations/pi/extensions/mere-run.ts new file mode 100644 index 00000000..57bedccb --- /dev/null +++ b/integrations/pi/extensions/mere-run.ts @@ -0,0 +1,140 @@ +import { createProvider, openAICompletionsApi, type Model } from "@earendil-works/pi-ai/compat"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; + +export interface MereRunModel { + id: string; + name?: string; + task?: string; + reasoning?: boolean; + thinking_levels?: ThinkingLevel[]; + tool_call?: boolean; + modalities?: { input: string[]; output: string[] }; + limit?: { context: number; output: number }; + openai_compat?: { + supports_store: boolean; + supports_developer_role: boolean; + supports_reasoning_effort: boolean; + supports_usage_in_streaming: boolean; + supports_finish_reason: boolean; + max_tokens_field: "max_tokens" | "max_completion_tokens"; + supports_strict_mode: boolean; + thinking_format?: "deepseek"; + thinking_level_map?: Partial>; + requires_reasoning_content_on_assistant_messages: boolean; + }; +} + +const thinkingLevels: ThinkingLevel[] = [ + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +]; + +export function mapThinkingLevels(model: MereRunModel) { + if (!model.reasoning || !model.thinking_levels?.length) return undefined; + const supported = new Set(model.thinking_levels); + const overrides = model.openai_compat?.thinking_level_map ?? {}; + const result: Partial> = {}; + for (const level of thinkingLevels) { + if (!supported.has(level)) result[level] = null; + else if (overrides[level]) result[level] = overrides[level]; + } + return result; +} + +export function mapModel(model: MereRunModel, baseUrl: string): Model<"openai-completions"> { + const compat = model.openai_compat; + return { + id: model.id, + name: model.name ?? model.id, + api: "openai-completions", + provider: "mere-run", + baseUrl, + reasoning: model.reasoning ?? false, + thinkingLevelMap: mapThinkingLevels(model), + input: (model.modalities?.input ?? ["text"]).filter( + (value): value is "text" | "image" => value === "text" || value === "image", + ), + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.limit?.context ?? 32_768, + maxTokens: model.limit?.output ?? 4_096, + compat: compat + ? { + supportsStore: compat.supports_store, + supportsDeveloperRole: compat.supports_developer_role, + supportsReasoningEffort: compat.supports_reasoning_effort, + supportsUsageInStreaming: compat.supports_usage_in_streaming, + supportsFinishReason: compat.supports_finish_reason, + maxTokensField: compat.max_tokens_field, + supportsStrictMode: compat.supports_strict_mode, + thinkingFormat: compat.thinking_format, + requiresReasoningContentOnAssistantMessages: + compat.requires_reasoning_content_on_assistant_messages, + } + : undefined, + }; +} + +export function selectPiModels(models: MereRunModel[], baseUrl: string) { + return models + .filter((model) => model.task === "chat.completions" && model.tool_call === true) + .map((model) => mapModel(model, baseUrl)); +} + +export async function discoverModels( + baseUrl: string, + apiKey: string, + signal: AbortSignal, +): Promise[]> { + const response = await fetch(`${baseUrl}/models`, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal, + }); + if (!response.ok) { + throw new Error(`mere.run model discovery failed: HTTP ${response.status}`); + } + const payload = (await response.json()) as { data?: MereRunModel[] }; + return selectPiModels(payload.data ?? [], baseUrl); +} + +export default async function mereRunExtension(pi: ExtensionAPI) { + const baseUrl = process.env.MERERUN_BASE_URL ?? "http://127.0.0.1:8080/v1"; + const fallbackAPIKey = process.env.MERERUN_API_KEY ?? "mere-run"; + const initialModels = await discoverModels( + baseUrl, + fallbackAPIKey, + AbortSignal.timeout(2_000), + ).catch(() => []); + + pi.registerProvider( + createProvider({ + id: "mere-run", + name: "mere.run Local", + baseUrl, + auth: { + apiKey: { + name: "mere.run local API key", + async resolve({ ctx, credential }) { + const key = credential?.key ?? (await ctx.env("MERERUN_API_KEY")) ?? fallbackAPIKey; + return { auth: { apiKey: key }, source: "mere.run local" }; + }, + }, + }, + models: initialModels, + async fetchModels(context) { + const key = + context.credential?.type === "api_key" + ? context.credential.key ?? fallbackAPIKey + : fallbackAPIKey; + return discoverModels(baseUrl, key, context.signal); + }, + api: openAICompletionsApi(), + }), + ); +} diff --git a/integrations/pi/package.json b/integrations/pi/package.json new file mode 100644 index 00000000..79b8149d --- /dev/null +++ b/integrations/pi/package.json @@ -0,0 +1,50 @@ +{ + "name": "@mere-run/pi", + "version": "0.1.0", + "description": "Dynamic mere.run provider for the Pi coding agent", + "type": "module", + "license": "Apache-2.0", + "keywords": [ + "pi-package", + "pi-coding-agent", + "mere.run", + "local-ai" + ], + "files": [ + "extensions", + "README.md" + ], + "pi": { + "extensions": [ + "./extensions/mere-run.ts" + ] + }, + "scripts": { + "test": "tsx --test test/*.test.ts", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@earendil-works/pi-ai": ">=0.84.2", + "@earendil-works/pi-coding-agent": ">=0.84.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ], + "ignoredBuiltDependencies": [ + "@google/genai", + "protobufjs" + ], + "overrides": { + "@smithy/core": "3.32.0", + "@smithy/node-http-handler": "4.7.3" + } + }, + "devDependencies": { + "@earendil-works/pi-ai": "0.84.2", + "@earendil-works/pi-coding-agent": "0.84.2", + "@types/node": "26.2.0", + "tsx": "4.23.12", + "typescript": "7.0.2" + } +} diff --git a/integrations/pi/pnpm-lock.yaml b/integrations/pi/pnpm-lock.yaml new file mode 100644 index 00000000..cb620e7f --- /dev/null +++ b/integrations/pi/pnpm-lock.yaml @@ -0,0 +1,1755 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@smithy/core': 3.32.0 + '@smithy/node-http-handler': 4.7.3 + +importers: + + .: + devDependencies: + '@earendil-works/pi-ai': + specifier: 0.84.2 + version: 0.84.2(ws@8.21.3) + '@earendil-works/pi-coding-agent': + specifier: 0.84.2 + version: 0.84.2(ws@8.21.3) + '@types/node': + specifier: 26.2.0 + version: 26.2.0 + tsx: + specifier: 4.23.12 + version: 4.23.12 + typescript: + specifier: 7.0.2 + version: 7.0.2 + +packages: + + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.7': + resolution: {integrity: sha512-I88Iov89NVmjSmJLKSv7Cn9M2J+a2942OkA8nZCbz+sl4ZeY4zEOcoLOrbt1GRfQ8zEQKnjAJdXixA3J/p1fDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.68': + resolution: {integrity: sha512-2a20A/IdNOwUvaDq91iqqS7BA0XlNMfW3iLGZGZLJv0EbUqhSxB0PIx4rQQqssvWj1uXImb3/UCCdHz/+1dOiA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.70': + resolution: {integrity: sha512-0yRem2Fs52r/Nn6UAqIlpjexfaYj8ziEozOe9tamtAVT/5bzFLKx8O2r7MaRqgS3hGKHIa1Jij9nKHSsNnb04A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.13': + resolution: {integrity: sha512-2M39DE02XpYYaSWYk/4AsImXYUU/1L2xmTMLUpMMWq7DfLv191/vCRy3baKtdr45AkJQyVgSjmuVOLm15SwrRQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.75': + resolution: {integrity: sha512-jaTESuJlQsoUZ44f/i2puyPt8VlF/dMMJ9HM3cStYtk7eKX4N9UWi83OLixUkoOJH3BwWlPLCq9YIK9nfWhVBg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.79': + resolution: {integrity: sha512-RIw5dof1EHkWubrZzPC941CDtnFG1iAXsxbFgLkhdYZXHc4icU13c/uxSMI0J5eUx9bxa7LjfpdjfClBB1QsDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.68': + resolution: {integrity: sha512-nLP3Pda2MQTFJ25hKBMmUuB9Uv+bTZQNlufbeCwklP549Vwnkd8bRLJoCKp5k6xjmdyptrPrOfGOhN0mKuca8A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.12': + resolution: {integrity: sha512-EmgyyHn+f9WCcelp3L/vci+LGbX8GigWaVphRArjVo5Pktkr9YnLy/mQ6VDkDyBD72dtfRNTgHmD2ts4rTDXKQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.74': + resolution: {integrity: sha512-0YfczxGXF3RjGj8z7QG/Ho2HnLGKDHfPSHiTs47UU1U/+mmwISDN+rvGKt2zh+3FX8NdT4xd95LGBGyhQw2dgQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.32': + resolution: {integrity: sha512-rlbmsMG7ZNgrVhWSqqXpq6y9hfiREyzCg3CNTk9UK+AoP7+65kOkqpWmqwLfV1UrRSHATdLnZF2rt9ZTUxYQJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.27': + resolution: {integrity: sha512-M7Ay1VpBpf/YFfic9kkjwE3wyCh4G0gEM4RypRXYm7aPjyfqi+D8FEYMR2E3IqbvN+qi2rEFYAiwWL0XHtQYdQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.50': + resolution: {integrity: sha512-gdcWRbmIf1dWA/prf44Bnnzgqj+AbsXX2yfhZhOQLwSm7NfKIYPmkRlPqP0CTepHzjxMIBdWBDdtQB+Y/dFUeg==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.42': + resolution: {integrity: sha512-XWRyon2MTHXD/zMoo0Mbge6Vwf+iE0qQaM/RyGO6NfZ9WukCFiQL27nQVZjYy2JwSIg+iXZxKOX95OBXqlSM4w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.44': + resolution: {integrity: sha512-ZSfQ35Qn4MhSY+A0Whyr+KBx+wJKZUyBsOrjB2pSHOafRzbFe47T8XcXM8hZqUAC69qnqIy0C9ArxTuud0CC2w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1108.0': + resolution: {integrity: sha512-rI80zxDxGJ6904eC/YbjkdjY6JdaZvQ01kOmrMvw7cFQGIHo27fhnIVbMSVDS4T6foQImjxYSRoOu/uSJscXDw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.3': + resolution: {integrity: sha512-ECAqfpNsef+7MO8qtR0h9KcFIBAygaE7Cm6UOiQl+ft+uVap+1G7bNEjs4mdJE2OnA4m6k7i8peH8uGIAsOMGw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.9': + resolution: {integrity: sha512-wB/ho7pTJKqWz3WYDt2ZWDWI8bxQpN/xwf+5ZQ1zWaj+HDY9B8Fn434i6qZ6j6ZG3aCiIJtZaQVqwajx5xYsQA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.38': + resolution: {integrity: sha512-grf7mzfVxBS5AlsuTvBN7uDpzqohFww9fRPCO+EBSUdvtsYMcPSKdz54h/7XiscqNcUM1Ae1MF7JLHmiYYuzbQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@earendil-works/pi-agent-core@0.84.2': + resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.84.2': + resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-client@0.84.2': + resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.84.2': + resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-protocol@0.84.2': + resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.84.2': + resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-tui@0.84.2': + resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==} + engines: {node: '>=22.19.0'} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@smithy/core@3.32.0': + resolution: {integrity: sha512-NAiCSC78fzbNIEWoheoF74Ob5ZorLijCHpMY26Fqvqg/+9LuyIqMfHDg2p8Yk1rqOyowtiL3y7WX0AW+teL6zw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.0': + resolution: {integrity: sha512-2jsPi+7Zv2hSzD9IXR9D7DTqSn7mv4XalzRm+bESh53jiaUS3NKEUbpQFTJP0HhQy9qzZvluxQ3yS24zdRrqsA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.0': + resolution: {integrity: sha512-W/exA8T0LEzCQtJ02w4IzaEQPIspgarqZprb7W8FwnYiDowgCrjl2fTQ6FvuSSUnJORuepBF81abmBJwqh+0XQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.0': + resolution: {integrity: sha512-hCynhm22wMJ8wTF9crcwu8mxggtUrSLLJgDcGUvYFBqpofxycYJCGKOMYg4xtPPFtgNiDJSYmhsWLTrcU/g59Q==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.0': + resolution: {integrity: sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grok-mermaid@0.2.2: + resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==} + engines: {node: '>=18'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + openai@6.40.0: + resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@anthropic-ai/sdk@0.91.1': + dependencies: + json-schema-to-ts: 3.1.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.3 + '@aws-sdk/util-locate-window': 3.965.9 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.3 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.3 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.977.7 + '@aws-sdk/credential-provider-node': 3.972.79 + '@aws-sdk/eventstream-handler-node': 3.972.32 + '@aws-sdk/middleware-eventstream': 3.972.27 + '@aws-sdk/middleware-websocket': 3.972.50 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/fetch-http-handler': 5.7.0 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.7': + dependencies: + '@aws-sdk/types': 3.974.3 + '@aws-sdk/xml-builder': 3.972.38 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.32.0 + '@smithy/signature-v4': 5.7.0 + '@smithy/types': 4.17.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.68': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/fetch-http-handler': 5.7.0 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.13': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/credential-provider-env': 3.972.68 + '@aws-sdk/credential-provider-http': 3.972.70 + '@aws-sdk/credential-provider-login': 3.972.75 + '@aws-sdk/credential-provider-process': 3.972.68 + '@aws-sdk/credential-provider-sso': 3.973.12 + '@aws-sdk/credential-provider-web-identity': 3.972.74 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/credential-provider-imds': 4.5.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.75': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.79': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.68 + '@aws-sdk/credential-provider-http': 3.972.70 + '@aws-sdk/credential-provider-ini': 3.973.13 + '@aws-sdk/credential-provider-process': 3.972.68 + '@aws-sdk/credential-provider-sso': 3.973.12 + '@aws-sdk/credential-provider-web-identity': 3.972.74 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/credential-provider-imds': 4.5.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.68': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.12': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/token-providers': 3.1108.0 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.74': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.32': + dependencies: + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.27': + dependencies: + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.50': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/fetch-http-handler': 5.7.0 + '@smithy/signature-v4': 5.7.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.42': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/signature-v4-multi-region': 3.996.44 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/fetch-http-handler': 5.7.0 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.44': + dependencies: + '@aws-sdk/types': 3.974.3 + '@smithy/signature-v4': 5.7.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1108.0': + dependencies: + '@aws-sdk/core': 3.977.7 + '@aws-sdk/nested-clients': 3.997.42 + '@aws-sdk/types': 3.974.3 + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.3': + dependencies: + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.9': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.38': + dependencies: + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@babel/runtime@7.29.7': {} + + '@earendil-works/pi-agent-core@0.84.2(ws@8.21.3)': + dependencies: + '@earendil-works/pi-ai': 0.84.2(ws@8.21.3) + '@earendil-works/pi-telemetry': 0.84.2 + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.3.7 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.84.2(ws@8.21.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1 + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@earendil-works/pi-telemetry': 0.84.2 + '@google/genai': 1.52.0 + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.40.0(ws@8.21.3) + partial-json: 0.1.7 + typebox: 1.3.7 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-client@0.84.2': + dependencies: + '@earendil-works/pi-protocol': 0.84.2 + + '@earendil-works/pi-coding-agent@0.84.2(ws@8.21.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.84.2(ws@8.21.3) + '@earendil-works/pi-ai': 0.84.2(ws@8.21.3) + '@earendil-works/pi-client': 0.84.2 + '@earendil-works/pi-protocol': 0.84.2 + '@earendil-works/pi-tui': 0.84.2 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + grok-mermaid: 0.2.2 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.3.7 + undici: 8.9.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-protocol@0.84.2': + dependencies: + typebox: 1.3.7 + + '@earendil-works/pi-telemetry@0.84.2': {} + + '@earendil-works/pi-tui@0.84.2': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@google/genai@1.52.0': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@opentelemetry/api@1.9.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@smithy/core@3.32.0': + dependencies: + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.0': + dependencies: + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.0': + dependencies: + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.0': + dependencies: + '@smithy/core': 3.32.0 + '@smithy/types': 4.17.0 + tslib: 2.8.1 + + '@smithy/types@4.17.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@types/retry@0.12.0': {} + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + agent-base@7.1.4: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bignumber.js@9.3.1: {} + + bowser@2.14.1: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + buffer-equal-constant-time@1.0.1: {} + + chalk@5.6.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + diff@8.0.4: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + extend@3.0.2: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fsevents@2.3.3: + optional: true + + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.1 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-east-asian-width@1.6.0: {} + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + graceful-fs@4.2.11: {} + + grok-mermaid@0.2.2: {} + + highlight.js@10.7.3: {} + + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.2 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ignore@7.0.5: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + long@5.3.2: {} + + lru-cache@11.5.2: {} + + marked@18.0.5: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.9 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + openai@6.40.0(ws@8.21.3): + optionalDependencies: + ws: 8.21.3 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + partial-json@0.1.7: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.2.0 + long: 5.3.2 + + retry@0.12.0: {} + + retry@0.13.1: {} + + safe-buffer@5.2.1: {} + + semver@7.8.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typebox@1.3.7: {} + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@8.3.0: {} + + undici@8.9.0: {} + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + ws@8.21.3: {} + + yaml@2.9.0: {} diff --git a/integrations/pi/test/mere-run.test.ts b/integrations/pi/test/mere-run.test.ts new file mode 100644 index 00000000..758ea406 --- /dev/null +++ b/integrations/pi/test/mere-run.test.ts @@ -0,0 +1,99 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:http"; +import test from "node:test"; + +import { + discoverModels, + mapModel, + mapThinkingLevels, + selectPiModels, + type MereRunModel, +} from "../extensions/mere-run.ts"; + +const deepSeek: MereRunModel = { + id: "text-agent-deepseek-v4-flash", + name: "DeepSeek V4 Flash", + task: "chat.completions", + reasoning: true, + thinking_levels: ["off", "minimal", "low", "medium", "high", "xhigh"], + tool_call: true, + modalities: { input: ["text", "image", "audio"], output: ["text"] }, + limit: { context: 32_768, output: 32_768 }, + openai_compat: { + supports_store: false, + supports_developer_role: false, + supports_reasoning_effort: true, + supports_usage_in_streaming: true, + supports_finish_reason: true, + max_tokens_field: "max_tokens", + supports_strict_mode: false, + thinking_format: "deepseek", + thinking_level_map: { minimal: "low" }, + requires_reasoning_content_on_assistant_messages: true, + }, +}; + +test("maps self-described mere.run capabilities into Pi model metadata", () => { + const mapped = mapModel(deepSeek, "http://127.0.0.1:8080/v1"); + + assert.equal(mapped.provider, "mere-run"); + assert.equal(mapped.contextWindow, 32_768); + assert.equal(mapped.maxTokens, 32_768); + assert.deepEqual(mapped.input, ["text", "image"]); + assert.equal(mapped.compat?.thinkingFormat, "deepseek"); + assert.equal(mapped.compat?.maxTokensField, "max_tokens"); + assert.equal(mapped.thinkingLevelMap?.minimal, "low"); + assert.equal(mapped.thinkingLevelMap?.max, null); + assert.equal(mapped.thinkingLevelMap?.off, undefined); +}); + +test("keeps only chat models that can execute Pi tools", () => { + const selected = selectPiModels( + [ + deepSeek, + { id: "plain-chat", task: "chat.completions", tool_call: false }, + { id: "embedding", task: "embeddings", tool_call: false }, + ], + "http://127.0.0.1:8080/v1", + ); + + assert.deepEqual(selected.map((model) => model.id), [deepSeek.id]); +}); + +test("discovers models from the live endpoint with bearer authentication", async () => { + const server = createServer((request, response) => { + assert.equal(request.url, "/v1/models"); + assert.equal(request.headers.authorization, "Bearer local-secret"); + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify({ data: [deepSeek] })); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const address = server.address(); + assert(address && typeof address !== "string"); + const models = await discoverModels( + `http://127.0.0.1:${address.port}/v1`, + "local-secret", + AbortSignal.timeout(1_000), + ); + assert.deepEqual(models.map((model) => model.id), [deepSeek.id]); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + +test("represents a fixed reasoning lane without inventing adjustable levels", () => { + const mapped = mapThinkingLevels({ + id: "text-agent-ornith-9b", + reasoning: true, + thinking_levels: ["high"], + }); + + assert.equal(mapped?.high, undefined); + assert.equal(mapped?.off, null); + assert.equal(mapped?.low, null); + assert.equal(mapped?.xhigh, null); +}); diff --git a/integrations/pi/tsconfig.json b/integrations/pi/tsconfig.json new file mode 100644 index 00000000..5e1cefe9 --- /dev/null +++ b/integrations/pi/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["extensions/**/*.ts", "test/**/*.ts"] +}