From 0253280f533fb14a382f4684ed05d9491222d5c5 Mon Sep 17 00:00:00 2001 From: Yan Weng Date: Sat, 24 Oct 2026 18:10:25 -0700 Subject: [PATCH 1/6] Add a thinking_budget request field POST /v1/chat/completions accepts "thinking_budget": N, an integer >= 0. - The engine counts reasoning tokens after the model's own . - When the count reaches N, the engine writes itself. Decoding then continues. - A budget of 0 masks at the first position. - ThinkBudget (thinking.py) sees every emitted token. It tells the engine when to force . - Each sampled packet is limited to the reasoning tokens left. Thus no sampled token is discarded, and the grammar never goes back. - Before a forced , block(..., feed_only=True) feeds the last reasoning token into the KV cache. It does one forward pass. It does not sample, call the grammar, or change the presence mask or RNG. - The server gives 400 before any header for: an invalid budget, a grammar that is not flint, a tokenizer without /, or a prompt with an unclosed . - /v1/messages gives 400 for a top-level thinking_budget. thinking.budget_tokens stays ignored. - usage has reasoning_tokens and thinking_forced_close only when the request sets thinking_budget. --- README.md | 12 + engine/src/rec_mlx/constants.py | 14 + engine/src/rec_mlx/engine.py | 61 ++- engine/src/rec_mlx/grammar_builder.py | 16 +- engine/src/rec_mlx/kernels/sampling.py | 7 + engine/src/rec_mlx/scheduler.py | 4 + engine/src/rec_mlx/server.py | 66 ++++ engine/src/rec_mlx/thinking.py | 60 +++ engine/src/rec_mlx/token_ids.py | 9 + engine/tests/e2e/conftest.py | 20 + engine/tests/e2e/e2e_config.py | 43 +++ engine/tests/e2e/test_thinking_budget_e2e.py | 126 +++++++ engine/tests/test_thinking_budget.py | 145 +++++++ engine/tests/test_thinking_budget_engine.py | 377 +++++++++++++++++++ engine/tests/test_thinking_budget_server.py | 219 +++++++++++ 15 files changed, 1165 insertions(+), 14 deletions(-) create mode 100644 engine/src/rec_mlx/constants.py create mode 100644 engine/src/rec_mlx/thinking.py create mode 100644 engine/src/rec_mlx/token_ids.py create mode 100644 engine/tests/e2e/conftest.py create mode 100644 engine/tests/e2e/e2e_config.py create mode 100644 engine/tests/e2e/test_thinking_budget_e2e.py create mode 100644 engine/tests/test_thinking_budget.py create mode 100644 engine/tests/test_thinking_budget_engine.py create mode 100644 engine/tests/test_thinking_budget_server.py diff --git a/README.md b/README.md index ab01e5f..c2a0838 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,18 @@ The response contains generated text in `choices`. For other clients, use base URL `http://127.0.0.1:11500/v1`, model `flint`, and the API key from `rec auth token`. +To bound the reasoning of a chat request, add `"thinking_budget": N`, a +nonnegative integer. It needs `grammar=flint` (the default). N counts the +tokens after the model's own ``; when N is reached, Flint closes the +`` block itself. A budget of 0 skips reasoning. A prompt that already +ends with `` is rejected with HTTP 400. A prompt with an +unclosed `` anywhere is also rejected. `/v1/messages` rejects only +`thinking_budget` with 400; `thinking.budget_tokens` is ignored as before. When +the request sets `thinking_budget` (0 included), the response `usage` reports +`reasoning_tokens` and `thinking_forced_close`. Streaming responses report them +only in the final usage chunk, so set `stream_options.include_usage`. Without a +budget these keys do not appear. + The Anthropic-compatible API is available at `http://127.0.0.1:11500/v1/messages`. Point `ANTHROPIC_BASE_URL` at `http://127.0.0.1:11500` and send the token from `rec auth token` as a bearer diff --git a/engine/src/rec_mlx/constants.py b/engine/src/rec_mlx/constants.py new file mode 100644 index 0000000..7ef183a --- /dev/null +++ b/engine/src/rec_mlx/constants.py @@ -0,0 +1,14 @@ +# Thinking budget. +THINK_OPEN = "" +THINK_CLOSE = "" +THINKING_BUDGET_FIELD = "thinking_budget" +# A forced token has probability 1 in the constrained process. +FORCED_TOKEN_LOGPROB = 0.0 + +# Engine metric keys read for usage when thinking_budget is set. +METRIC_REASONING_TOKENS = "reasoning_tokens" +METRIC_FORCED_CLOSE = "thinking_forced_close" + +# Usage fields added to chat completions that set thinking_budget. +USAGE_REASONING_TOKENS = "reasoning_tokens" +USAGE_FORCED_CLOSE = "thinking_forced_close" diff --git a/engine/src/rec_mlx/engine.py b/engine/src/rec_mlx/engine.py index 15ad765..a251971 100644 --- a/engine/src/rec_mlx/engine.py +++ b/engine/src/rec_mlx/engine.py @@ -11,8 +11,10 @@ from .chat import load_tokenizer, normalize_messages from .checkpoint import validate +from .constants import FORCED_TOKEN_LOGPROB from .detokenize import StreamingText from .kernels.device import profile_for_device +from .thinking import ThinkBudget, resolve_think_ids class ContextLimitError(ValueError): @@ -77,6 +79,11 @@ def __init__( self._prefix_preparation = None self.mx = mx self.tokenizer = load_tokenizer(directory) + try: + self.think_ids = resolve_think_ids(self.tokenizer) + except ValueError: + # Without / the engine serves requests that carry no thinking budget. + self.think_ids = None self._constraints = None self.enforce_tool_schemas = enforce_tool_schemas self._constraints_lock = threading.Lock() @@ -110,6 +117,13 @@ def __init__( "kv_format": self.identity["kv_format"], } + def validate_thinking_budget(self, ids, budget): + if self.think_ids is None: + raise ValueError("thinking_budget requires and tokenizer tokens") + opens = [i for i, t in enumerate(ids) if t == self.think_ids[0]] + if opens and self.think_ids[1] not in ids[opens[-1] + 1 :]: + raise ValueError("thinking_budget requires a prompt without an unclosed ") + @property def constraints(self): with self._constraints_lock: @@ -149,6 +163,7 @@ def generate_tokens( should_stop=lambda: False, grammar=None, return_logprobs=False, + thinking_budget=None, ): # Metal streams are thread-local; HTTP handlers run outside the model-loading thread. with self.mx.stream(self.mx.new_stream(self.mx.gpu)): @@ -162,6 +177,7 @@ def generate_tokens( should_stop=should_stop, grammar=constraint, return_logprobs=return_logprobs, + thinking_budget=thinking_budget, ) except PrefillCancelled: self.mx.synchronize() @@ -221,7 +237,21 @@ def _take_prefix(self, ids): prefix = prepared return prefix - def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_logprobs=False): + def _generate_tokens( + self, + ids, + sampling, + *, + should_stop, + grammar=None, + return_logprobs=False, + thinking_budget=None, + ): + if thinking_budget is not None and grammar is None: + raise ValueError("thinking_budget requires the flint grammar") + if thinking_budget is not None: + # The server validates before any response header; this is the backstop. + self.validate_thinking_budget(ids, thinking_budget) if len(ids) + sampling.max_tokens > self.context_limit: raise ContextLimitError( "Prompt plus output allowance exceeds the configured context limit" @@ -265,6 +295,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l if sampling.presence_penalty: session._state["presence_mask"] = mx.zeros(logits.size, mx.float32) mx.eval(logits) + think = ThinkBudget(thinking_budget, *(self.think_ids or (None, None))) prefill_end = time.perf_counter() prefill_peak = mx.get_peak_memory() first = None @@ -274,9 +305,32 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l if should_stop(): reason = "stop" break - if index: + forced = think.forced_token() if index else None + if forced is not None: + # The budget caps every packet, so a forced close always starts a new one. + # The last reasoning token is returned but unfed; feed it before . + # feed_only runs one forward pass: no sampling, grammar, presence or RNG + # change, and the packet seed schedule (seed + index) is untouched. + self.resident_sampler.block( + session._state, + generated[-1], + 1, + sampling.seed + index, + feed_only=True, + presence_penalty=sampling.presence_penalty, + ) + token = forced + grammar.accept_first(token, session._state) + if sampling.presence_penalty: + session._state["presence_mask"][token] = 1.0 + if return_logprobs: + token_logprobs.append(FORCED_TOKEN_LOGPROB) + computed_tokens += 1 + elif index: if not packet: count = min(8, sampling.max_tokens - index) + if think.remaining() is not None: + count = min(count, think.remaining()) options = {"logprobs": token_logprobs} if return_logprobs else {} packet = self.resident_sampler.block( session._state, @@ -298,6 +352,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l flat = logits.reshape(-1).astype(mx.float32) if grammar is not None: flat = grammar.first_logits(flat) + flat = think.mask_first_logits(flat) if sampling.temperature == 0: token = int(mx.argmax(flat).item()) elif unfiltered_t1: @@ -318,6 +373,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l if first is None: first = time.perf_counter() generated.append(token) + think.accept(token) details = {"logprob": token_logprobs[index]} if return_logprobs else {} if token in stops: reason = "stop" @@ -378,6 +434,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l "process_peak_rss_bytes": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, "computed_tokens": computed_tokens, "decode_strategy": "resident_sampling", + **think.report(), **(grammar.metrics() if grammar is not None else {}), }, } diff --git a/engine/src/rec_mlx/grammar_builder.py b/engine/src/rec_mlx/grammar_builder.py index 6c25f93..a56159b 100644 --- a/engine/src/rec_mlx/grammar_builder.py +++ b/engine/src/rec_mlx/grammar_builder.py @@ -14,6 +14,8 @@ TokenFormat, ) +from .token_ids import dedicated_token_id + _PROTOCOL_MARKERS = ( "", "", @@ -41,7 +43,8 @@ def build_flint_glm_grammar( enforce_tool_schemas: bool = False, ) -> StructuralTag: ids = { - marker: _token_id(tokenizer, marker) for marker in (*_STOP_MARKERS, "", "") + marker: dedicated_token_id(tokenizer, marker) + for marker in (*_STOP_MARKERS, "", "") } if tokenizer.eos_token_id != ids["<|end_of_text|>"]: raise ValueError("Flint grammar requires end-of-text as the tokenizer EOS") @@ -80,14 +83,3 @@ def build_flint_glm_grammar( ] ) ) - - -def _token_id(tokenizer: PreTrainedTokenizerBase, marker: str) -> int: - token_id = tokenizer.convert_tokens_to_ids(marker) - if ( - not isinstance(token_id, int) - or tokenizer.convert_ids_to_tokens(token_id) != marker - or tokenizer.encode(marker, add_special_tokens=False) != [token_id] - ): - raise ValueError(f"Flint grammar requires {marker!r} to be a dedicated vocabulary token") - return token_id diff --git a/engine/src/rec_mlx/kernels/sampling.py b/engine/src/rec_mlx/kernels/sampling.py index 6a83633..6ca1147 100644 --- a/engine/src/rec_mlx/kernels/sampling.py +++ b/engine/src/rec_mlx/kernels/sampling.py @@ -72,8 +72,15 @@ def block( presence_penalty=0.0, grammar=None, logprobs=None, + feed_only=False, ): module, engine = self.module, self.engine + if feed_only: + # Write `token` into the KV and advance one position without sampling anything. + if count != 1: + raise ValueError("feed_only feeds exactly one token") + engine.forward(state, token) + return [] if not 1 <= count <= WIDTH: raise ValueError("Resident packet must contain between one and eight tokens") engine.ensure_capacity(state, count) diff --git a/engine/src/rec_mlx/scheduler.py b/engine/src/rec_mlx/scheduler.py index 17e5a94..fcc8947 100644 --- a/engine/src/rec_mlx/scheduler.py +++ b/engine/src/rec_mlx/scheduler.py @@ -14,6 +14,7 @@ from mlx_lm.tokenizer_utils import NaiveStreamingDetokenizer, TokenizerWrapper from .chat import parse_tool_call, seeded_tool_parser +from .constants import THINKING_BUDGET_FIELD from .engine import Sampling @@ -168,6 +169,8 @@ def cancelled(): options["grammar"] = request.constraint if args.logprobs: options["return_logprobs"] = True + if request.thinking_budget is not None: + options[THINKING_BUDGET_FIELD] = request.thinking_budget raw = self.engine.generate_tokens( ids, sampling, @@ -186,6 +189,7 @@ def responses(self, raw, ctx, sm, sampling, cancelled, logprobs=False): for event in raw: if "metrics" in event: ctx.prompt_cache_count = event["metrics"]["cached_prompt_tokens"] + ctx.metrics = event["metrics"] self.record( sampling, "cancelled" if cancelled() else "complete", diff --git a/engine/src/rec_mlx/server.py b/engine/src/rec_mlx/server.py index ee4f3b8..c24d698 100644 --- a/engine/src/rec_mlx/server.py +++ b/engine/src/rec_mlx/server.py @@ -15,6 +15,13 @@ chat_to_message, messages_to_chat, ) +from .constants import ( + METRIC_FORCED_CLOSE, + METRIC_REASONING_TOKENS, + THINKING_BUDGET_FIELD, + USAGE_FORCED_CLOSE, + USAGE_REASONING_TOKENS, +) from .engine import Sampling @@ -22,6 +29,18 @@ class RequestError(ValueError): pass +class MissingMetricsError(RuntimeError): + pass + + +def reject_messages_budget(body): + """/v1/messages has no thinking budget; refuse the field instead of dropping it.""" + if THINKING_BUDGET_FIELD in body: + raise ValueError( + f"{THINKING_BUDGET_FIELD} is not supported on /v1/messages; use /v1/chat/completions" + ) + + def make_server( engine, address, metrics_path, *, defaults=Sampling(), api_key=None, grammar_mode="flint" ): @@ -113,6 +132,14 @@ def do_POST(self): super().do_POST() except RequestError as error: self._reject(400, str(error)) + except MissingMetricsError as error: + if self.stream: + # Headers are already sent; report the failure inside the stream. + self.wfile.write(f"data: {json.dumps({'error': str(error)})}\n\n".encode()) + else: + # mlx-lm buffered a 200 status line before building the response. + self._headers_buffer = [] + self._reject(500, str(error)) except (BrokenPipeError, ConnectionResetError): logging.info("Client disconnected; cancelling its generation") finally: @@ -122,6 +149,7 @@ def _prepare_messages(self): length = int(self.headers["Content-Length"]) try: body = json.loads(self.rfile.read(length).decode()) + reject_messages_budget(body) chat, ignored, include_thinking = messages_to_chat(body) except (json.JSONDecodeError, UnicodeDecodeError) as error: raise RequestError(f"Invalid JSON in request body: {error}") from error @@ -157,8 +185,36 @@ def _set_completion_headers(self, status): self.completion_status = status super()._set_completion_headers(status) + def usage_extras(self): + # Usage keys exist only when the request set thinking_budget (0 counts). + if self.current_request.thinking_budget is None: + return {} + metrics = getattr(self.current_request.generation_context, "metrics", None) + if metrics is None: + raise MissingMetricsError( + "Generation ended without a metrics event; usage is unavailable" + ) + missing = [ + key for key in (METRIC_REASONING_TOKENS, METRIC_FORCED_CLOSE) if key not in metrics + ] + if missing: + raise MissingMetricsError( + f"thinking_budget was set but metrics lack {missing}; usage is unavailable" + ) + return { + USAGE_REASONING_TOKENS: metrics[METRIC_REASONING_TOKENS], + USAGE_FORCED_CLOSE: metrics[METRIC_FORCED_CLOSE], + } + + def completion_usage_response(self, *args, **kwargs): + response = super().completion_usage_response(*args, **kwargs) + response["usage"].update(self.usage_extras()) + return response + def generate_response(self, *args, **kwargs): response = super().generate_response(*args, **kwargs) + if "usage" in response: + response["usage"].update(self.usage_extras()) for choice in response["choices"]: if "logprobs" in choice: for entry in choice["logprobs"]["content"]: @@ -204,6 +260,7 @@ def drop_unknown_fields(self): "store", "grammar", "parallel_tool_calls", + THINKING_BUDGET_FIELD, } unknown = set(self.body) - allowed for field in unknown: @@ -229,6 +286,9 @@ def validate_request(self): super().validate_model_parameters() if self.stream and self.logprobs: raise ValueError("Token logprobs require stream=false") + budget = self.body.get(THINKING_BUDGET_FIELD) + if THINKING_BUDGET_FIELD in self.body and (type(budget) is not int or budget < 0): + raise ValueError(f"{THINKING_BUDGET_FIELD} must be a nonnegative integer") if self.requested_model != "flint": raise ValueError("The served model is flint") for key in ("frequency_penalty", "min_p"): @@ -255,6 +315,8 @@ def validate_request(self): mode = self.body["grammar"] if "grammar" in self.body else grammar_mode if mode not in ("none", "flint"): raise ValueError("grammar must be none or flint") + if budget is not None and mode != "flint": + raise ValueError(f"{THINKING_BUDGET_FIELD} requires grammar=flint") if mode == "flint" and self.body.get("stop"): raise ValueError("Flint grammar cannot be combined with custom stop strings") if ( @@ -302,6 +364,8 @@ def handle_completion(self, request, stop_words): self.max_tokens = ( remaining if explicit_maximum is None else min(self.max_tokens, remaining) ) + if self.body.get(THINKING_BUDGET_FIELD) is not None: + engine.validate_thinking_budget(ids, self.body[THINKING_BUDGET_FIELD]) Sampling( temperature=self.temperature, top_p=self.top_p, @@ -326,6 +390,8 @@ def handle_completion(self, request, stop_words): else: self.wfile = ErrorTranslator(self.wfile, status) request.prompt_tokens = ids + self.current_request = request + request.thinking_budget = self.body.get(THINKING_BUDGET_FIELD) request.is_disconnected = self.is_disconnected request.generation_context = None try: diff --git a/engine/src/rec_mlx/thinking.py b/engine/src/rec_mlx/thinking.py new file mode 100644 index 0000000..51cc4ba --- /dev/null +++ b/engine/src/rec_mlx/thinking.py @@ -0,0 +1,60 @@ +from .constants import METRIC_FORCED_CLOSE, METRIC_REASONING_TOKENS, THINK_CLOSE, THINK_OPEN +from .token_ids import dedicated_token_id + + +def resolve_think_ids(tokenizer): + return dedicated_token_id(tokenizer, THINK_OPEN), dedicated_token_id(tokenizer, THINK_CLOSE) + + +class ThinkBudget: + """Counts tokens between and and says when must be forced. + + The engine feeds every emitted token (sampled or forced) to accept(). A budget of None + counts but never forces; a budget of 0 only masks at the first position. + """ + + def __init__(self, budget, think_id, close_id): + if budget is not None and (type(budget) is not int or budget < 0): + raise ValueError("thinking budget must be None or a nonnegative integer") + self.budget = budget + self.think_id = think_id + self.close_id = close_id + self.reasoning_tokens = 0 + self.forced_close = False + self.inside_think = False + self._opened = False + + def remaining(self): + """Tokens that may still be sampled before the close is forced; None = unlimited.""" + if self.budget is None or not self.inside_think: + return None + left = self.budget - self.reasoning_tokens + if left < 0: + raise RuntimeError("The thinking budget was exceeded without a forced close") + return left + + def forced_token(self): + return self.close_id if self.remaining() == 0 else None + + def accept(self, token): + if self.inside_think: + if token == self.close_id: + self.forced_close = self.forced_token() is not None + self.inside_think = False + else: + self.reasoning_tokens += 1 + elif token == self.think_id and not self._opened: + self._opened = self.inside_think = True + + def report(self): + return { + METRIC_REASONING_TOKENS: self.reasoning_tokens, + METRIC_FORCED_CLOSE: self.forced_close, + } + + def mask_first_logits(self, logits): + if self.budget != 0: + return logits + import mlx.core as mx + + return mx.where(mx.arange(logits.shape[-1]) == self.think_id, -float("inf"), logits) diff --git a/engine/src/rec_mlx/token_ids.py b/engine/src/rec_mlx/token_ids.py new file mode 100644 index 0000000..5dc9225 --- /dev/null +++ b/engine/src/rec_mlx/token_ids.py @@ -0,0 +1,9 @@ +def dedicated_token_id(tokenizer, marker: str) -> int: + token_id = tokenizer.convert_tokens_to_ids(marker) + if ( + not isinstance(token_id, int) + or tokenizer.convert_ids_to_tokens(token_id) != marker + or tokenizer.encode(marker, add_special_tokens=False) != [token_id] + ): + raise ValueError(f"The tokenizer must define {marker!r} as a dedicated vocabulary token") + return token_id diff --git a/engine/tests/e2e/conftest.py b/engine/tests/e2e/conftest.py new file mode 100644 index 0000000..511d9a1 --- /dev/null +++ b/engine/tests/e2e/conftest.py @@ -0,0 +1,20 @@ +"""Shared session-scoped (one resident engine for all e2e modules) fixtures for the real-weights e2e tests.""" + +import pytest +from e2e_config import CONTEXT_LIMIT, MANIFEST_NAME, MODEL_DIR + +from rec_mlx import thinking + + +@pytest.fixture(scope="session") +def engine(): + if MODEL_DIR is None: + pytest.skip("REC_E2E_MODEL_DIR is not set (flint-int4 weights unavailable)") + from rec_mlx.engine import Engine + + return Engine(MODEL_DIR, MODEL_DIR / MANIFEST_NAME, context_limit=CONTEXT_LIMIT) + + +@pytest.fixture(scope="session") +def think(engine): + return thinking.resolve_think_ids(engine.tokenizer) diff --git a/engine/tests/e2e/e2e_config.py b/engine/tests/e2e/e2e_config.py new file mode 100644 index 0000000..8df1345 --- /dev/null +++ b/engine/tests/e2e/e2e_config.py @@ -0,0 +1,43 @@ +"""Shared constants and skip rule for the real-weights e2e tests (no test module imports).""" + +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + +MODEL_DIR = Path(os.environ["REC_E2E_MODEL_DIR"]) if os.environ.get("REC_E2E_MODEL_DIR") else None +MANIFEST_NAME = "flint-int4.json" +CONTEXT_LIMIT = 16_384 +MIN_AVAILABLE_BYTES = 4 * 1024**3 # free + inactive + speculative + purgeable pages +AVAILABLE_PAGE_KINDS = ("Pages free", "Pages inactive", "Pages speculative", "Pages purgeable") + + +def _vm_available_bytes() -> int: + out = subprocess.check_output(["vm_stat"], text=True) + page = int(re.search(r"page size of (\d+) bytes", out).group(1)) + total = 0 + for kind in AVAILABLE_PAGE_KINDS: + total += int(re.search(rf"{kind}:\s+(\d+)\.", out).group(1)) + return total * page + + +def _skip_reason() -> str | None: + if MODEL_DIR is None: + return "REC_E2E_MODEL_DIR is not set (flint-int4 weights unavailable)" + if not (MODEL_DIR / MANIFEST_NAME).exists(): + return f"{MODEL_DIR / MANIFEST_NAME} not found (flint-int4 weights unavailable)" + try: + import mlx.core as mx + except ImportError: + return "mlx is not installed" + if not mx.metal.is_available(): + return "no Apple GPU (Metal) available" + available = _vm_available_bytes() + if available < MIN_AVAILABLE_BYTES: + return f"memory preflight: vm_stat available {available} B < {MIN_AVAILABLE_BYTES} B" + return None + + +SKIP_REASON = _skip_reason() diff --git a/engine/tests/e2e/test_thinking_budget_e2e.py b/engine/tests/e2e/test_thinking_budget_e2e.py new file mode 100644 index 0000000..05afbcf --- /dev/null +++ b/engine/tests/e2e/test_thinking_budget_e2e.py @@ -0,0 +1,126 @@ +"""End-to-end thinking budget tests on the real Flint weights and an Apple GPU. + +They skip, with a reason, when the weights or the GPU are unavailable or free memory is low: + + REC_E2E_MODEL_DIR=/path/to/flint-int4 python -m pytest tests/e2e + +The tests call Engine.generate_tokens directly, so no server or port is involved. +""" + +from __future__ import annotations + +import numpy as np +import pytest +from e2e_config import SKIP_REASON + +PARITY_BUDGET = 100_000 # larger than any generation here: the model must close on its own +PARITY_MAX_TOKENS = 384 +FORCED_BUDGET = 20 +PROBE_MAX_TOKENS = 1024 # baseline run proving the prompt reasons past FORCED_BUDGET +TAIL = 24 # tokens generated after the forced close +FOLLOW_UP_TOKENS = 24 +PLAIN_PROMPTS = [ + "What is 17 times 23? Answer with the number.", + "In one sentence, what does the Unix command `ls -la` do?", +] +REASONING_PROMPT = ( + "A farmer has 17 sheep. All but 9 run away, then he buys twice as many as remain. " + "How many sheep does he have? Think carefully and show the arithmetic." +) + +pytestmark = pytest.mark.skipif(SKIP_REASON is not None, reason=SKIP_REASON or "") + + +def _user(text): + return [{"role": "user", "content": text}] + + +def _run(engine, text, *, max_tokens, **options): + """Greedy generation through Engine.generate_tokens; returns (prompt ids, token ids, metrics).""" + from rec_mlx.engine import Sampling + + ids = engine.prompt(_user(text)) + events = list( + engine.generate_tokens( + ids, + Sampling(temperature=0.0, top_p=1.0, top_k=0, max_tokens=max_tokens, seed=0), + grammar=engine.constraints.tools(None), + **options, + ) + ) + metrics = next(e["metrics"] for e in events if "metrics" in e) + return ids, list(metrics["token_ids"]), metrics + + +def _cold_logits(engine, ids): + """Last-position logits of a cold prefill (no prefix) as float32 numpy.""" + mx = engine.mx + with mx.stream(mx.new_stream(mx.gpu)): + logits = engine.runtime.make_session(max_new_tokens=1).prefill(ids, prefix=None) + mx.eval(logits) + out = np.array(logits.reshape(-1).astype(mx.float32)) + mx.synchronize() + return out + + +def test_large_budget_is_token_identical_to_no_budget(engine): + for prompt in PLAIN_PROMPTS: + _, base, _ = _run(engine, prompt, max_tokens=PARITY_MAX_TOKENS) + _, budgeted, metrics = _run( + engine, prompt, max_tokens=PARITY_MAX_TOKENS, thinking_budget=PARITY_BUDGET + ) + assert budgeted == base + assert metrics["thinking_forced_close"] is False + + +def test_forced_close_continues_exactly_as_a_cold_model_would(engine, think): + think_id, close_id = think + _, base, _ = _run(engine, REASONING_PROMPT, max_tokens=PROBE_MAX_TOKENS) + assert base[0] == think_id + assert close_id not in base[: 2 + FORCED_BUDGET], "prompt must reason past the budget" + ids, tokens, metrics = _run( + engine, REASONING_PROMPT, max_tokens=1 + FORCED_BUDGET + 1 + TAIL, + thinking_budget=FORCED_BUDGET, + ) # fmt: skip + assert metrics["reasoning_tokens"] == FORCED_BUDGET + assert metrics["thinking_forced_close"] is True + assert metrics["computed_tokens"] == metrics["generated_tokens"] + close_at = tokens.index(close_id) + assert close_at == 1 + FORCED_BUDGET and tokens[close_at + 1 :] + # The KV written around the forced close must predict what a cold prefill predicts. + for end in range(close_at, len(tokens) - 1): + logits = _cold_logits(engine, ids + tokens[: end + 1]) + assert int(np.argmax(logits)) == tokens[end + 1], f"divergence after position {end}" + + +def test_budget_zero_has_no_think_token(engine, think): + _, tokens, metrics = _run(engine, REASONING_PROMPT, max_tokens=64, thinking_budget=0) + assert think[0] not in tokens + assert metrics["reasoning_tokens"] == 0 and metrics["thinking_forced_close"] is False + + +def test_follow_up_reusing_the_prefix_equals_a_cold_run(engine, think): + """A follow-up turn that reuses the KV of a forced-close answer decodes as a cold run does.""" + from rec_mlx.engine import Sampling + + ids, tokens, metrics = _run( + engine, REASONING_PROMPT, max_tokens=1 + FORCED_BUDGET + 1 + TAIL, + thinking_budget=FORCED_BUDGET, + ) # fmt: skip + assert metrics["thinking_forced_close"] is True, "prompt must reason past the budget" + engine.prepare_prefix() + sampling = Sampling(temperature=0.0, top_p=1.0, top_k=0, max_tokens=FOLLOW_UP_TOKENS, seed=0) + + def follow(): + events = list( + engine.generate_tokens(ids + tokens, sampling, grammar=engine.constraints.tools(None)) + ) + return events[-1]["metrics"] + + warm = follow() + assert warm["cached_prompt_tokens"] > 0, "the follow-up must reuse the prefix" + engine._prompt_cache = None + engine._prefix_preparation = None + cold = follow() + assert cold["cached_prompt_tokens"] == 0 + assert warm["token_ids"] == cold["token_ids"] diff --git a/engine/tests/test_thinking_budget.py b/engine/tests/test_thinking_budget.py new file mode 100644 index 0000000..2496d4a --- /dev/null +++ b/engine/tests/test_thinking_budget.py @@ -0,0 +1,145 @@ +"""ThinkBudget state machine, think-token resolution and the budget-0 first-token mask.""" + +import numpy as np +import pytest + +from rec_mlx import thinking +from rec_mlx.token_ids import dedicated_token_id + +THINK_ID = 7 +CLOSE_ID = 8 +VOCAB = 12 +TEXT = 3 + + +def tb(budget): + return thinking.ThinkBudget(budget, THINK_ID, CLOSE_ID) + + +def feed(state, *tokens): + for token in tokens: + state.accept(token) + + +class DedicatedTokenizer: + def __init__(self, vocab): + self.vocab = vocab + + def convert_tokens_to_ids(self, token): + return self.vocab.get(token) + + def convert_ids_to_tokens(self, idx): + return next((k for k, v in self.vocab.items() if v == idx), None) + + def encode(self, text, add_special_tokens=False): + return [self.vocab[text]] if text in self.vocab else [101, 102] + + +def test_resolve_think_ids_needs_two_dedicated_tokens(): + vocab = {"": THINK_ID, "": CLOSE_ID} + assert thinking.resolve_think_ids(DedicatedTokenizer(vocab)) == (THINK_ID, CLOSE_ID) + for missing in vocab: # a marker that encodes to several ids is not a vocabulary token + partial = {k: v for k, v in vocab.items() if k != missing} + with pytest.raises(ValueError): + thinking.resolve_think_ids(DedicatedTokenizer(partial)) + + +@pytest.mark.parametrize("n", [1, 5]) +def test_close_is_required_after_exactly_n_reasoning_tokens(n): + s = tb(n) + feed(s, THINK_ID) # does not count + for i in range(n): + assert s.forced_token() is None, f"forced early after {i} tokens" + s.accept(TEXT) + assert s.forced_token() == CLOSE_ID + assert s.forced_token() == CLOSE_ID # a query, not a state change + s.accept(CLOSE_ID) # does not count + assert (s.reasoning_tokens, s.forced_close, s.inside_think) == (n, True, False) + assert s.forced_token() is None + + +def test_tokens_outside_think_are_neither_counted_nor_forced(): + cases = { # tokens -> (reasoning_tokens, forced_close) + "natural close": ((THINK_ID, TEXT, 4, CLOSE_ID), (2, False)), + "empty reasoning": ((THINK_ID, CLOSE_ID), (0, False)), + "after close": ((THINK_ID, TEXT, 4, CLOSE_ID, *[TEXT] * 50), (2, True)), + "no think": ((TEXT, 4, 5, 6), (0, False)), + } + for name, (tokens, expected) in cases.items(): + s = tb(2 if name == "after close" else 5) + feed(s, *tokens) + assert (s.reasoning_tokens, s.forced_close) == expected, name + assert s.forced_token() is None, name + + +def test_absent_budget_counts_but_never_forces(): + s = tb(None) + feed(s, THINK_ID, *[TEXT] * 10_000) + assert s.forced_token() is None and s.reasoning_tokens == 10_000 + s.accept(CLOSE_ID) + assert s.forced_close is False + + +def test_remaining_raises_when_the_budget_is_overshot(): + s = tb(2) + feed(s, THINK_ID, TEXT, TEXT) + assert s.remaining() == 0 + s.accept(TEXT) # a third reasoning token without a forced close + with pytest.raises(RuntimeError): + s.remaining() + + +def test_invalid_budget_values_are_rejected(): + for value in (-1, 1.5, "5", True, False, []): + with pytest.raises(ValueError): + tb(value) + + +def test_only_budget_zero_masks_think_at_the_first_token(): + logits = np.linspace(-1.0, 1.0, VOCAB).astype(np.float32) + logits[THINK_ID] = 50.0 # the model would pick + before = logits.copy() + out = np.asarray(tb(0).mask_first_logits(logits)) + assert out[THINK_ID] == -np.inf and int(np.argmax(out)) != THINK_ID + keep = [i for i in range(VOCAB) if i != THINK_ID] + np.testing.assert_array_equal(out[keep], before[keep]) + np.testing.assert_array_equal(logits, before) + for budget in (None, 1, 64): + np.testing.assert_array_equal(np.asarray(tb(budget).mask_first_logits(logits)), before) + + +class BosTokenizer(DedicatedTokenizer): + """encode() prepends a BOS id unless add_special_tokens is False.""" + + def encode(self, text, add_special_tokens=False): + return ([100] if add_special_tokens else []) + super().encode(text) + + +@pytest.mark.parametrize("tokenizer", [BosTokenizer, DedicatedTokenizer]) +def test_dedicated_token_id_returns_the_vocabulary_id(tokenizer): + assert dedicated_token_id(tokenizer({"": THINK_ID}), "") == THINK_ID + + +class NonIntTokenizer(DedicatedTokenizer): + def convert_tokens_to_ids(self, token): + return str(THINK_ID) + + +class WrongRoundTripTokenizer(DedicatedTokenizer): + def convert_ids_to_tokens(self, idx): + return "" + + +class SplitEncodeTokenizer(DedicatedTokenizer): + def encode(self, text, add_special_tokens=False): + return [THINK_ID, 1] + + +@pytest.mark.parametrize( + "tokenizer", + [NonIntTokenizer, WrongRoundTripTokenizer, SplitEncodeTokenizer, DedicatedTokenizer], +) +def test_dedicated_token_id_rejects_markers_that_are_not_one_vocabulary_token(tokenizer): + vocab = {} if tokenizer is DedicatedTokenizer else {"": THINK_ID} # {} -> unknown id + with pytest.raises(ValueError, match="dedicated vocabulary token"): + dedicated_token_id(tokenizer(vocab), "") diff --git a/engine/tests/test_thinking_budget_engine.py b/engine/tests/test_thinking_budget_engine.py new file mode 100644 index 0000000..eac2b8b --- /dev/null +++ b/engine/tests/test_thinking_budget_engine.py @@ -0,0 +1,377 @@ +"""Thinking budget in the decode loop, driven through a stub session, sampler and grammar.""" + +import json +import subprocess +import sys +from types import SimpleNamespace + +import mlx.core as mx +import pytest +from test_thinking_budget import CLOSE_ID, THINK_ID, DedicatedTokenizer +from tokenizers import Tokenizer, decoders, models, pre_tokenizers +from transformers import PreTrainedTokenizerFast + +from rec_mlx import constants +from rec_mlx.engine import Engine, Sampling +from rec_mlx.grammar import ConstraintCompiler +from rec_mlx.grammar_builder import _PROTOCOL_MARKERS +from rec_mlx.kernels.sampling import WIDTH, ResidentSampler + +VOCAB = 16 +EOS = 0 +TEXT = 3 +FIRST_TEXT = 4 +REASON = 5 +AFTER = 9 +SEED = 100 +PROMPT = [1, 2] +LOGPROB = -0.5 +PACKET = 8 # the decode packet size, a literal in Engine._generate_tokens + + +class RecordingSampler: + """Scripted tokens with the real block() feed semantics, recording every call. + + block(state, token, count) feeds `token` at state["position"], returns the packet, feeds all + but its last token and advances the position. feed_only feeds `token` and returns nothing. + """ + + def __init__(self, stream): + self.stream = list(stream) + self.calls = [] + self.fed = [] + + def block(self, state, token, count, seed, *, logprobs=None, feed_only=False, **options): + self.calls.append( + {"last": token, "count": count, "seed": seed, "feed_only": feed_only, **options} + ) + position = state["position"] + if feed_only: + self.fed.append((position, token)) + state["position"] = position + 1 + return [] + out = [self.stream.pop(0) if self.stream else TEXT for _ in range(count)] + self.fed.append((position, token)) + self.fed.extend((position + 1 + i, t) for i, t in enumerate(out[:-1])) + state["position"] = position + len(out) + if logprobs is not None: + logprobs.extend([LOGPROB] * len(out)) + return out + + +def stub_session(first): + logits = [0.0] * VOCAB + logits[first] = 10.0 + state = {"kv": SimpleNamespace(nbytes=0), "capacity": 0, "growth_count": 0} + state |= {"growth_seconds": 0.0, "position": len(PROMPT)} + return SimpleNamespace( + prefill=lambda ids, prefix=None, should_stop=None: mx.array([logits], mx.float32), + cached_tokens=0, + common_prefix_tokens=0, + prompt_cache=SimpleNamespace(layers=[], checkpoint_bytes=0, tokens=[]), + _state=state, + ) + + +class StubGrammar: + stops = (EOS,) + + def __init__(self): + self.accepted = [] + + def first_logits(self, logits): + return logits + + def accept_first(self, token, state): + self.accepted.append(token) + + def metrics(self): + return {} + + +def build(monkeypatch, stream, first=FIRST_TEXT): + monkeypatch.setattr( + "rec_mlx.engine.StreamingText", + lambda tokenizer: SimpleNamespace(add_token=lambda t: "", finish=lambda toks: ""), + ) + engine = Engine.__new__(Engine) + engine.mx = mx + engine.eos = {EOS} + engine.context_limit = 10_000 + engine.think_ids = (THINK_ID, CLOSE_ID) + engine.tokenizer = DedicatedTokenizer({"": THINK_ID, "": CLOSE_ID}) + engine.sampler = engine.resident_sampler = RecordingSampler(stream) + engine.session = stub_session(first) + engine.runtime = SimpleNamespace( + make_session=lambda max_new_tokens: engine.session, prefill_chunk_size=10**9, metadata=[] + ) + engine._prompt_cache = engine.session.prompt_cache + engine._prefix_preparation = None + return engine + + +def run( + engine, max_tokens, *, budget=None, grammar=True, ids=PROMPT, logprobs=False, seed=SEED, **kw +): + if grammar is True: + grammar = StubGrammar() if budget is not None else None # a budget requires a grammar + options = {"temperature": 0.0, "top_p": 1.0, "top_k": 0, **kw} + extra = {} if budget is None else {"thinking_budget": budget} + events = list( + engine._generate_tokens( + ids, + Sampling(max_tokens=max_tokens, seed=seed, **options), + should_stop=lambda: False, + grammar=grammar, + return_logprobs=logprobs, + **extra, + ) + ) + return [e["token"] for e in events[:-1]], events[:-1], events[-1]["metrics"] + + +def assert_fed_once_in_order(engine, tokens): + """Every generated token except the last is written to the KV exactly once, in order.""" + fed = engine.sampler.fed + assert [p for p, _ in fed] == list(range(len(PROMPT), len(PROMPT) + len(fed))) + assert [t for _, t in fed] == tokens[: len(fed)] + assert len(fed) == len(tokens) - 1 + + +def test_unbudgeted_run_is_unchanged(monkeypatch): + stream = [5, 6, 7, 5, 6, 7, 5, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9] + engine = build(monkeypatch, stream) + tokens, events, metrics = run(engine, 20, logprobs=True) + assert tokens == [FIRST_TEXT, *stream] + calls = engine.sampler.calls + assert [c["count"] for c in calls] == [PACKET, PACKET, 3] + assert [c["seed"] for c in calls] == [SEED + 1, SEED + 9, SEED + 17] + assert [c["last"] for c in calls] == [FIRST_TEXT, stream[7], stream[15]] + assert events[-1]["finish_reason"] == "length" + assert metrics["generated_tokens"] == metrics["computed_tokens"] == 20 + assert metrics["token_ids"] == tokens + assert metrics["thinking_forced_close"] is False + assert_fed_once_in_order(engine, tokens) + + +def construct(monkeypatch, tmp_path, tokenizer): + """The real Engine.__init__ with hardware, weights and decoder replaced by stubs.""" + (tmp_path / "config.json").write_text(json.dumps({"eos_token_id": [EOS]})) + tokenizer.chat_template = "template" + runtime = SimpleNamespace( + model=SimpleNamespace(native=SimpleNamespace(parameters=lambda: {})), + prefill_chunk_size=10**9, + metadata=[], + ) + patches = { + "rec_mlx.engine.platform.system": lambda: "Darwin", + "rec_mlx.engine.platform.machine": lambda: "arm64", + "rec_mlx.engine.subprocess.check_output": lambda *a, **k: str(64 * 1024**3), + "rec_mlx.engine.profile_for_device": lambda device: SimpleNamespace(chip="x"), + "rec_mlx.engine.validate": lambda *a: { + "max_context": 4096, + "weight_format": "w", + "kv_format": "k", + }, + "rec_mlx.engine.load_tokenizer": lambda directory: tokenizer, + "rec_mlx.kernels.build_decoder": lambda model, optimized: (runtime, None), + } + for target, value in patches.items(): + monkeypatch.setattr(target, value) + monkeypatch.setattr( + "rec_mlx.quantized.QATModel", + type("QAT", (), {"context_limit": 4096, "__init__": lambda s, d: None}), + ) + return Engine(tmp_path, tmp_path / "m.json", context_limit=4096) + + +def test_engine_resolves_think_ids_and_tolerates_a_tokenizer_without_them(monkeypatch, tmp_path): + vocab = {"": THINK_ID, "": CLOSE_ID} + assert construct(monkeypatch, tmp_path, DedicatedTokenizer(vocab)).think_ids == ( + THINK_ID, + CLOSE_ID, + ) + engine = construct(monkeypatch, tmp_path, DedicatedTokenizer({})) + assert engine.think_ids is None # such an engine still serves requests without a budget + + +def test_unbudgeted_run_needs_no_think_tokens(monkeypatch): + engine = build(monkeypatch, []) + engine.think_ids = None + assert len(run(engine, 4)[0]) == 4 + + +@pytest.mark.parametrize("budget", [3, 2 * PACKET + 1]) +def test_forced_close_after_n_reasoning_tokens(monkeypatch, budget): + reasoning = [100 + i for i in range(budget)] # distinct, so a wrong t_k is visible + engine = build(monkeypatch, reasoning + [AFTER] * 30, first=THINK_ID) + grammar = StubGrammar() + tokens, _, metrics = run(engine, 1 + budget + 1 + 12, budget=budget, grammar=grammar) + assert tokens[: 2 + budget] == [THINK_ID, *reasoning, CLOSE_ID] + assert tokens[2 + budget :] == [AFTER] * 12 + assert grammar.accepted.count(CLOSE_ID) == 1 + assert metrics["reasoning_tokens"] == budget and metrics["thinking_forced_close"] is True + assert metrics["computed_tokens"] == metrics["generated_tokens"] == len(tokens) + # t_k, the last reasoning token, is fed alone before ; no sampling past the close. + (feed,) = [c for c in engine.sampler.calls if c["feed_only"]] + assert (feed["count"], feed["last"], feed["seed"]) == (1, reasoning[-1], SEED + 1 + budget) + assert_fed_once_in_order(engine, tokens) + + +def test_forced_close_logprobs_stay_aligned_with_tokens(monkeypatch): + engine = build(monkeypatch, [REASON, REASON] + [AFTER] * 6, first=THINK_ID) + tokens, events, metrics = run(engine, 6, budget=2, logprobs=True) + assert tokens == [THINK_ID, REASON, REASON, CLOSE_ID, AFTER, AFTER] + reported = [e["logprob"] for e in events] + assert reported[1:4] == [LOGPROB, LOGPROB, constants.FORCED_TOKEN_LOGPROB] + assert metrics["token_logprobs"] == reported + + +def test_forced_close_marks_presence_state_and_keeps_penalty(monkeypatch): + engine = build(monkeypatch, [REASON, REASON, AFTER, AFTER], first=THINK_ID) + run(engine, 5, budget=2, presence_penalty=0.5) + mask = engine.session._state["presence_mask"] + assert float(mask[CLOSE_ID]) == 1.0 and float(mask[THINK_ID]) == 1.0 + assert all(call["presence_penalty"] == 0.5 for call in engine.sampler.calls) + + +@pytest.mark.parametrize(("max_tokens", "forced"), [(4, True), (3, False)]) +def test_max_tokens_decides_whether_the_close_is_reached(monkeypatch, max_tokens, forced): + engine = build(monkeypatch, [REASON, REASON, AFTER], first=THINK_ID) + tokens, _, metrics = run(engine, max_tokens, budget=2) + assert tokens == [THINK_ID, REASON, REASON, CLOSE_ID][:max_tokens] + assert metrics["thinking_forced_close"] is forced + assert_fed_once_in_order(engine, tokens) + + +def test_natural_close_before_budget_is_not_forced(monkeypatch): + engine = build(monkeypatch, [REASON, CLOSE_ID] + [AFTER] * 10, first=THINK_ID) + tokens, _, metrics = run(engine, 6, budget=4) + assert tokens[:3] == [THINK_ID, REASON, CLOSE_ID] + assert metrics["thinking_forced_close"] is False and metrics["reasoning_tokens"] == 1 + assert_fed_once_in_order(engine, tokens) + + +def test_budget_zero_never_opens_think(monkeypatch): + for sampling in ({"temperature": 0.0}, {"temperature": 0.7, "top_p": 0.9, "top_k": 5}): + for seed in range(5): + engine = build(monkeypatch, [REASON] * 4, first=THINK_ID) # the model wants + tokens, _, metrics = run(engine, 2, budget=0, seed=seed, **sampling) + assert tokens[0] != THINK_ID, f"seed {seed}" + assert metrics["reasoning_tokens"] == 0 + + +def test_budget_without_a_grammar_raises_before_any_decode(monkeypatch): + for budget in (0, 2): + engine = build(monkeypatch, []) + with pytest.raises(ValueError, match="grammar"): + run(engine, 20, budget=budget, grammar=None) + assert engine.sampler.calls == [] + + +@pytest.mark.parametrize( + ("budget", "max_tokens", "counts"), + [(3, 100, [3]), (PACKET + 2, 100, [PACKET, 2]), (1000, 100, [PACKET]), (1000, 4, [3])], +) +def test_packets_never_sample_past_the_forced_close(monkeypatch, budget, max_tokens, counts): + engine = build(monkeypatch, [], first=THINK_ID) + run(engine, max_tokens, budget=budget) + assert [c["count"] for c in engine.sampler.calls][: len(counts)] == counts + + +def test_validate_rejects_an_unclosed_think_in_the_prompt(): + engine = SimpleNamespace(think_ids=(THINK_ID, CLOSE_ID)) + for ids in ([1, THINK_ID], [1, THINK_ID, 5, 6], [1, THINK_ID, 5, CLOSE_ID, THINK_ID, 6]): + with pytest.raises(ValueError, match="think"): + Engine.validate_thinking_budget(engine, ids, 3) + # An empty closed block ( right after ) is closed. + for ids in ([1, THINK_ID, 5, CLOSE_ID, 6], [1, THINK_ID, CLOSE_ID, 6], [1, 2]): + Engine.validate_thinking_budget(engine, ids, 3) + + +class FakeKernelEngine: + """Records every call; any engine call other than forward fails the feed_only tests.""" + + def __init__(self): + self.calls = [] + + def forward(self, state, token): + self.calls.append(("forward", state, token)) + + def __getattr__(self, name): + raise AssertionError(f"feed_only must not touch engine.{name}") + + +def resident_sampler(): + sampler = object.__new__(ResidentSampler) # skips the Metal kernel compilation + sampler.module, sampler.engine = None, FakeKernelEngine() + return sampler + + +def test_resident_feed_only_runs_one_forward_pass_and_samples_nothing(): + sampler, state = resident_sampler(), {"position": 3} + assert sampler.block(state, REASON, 1, SEED, feed_only=True, grammar=object()) == [] + assert sampler.engine.calls == [("forward", state, REASON)] + + +@pytest.mark.parametrize( + ("options", "message"), + [ + ({"feed_only": True, "count": 2}, "exactly one"), + ({"feed_only": True, "count": 0}, "exactly one"), + ({"count": WIDTH + 1}, "between one and eight"), # feed_only defaults to False + ({"feed_only": False, "count": 0}, "between one and eight"), + ], +) +def test_resident_block_rejects_bad_counts_before_touching_the_engine(options, message): + sampler = resident_sampler() + with pytest.raises(ValueError, match=message): + sampler.block({}, REASON, options.pop("count"), SEED, **options) + assert sampler.engine.calls == [] + + +def test_validate_rejects_a_tokenizer_without_think_tokens(): + with pytest.raises(ValueError, match="think"): + Engine.validate_thinking_budget(SimpleNamespace(think_ids=None), [1, 2], 3) + + +def test_decode_backstop_rejects_an_open_think_before_sampling(monkeypatch): + engine = build(monkeypatch, []) + with pytest.raises(ValueError, match="think"): + run(engine, 4, budget=2, ids=[*PROMPT, THINK_ID]) + assert engine.sampler.calls == [] + + +@pytest.fixture(scope="module") +def tokenizer(): + alphabet = sorted(pre_tokenizers.ByteLevel.alphabet()) + model = Tokenizer(models.BPE(vocab={c: i for i, c in enumerate(alphabet)}, merges=[])) + model.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) + model.decoder = decoders.ByteLevel() + tok = PreTrainedTokenizerFast(tokenizer_object=model, eos_token="<|end_of_text|>") + tok.add_tokens([x for x in _PROTOCOL_MARKERS if x != "<|end_of_text|>"]) + return tok + + +def test_flint_grammar_accepts_a_forced_close_after_reasoning(tokenizer): + think, close = (tokenizer.convert_tokens_to_ids(x) for x in ("", "")) + text = tokenizer.encode("abc", add_special_tokens=False) + with ConstraintCompiler(tokenizer).tools().session() as session: + state = {} + for token in [think, *text, close, *text, tokenizer.eos_token_id]: + session.accept_first(token, state) + + +def test_budget_modules_import_without_mlx(): + probe = """ +import importlib, json, sys +for name in ("rec_mlx.engine", "rec_mlx.thinking", "rec_mlx.grammar_builder"): + importlib.import_module(name) +b = sys.modules["rec_mlx.thinking"].ThinkBudget(1, 7, 8) +for token in (7, 3, 8): + b.accept(token) +print(json.dumps(sorted(m for m in sys.modules if m == "mlx" or m.startswith("mlx.")))) +""" + done = subprocess.run([sys.executable, "-c", probe], capture_output=True, text=True) + assert done.returncode == 0, done.stderr + assert done.stdout.strip() == "[]" diff --git a/engine/tests/test_thinking_budget_server.py b/engine/tests/test_thinking_budget_server.py new file mode 100644 index 0000000..a249e6c --- /dev/null +++ b/engine/tests/test_thinking_budget_server.py @@ -0,0 +1,219 @@ +"""thinking_budget over HTTP: request validation, early 400s, /v1/messages and usage fields.""" + +import http.client +import json +import tempfile +import threading +import unittest +from pathlib import Path + +from test_serving import Engine as BaseEngine + +from rec_mlx.server import make_server + +THINK_ID = 7 +BUDGET = 3 +OK, BAD, ERROR = 200, 400, 500 +JSON_HEADERS = {"Content-Type": "application/json"} +# Usage keys before the thinking budget existed (origin/main); mlx-lm omits +# prompt_tokens_details when no metrics event arrives. +BASE_USAGE = {"prompt_tokens", "completion_tokens", "total_tokens", "prompt_tokens_details"} +THINKING_USAGE = {"reasoning_tokens", "thinking_forced_close"} + + +class ThinkEngine(BaseEngine): + """BaseEngine that records extra generate_tokens kwargs and reports thinking metrics.""" + + def __init__(self): + super().__init__() + self.extra = [] + self.reasoning, self.forced = 7, True + self.drop_metrics = False + self.drop_key = None + self.think_ids = (THINK_ID, 8) + self.prompt_ids = None + self.validated = [] + + def prompt(self, messages, tools=None): + return self.prompt_ids or super().prompt(messages, tools) + + def validate_thinking_budget(self, ids, budget): + self.validated.append((list(ids), budget)) + if self.think_ids is None: + raise ValueError("thinking_budget requires and tokenizer tokens") + if ids and ids[-1] == self.think_ids[0]: + raise ValueError("thinking_budget requires a prompt that does not already open ") + + def generate_tokens(self, ids, sampling, *, should_stop, grammar=None, **extra): + self.calls.append((chr(ids[0]), sampling.max_tokens)) + self.grammars.append(grammar) + self.extra.append(extra) + for index in range(sampling.max_tokens): + yield { + "token": ord("x"), + "text": "x", + "finish_reason": "length" if index + 1 == sampling.max_tokens else None, + } + if not self.drop_metrics: + metrics = { + "prompt_tokens": len(ids), + "cached_prompt_tokens": 1, + "generated_tokens": sampling.max_tokens, + "computed_tokens": sampling.max_tokens, + "reasoning_tokens": self.reasoning, + "thinking_forced_close": self.forced, + } + metrics.pop(self.drop_key, None) + yield {"metrics": metrics} + + +class ServerCase(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.engine = ThinkEngine() + self.server = make_server(self.engine, ("127.0.0.1", 0), Path(self.temp.name) / "metrics") + self.thread = threading.Thread( + target=self.server.serve_forever, kwargs={"poll_interval": 0.01} + ) + self.thread.start() + + def tearDown(self): + self.server.generator.cancel() + self.server.shutdown() + self.server.generator.join() + self.server.server_close() + self.thread.join() + self.temp.cleanup() + + def post(self, path, body): + conn = http.client.HTTPConnection(*self.server.server_address, timeout=5) + try: + conn.request("POST", path, json.dumps(body), JSON_HEADERS) + response = conn.getresponse() + return response.status, response.read().decode() + finally: + conn.close() + + def chat(self, **options): + body = {"model": "flint", "messages": [{"role": "user", "content": "BB"}], **options} + return self.post("/v1/chat/completions", body) + + def usage(self, **options): + """The usage object of a non-stream response, or of the final chunk of a stream.""" + status, text = self.chat(max_tokens=2, **options) + self.assertEqual(status, OK, text) + return json.loads(text)["usage"] + + def stream_usage(self, **options): + options["stream_options"] = {"include_usage": True} + status, text = self.chat(max_tokens=2, stream=True, **options) + self.assertEqual(status, OK, text) + chunks = [json.loads(x[6:]) for x in text.splitlines() if x.startswith("data: {")] + self.assertTrue(all("usage" not in chunk for chunk in chunks[:-1])) + return chunks[-1]["usage"] + + +class RequestTest(ServerCase): + def test_invalid_budget_values_are_400_and_never_reach_the_engine(self): + for value in (-1, 1.5, "5", True, False, None, [], {}): + with self.subTest(value=value): + status, text = self.chat(max_tokens=1, thinking_budget=value) + self.assertEqual(status, BAD, text) + self.assertEqual(self.engine.calls, []) + + def test_valid_budgets_reach_the_engine_and_are_not_dropped(self): + with self.assertNoLogs(level="WARNING"): + for value in (0, 1, 4096): + self.assertEqual(self.chat(max_tokens=1, thinking_budget=value)[0], OK) + self.assertEqual(self.engine.extra[-1]["thinking_budget"], value) + + def test_budget_needs_the_flint_grammar(self): + status, text = self.chat(max_tokens=1, grammar="none", thinking_budget=2) + self.assertEqual(status, BAD, text) + self.assertEqual(self.engine.calls, []) + + def test_unusable_prompt_or_tokenizer_is_400_before_any_header(self): + self.engine.prompt_ids = [ord("B"), THINK_ID] # the prompt already opens + for stream in (False, True): + status, text = self.chat(max_tokens=2, thinking_budget=BUDGET, stream=stream) + self.assertEqual((status, "think" in text), (BAD, True), text) + self.engine.prompt_ids, self.engine.think_ids = None, None + for stream in (False, True): + self.assertEqual(self.chat(max_tokens=2, thinking_budget=BUDGET, stream=stream)[0], BAD) + self.assertEqual(self.engine.calls, [], "generation must not start") + + def test_validation_gets_the_prompt_and_only_runs_with_a_budget(self): + self.chat(max_tokens=1, thinking_budget=BUDGET) + self.assertEqual(self.engine.validated, [([ord("B"), ord("B")], BUDGET)]) + self.engine.think_ids = None + status, text = self.chat(max_tokens=1) + self.assertEqual(status, OK, text) + self.assertEqual(len(self.engine.validated), 1) + self.assertNotIn("thinking_budget", self.engine.extra[-1]) + + +class MessagesTest(ServerCase): + def messages(self, **extra): + body = {"model": "flint", "max_tokens": 4, "messages": [{"role": "user", "content": "BB"}]} + return self.post("/v1/messages", {**body, **extra}) + + def test_top_level_thinking_budget_is_rejected_not_dropped(self): + status, text = self.messages(thinking_budget=BUDGET) + self.assertEqual(status, BAD, text) + self.assertIn("thinking_budget", text) + self.assertEqual(self.engine.calls, []) + + def test_anthropic_budget_tokens_is_accepted_and_not_forwarded(self): + status, text = self.messages(thinking={"type": "enabled", "budget_tokens": 1024}) + self.assertEqual(status, OK, text) + self.assertEqual(self.engine.extra, [{}]) + + +class UsageTest(ServerCase): + def test_usage_reports_reasoning_only_when_a_budget_is_set(self): + cases = [ # (options, engine reasoning, engine forced) + ({"thinking_budget": 64}, 7, True), + ({"thinking_budget": 64}, 3, False), + ({"thinking_budget": 0}, 0, False), + ] + for options, reasoning, forced in cases: + self.engine.reasoning, self.engine.forced = reasoning, forced + for read in (self.usage, self.stream_usage): + with self.subTest(read=read.__name__, reasoning=reasoning, forced=forced): + usage = read(**options) + self.assertEqual(set(usage), BASE_USAGE | THINKING_USAGE) + self.assertEqual(usage["reasoning_tokens"], reasoning) + self.assertIs(usage["thinking_forced_close"], forced) + self.assertEqual(usage["completion_tokens"], 2) + self.assertEqual(usage["prompt_tokens_details"]["cached_tokens"], 1) + for read in (self.usage, self.stream_usage): + self.assertEqual(set(read()), BASE_USAGE) + + def test_missing_metrics_fail_cleanly_only_with_a_budget(self): + self.engine.drop_metrics = True + status, text = self.chat(max_tokens=2, thinking_budget=1) + self.assertEqual(status, ERROR, text) + self.assertIn("metrics", text.lower()) + self.assertNotIn("NoneType", text) + status, text = self.chat( + max_tokens=2, stream=True, thinking_budget=1, stream_options={"include_usage": True} + ) + self.assertIn('"error"', text) # the header is sent; the stream carries the failure + self.assertIn("metrics", text.lower()) + self.assertEqual(set(self.usage()), BASE_USAGE - {"prompt_tokens_details"}) + self.engine.drop_metrics = False # the server keeps serving after the error + self.assertEqual(set(self.usage(thinking_budget=1)), BASE_USAGE | THINKING_USAGE) + + def test_a_metrics_event_missing_a_thinking_key_is_a_500_naming_the_key(self): + for key in sorted(THINKING_USAGE): + self.engine.drop_key = key + with self.subTest(key=key): + status, text = self.chat(max_tokens=2, thinking_budget=1) + self.assertEqual(status, ERROR, text) + self.assertIn("usage is unavailable", text) + self.assertIn(key, text) + self.assertEqual(set(self.usage()), BASE_USAGE) # without a budget it still serves + + +if __name__ == "__main__": + unittest.main() From 16e36eceab8c3fa548fcd20eb403def81fe77b94 Mon Sep 17 00:00:00 2001 From: Yan Weng Date: Sat, 24 Oct 2026 16:54:26 -0700 Subject: [PATCH 2/6] tests: auto worker count for parallel engine tests --- engine/tests/test_parallel_workers.py | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 engine/tests/test_parallel_workers.py diff --git a/engine/tests/test_parallel_workers.py b/engine/tests/test_parallel_workers.py new file mode 100644 index 0000000..3e3fc84 --- /dev/null +++ b/engine/tests/test_parallel_workers.py @@ -0,0 +1,33 @@ +import os +import unittest + +import pytest + +from parallel_workers import RESERVED_CORES, WORKERS_ENV, auto_worker_count, available_cores + + +class AutoWorkerCountTest(unittest.TestCase): + def test_leaves_reserved_cores_for_the_desktop(self): + self.assertEqual(auto_worker_count({}, RESERVED_CORES + 6), 6) + + def test_never_drops_below_one_worker(self): + for cores in range(1, RESERVED_CORES + 1): + self.assertEqual(auto_worker_count({}, cores), 1) + + def test_environment_overrides_the_core_count(self): + self.assertEqual(auto_worker_count({WORKERS_ENV: "3"}, 64), 3) + + def test_invalid_override_fails_instead_of_being_ignored(self): + for value in ("many", "0", "-2"): + with self.subTest(value=value), self.assertRaises(ValueError): + auto_worker_count({WORKERS_ENV: value}, 8) + + def test_available_cores_is_positive(self): + self.assertGreaterEqual(available_cores(), 1) + + +def test_xdist_auto_uses_this_worker_count(pytestconfig): + if not pytestconfig.pluginmanager.hasplugin("xdist"): + pytest.skip("pytest-xdist is not installed") + result = pytestconfig.hook.pytest_xdist_auto_num_workers(config=pytestconfig) + assert result == auto_worker_count(os.environ, available_cores()) From 10b2f6462a2b4788795e62ea5747e572706eae9f Mon Sep 17 00:00:00 2001 From: Yan Weng Date: Sat, 24 Oct 2026 16:55:25 -0700 Subject: [PATCH 3/6] Run engine tests in parallel with a desktop-friendly worker count --- CONTRIBUTING.md | 3 +- Makefile | 2 +- engine/pyproject.toml | 3 +- engine/tests/conftest.py | 10 +++++ engine/tests/parallel_workers.py | 44 +++++++++++++++++++++ engine/tests/test_thinking_budget_engine.py | 2 +- engine/tests/test_thinking_budget_server.py | 3 +- 7 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 engine/tests/conftest.py create mode 100644 engine/tests/parallel_workers.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9159c55..24fc5e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,8 @@ brew install go node uv 3. Add or update tests for changed behavior. 4. Update the documentation when you change commands, flags, output, or requirements. -5. Run `make test`. +5. Run `make test`. It runs the engine tests in parallel; set + `PYTEST_XDIST_AUTO_NUM_WORKERS` to choose the worker count. ## Open a pull request diff --git a/Makefile b/Makefile index 5a88593..e82df61 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ PYTHON_ROOT = $(shell uv python dir)/cpython-3.12.12-macos-aarch64-none test-engine: cd engine && uv venv --allow-existing --python 3.12 && uv pip install '.[validation]' - cd engine && .venv/bin/pytest -v -rs + cd engine && nice -n 10 .venv/bin/pytest -v -rs -n auto --dist loadgroup uv python install 3.12.12 rm -rf .build/bundle-mlx .build/smoke $(PYTHON_ROOT)/bin/python3.12 packaging/build-mlx-runtime.py --python-root $(PYTHON_ROOT) --output .build/bundle-mlx diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 59b1dde..7071771 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ ] [project.optional-dependencies] -validation = ["pytest==8.4.2"] +validation = ["pytest==8.4.2", "pytest-xdist==3.8.0"] [project.scripts] rec-mlx = "rec_mlx.cli:main" @@ -26,6 +26,7 @@ packages = ["src/rec_mlx"] [tool.ruff] extend = "../ruff.toml" extend-exclude = ["src/rec_mlx/kernels"] +src = ["src", "tests"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/engine/tests/conftest.py b/engine/tests/conftest.py new file mode 100644 index 0000000..042268c --- /dev/null +++ b/engine/tests/conftest.py @@ -0,0 +1,10 @@ +import os + +import pytest + +from parallel_workers import auto_worker_count, available_cores + + +@pytest.hookimpl(optionalhook=True) +def pytest_xdist_auto_num_workers(config): + return auto_worker_count(os.environ, available_cores()) diff --git a/engine/tests/parallel_workers.py b/engine/tests/parallel_workers.py new file mode 100644 index 0000000..0930559 --- /dev/null +++ b/engine/tests/parallel_workers.py @@ -0,0 +1,44 @@ +"""Worker count for `pytest -n auto` that leaves the desktop responsive.""" + +import os +import subprocess +import sys + +# Cores left for the OS and apps during `pytest -n auto`. +RESERVED_CORES = 2 +# Same variable pytest-xdist reads for its own auto count. +WORKERS_ENV = "PYTEST_XDIST_AUTO_NUM_WORKERS" +MACOS_PERFORMANCE_CORES_KEY = "hw.perflevel0.logicalcpu" +MACOS_LOGICAL_CORES_KEY = "hw.logicalcpu" + + +def _sysctl(key): + result = subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, check=False) + if result.returncode != 0: + return None + return int(result.stdout.strip()) + + +def available_cores(): + if sys.platform == "darwin": + # Efficiency cores are slow for this suite; Intel Macs have no perflevel key. + cores = _sysctl(MACOS_PERFORMANCE_CORES_KEY) + if cores is None: + cores = _sysctl(MACOS_LOGICAL_CORES_KEY) + elif sys.platform.startswith("linux"): + cores = len(os.sched_getaffinity(0)) + else: + cores = os.cpu_count() + if not cores or cores < 1: + raise RuntimeError("cannot determine the number of CPU cores") + return cores + + +def auto_worker_count(environ, cores): + override = environ.get(WORKERS_ENV) + if override is not None: + count = int(override) + if count < 1: + raise ValueError(f"{WORKERS_ENV} must be >= 1, got {override!r}") + return count + return max(1, cores - RESERVED_CORES) diff --git a/engine/tests/test_thinking_budget_engine.py b/engine/tests/test_thinking_budget_engine.py index eac2b8b..5b0e929 100644 --- a/engine/tests/test_thinking_budget_engine.py +++ b/engine/tests/test_thinking_budget_engine.py @@ -7,7 +7,6 @@ import mlx.core as mx import pytest -from test_thinking_budget import CLOSE_ID, THINK_ID, DedicatedTokenizer from tokenizers import Tokenizer, decoders, models, pre_tokenizers from transformers import PreTrainedTokenizerFast @@ -16,6 +15,7 @@ from rec_mlx.grammar import ConstraintCompiler from rec_mlx.grammar_builder import _PROTOCOL_MARKERS from rec_mlx.kernels.sampling import WIDTH, ResidentSampler +from test_thinking_budget import CLOSE_ID, THINK_ID, DedicatedTokenizer VOCAB = 16 EOS = 0 diff --git a/engine/tests/test_thinking_budget_server.py b/engine/tests/test_thinking_budget_server.py index a249e6c..1fe199c 100644 --- a/engine/tests/test_thinking_budget_server.py +++ b/engine/tests/test_thinking_budget_server.py @@ -7,9 +7,8 @@ import unittest from pathlib import Path -from test_serving import Engine as BaseEngine - from rec_mlx.server import make_server +from test_serving import Engine as BaseEngine THINK_ID = 7 BUDGET = 3 From 7296cdf52fd14093554c761960f622228ea12b19 Mon Sep 17 00:00:00 2001 From: Daniel Sali Date: Mon, 26 Oct 2026 09:46:47 +0100 Subject: [PATCH 4/6] support for oh-my-pi (omp) as a harness --- Makefile | 4 +- README.md | 9 +- cli/clients.go | 6 + cli/clients/omp/package-lock.json | 2564 +++++++++++++++++++++++++++++ cli/clients/omp/package.json | 5 + cli/exec.go | 7 +- cli/help.go | 4 +- cli/launch.go | 21 +- cli/omp.go | 151 ++ cli/omp_test.go | 180 ++ tests/launch_client_test.py | 27 +- 11 files changed, 2954 insertions(+), 24 deletions(-) create mode 100644 cli/clients/omp/package-lock.json create mode 100644 cli/clients/omp/package.json create mode 100644 cli/omp.go create mode 100644 cli/omp_test.go diff --git a/Makefile b/Makefile index 5a88593..6eef8ad 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ # The OpenCode client is built for Apple Silicon only. ifeq ($(shell uname -sm),Darwin arm64) test: test-cli test-engine -CLIENTS = opencode pi +CLIENTS = opencode pi omp else test: test-cli -CLIENTS = pi +CLIENTS = pi omp endif test-cli: diff --git a/README.md b/README.md index ab01e5f..57db11d 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,12 @@ terminals. All other data lives under `~/.local/share/rec`. We find the best performance from our models when paired with the [pi](https://pi.dev/) harness. For convenience, rec provides pre-configured -recipes for running pi and [OpenCode](https://opencode.ai) harnesses. Run +recipes for running pi, omp, and [OpenCode](https://opencode.ai) harnesses. Run them using: ```bash rec launch pi # preferred +rec launch omp rec launch opencode ``` @@ -65,7 +66,7 @@ run downloads the model and harness. The model server continues to run after you close the terminal. Run `rec stop` to stop it. These harnesses are kept separate from anything on your system. Use -`rec exec pi|opencode ARGS` to run their own commands, for example: +`rec exec pi|omp|opencode ARGS` to run their own commands, for example: ```bash rec exec pi install npm:PACKAGE @@ -119,8 +120,8 @@ token or as `x-api-key`. The model is `flint`. | Command | What it does | |---|---| -| `rec launch pi\|opencode` | Start the server if needed and open a harness | -| `rec exec pi\|opencode ARGS` | Run a harness command, such as installing a plugin | +| `rec launch pi\|omp\|opencode` | Start the server if needed and open a harness | +| `rec exec pi\|omp\|opencode ARGS` | Run a harness command, such as installing a plugin | | `rec serve` | Start the model server in this terminal | | `rec status` | Show whether the server is running | | `rec stop` | Stop the server | diff --git a/cli/clients.go b/cli/clients.go index 5c01be7..4ef8b3b 100644 --- a/cli/clients.go +++ b/cli/clients.go @@ -14,6 +14,12 @@ var openCodeLogo embed.FS //go:embed clients/pi/package.json var piPackage []byte +//go:embed clients/omp/package.json +var ompPackage []byte + +//go:embed clients/omp/package-lock.json +var ompLock []byte + //go:embed clients/pi/package-lock.json var piLock []byte diff --git a/cli/clients/omp/package-lock.json b/cli/clients/omp/package-lock.json new file mode 100644 index 0000000..fe3d293 --- /dev/null +++ b/cli/clients/omp/package-lock.json @@ -0,0 +1,2564 @@ +{ + "name": "omp", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@oh-my-pi/pi-coding-agent": "18.12.6" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.2.0.tgz", + "integrity": "sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@huggingface/transformers": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.1.tgz", + "integrity": "sha512-5+voSeUCFK6PCKlY15dPhNOKq3/zOTxDN7oC+Wy9dFb1QZ0TSQomZRRy7URRK8YRuUoE9fGMQTTPC5d/pWBJhA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@huggingface/jinja": "^0.5.10", + "@huggingface/tokenizers": "^0.2.0", + "onnxruntime-node": "1.30.0", + "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0", + "sharp": "^0.35.4" + } + }, + "node_modules/@huggingface/transformers/node_modules/global-agent": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.6.tgz", + "integrity": "sha512-V9u791atI/bS7RryfRTK2oixDWo6/RCrIikjwuQ4kADPRZZml1C6LUJXc7y6qRhaAF8geo3bIvE/siAGJCw38A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/@huggingface/transformers/node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.30.0.tgz", + "integrity": "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==", + "license": "MIT", + "optional": true + }, + "node_modules/@huggingface/transformers/node_modules/onnxruntime-node": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.30.0.tgz", + "integrity": "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.6.0", + "global-agent": "^4.1.3", + "onnxruntime-common": "1.30.0" + } + }, + "node_modules/@huggingface/transformers/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@huggingface/transformers/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oh-my-pi/omp-stats": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/omp-stats/-/omp-stats-18.12.6.tgz", + "integrity": "sha512-BvRe/++D5UOTSkxLc2DWYVnrFzkEk0wIxUHKIGQcirO0ovFqUbEr25lbqE0vnP1KhU5NW24MzvDmBBkAr8bUbA==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "lucide-react": "^1.24.0", + "react": "19.2.7", + "react-dom": "19.2.7" + }, + "bin": { + "omp-stats": "src/index.ts" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/omptype": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/omptype/-/omptype-18.12.6.tgz", + "integrity": "sha512-s0KVs/0RrBwS01TGZKm30R10npWG+WWnr2uzs2zqpX//cLucHus7B3VXeZ1dCwVUggmnFofgS3dKo2oUrLMVog==", + "license": "MIT", + "engines": { + "bun": ">=1.3.14", + "node": ">=20" + } + }, + "node_modules/@oh-my-pi/pi-agent-core": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-agent-core/-/pi-agent-core-18.12.6.tgz", + "integrity": "sha512-BS85MS7yDPXC3ooSWMKGFZIFywECGfZHaeTmx56iV9ZBaJEw4f3jAEteoOo6imWTY9klZZlmoi79e2N9Um4ANw==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "@oh-my-pi/pi-wire": "18.12.6", + "@oh-my-pi/snapcompact": "18.12.6", + "@opentelemetry/api": "^1.9.1" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-ai": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-ai/-/pi-ai-18.12.6.tgz", + "integrity": "sha512-Ld54zgvMLYPONS1aqj4dIyhwFsNBvjP7vLosP2HoKNfW4q2lTObdGcHFsshoA7TtgVVgUh0R8zjsmoET1AX3JQ==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/omptype": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "@oh-my-pi/pi-wire": "18.12.6" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-catalog": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-catalog/-/pi-catalog-18.12.6.tgz", + "integrity": "sha512-3f6K8AgDTHGoGDkxUB2+NkSc+HTF4NA5b2EWjqbObxiZQTJtBK58DBMH78anRSptdgAt/xH5a4TOLo4dyK9sWQ==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/omptype": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-coding-agent": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-coding-agent/-/pi-coding-agent-18.12.6.tgz", + "integrity": "sha512-Wo+FcJ7A3RY/OFuafj4hcdIAXc/W7NMxbanwQh3Cmy8s57+ihLOLgAv+HROxHBkssEMg0MaScwkQ9xwqLPiC/g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@oh-my-pi/omp-stats": "18.12.6", + "@oh-my-pi/omptype": "18.12.6", + "@oh-my-pi/pi-agent-core": "18.12.6", + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-mnemopi": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-tui": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "@oh-my-pi/pi-wire": "18.12.6", + "@oh-my-pi/snapcompact": "18.12.6", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-logs": "^0.220.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@opentelemetry/sdk-trace-node": "^2.9.0", + "axe-core": "4.13.0", + "puppeteer-core": "25.3.0" + }, + "bin": { + "omp": "dist/cli.js" + }, + "engines": { + "bun": ">=1.3.14" + }, + "optionalDependencies": { + "@huggingface/transformers": "^4.2.0", + "sherpa-onnx-node": "1.13.2" + } + }, + "node_modules/@oh-my-pi/pi-mnemopi": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-mnemopi/-/pi-mnemopi-18.12.6.tgz", + "integrity": "sha512-DFwZbuo5uWz2K2qZ4jGaUgAwg6o6rDPcd4RrbRVbAsVLx8bKpKGF2XRBHTUO3ddtkYt/9KMa7fnKZGcnny+ewQ==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6" + }, + "bin": { + "mnemopi": "src/cli.ts" + }, + "engines": { + "bun": ">=1.3.14" + }, + "peerDependencies": { + "fastembed": "3.0.0", + "onnxruntime-node": "1.21.0" + }, + "peerDependenciesMeta": { + "fastembed": { + "optional": true + }, + "onnxruntime-node": { + "optional": true + } + } + }, + "node_modules/@oh-my-pi/pi-natives": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives/-/pi-natives-18.12.6.tgz", + "integrity": "sha512-+Wkya/qqxnH2PlukMunxovOb1brrY+A/kIl0dgU1oFkYa6/4C1GsPrc5TIpR04MvJWZb6aH9uR5DmxlmTsR0QA==", + "license": "MIT", + "engines": { + "bun": ">=1.3.14" + }, + "optionalDependencies": { + "@oh-my-pi/pi-natives-darwin-arm64": "18.12.6", + "@oh-my-pi/pi-natives-darwin-x64": "18.12.6", + "@oh-my-pi/pi-natives-linux-arm64": "18.12.6", + "@oh-my-pi/pi-natives-linux-x64": "18.12.6", + "@oh-my-pi/pi-natives-win32-arm64": "18.12.6", + "@oh-my-pi/pi-natives-win32-x64": "18.12.6" + } + }, + "node_modules/@oh-my-pi/pi-natives-darwin-arm64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-arm64/-/pi-natives-darwin-arm64-18.12.6.tgz", + "integrity": "sha512-sslGAG+6Zi9o/AA9RF1zkqb4hKGv+l3ccFB/xy4v9d83Y+GUeHn53y7HzBUVIDfX53wWTJBBeDu2sBWBjZvl+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-natives-darwin-x64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-x64/-/pi-natives-darwin-x64-18.12.6.tgz", + "integrity": "sha512-GjPk+rerUu7uKgEn37TVVYVUQ799uptvTQUHXJVik1KCtWadob90fEmMiSl7Fnu9xm4eJo1bxe9QBKHMWqE+HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-natives-linux-arm64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-arm64/-/pi-natives-linux-arm64-18.12.6.tgz", + "integrity": "sha512-5T06xr0GZLiC6GQMoqaInJDOk7YbciBpR4K6eCvLlg07KAo7tDD29B0BTp0iSEMxXYcav8H/rLqV3K1IaR+ReQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-natives-linux-x64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-x64/-/pi-natives-linux-x64-18.12.6.tgz", + "integrity": "sha512-Dr03a4R2sU03/Qn5I9JgY8c3dnt7O2JNeV8FaFEJsiAlp8DKgfKfrsbVHDk4kqYgiNVfD4Zbyr20e3eFra6rSg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-natives-win32-arm64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-arm64/-/pi-natives-win32-arm64-18.12.6.tgz", + "integrity": "sha512-u/S6NUS3t0Na+SeSpD+/vCArWI6mD2EaA9WoT4b008c/exjitqybDsvUcda8F+vW50hWRzcFOONji2rAVbN2uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-natives-win32-x64": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-x64/-/pi-natives-win32-x64-18.12.6.tgz", + "integrity": "sha512-i7ggXk1MD/toSvkdBImtr15pEkIwL/+h6MjVNVR9b5zfJJv9vBEpGLqgKymV/FisI2K1U8rb3EvSDkSgiD4y9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-tui": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-tui/-/pi-tui-18.12.6.tgz", + "integrity": "sha512-3jW1LDk6PcmfktGop6Q2+NJBtvMaBgwLmTBKYb2mKFMOTubpnhDs50djKp/zdc2UIEYcEck6Yh83gZA+H8qrdg==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/omptype": "18.12.6", + "@oh-my-pi/pi-agent-core": "18.12.6", + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "@oh-my-pi/pi-wire": "18.12.6", + "@oh-my-pi/snapcompact": "18.12.6" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-utils": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-utils/-/pi-utils-18.12.6.tgz", + "integrity": "sha512-+dX5BgX3p50HoeuItmXwiOkcX8EbLMfvObhfjYBYTUCB5qznAxkbKMVu2ZjM0MRx44yzXuYAG8Okk7OOcvreKw==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/pi-natives": "18.12.6" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/pi-wire": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-wire/-/pi-wire-18.12.6.tgz", + "integrity": "sha512-NPREkkLEUqbcqJ5T/NOg2xF3d5wYSE92CWoEwxMvbyBerdyU0JhZKFgciiAiHTg5F/cH9oYJNr3Dm8MRVrpDWA==", + "license": "MIT", + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@oh-my-pi/snapcompact": { + "version": "18.12.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/snapcompact/-/snapcompact-18.12.6.tgz", + "integrity": "sha512-w34sYdM5vw6Pilqf5krZZgcFVpLnfaYsLT2yA/G30j+YNlg8oB6WxmShmY8LqNFlxbP373Fet8bLkuqRdHuBCQ==", + "license": "MIT", + "dependencies": { + "@oh-my-pi/pi-ai": "18.12.6", + "@oh-my-pi/pi-catalog": "18.12.6", + "@oh-my-pi/pi-natives": "18.12.6", + "@oh-my-pi/pi-utils": "18.12.6", + "@oh-my-pi/pi-wire": "18.12.6" + }, + "engines": { + "bun": ">=1.3.14" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.12.0.tgz", + "integrity": "sha512-iK/1nD7mmZVkuYjVjkdFBiCvzntJdFny1S3CYbUYyOq7V5enL6XX+gJ7eh965L1LnS11VA8mQ7Ej/ck04SB0MQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.220.0.tgz", + "integrity": "sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", + "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.220.0.tgz", + "integrity": "sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz", + "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.0.tgz", + "integrity": "sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.0.tgz", + "integrity": "sha512-mVMaEHwCUJuY3S4Esn+nT0yQA3qCEGh+SOPJsXygU0Gl453WcsDNjtEa8EfdewMwoLyr1tlofB4fXytVpiXuow==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.0.tgz", + "integrity": "sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-trace": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", + "integrity": "sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.12.0.tgz", + "integrity": "sha512-9DufH1IlmQMoKAfot5y/FhTP1RnmhLb5K6eKmJO/bvzPjYwPAazoiAUbTZFdPbqxOXN0FEbhZx/e6SB1Og4jmg==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.12.0", + "@opentelemetry/core": "2.12.0", + "@opentelemetry/sdk-trace-base": "2.12.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz", + "integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==", + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/adm-zip": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/devtools-protocol": { + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/lucide-react": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz", + "integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", + "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "peer": true, + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.31.0-dev.20260914-8d85527a0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.31.0-dev.20260914-8d85527a0.tgz", + "integrity": "sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.31.0-dev.20260911-2a43ec07e", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.31.0-dev.20260911-2a43ec07e", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.0-dev.20260911-2a43ec07e.tgz", + "integrity": "sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw==", + "license": "MIT", + "optional": true + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT", + "optional": true + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "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.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz", + "integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.6", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sherpa-onnx-darwin-arm64": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.13.8.tgz", + "integrity": "sha512-FPNgJMgnWVl/KhRTIhG3KL3A4Om63Rn4YKXc9/uHY7SzLcvqLJLc/h7UBWJwduXvv7K18t5NpxHR6XgXn4sjWw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sherpa-onnx-darwin-x64": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.13.8.tgz", + "integrity": "sha512-7BLRpjM6w4f9W46/nmkmq8lEKUayhebvcpslCVQ+6QN2uReYlZEMDZlSpXMjme+hUFrPfRz8P3UNq8ep/4d19g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sherpa-onnx-linux-arm64": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.13.8.tgz", + "integrity": "sha512-Tlg7a70b/Wge3OF8IgTHF9jhSVCsLyKQKhwc4BsJ5A+dL/SrFtGBjzuHp4XeLhiiOT7afCxX5PdSn/D4c8Lnuw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sherpa-onnx-linux-x64": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.13.8.tgz", + "integrity": "sha512-6plnhjagsSeTntCgnlag86hWbs/uZE9Crms1LgOb68/1nKsIQjMd+WG519m+aPwT6TrsBOiEMzrx41t8sL5L5g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sherpa-onnx-node": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.13.2.tgz", + "integrity": "sha512-uIH6SA5Or4pb8HlCYWB3K54XkMtzdef4/tkw1amtIf8GB1tt6hQLpur9p2jSFNfTYRyzZ8XrXofxefXQ0A7EUA==", + "license": "Apache-2.0", + "optional": true, + "optionalDependencies": { + "sherpa-onnx-darwin-arm64": "^1.13.2", + "sherpa-onnx-darwin-x64": "^1.13.2", + "sherpa-onnx-linux-arm64": "^1.13.2", + "sherpa-onnx-linux-x64": "^1.13.2", + "sherpa-onnx-win-ia32": "^1.13.2", + "sherpa-onnx-win-x64": "^1.13.2" + } + }, + "node_modules/sherpa-onnx-win-ia32": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.13.8.tgz", + "integrity": "sha512-H0Ojln9hfvM+pxWqW0cnjB/XK8/JPq4/k0IWu2JpY5Z458M3zv+XC3Hu+wmot3AoH/K1Bgt23n/tTKtPV9x8qg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/sherpa-onnx-win-x64": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.13.8.tgz", + "integrity": "sha512-oZF1c9VPOKtMwn83Bboc5XSWL+76BRoyB3eUuVnCknBKxwSULZU2Foia9VHWzU+n4I12rPsP6z6H9Rp1hD9o8g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "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 + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/cli/clients/omp/package.json b/cli/clients/omp/package.json new file mode 100644 index 0000000..0a629ee --- /dev/null +++ b/cli/clients/omp/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@oh-my-pi/pi-coding-agent": "18.12.6" + } +} diff --git a/cli/exec.go b/cli/exec.go index 58242a0..1b15fb4 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -15,8 +15,8 @@ func execClient(p Paths, args []string) error { fmt.Println("the server or the Flint provider. Arguments go to the harness unchanged.") return nil } - if args[0] != "opencode" && args[0] != "pi" { - return fmt.Errorf("unsupported harness %q; use rec exec pi or rec exec opencode", args[0]) + if args[0] != "opencode" && args[0] != "pi" && args[0] != "omp" { + return fmt.Errorf("unsupported harness %q; use rec exec pi, rec exec omp, or rec exec opencode", args[0]) } rest := args[1:] if args[0] == "opencode" && len(rest) > 0 && (rest[0] == "upgrade" || rest[0] == "uninstall") { @@ -31,6 +31,9 @@ func execClient(p Paths, args []string) error { return err } h := piUser(user) + if args[0] == "omp" { + h = ompUser(user) + } if args[0] == "opencode" { h = openCodeUser(user) } diff --git a/cli/help.go b/cli/help.go index 2bc4474..5524f8a 100644 --- a/cli/help.go +++ b/cli/help.go @@ -20,10 +20,10 @@ var developmentOptions = [][2]string{ } var commands = []commandHelp{ - {"launch", "pi|opencode", "Start the server if needed and open a harness", [][2]string{ + {"launch", "pi|omp|opencode", "Start the server if needed and open a harness", [][2]string{ {"--args", "Print the harness command instead of running it; needs a running server"}, }}, - {"exec", "pi|opencode ARGS", "Run a harness command, such as installing a plugin", nil}, + {"exec", "pi|omp|opencode ARGS", "Run a harness command, such as installing a plugin", nil}, {"serve", "[MODEL]", "Start the model server in this terminal", append([][2]string{ {"--context TOKENS", "Total input and output tokens (default: set by the release)"}, {"--port PORT", "Local port (default: 11500)"}, diff --git a/cli/launch.go b/cli/launch.go index 57b6e59..b19b1a3 100644 --- a/cli/launch.go +++ b/cli/launch.go @@ -29,11 +29,11 @@ func launch(p Paths, args []string) error { printUsage(os.Stdout, "launch") fmt.Println("\nArguments after -- go to the harness. Reuses a running server, which keeps") fmt.Println("running after the harness exits; run rec stop to stop it. Pi requires Node.js") - fmt.Println("and npm. The first launch installs the pinned harness.") + fmt.Println("and npm; omp also requires Bun. The first launch installs the pinned harness.") return nil } - if args[0] != "opencode" && args[0] != "pi" { - return fmt.Errorf("unsupported harness %q; use rec launch pi or rec launch opencode", args[0]) + if args[0] != "opencode" && args[0] != "pi" && args[0] != "omp" { + return fmt.Errorf("unsupported harness %q; use rec launch pi, rec launch omp, or rec launch opencode", args[0]) } rest := args[1:] if len(rest) > 0 && rest[0] == "--" { @@ -59,7 +59,7 @@ func launch(p Paths, args []string) error { if err != nil { return err } - openingClient(map[string]string{"pi": "Pi", "opencode": "OpenCode"}[args[0]], s) + openingClient(map[string]string{"pi": "Pi", "omp": "omp", "opencode": "OpenCode"}[args[0]], s) return syscall.Exec(h.argv[0], h.argv, h.environ(os.Environ())) } @@ -87,6 +87,16 @@ func harnessCommand(p Paths, name string, s Status, env []string, key string, re if err != nil { return harness{}, err } + if name == "omp" { + config, err := ompConfig(p, s, user) + if err != nil { + return harness{}, err + } + h := ompUser(user) + h.argv = append([]string{ompExecutable(p), "--config", config, "--model", "rec/flint"}, rest...) + h.set = append(h.set, [2]string{"REC_API_KEY", key}) + return h, nil + } if name == "pi" { extension, config, err := piConfig(p, s) if err != nil { @@ -389,6 +399,9 @@ func installClient(p Paths, name string) (string, error) { if name == "pi" { return installPi(p, os.Stderr) } + if name == "omp" { + return installOmp(p, os.Stderr) + } pin, err := loadOpenCodePin() if err != nil { return "", err diff --git a/cli/omp.go b/cli/omp.go new file mode 100644 index 0000000..32ab1e6 --- /dev/null +++ b/cli/omp.go @@ -0,0 +1,151 @@ +//go:build darwin || linux + +package main + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" +) + +func ompInstallDir(p Paths) string { + sum := sha256.Sum256(append(append([]byte{}, ompPackage...), ompLock...)) + return filepath.Join(p.Data, "clients", "omp", "npm-"+hex.EncodeToString(sum[:8])) +} + +func ompExecutable(p Paths) string { + return filepath.Join(ompInstallDir(p), "node_modules", ".bin", "omp") +} + +func installOmp(p Paths, out io.Writer) (string, error) { + dir := ompInstallDir(p) + parent := filepath.Dir(dir) + executable := ompExecutable(p) + if _, err := os.Stat(executable); err == nil { + return executable, nil + } + if _, err := exec.LookPath("bun"); err != nil { + return "", fmt.Errorf("rec launch omp requires Bun; install Bun, then retry") + } + npm, err := exec.LookPath("npm") + if err != nil { + return "", fmt.Errorf("rec launch omp requires Node.js/npm; install Node.js, then retry") + } + if err := os.MkdirAll(parent, 0700); err != nil { + return "", err + } + l, err := lock(filepath.Join(p.State, "omp-install.lock")) + if err != nil { + return "", fmt.Errorf("another launcher is installing omp; retry when it finishes") + } + defer unlock(l) + if _, err := os.Stat(executable); err == nil { + return executable, nil + } + // Remove staging left by an interrupted install. Earlier pins are kept, + // since a running omp may still load modules from them. + leftovers, err := filepath.Glob(filepath.Join(parent, ".install-*")) + if err != nil { + return "", err + } + for _, path := range leftovers { + if err := os.RemoveAll(path); err != nil { + return "", err + } + } + staging, err := os.MkdirTemp(parent, ".install-") + if err != nil { + return "", err + } + defer os.RemoveAll(staging) + if err := os.WriteFile(filepath.Join(staging, "package.json"), ompPackage, 0600); err != nil { + return "", err + } + if err := os.WriteFile(filepath.Join(staging, "package-lock.json"), ompLock, 0600); err != nil { + return "", err + } + installing := startActivity(out, "Installing omp", "Installed omp") + cmd := exec.Command(npm, "ci", "--no-audit", "--no-fund", "--loglevel=error") + cmd.Dir = staging + if output, err := cmd.CombinedOutput(); err != nil { + installing.fail() + _, _ = out.Write(output) + return "", fmt.Errorf("installing omp with npm ci: %w", err) + } + installing.succeed() + if err := os.Rename(staging, dir); err != nil { + return "", err + } + return executable, nil +} + +func ompConfig(p Paths, s Status, user string) (string, error) { + if err := validateClientStatus(s); err != nil { + return "", err + } + dir := filepath.Join(p.Data, "clients", "omp", Version) + if err := os.MkdirAll(dir, 0700); err != nil { + return "", err + } + if err := os.MkdirAll(user, 0700); err != nil { + return "", err + } + models := fmt.Sprintf(`providers: + rec: + baseUrl: %s + apiKey: REC_API_KEY + api: openai-completions + models: + - id: flint + name: Flint + reasoning: false + input: [text] + contextWindow: %d + maxTokens: %d + compat: + supportsStore: false + supportsDeveloperRole: false + supportsReasoningEffort: false + maxTokensField: max_tokens +`, s.Endpoint, s.Context, s.Context) + if err := atomicWrite(filepath.Join(user, "models.yml"), []byte(models), 0600); err != nil { + return "", err + } + settings := `modelRoles: + default: rec/flint + smol: rec/flint + slow: rec/flint + plan: rec/flint + task: rec/flint + advisor: rec/flint + tiny: rec/flint +retry: + modelFallback: false + fallbackChains: + default: [] + smol: [] + slow: [] + plan: [] + task: [] + advisor: [] + tiny: [] +enabledProviders: + - rec +` + path := filepath.Join(dir, "rec-omp.yml") + if err := atomicWrite(path, []byte(settings), 0600); err != nil { + return "", err + } + return path, nil +} + +func ompUser(user string) harness { + return harness{ + unset: []string{"PI_CODING_AGENT_SESSION_DIR"}, + set: [][2]string{{"PI_CODING_AGENT_DIR", user}}, + } +} diff --git a/cli/omp_test.go b/cli/omp_test.go new file mode 100644 index 0000000..75fbd48 --- /dev/null +++ b/cli/omp_test.go @@ -0,0 +1,180 @@ +//go:build darwin || linux + +package main + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +func TestOmpConfig(t *testing.T) { + p := Paths{Data: t.TempDir()} + user := filepath.Join(p.Data, "clients", "omp", "user") + s := Status{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"} + path, err := ompConfig(p, s, user) + if err != nil { + t.Fatal(err) + } + if path != ompConfigPath(p) { + t.Fatalf("wrong overlay path: %s", path) + } + settings, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + models, err := os.ReadFile(filepath.Join(user, "models.yml")) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "default: rec/flint", + "modelFallback: false", + "enabledProviders:\n - rec", + } { + if !strings.Contains(string(settings), want) { + t.Fatalf("omp settings missing %q:\n%s", want, settings) + } + } + for _, want := range []string{ + "baseUrl: http://127.0.0.1:11500/v1", + "apiKey: REC_API_KEY", + "api: openai-completions", + "contextWindow: 8192", + "maxTokens: 8192", + "maxTokensField: max_tokens", + } { + if !strings.Contains(string(models), want) { + t.Fatalf("omp models.yml missing %q:\n%s", want, models) + } + } + if strings.Contains(string(settings)+string(models), "openrouter") { + t.Fatal("omp config must not reference outside providers") + } +} + +func TestOmpEnvUsesRecUserDirectory(t *testing.T) { + p := Paths{Data: t.TempDir()} + h, err := harnessCommand(p, "omp", Status{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"}, nil, "secret", []string{"-p", "hi"}) + if err != nil { + t.Fatal(err) + } + if got := h.argv[0]; got != ompExecutable(p) { + t.Fatalf("wrong omp executable: %s", got) + } + if !reflect.DeepEqual(h.argv[1:4], []string{"--config", ompConfigPath(p), "--model"}) { + t.Fatalf("wrong omp argv prefix: %q", h.argv) + } + if h.argv[4] != "rec/flint" || !reflect.DeepEqual(h.argv[5:], []string{"-p", "hi"}) { + t.Fatalf("wrong omp model/args: %q", h.argv) + } + got := h.environ([]string{"PATH=/bin", "REC_API_KEY=old", "PI_CODING_AGENT_DIR=/custom", "PI_CODING_AGENT_SESSION_DIR=/sessions"}) + want := []string{"PATH=/bin", "PI_CODING_AGENT_DIR=" + filepath.Join(p.Data, "clients", "omp", "user"), "REC_API_KEY=secret"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("incorrect environment merge: %q", got) + } +} + +func ompConfigPath(p Paths) string { + return filepath.Join(p.Data, "clients", "omp", Version, "rec-omp.yml") +} + +func TestOmpPinIsLocked(t *testing.T) { + var pkg struct{ Dependencies map[string]string } + var lock struct { + Packages map[string]struct { + Version string + Dependencies map[string]string + } + } + if err := json.Unmarshal(ompPackage, &pkg); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(ompLock, &lock); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(lock.Packages[""].Dependencies, pkg.Dependencies) { + t.Fatal("package-lock.json is stale; run npm install --package-lock-only in cli/clients/omp") + } + for name, version := range pkg.Dependencies { + if lock.Packages["node_modules/"+name].Version != version { + t.Fatalf("%s must be pinned to an exact locked version", name) + } + } +} + +func TestInstallOmp(t *testing.T) { + bin := t.TempDir() + calls := filepath.Join(t.TempDir(), "calls") + script := "#!/bin/sh\necho \"$*\" >> " + calls + "\n" + + "cmp -s package.json \"$OMP_PACKAGE\" && cmp -s package-lock.json \"$OMP_LOCK\" || exit 3\n" + + "[ -z \"$FAIL\" ] || exit 4\n" + + "mkdir -p node_modules/.bin && touch node_modules/.bin/omp\n" + for _, name := range []string{"npm", "bun"} { + if err := os.WriteFile(filepath.Join(bin, name), []byte(script), 0700); err != nil { + t.Fatal(err) + } + } + fixtures := t.TempDir() + for name, data := range map[string][]byte{"OMP_PACKAGE": ompPackage, "OMP_LOCK": ompLock} { + path := filepath.Join(fixtures, name) + if err := os.WriteFile(path, data, 0600); err != nil { + t.Fatal(err) + } + t.Setenv(name, path) + } + t.Setenv("PATH", bin+":/usr/bin:/bin") + p := dataPaths(t.TempDir()) + if err := os.MkdirAll(p.State, 0700); err != nil { + t.Fatal(err) + } + parent := filepath.Join(p.Data, "clients", "omp") + earlier := filepath.Join(parent, "npm-0000000000000000") + interrupted := filepath.Join(parent, ".install-interrupted") + for _, dir := range []string{earlier, interrupted} { + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } + } + + t.Setenv("FAIL", "1") + if _, err := installOmp(p, io.Discard); err == nil { + t.Fatal("failed npm ci was accepted") + } + entries, err := os.ReadDir(parent) + if err != nil || len(entries) != 1 || entries[0].Name() != filepath.Base(earlier) { + t.Fatalf("want only the earlier install after a failed install, got %v %v", entries, err) + } + + t.Setenv("FAIL", "") + first, err := installOmp(p, io.Discard) + if err != nil { + t.Fatal(err) + } + second, err := installOmp(p, io.Discard) + if err != nil || second != first { + t.Fatalf("reinstall: %v %s %s", err, first, second) + } + if _, err := os.Stat(earlier); err != nil { + t.Fatal("an earlier install may still be running and must be kept") + } + data, err := os.ReadFile(calls) + if err != nil || string(data) != "ci --no-audit --no-fund --loglevel=error\nci --no-audit --no-fund --loglevel=error\n" { + t.Fatalf("want one failed and one successful npm ci, got %q", data) + } +} + +func TestInstallOmpRequiresBun(t *testing.T) { + t.Setenv("PATH", t.TempDir()+":/usr/bin:/bin") + p := dataPaths(t.TempDir()) + if err := os.MkdirAll(p.State, 0700); err != nil { + t.Fatal(err) + } + if _, err := installOmp(p, io.Discard); err == nil || !strings.Contains(err.Error(), "Bun") { + t.Fatalf("want a Bun requirement error, got %v", err) + } +} diff --git a/tests/launch_client_test.py b/tests/launch_client_test.py index 55c62bf..c2673c1 100644 --- a/tests/launch_client_test.py +++ b/tests/launch_client_test.py @@ -12,8 +12,8 @@ import time binary = str(pathlib.Path(sys.argv[1]).resolve()) -clients = sys.argv[2:] or ["opencode", "pi"] -assert all(client in ("opencode", "pi") for client in clients) +clients = sys.argv[2:] or ["opencode", "pi", "omp"] +assert all(client in ("opencode", "pi", "omp") for client in clients) class Quiet(http.server.BaseHTTPRequestHandler): @@ -176,10 +176,12 @@ def do_GET(self): (extensions / "legacy.mjs").write_text( "export default function(pi) { pi.on('before_provider_request', (e) => ({...e.payload, temperature:0.1, top_p:0.95, top_k:20, max_tokens:128})); }\n" ) - client_args = ( - ["run", "--format", "json", "Answer with 42."] - if client == "opencode" - else [ + if client == "opencode": + client_args = ["run", "--format", "json", "Answer with 42."] + elif client == "omp": + client_args = ["--no-skills", "--mode", "json", "-p", "Answer with 42."] + else: + client_args = [ "--no-skills", "--no-context-files", "--no-prompt-templates", @@ -190,7 +192,6 @@ def do_GET(self): "-p", "Answer with 42.", ] - ) try: r = subprocess.run( [binary, "launch", client, "--", *client_args], @@ -226,8 +227,9 @@ def do_GET(self): ) assert r.returncode == 0, (r.returncode, r.stderr[-2000:], r.stdout[-2000:]) assert len(requests) == 2, "rec launch --args must reproduce the launched turn" - assert auth.read_bytes() == original_auth, "Pi auth.json was modified" - assert models.read_bytes() == original_models, "Pi models.json was modified" + if client == "pi": + assert auth.read_bytes() == original_auth, "Pi auth.json was modified" + assert models.read_bytes() == original_models, "Pi models.json was modified" assert (root / "data/rec/clients" / client / "user").is_dir(), "No rec user directory" assert not (root / "data/opencode").exists(), "OpenCode used the system data directory" for path, auth, body in requests: @@ -237,7 +239,12 @@ def do_GET(self): actual = tuple(body.get(k) for k in ("temperature", "top_p", "top_k")) + ( body.get("max_tokens", body.get("max_completion_tokens")), ) - assert actual == want, (actual, want) + if client == "omp": + # omp reserves headroom for its system prompt and tools. + assert actual[:3] == want[:3], (actual, want) + assert 0 < actual[3] <= want[3], (actual, want) + else: + assert actual == want, (actual, want) print( json.dumps( { From 864546c09a56aec3ad6e9cae03753628172fbe1e Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Mon, 26 Oct 2026 07:41:33 -0400 Subject: [PATCH 5/6] Bump mlx to 0.32.4 and nanobind to 3.1.0 --- engine/pyproject.toml | 4 ++-- engine/src/rec_mlx/kernels/masked_sampling.py | 3 ++- engine/tests/validate_resident.py | 12 ++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 59b1dde..bfccff4 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling>=1.27,<2", "mlx==0.32.0", "nanobind==2.13.0"] +requires = ["hatchling>=1.27,<2", "mlx==0.32.4", "nanobind==3.1.0"] build-backend = "hatchling.build" [project] @@ -8,7 +8,7 @@ version = "0.1.0" description = "Flint inference engine for Apple Silicon" requires-python = ">=3.12" dependencies = [ - "mlx==0.32.0", + "mlx==0.32.4", "mlx-lm==0.31.3", "transformers==5.16.1", "xgrammar==0.2.1", diff --git a/engine/src/rec_mlx/kernels/masked_sampling.py b/engine/src/rec_mlx/kernels/masked_sampling.py index daa9370..d1aca47 100644 --- a/engine/src/rec_mlx/kernels/masked_sampling.py +++ b/engine/src/rec_mlx/kernels/masked_sampling.py @@ -205,7 +205,8 @@ def block( if logprobs is not None: logprobs.extend(out[7 if presence_penalty else 6][:produced].tolist()) # Restore the exact scalar sampler's key when EOS truncates the packet. - mx.random.state[0] = out[5][produced - 1] + hi, lo = out[5][produced - 1].tolist() + mx.random.seed((int(hi) << 32) | int(lo)) if presence_penalty: state["presence_mask"] = out[6] state["position"] += produced diff --git a/engine/tests/validate_resident.py b/engine/tests/validate_resident.py index bfed5c5..e154325 100644 --- a/engine/tests/validate_resident.py +++ b/engine/tests/validate_resident.py @@ -58,7 +58,8 @@ def pick(logits, index): for token in tokens[:begin]: reference.forward_token(token) candidate.forward_token(token) - mx.random.state[0] = keys[begin] + hi, lo = keys[begin].tolist() + mx.random.seed((int(hi) << 32) | int(lo)) expected = [ pick(reference.forward_token(tokens[begin + i]), begin + i + 1) for i in range(stop) @@ -67,7 +68,8 @@ def pick(logits, index): model.eos_tokens = [expected[-1]] # Each compiled graph must capture this fixture's EOS, not a previous one. resident.filtered = FilteredResidentSampler(resident.module, engine, specification) - mx.random.state[0] = keys[begin] + hi, lo = keys[begin].tolist() + mx.random.seed((int(hi) << 32) | int(lo)) actual = resident.block(candidate._state, tokens[begin], 8, 43 + begin, **settings) assert actual == expected and len(actual) == stop, "EOS truncation mismatch" assert mx.array_equal(mx.random.state[0], expected_key).item(), "EOS RNG mismatch" @@ -227,7 +229,8 @@ def choose(logits, index): count = min((1, 3, 8)[packets % 3], steps - position) packet_input = pending expected, logits, traces = [], [], [] - mx.random.state[0] = scalar_key + hi, lo = scalar_key.tolist() + mx.random.seed((int(hi) << 32) | int(lo)) for offset in range(count): value, trace = engine.forward(reference._state, pending) if temperature == 0 and position + offset < 8: @@ -252,7 +255,8 @@ def choose(logits, index): if pending in model.eos_tokens: break scalar_key = mx.random.state[0] - mx.random.state[0] = packet_key + hi, lo = packet_key.tolist() + mx.random.seed((int(hi) << 32) | int(lo)) actual = resident.block( candidate._state, packet_input, From 944f2d28707913ba3fb0748e24e262383f78f8e1 Mon Sep 17 00:00:00 2001 From: keithsiilats Date: Wed, 28 Oct 2026 14:54:57 -0400 Subject: [PATCH 6/6] Update thinking-budget test runtime for current device profiles --- engine/tests/test_thinking_budget_engine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/tests/test_thinking_budget_engine.py b/engine/tests/test_thinking_budget_engine.py index 5b0e929..c116c88 100644 --- a/engine/tests/test_thinking_budget_engine.py +++ b/engine/tests/test_thinking_budget_engine.py @@ -103,7 +103,10 @@ def build(monkeypatch, stream, first=FIRST_TEXT): engine.sampler = engine.resident_sampler = RecordingSampler(stream) engine.session = stub_session(first) engine.runtime = SimpleNamespace( - make_session=lambda max_new_tokens: engine.session, prefill_chunk_size=10**9, metadata=[] + make_session=lambda max_new_tokens: engine.session, + prefill_chunk_size=10**9, + metadata=[], + device_profile=SimpleNamespace(chip="x"), ) engine._prompt_cache = engine.session.prompt_cache engine._prefix_preparation = None