Repository navigation
Add Mistral AI block (Mistral Large 4 via OpenRouter) (#3140) #3147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e137e8a
247e74c
de44a18
7e01cf6
8cb5202
418614b
40e6a39
fb5ab73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| __version__ = "1.7.3" | ||
| __version__ = "1.7.3-post1" | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| # keep in sync with workflows/pyproject.toml version | ||
| roboflow-workflows[enterprise]==0.2.4 | ||
| roboflow-workflows[enterprise]==0.2.4.post1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from inference.core.env import WORKFLOWS_MAX_CONCURRENT_STEPS | ||
| from inference.core.managers.base import ModelManager | ||
| from inference.core.workflows.core_steps.common.entities import StepExecutionMode | ||
| from inference.core.workflows.execution_engine.core import ExecutionEngine | ||
|
|
||
| OBJECT_DETECTION_WORKFLOW = { | ||
| "version": "1.0", | ||
| "inputs": [ | ||
| {"type": "WorkflowImage", "name": "image"}, | ||
| {"type": "WorkflowParameter", "name": "api_key"}, | ||
| {"type": "WorkflowParameter", "name": "classes"}, | ||
| ], | ||
| "steps": [ | ||
| { | ||
| "type": "roboflow_core/mistral_vlm@v1", | ||
| "name": "mistral", | ||
| "images": "$inputs.image", | ||
| "model_version": "Mistral Large 4", | ||
| "task_type": "object-detection", | ||
| "classes": "$inputs.classes", | ||
| "api_key": "$inputs.api_key", | ||
| }, | ||
| { | ||
| "type": "roboflow_core/bounding_box_visualization@v1", | ||
| "name": "visualization", | ||
| "image": "$inputs.image", | ||
| "predictions": "$steps.mistral.predictions", | ||
| }, | ||
| ], | ||
| "outputs": [ | ||
| { | ||
| "type": "JsonField", | ||
| "name": "mistral_result", | ||
| "selector": "$steps.mistral.output", | ||
| }, | ||
| { | ||
| "type": "JsonField", | ||
| "name": "predictions", | ||
| "selector": "$steps.mistral.predictions", | ||
| }, | ||
| { | ||
| "type": "JsonField", | ||
| "name": "visualization", | ||
| "selector": "$steps.visualization.image", | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| def test_object_detection_workflow_compiles(model_manager: ModelManager) -> None: | ||
| execution_engine = ExecutionEngine.init( | ||
| workflow_definition=OBJECT_DETECTION_WORKFLOW, | ||
| init_parameters={ | ||
| "workflows_core.model_manager": model_manager, | ||
| "workflows_core.step_execution_mode": StepExecutionMode.LOCAL, | ||
| }, | ||
| max_concurrent_steps=WORKFLOWS_MAX_CONCURRENT_STEPS, | ||
| ) | ||
| assert execution_engine is not None |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,6 +57,12 @@ | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| logger = get_logger(__name__) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Upper bound the Roboflow proxy accepts for `max_tokens` (observed live: | ||||||||||||||||||||||||||
| # "Parameter 'max_tokens' must be an integer in [1, 16384]"). Sent when a | ||||||||||||||||||||||||||
| # block leaves `max_tokens` unset, since omitting it makes the proxy apply | ||||||||||||||||||||||||||
| # its own 500-token default. | ||||||||||||||||||||||||||
| PROXY_MAX_TOKENS_CEILING = 16384 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
| # Privacy level | ||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
|
|
@@ -274,7 +280,7 @@ def execute_openrouter_batch( | |||||||||||||||||||||||||
| openrouter_api_key: str, | ||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||
| prompts: List[List[dict]], | ||||||||||||||||||||||||||
| max_tokens: int, | ||||||||||||||||||||||||||
| max_tokens: Optional[int], | ||||||||||||||||||||||||||
| temperature: Optional[float], | ||||||||||||||||||||||||||
| privacy_level: str, | ||||||||||||||||||||||||||
| max_concurrent_requests: Optional[int], | ||||||||||||||||||||||||||
|
|
@@ -310,7 +316,7 @@ def execute_openrouter_batch_with_usage( | |||||||||||||||||||||||||
| openrouter_api_key: str, | ||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||
| prompts: List[List[dict]], | ||||||||||||||||||||||||||
| max_tokens: int, | ||||||||||||||||||||||||||
| max_tokens: Optional[int], | ||||||||||||||||||||||||||
| temperature: Optional[float], | ||||||||||||||||||||||||||
| privacy_level: str, | ||||||||||||||||||||||||||
| max_concurrent_requests: Optional[int], | ||||||||||||||||||||||||||
|
|
@@ -325,7 +331,10 @@ def execute_openrouter_batch_with_usage( | |||||||||||||||||||||||||
| on the returned :class:`OpenRouterResult` objects. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ``temperature`` set to ``None`` omits the parameter so the provider | ||||||||||||||||||||||||||
| default applies. ``reasoning`` is an optional OpenRouter reasoning | ||||||||||||||||||||||||||
| default applies. ``max_tokens`` set to ``None`` does the same on the | ||||||||||||||||||||||||||
| direct path; the proxied path sends :data:`PROXY_MAX_TOKENS_CEILING` | ||||||||||||||||||||||||||
| instead because the proxy requires the field. | ||||||||||||||||||||||||||
| ``reasoning`` is an optional OpenRouter reasoning | ||||||||||||||||||||||||||
| config object (e.g. ``{"effort": "low"}`` or ``{"enabled": False}``) | ||||||||||||||||||||||||||
| forwarded verbatim; when the target model rejects the config, the | ||||||||||||||||||||||||||
| request is retried once without it. | ||||||||||||||||||||||||||
|
|
@@ -469,7 +478,7 @@ def _execute_proxied_openrouter_request( | |||||||||||||||||||||||||
| openrouter_api_key: str, | ||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||
| messages: List[dict], | ||||||||||||||||||||||||||
| max_tokens: int, | ||||||||||||||||||||||||||
| max_tokens: Optional[int], | ||||||||||||||||||||||||||
| temperature: Optional[float], | ||||||||||||||||||||||||||
| privacy_level: str, | ||||||||||||||||||||||||||
| reasoning: Optional[dict] = None, | ||||||||||||||||||||||||||
|
|
@@ -479,7 +488,11 @@ def _execute_proxied_openrouter_request( | |||||||||||||||||||||||||
| "openrouter_api_key": openrouter_api_key, | ||||||||||||||||||||||||||
| "model": model, | ||||||||||||||||||||||||||
| "messages": messages, | ||||||||||||||||||||||||||
| "max_tokens": max_tokens, | ||||||||||||||||||||||||||
| # The proxy requires the field (absent -> its own 500-token default, | ||||||||||||||||||||||||||
| # which truncates detection JSON) and rejects values above the cap. | ||||||||||||||||||||||||||
| "max_tokens": ( | ||||||||||||||||||||||||||
| max_tokens if max_tokens is not None else PROXY_MAX_TOKENS_CEILING | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
|
Comment on lines
+491
to
+495
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '460,510p' workflows/roboflow_workflows/core_steps/common/openrouter.py
sed -n '475,535p' workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm/v1.py
rg -n '16384|MAX_TOKENS|maximum.*token|max_tokens.*le=' workflows/roboflow_workflows/core_steps/common workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlmRepository: roboflow/inference Length of output: 5112 🏁 Script executed: set -o pipefail
printf '%s\n' '--- OpenRouter constants and execution declarations ---'
nl -ba workflows/roboflow_workflows/core_steps/common/openrouter.py | sed -n '40,75p;300,370p;430,515p;515,610p'
printf '%s\n' '--- Mistral manifest, validation, and run path ---'
rg -n -F --glob '*.py' -- 'class Mistral' workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm
rg -n -F --glob '*.py' -- 'get_manifest' workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm
rg -n -F --glob '*.py' -- 'execute_openrouter' workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm workflows/roboflow_workflows/core_steps
nl -ba workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm/v1.py | sed -n '1,140p;380,445p;490,590p;590,700p'
printf '%s\n' '--- max_tokens callers and proxy-related implementations ---'
rg -n -F --glob '*.py' -- 'max_tokens' workflows/roboflow_workflows/core_steps | sed -n '1,240p'
rg -n -i -F --glob '*.py' -- 'apiproxy/openrouter' . || true
rg -n -i --glob '*.py' -- 'openrouter.*max_tokens|max_tokens.*16384|must be an integer in \[1, 16384\]' . || true
printf '%s\n' '--- Mistral registration and tests ---'
rg -n -i -F --glob '*.py' -- 'mistral_vlm' workflows/roboflow_workflows | sed -n '1,180p'
rg -n -i -F --glob '*.py' -- 'max_tokens' workflows/tests tests 2>/dev/null | sed -n '1,220p' || trueRepository: roboflow/inference Length of output: 42179 🏁 Script executed: printf '%s\n' '--- Mistral manifest and run ---'
nl -ba workflows/roboflow_workflows/core_steps/models/foundation/mistral_vlm/v1.py | sed -n '640,735p'
printf '%s\n' '--- Mistral loader/registration ---'
rg -n -F --glob '*.py' -- 'MistralVlmBlockV1' workflows/roboflow_workflows tests workflows/tests
printf '%s\n' '--- Mistral max_tokens tests ---'
nl -ba workflows/tests/unit_tests/core_steps/models/foundation/test_mistral_vlm_v1.py | sed -n '90,205p;220,285p'
printf '%s\n' '--- OpenRouter proxy boundary tests ---'
rg -n -F --glob '*.py' -- '16384' tests/workflows/unit_tests/core_steps/common workflows/tests/unit_tests/core_steps/common
rg -n -F --glob '*.py' -- 'test_openrouter_max_tokens' tests workflows || true
nl -ba tests/workflows/unit_tests/core_steps/common/test_openrouter.py | sed -n '130,215p;280,345p'Repository: roboflow/inference Length of output: 19958 Cap The Mistral manifest accepts values above Suggested fix "max_tokens": (
- max_tokens if max_tokens is not None else PROXY_MAX_TOKENS_CEILING
+ PROXY_MAX_TOKENS_CEILING
+ if max_tokens is None
+ else min(max_tokens, PROXY_MAX_TOKENS_CEILING)
),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| "privacy_level": privacy_level, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if temperature is not None: | ||||||||||||||||||||||||||
|
|
@@ -558,7 +571,7 @@ def _execute_direct_openrouter_request( | |||||||||||||||||||||||||
| api_key: str, | ||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||
| messages: List[dict], | ||||||||||||||||||||||||||
| max_tokens: int, | ||||||||||||||||||||||||||
| max_tokens: Optional[int], | ||||||||||||||||||||||||||
| temperature: Optional[float], | ||||||||||||||||||||||||||
| privacy_level: str, | ||||||||||||||||||||||||||
| reasoning: Optional[dict] = None, | ||||||||||||||||||||||||||
|
|
@@ -574,8 +587,9 @@ def _execute_direct_openrouter_request( | |||||||||||||||||||||||||
| request_kwargs: Dict[str, Any] = { | ||||||||||||||||||||||||||
| "model": model, | ||||||||||||||||||||||||||
| "messages": messages, | ||||||||||||||||||||||||||
| "max_tokens": max_tokens, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if max_tokens is not None: | ||||||||||||||||||||||||||
| request_kwargs["max_tokens"] = max_tokens | ||||||||||||||||||||||||||
| if temperature is not None: | ||||||||||||||||||||||||||
| request_kwargs["temperature"] = temperature | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High — release PR leaves its changelog entries under
## UnreleasedThis PR bumps the package version (
workflows/pyproject.toml→0.2.4-post1, pinroboflow-workflows[enterprise]==0.2.4.post1,uv.lock→0.2.4.post1,inference/core/version.py→1.7.3-post1). But the three new entries stay under## Unreleasedand there is no release heading for them..cursor/rules/execution-engine-version-changelog.mdc(Maintainer release updates, steps 3–4) says a release must move theUnreleasedentries into the final version section, recordBundled execution engine: `X.Y.Z`.right under that heading, and leave a fresh, empty## Unreleased.What goes wrong: the
0.2.4.post1wheel would ship with a changelog that doesn't list the Mistral block, thexyxy_0_999format or themax_tokens=Noneexecutor change. The next release would then sweep these entries into its own section and credit them to the wrong version.Fix: something like
(
EXECUTION_ENGINE_V1_VERSIONis still1.16.1inworkflows/roboflow_workflows/execution_engine/v1/core.py, and this PR doesn't change engine behavior.) Spell the heading the same way the pin and lockfile do (0.2.4.post1).