commit a09be2b0d1e862d276eec5158572f96be42bd635
parent d1b66559f0fb9f51993ae88cf15bbf63a628b747
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 16:06:02 -0700
provider: port max local adapter source
- add pinned flare Git dependency through Pixi
- reintroduce max_local provider config, client, health, schema, and parser modules
- map provider config from strict runtime TOML instead of environment variables
- add adapter tests for config, request body, and response parsing
Diffstat:
10 files changed, 677 insertions(+), 1 deletion(-)
diff --git a/pixi.lock b/pixi.lock
@@ -61,6 +61,7 @@ environments:
- conda: https://conda.modular.com/max-nightly/linux-64/mojo-compiler-1.0.0b1-release.conda
- conda: https://conda.modular.com/max-nightly/noarch/mblack-26.3.0-release.conda
- conda: https://conda.modular.com/max-nightly/noarch/mojo-python-1.0.0b1-release.conda
+ - conda_source: flare[373ea143] @ git+https://github.com/triesap/mojo_flare?rev=14aa338d8c4f352ebb2d590cddfde0bde5ea1971#14aa338d8c4f352ebb2d590cddfde0bde5ea1971
- conda_source: json[9dd23c1f] @ git+https://github.com/triesap/mojo_json?rev=56025d73631054d7803143fa5e68c5de578e33f5#56025d73631054d7803143fa5e68c5de578e33f5
- conda_source: morph[39ce1c7e] @ git+https://github.com/triesap/mojo_morph?rev=ea73f909990f3d3f5d12b786fec2f09e1e09f462#ea73f909990f3d3f5d12b786fec2f09e1e09f462
osx-arm64:
@@ -109,6 +110,7 @@ environments:
- conda: https://conda.modular.com/max-nightly/noarch/mojo-python-1.0.0b1-release.conda
- conda: https://conda.modular.com/max-nightly/osx-arm64/mojo-1.0.0b1-release.conda
- conda: https://conda.modular.com/max-nightly/osx-arm64/mojo-compiler-1.0.0b1-release.conda
+ - conda_source: flare[c903ec05] @ git+https://github.com/triesap/mojo_flare?rev=14aa338d8c4f352ebb2d590cddfde0bde5ea1971#14aa338d8c4f352ebb2d590cddfde0bde5ea1971
- conda_source: json[1efb775d] @ git+https://github.com/triesap/mojo_json?rev=56025d73631054d7803143fa5e68c5de578e33f5#56025d73631054d7803143fa5e68c5de578e33f5
- conda_source: morph[70142729] @ git+https://github.com/triesap/mojo_morph?rev=ea73f909990f3d3f5d12b786fec2f09e1e09f462#ea73f909990f3d3f5d12b786fec2f09e1e09f462
packages:
@@ -1812,6 +1814,102 @@ packages:
run_exports: {}
size: 67503416
timestamp: 1777596016502
+- conda_source: flare[373ea143] @ git+https://github.com/triesap/mojo_flare?rev=14aa338d8c4f352ebb2d590cddfde0bde5ea1971#14aa338d8c4f352ebb2d590cddfde0bde5ea1971
+ version: 0.8.0
+ build: hb0f4dca_0
+ subdir: linux-64
+ variants:
+ target_platform: linux-64
+ depends:
+ - mojo ==1.0.0b1
+ - json >=0.2.0,<0.3.0
+ - openssl >=3.0,<4
+ - openssl >=3.6.3,<4.0a0
+ - ca-certificates
+ - libstdcxx >=15
+ - libgcc >=15
+ license: MIT
+ build_packages:
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45.1-default_hfdba357_102.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.45.1-default_h4852527_102.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-he0086c7_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.2.0-h7be306e_27.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.2.0-hda75c37_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.2.0-hcb00b6d_27.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-h90f66d4_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_119.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.2.0-hd446a21_119.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ host_packages:
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
+- conda_source: flare[c903ec05] @ git+https://github.com/triesap/mojo_flare?rev=14aa338d8c4f352ebb2d590cddfde0bde5ea1971#14aa338d8c4f352ebb2d590cddfde0bde5ea1971
+ version: 0.8.0
+ build: h60d57d3_0
+ subdir: osx-arm64
+ variants:
+ target_platform: osx-arm64
+ depends:
+ - mojo ==1.0.0b1
+ - json >=0.2.0,<0.3.0
+ - openssl >=3.0,<4
+ - openssl >=3.6.3,<4.0a0
+ - ca-certificates
+ - libcxx >=22
+ license: MIT
+ build_packages:
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt22_osx-arm64-22.1.7-h7e67a1e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-22.1.7-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/libcxx-headers-22.1.7-h707e725_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/sdkroot_env_osx-arm64-26.0-ha3f98da_7.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_impl_osx-arm64-1030.6.3-llvm22_1_hb5e89dc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1030.6.3-llvm22_1_hbe26303_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-22-22.1.7-default_hd632d02_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-scan-deps-22.1.7-default_h8e162e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-22.1.7-default_h17d1ed9_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-22.1.7-hf119d2b_32.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-22.1.7-default_h17d1ed9_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-22.1.7-hf119d2b_32.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-22.1.7-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt22-22.1.7-hd34ed20_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm22_1_h692d5aa_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp22.1-22.1.7-default_h8e162e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-22.1.7-default_h6dd9417_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcompiler-rt-22.1.7-hd34ed20_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.7-h55c6f16_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-22.1.7-h6dc3340_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm22-22.1.7-h89af1be_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22-22.1.7-hb545844_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22.1.7-hd34ed20_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-h997e182_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
+ host_packages:
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.7-h55c6f16_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda
- conda_source: json[1efb775d] @ git+https://github.com/triesap/mojo_json?rev=56025d73631054d7803143fa5e68c5de578e33f5#56025d73631054d7803143fa5e68c5de578e33f5
version: 0.2.0
build: h60d57d3_0
diff --git a/pixi.toml b/pixi.toml
@@ -15,6 +15,7 @@ preview = ["pixi-build"]
mojo = "==1.0.0b1"
json = { git = "https://github.com/triesap/mojo_json.git", rev = "56025d73631054d7803143fa5e68c5de578e33f5" }
morph = { git = "https://github.com/triesap/mojo_morph.git", rev = "ea73f909990f3d3f5d12b786fec2f09e1e09f462" }
+flare = { git = "https://github.com/triesap/mojo_flare.git", rev = "14aa338d8c4f352ebb2d590cddfde0bde5ea1971" }
[tasks]
run = "mojo run -I src src/main.mojo"
@@ -22,9 +23,14 @@ test-unit = "mojo -I src tests/test_hyf.mojo"
test-runtime = "mojo -I src tests/test_runtime_paths.mojo"
test-repo-local-process = "mojo -I src tests/test_repo_local_process_contract.mojo"
test-stdio = "mojo -I src tests/test_stdio_contract.mojo"
+test-provider-adapter = "mojo -I src tests/test_provider_adapter.mojo"
test-runtime-contract = { depends-on = [
"test-runtime",
"test-repo-local-process",
"test-stdio",
] }
-test = { depends-on = ["test-unit", "test-runtime-contract"] }
+test = { depends-on = [
+ "test-unit",
+ "test-provider-adapter",
+ "test-runtime-contract",
+] }
diff --git a/src/hyf_provider/__init__.mojo b/src/hyf_provider/__init__.mojo
diff --git a/src/hyf_provider/client.mojo b/src/hyf_provider/client.mojo
@@ -0,0 +1,17 @@
+from flare.http import HttpClient
+
+from hyf_provider.config import MaxLocalProviderConfig
+
+
+def _trim_trailing_slash(url: String) -> String:
+ if url.endswith("/") and url.byte_length() > 1:
+ return String(url[byte = 0 : url.byte_length() - 1])
+ return String(url)
+
+
+def make_max_local_http_client(config: MaxLocalProviderConfig) -> HttpClient:
+ return HttpClient(timeout_ms=config.request_timeout_ms)
+
+
+def max_local_chat_completions_url(config: MaxLocalProviderConfig) -> String:
+ return _trim_trailing_slash(config.base_url) + "/chat/completions"
diff --git a/src/hyf_provider/config.mojo b/src/hyf_provider/config.mojo
@@ -0,0 +1,29 @@
+from hyf_runtime.config import (
+ HyfLoadedRuntimeConfig,
+ max_local_provider_configured,
+)
+
+
+@fieldwise_init
+struct MaxLocalProviderConfig(Copyable, Movable):
+ var base_url: String
+ var health_url: String
+ var model: String
+ var route: String
+ var request_timeout_ms: Int
+
+
+def max_local_provider_config_from_runtime(
+ config: HyfLoadedRuntimeConfig,
+) raises -> MaxLocalProviderConfig:
+ if not max_local_provider_configured(config):
+ raise Error("max_local provider runtime is not configured")
+
+ var source = config.effective.assisted.max_local.copy()
+ return MaxLocalProviderConfig(
+ base_url=String(source.base_url),
+ health_url=String(source.health_url),
+ model=String(source.model),
+ route=String(source.route),
+ request_timeout_ms=source.request_timeout_ms,
+ )
diff --git a/src/hyf_provider/health.mojo b/src/hyf_provider/health.mojo
@@ -0,0 +1,47 @@
+from hyf_provider.client import make_max_local_http_client
+from hyf_provider.config import MaxLocalProviderConfig
+from hyf_provider.result import MaxLocalProviderStatus
+
+
+def _provider_status(
+ config: MaxLocalProviderConfig,
+ reachable: Bool,
+ state: String,
+ reason: String,
+) -> MaxLocalProviderStatus:
+ return MaxLocalProviderStatus(
+ backend_kind="max_local",
+ provider="max_local",
+ route=String(config.route),
+ model=String(config.model),
+ reachable=reachable,
+ state=String(state),
+ reason=String(reason),
+ )
+
+
+def _classify_health_error(message: String) -> String:
+ var lower = message.lower()
+ if lower.find("timeout") >= 0 or lower.find("timed out") >= 0:
+ return "timeout"
+ if lower.find("url") >= 0 or lower.find("scheme") >= 0:
+ return "invalid_url"
+ return "connection_failed"
+
+
+def resolve_max_local_provider_status(
+ config: MaxLocalProviderConfig,
+) -> MaxLocalProviderStatus:
+ try:
+ with make_max_local_http_client(config) as client:
+ var response = client.get(config.health_url)
+ if response.ok():
+ return _provider_status(config, True, "ready", "ready")
+ return _provider_status(config, False, "unavailable", "non_2xx")
+ except e:
+ return _provider_status(
+ config,
+ False,
+ "unavailable",
+ _classify_health_error(String(e)),
+ )
diff --git a/src/hyf_provider/max_local.mojo b/src/hyf_provider/max_local.mojo
@@ -0,0 +1,61 @@
+from hyf_core.capabilities.query_analysis import QueryAnalysis
+from hyf_core.request_context import RequestContext
+from hyf_provider.client import (
+ make_max_local_http_client,
+ max_local_chat_completions_url,
+)
+from hyf_provider.config import MaxLocalProviderConfig
+from hyf_provider.health import resolve_max_local_provider_status
+from hyf_provider.result import (
+ MaxLocalProviderStatus,
+ parse_query_analysis_from_chat_completion,
+)
+from hyf_provider.schema import (
+ build_query_rewrite_request_body,
+ query_rewrite_prompt_version,
+ query_rewrite_schema_version,
+)
+
+
+@fieldwise_init
+struct MaxLocalQueryRewriteResult(Copyable, Movable):
+ var analysis: QueryAnalysis
+ var provider: String
+ var route: String
+ var model: String
+ var latency_ms: Int
+ var schema_version: Int
+ var prompt_version: String
+
+
+def execute_query_rewrite_via_max_local_provider(
+ config: MaxLocalProviderConfig, text: String, context: RequestContext
+) raises -> MaxLocalQueryRewriteResult:
+ with make_max_local_http_client(config) as client:
+ var response = client.post(
+ max_local_chat_completions_url(config),
+ build_query_rewrite_request_body(config, text, context),
+ )
+ if not response.ok():
+ raise Error(
+ "max_local provider returned HTTP "
+ + String(response.status)
+ )
+
+ return MaxLocalQueryRewriteResult(
+ analysis=parse_query_analysis_from_chat_completion(
+ response.json()
+ ),
+ provider="max_local",
+ route=String(config.route),
+ model=String(config.model),
+ latency_ms=0,
+ schema_version=query_rewrite_schema_version(),
+ prompt_version=query_rewrite_prompt_version(),
+ )
+
+
+def max_local_provider_status(
+ config: MaxLocalProviderConfig,
+) -> MaxLocalProviderStatus:
+ return resolve_max_local_provider_status(config)
diff --git a/src/hyf_provider/result.mojo b/src/hyf_provider/result.mojo
@@ -0,0 +1,121 @@
+from std.collections import List
+
+from json import Value, loads, validate
+
+from hyf_core.capabilities.query_analysis import (
+ ExtractedFilters,
+ QueryAnalysis,
+)
+from hyf_provider.schema import query_rewrite_schema
+
+
+@fieldwise_init
+struct MaxLocalProviderStatus(Copyable, Movable):
+ var backend_kind: String
+ var provider: String
+ var route: String
+ var model: String
+ var reachable: Bool
+ var state: String
+ var reason: String
+
+
+def _has_key(value: Value, key: String) -> Bool:
+ for candidate in value.object_keys():
+ if candidate == key:
+ return True
+ return False
+
+
+def _string_array(value: Value, context: String) raises -> List[String]:
+ if not value.is_array():
+ raise Error(context + " must be an array")
+
+ var items = List[String]()
+ for item in value.array_items():
+ if not item.is_string():
+ raise Error(context + " items must be strings")
+ items.append(item.string_value())
+ return items^
+
+
+def _first_validation_error(value: Value) raises -> String:
+ var validation = validate(value, query_rewrite_schema())
+ if validation.valid:
+ return ""
+ if len(validation.errors) == 0:
+ return "query_rewrite structured output failed schema validation"
+ var error = validation.errors[0].copy()
+ if error.path == "":
+ return String(error.message)
+ return String(error.path) + ": " + String(error.message)
+
+
+def extract_chat_completion_text(response: Value) raises -> String:
+ if not response.is_object():
+ raise Error("max_local response must be a JSON object")
+ if not _has_key(response, "choices"):
+ raise Error("max_local response must contain choices")
+ if (
+ not response["choices"].is_array()
+ or len(response["choices"].array_items()) == 0
+ ):
+ raise Error("max_local response choices must be a non-empty array")
+
+ var message = response["choices"][0]["message"].clone()
+ if not message.is_object():
+ raise Error("max_local response choice message must be an object")
+
+ var content = message["content"].clone()
+ if content.is_string():
+ return content.string_value()
+
+ if content.is_array():
+ var collected = String("")
+ for part in content.array_items():
+ if (
+ part.is_object()
+ and _has_key(part, "type")
+ and part["type"].is_string()
+ and part["type"].string_value() == "text"
+ and _has_key(part, "text")
+ and part["text"].is_string()
+ ):
+ collected += part["text"].string_value()
+
+ if collected != "":
+ return collected^
+
+ raise Error("max_local response contained no text content")
+
+
+def parse_query_analysis_json(value: Value) raises -> QueryAnalysis:
+ if not value.is_object():
+ raise Error("query_rewrite structured output must be an object")
+
+ var validation_error = _first_validation_error(value.clone())
+ if validation_error != "":
+ raise Error(validation_error)
+
+ var filters = value["extracted_filters"].clone()
+ return QueryAnalysis(
+ original_text=value["original_text"].string_value(),
+ normalized_text=value["normalized_text"].string_value(),
+ rewritten_text=value["rewritten_text"].string_value(),
+ query_terms=_string_array(value["query_terms"], "query_terms"),
+ normalization_signals=_string_array(
+ value["normalization_signals"], "normalization_signals"
+ ),
+ ranking_hints=_string_array(value["ranking_hints"], "ranking_hints"),
+ extracted_filters=ExtractedFilters(
+ local_intent=filters["local_intent"].bool_value(),
+ fulfillment=filters["fulfillment"].string_value(),
+ time_window=filters["time_window"].string_value(),
+ ),
+ )
+
+
+def parse_query_analysis_from_chat_completion(
+ response: Value,
+) raises -> QueryAnalysis:
+ return parse_query_analysis_json(loads(extract_chat_completion_text(response)))
diff --git a/src/hyf_provider/schema.mojo b/src/hyf_provider/schema.mojo
@@ -0,0 +1,133 @@
+from json import Value, loads
+
+from hyf_core.request_context import RequestContext
+from hyf_provider.config import MaxLocalProviderConfig
+
+
+def query_rewrite_schema_version() -> Int:
+ return 1
+
+
+def query_rewrite_prompt_version() -> String:
+ return "max_local_query_rewrite_v1"
+
+
+def query_rewrite_schema() raises -> Value:
+ var schema = loads("{}")
+ schema.set("type", Value("object"))
+ schema.set("additionalProperties", Value(False))
+
+ var required = loads("[]")
+ required.append(Value("original_text"))
+ required.append(Value("normalized_text"))
+ required.append(Value("rewritten_text"))
+ required.append(Value("query_terms"))
+ required.append(Value("normalization_signals"))
+ required.append(Value("ranking_hints"))
+ required.append(Value("extracted_filters"))
+ schema.set("required", required)
+
+ var properties = loads("{}")
+ properties.set("original_text", loads('{"type":"string"}'))
+ properties.set("normalized_text", loads('{"type":"string"}'))
+ properties.set("rewritten_text", loads('{"type":"string"}'))
+ properties.set(
+ "query_terms",
+ loads('{"type":"array","items":{"type":"string"}}'),
+ )
+ properties.set(
+ "normalization_signals",
+ loads('{"type":"array","items":{"type":"string"}}'),
+ )
+ properties.set(
+ "ranking_hints",
+ loads('{"type":"array","items":{"type":"string"}}'),
+ )
+ properties.set(
+ "extracted_filters",
+ loads(
+ '{"type":"object","additionalProperties":false,"required":["local_intent","fulfillment","time_window"],"properties":{"local_intent":{"type":"boolean"},"fulfillment":{"type":"string"},"time_window":{"type":"string"}}}'
+ ),
+ )
+ schema.set("properties", properties)
+ return schema^
+
+
+def query_rewrite_system_prompt() -> String:
+ return (
+ "Return only strict JSON matching the supplied schema. Preserve "
+ + "original_text, normalized_text, rewritten_text, query_terms, "
+ + "normalization_signals, ranking_hints, and extracted_filters."
+ )
+
+
+def build_query_rewrite_user_prompt(
+ text: String, context: RequestContext
+) -> String:
+ var prompt = (
+ "Rewrite the market search query into normalized search terms and "
+ + "extracted filters.\nquery: "
+ + text
+ + "\n"
+ )
+
+ if context.scope and len(context.scope.value().listing_ids) > 0:
+ var first = True
+ prompt += "scope_listing_ids: "
+ for listing_id in context.scope.value().listing_ids:
+ if not first:
+ prompt += ","
+ prompt += String(listing_id)
+ first = False
+ prompt += "\n"
+
+ if context.time_range:
+ prompt += (
+ "time_range: "
+ + context.time_range.value().start
+ + " -> "
+ + context.time_range.value().end
+ + "\n"
+ )
+
+ prompt += "consistency: " + context.consistency + "\n"
+ prompt += "evidence_limit: " + String(context.evidence_limit) + "\n"
+ prompt += "explain_plan: " + String(context.explain_plan) + "\n"
+ return prompt^
+
+
+def build_query_rewrite_request_body(
+ config: MaxLocalProviderConfig, text: String, context: RequestContext
+) raises -> Value:
+ var body = loads("{}")
+ body.set("model", Value(String(config.model)))
+
+ var messages = loads("[]")
+
+ var system_message = loads("{}")
+ system_message.set("role", Value("system"))
+ system_message.set("content", Value(query_rewrite_system_prompt()))
+ messages.append(system_message)
+
+ var user_message = loads("{}")
+ user_message.set("role", Value("user"))
+ user_message.set(
+ "content", Value(build_query_rewrite_user_prompt(text, context))
+ )
+ messages.append(user_message)
+
+ body.set("messages", messages)
+ body.set("temperature", Value(0.1))
+ body.set("max_tokens", Value(256))
+
+ var response_format = loads("{}")
+ response_format.set("type", Value("json_schema"))
+
+ var json_schema = loads("{}")
+ json_schema.set("name", Value("query_rewrite"))
+ json_schema.set("strict", Value(True))
+ json_schema.set("schema", query_rewrite_schema())
+ response_format.set("json_schema", json_schema)
+
+ body.set("response_format", response_format)
+ return body^
diff --git a/tests/test_provider_adapter.mojo b/tests/test_provider_adapter.mojo
@@ -0,0 +1,164 @@
+from std.testing import TestSuite, assert_equal, assert_raises, assert_true
+
+from json import Value, loads
+
+from hyf_core.request_context import default_request_context
+from hyf_provider.client import max_local_chat_completions_url
+from hyf_provider.config import (
+ MaxLocalProviderConfig,
+ max_local_provider_config_from_runtime,
+)
+from hyf_provider.result import parse_query_analysis_from_chat_completion
+from hyf_provider.schema import build_query_rewrite_request_body
+from hyf_runtime.config import (
+ HyfAssistedRuntimeConfig,
+ HyfExecutionRuntimeConfig,
+ HyfLoadedRuntimeConfig,
+ HyfMaxLocalProviderRuntimeConfig,
+ HyfRuntimeConfig,
+ HyfServiceRuntimeConfig,
+ default_loaded_runtime_config,
+)
+
+
+def _provider_runtime_config() -> HyfLoadedRuntimeConfig:
+ return HyfLoadedRuntimeConfig(
+ artifact_present=True,
+ loaded=True,
+ compiled_defaults_active=False,
+ load_state="loaded",
+ load_error="",
+ effective=HyfRuntimeConfig(
+ service=HyfServiceRuntimeConfig(transport="stdio"),
+ runtime=HyfExecutionRuntimeConfig(
+ default_execution_mode="deterministic",
+ allow_assisted=True,
+ ),
+ assisted=HyfAssistedRuntimeConfig(
+ provider="max_local",
+ max_local=HyfMaxLocalProviderRuntimeConfig(
+ enabled=True,
+ base_url="http://127.0.0.1:8000/v1/",
+ health_url="http://127.0.0.1:8000/health",
+ model="max-local-query-rewrite",
+ route="provider_runtime.query_rewrite.max_local",
+ request_timeout_ms=15000,
+ ),
+ ),
+ ),
+ )
+
+
+def _provider_config() -> MaxLocalProviderConfig:
+ return MaxLocalProviderConfig(
+ base_url="http://127.0.0.1:8000/v1/",
+ health_url="http://127.0.0.1:8000/health",
+ model="max-local-query-rewrite",
+ route="provider_runtime.query_rewrite.max_local",
+ request_timeout_ms=15000,
+ )
+
+
+def _analysis_json_text() -> String:
+ return (
+ '{"original_text":"eggs near me",'
+ '"normalized_text":"eggs near me",'
+ '"rewritten_text":"eggs",'
+ '"query_terms":["eggs"],'
+ '"normalization_signals":["local_intent_detected"],'
+ '"ranking_hints":["prefer_local_results"],'
+ '"extracted_filters":{'
+ '"local_intent":true,'
+ '"fulfillment":"unspecified",'
+ '"time_window":"unspecified"'
+ "}}"
+ )
+
+
+def _chat_completion_response() raises -> Value:
+ var response = loads("{}")
+ var choices = loads("[]")
+ var choice = loads("{}")
+ var message = loads("{}")
+ message.set("content", Value(_analysis_json_text()))
+ choice.set("message", message)
+ choices.append(choice)
+ response.set("choices", choices)
+ return response^
+
+
+def test_provider_config_maps_runtime_config() raises:
+ var config = max_local_provider_config_from_runtime(
+ _provider_runtime_config()
+ )
+
+ assert_equal(config.base_url, "http://127.0.0.1:8000/v1/")
+ assert_equal(config.health_url, "http://127.0.0.1:8000/health")
+ assert_equal(config.model, "max-local-query-rewrite")
+ assert_equal(config.route, "provider_runtime.query_rewrite.max_local")
+ assert_equal(config.request_timeout_ms, 15000)
+
+
+def test_provider_config_rejects_unconfigured_runtime() raises:
+ with assert_raises():
+ _ = max_local_provider_config_from_runtime(
+ default_loaded_runtime_config()
+ )
+
+
+def test_max_local_chat_completions_url_trims_base_url() raises:
+ assert_equal(
+ max_local_chat_completions_url(_provider_config()),
+ "http://127.0.0.1:8000/v1/chat/completions",
+ )
+
+
+def test_query_rewrite_request_body_sets_schema_contract() raises:
+ var context = default_request_context()
+ context.return_provenance = True
+ var body = build_query_rewrite_request_body(
+ _provider_config(), "eggs near me", context
+ )
+
+ assert_equal(body["model"].string_value(), "max-local-query-rewrite")
+ assert_equal(body["messages"][0]["role"].string_value(), "system")
+ assert_equal(body["messages"][1]["role"].string_value(), "user")
+ assert_true(
+ body["messages"][1]["content"].string_value().find("eggs near me")
+ >= 0
+ )
+ assert_equal(body["response_format"]["type"].string_value(), "json_schema")
+ assert_equal(
+ body["response_format"]["json_schema"]["name"].string_value(),
+ "query_rewrite",
+ )
+ assert_equal(
+ body["response_format"]["json_schema"]["strict"].bool_value(), True
+ )
+ assert_equal(
+ body["response_format"]["json_schema"]["schema"]["type"]
+ .string_value(),
+ "object",
+ )
+
+
+def test_chat_completion_response_parses_query_analysis() raises:
+ var analysis = parse_query_analysis_from_chat_completion(
+ _chat_completion_response()
+ )
+
+ assert_equal(analysis.original_text, "eggs near me")
+ assert_equal(analysis.normalized_text, "eggs near me")
+ assert_equal(analysis.rewritten_text, "eggs")
+ assert_equal(len(analysis.query_terms), 1)
+ assert_equal(analysis.query_terms[0], "eggs")
+ assert_equal(analysis.extracted_filters.local_intent, True)
+
+
+def test_chat_completion_response_rejects_empty_choices() raises:
+ with assert_raises():
+ _ = parse_query_analysis_from_chat_completion(loads('{"choices":[]}'))
+
+
+def main() raises:
+ TestSuite.discover_tests[__functions_in_module()]().run()