Repository navigation
refactor(sandbox)!: forward any attribution key as sandbox tags instead of a fixed trio #49
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
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 |
|---|---|---|
|
|
@@ -253,11 +253,9 @@ async def create_container( | |
| effective = self.effective_network_policy(network_policy) | ||
| attribution = dict(attribution or {}) | ||
|
|
||
| # Modal billing breaks cost down by App tags; sandbox-level tags don't show up in billing. | ||
| app_tags = _build_cost_attribution_tags(attribution) | ||
| app_tags = _build_app_tags() | ||
| app_name = self._app_name | ||
| app = await self._get_app(app_name, app_tags) | ||
| # Not in billing (see above); makes sandboxes filterable by the step that deployed them. | ||
| sandbox_tags = _build_sandbox_tags(attribution) | ||
|
|
||
| image_store = get_config().get_image_store_at(image_name) | ||
|
|
@@ -401,33 +399,27 @@ async def get_sandbox(self, sandbox_id: str) -> Sandbox: | |
| return self._sandbox_cls(sb, tunnel_urls, i6pn_address=i6pn_address) | ||
|
|
||
|
|
||
| def _build_cost_attribution_tags(attribution: Attribution) -> dict[str, str]: | ||
| """Build the Modal App tag set used for cost attribution. | ||
| def _build_app_tags() -> dict[str, str]: | ||
| """Modal App tags: the deployment-wide config.toml ``[sandbox.attribution]`` defaults. | ||
|
|
||
| Unset dimensions fall back to config.toml ``[sandbox.attribution]``; a dimension with | ||
| no value anywhere is omitted rather than emitted as a null tag. Returns a flat | ||
| ``dict[str, str]`` for ``modal.App.set_tags``. (priority is a scheduling concern, | ||
| not attribution, so it is not included here.) | ||
| """ | ||
| resolved = apply_default_attribution(attribution) | ||
| return { | ||
| name: resolved[name] | ||
| for name in ("product", "customer", "team") | ||
| if resolved.get(name) is not None | ||
| } | ||
| Modal's billing report breaks cost down by App tags, and one App serves every run with its | ||
| tags set on first lookup, so per-run attribution goes on sandbox tags instead.""" | ||
| return _modal_tags(apply_default_attribution({})) | ||
|
|
||
|
|
||
| def _build_sandbox_tags(attribution: Attribution) -> dict[str, str]: | ||
| """Build the Modal Sandbox tag set: ``pipeline_step`` and ``run_id``, each when set. | ||
| """Modal Sandbox tags: the run's attribution, unset keys filled from ``[sandbox.attribution]``, | ||
| including ``pipeline_step`` and ``run_id`` when the deploy step set them.""" | ||
| return _modal_tags(apply_default_attribution(attribution)) | ||
|
|
||
| Modal rejects the whole create on an invalid tag, so values are sanitized to | ||
| ``[a-zA-Z0-9._-]``; past Modal's 63-char limit a value keeps a prefix plus a hash of the | ||
| full value, so steps of a long-named task still get distinct tags. | ||
| """ | ||
|
|
||
| def _modal_tags(attribution: Attribution) -> dict[str, str]: | ||
| """Modal rejects a whole create on one invalid tag: keys and values are made Modal-valid and a | ||
| value that is unset or empty is left out, since Modal has no empty tag.""" | ||
| return { | ||
| key: _modal_tag_value(attribution[key]) | ||
| for key in (PIPELINE_STEP_KEY, RUN_ID_KEY) | ||
| if attribution.get(key) | ||
| _modal_tag_value(key): _modal_tag_value(str(value)) | ||
|
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/modal_sandbox.py
Line: 420
Comment:
**Modal drops an attribution label**
If a run has both `billing team` and `billing/team`, `_modal_tag_value` turns both keys into `billing-team`. The later value silently replaces the earlier one, so the run loses a label. Reject keys that collide or give them distinct tag names.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| for key, value in attribution.items() | ||
| if value is not None and str(value) != "" | ||
|
Comment on lines
+420
to
+422
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/modal_sandbox.py
Line: 420-422
Comment:
**Empty key stops a sandbox**
If a step supplies `{"": "value"}`, `_modal_tags` keeps the empty key because it filters only empty values. The key then reaches Modal even though the tag format requires at least one character, so Modal can reject sandbox creation. Reject empty keys before sending tags.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
|
|
||
|
|
||
|
|
@@ -457,7 +449,8 @@ async def _log_sandbox_started( | |
| "modal_sandbox_id": sb.object_id, | ||
| "modal_container_id": container_id, | ||
| "modal_app_name": app_name, | ||
| **sandbox_tags, | ||
| "modal_sandbox_tags": sandbox_tags, | ||
| **{key: sandbox_tags[key] for key in (PIPELINE_STEP_KEY, RUN_ID_KEY) if key in sandbox_tags}, | ||
| "cpu": cpu, | ||
| "memory_mb": memory, | ||
| "gpu": gpu, | ||
|
|
@@ -466,6 +459,8 @@ async def _log_sandbox_started( | |
|
|
||
|
|
||
| def _modal_tag_value(value: str) -> str: | ||
| """``value`` within Modal's ``[a-zA-Z0-9._-]{1,63}``: other characters become ``-``, and past | ||
| 63 characters a prefix plus a hash of the full value keeps long values distinct.""" | ||
| slug = re.sub(r"[^a-zA-Z0-9._-]", "-", value) | ||
| if len(slug) > _MODAL_TAG_MAX_LEN: | ||
| digest = hashlib.sha256(value.encode()).hexdigest()[:8] | ||
|
|
||
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.
When
[sandbox.attribution]has no defaults,_build_app_tagsreturns{}. Both Modal providers skipset_tagsfor an empty dictionary, so an existing App keeps tags from earlier runs or an earlier configuration. After an upgrade or removal of defaults, Modal can keep grouping billing under those old labels. Clear the App's tags when the configured set is empty.Prompt To Fix With AI