Skip to content

[Enhancement] Add optional worker and model filters to sys_list_models #3130

Description

@ptr-syrto

Problem

sys_list_models currently takes no parameters and returns every model exposed
by every declared worker's provider.

An orchestrator commonly needs to validate only a small set of configured
worker/model combinations before dispatch. When a provider exposes a large
catalogue, such as OpenRouter, the complete response can be unnecessarily large
and remains in conversation history for later model calls.

Reproduction

An agent with six declared workers needed to validate seven configured model
IDs.

Measured serialized response sizes:

Request Response size
Current unfiltered response 30,618 characters
Required workers and model IDs only 1,870 characters

The filtered response was approximately 94% smaller while retaining all
information required for dispatch preflight.

Example relevant IDs:

  • xiaomi/mimo-v2.5-pro
  • z-ai/glm-5.2
  • moonshotai/kimi-k3
  • gpt-5.6-sol
  • gpt-5.6-terra
  • claude-sonnet-5
  • claude-opus-4-8

Proposed API

Add two optional parameters:

{
  "workers": [
    "pi",
    "codex",
    "codex_deep",
    "codex_fast",
    "claude_code",
    "claude_deep"
  ],
  "model_ids": [
    "xiaomi/mimo-v2.5-pro",
    "z-ai/glm-5.2",
    "moonshotai/kimi-k3",
    "gpt-5.6-sol",
    "gpt-5.6-terra",
    "claude-sonnet-5",
    "claude-opus-4-8"
  ]
}

Semantics:

  • workers limits the returned worker rows.
  • model_ids retains only exact matching IDs in each selected row.
  • A selected worker with no matching models remains present with models: [],
    allowing callers to detect unavailable configured models.
  • Omitting either filter leaves that dimension unfiltered.
  • Calling sys_list_models({}) preserves the existing complete response.

Backward compatibility

The existing schema and response remain valid when the optional filters are
omitted:

{}

Fields such as source, verified, and note remain unchanged. Filtering
only affects which worker rows and model entries are returned.

Proposed implementation

A shared filtering helper can keep the in-process and runner-local paths
consistent.

omnigent/tools/builtins/list_models.py

Extend the tool schema:

"parameters": {
    "type": "object",
    "properties": {
        "workers": {
            "type": "array",
            "items": {"type": "string"},
            "uniqueItems": True,
            "description": (
                "Optional worker names to return. "
                "Omit to return every worker."
            ),
        },
        "model_ids": {
            "type": "array",
            "items": {"type": "string"},
            "uniqueItems": True,
            "description": (
                "Optional exact model IDs to retain in each row. "
                "Rows without a match remain present with models: []."
            ),
        },
    },
    "required": [],
    "additionalProperties": False,
},

Add a shared filtering function:

def filter_model_catalog(
    catalog: dict[str, Any],
    arguments: dict[str, Any],
) -> dict[str, Any]:
    """Return a worker/model-filtered copy of a model catalogue."""
    workers = arguments.get("workers")
    worker_names = set(workers) if isinstance(workers, list) else None

    model_ids = arguments.get("model_ids")
    selected_models = set(model_ids) if isinstance(model_ids, list) else None

    filtered: dict[str, Any] = {}

    for worker, raw_row in catalog.items():
        if worker_names is not None and worker not in worker_names:
            continue

        if not isinstance(raw_row, dict):
            filtered[worker] = raw_row
            continue

        row = dict(raw_row)

        if selected_models is not None and isinstance(row.get("models"), list):
            row["models"] = [
                model
                for model in row["models"]
                if isinstance(model, dict)
                and model.get("id") in selected_models
            ]

        filtered[worker] = row

    return filtered

Use it in the in-process path:

def invoke(self, arguments: str, ctx: ToolContext) -> str:
    del ctx

    from omnigent.model_catalog import catalog_for_spec

    try:
        args = json.loads(arguments or "{}")
    except json.JSONDecodeError as exc:
        return f"Error: sys_list_models invalid JSON arguments: {exc}"

    if not isinstance(args, dict):
        return "Error: sys_list_models arguments must be a JSON object"

    catalog = catalog_for_spec(self._spec)
    return json.dumps(filter_model_catalog(catalog, args))

omnigent/runner/tool_dispatch.py

Pass the tool arguments through the runner-local path:

async def _execute_list_models_tool(
    args: dict[str, Any],
    *,
    agent_spec: Any | None,
) -> str:
    if agent_spec is None:
        return "Error: sys_list_models requires an agent spec"

    from omnigent.model_catalog import catalog_for_spec
    from omnigent.tools.builtins.list_models import filter_model_catalog

    catalog = await asyncio.to_thread(catalog_for_spec, agent_spec)
    return json.dumps(filter_model_catalog(catalog, args))

Update the dispatch branch:

elif tool_name in _LIST_MODELS_TOOLS:
    output = await _execute_list_models_tool(
        args,
        agent_spec=agent_spec,
    )

This preserves the current behavior for sys_list_models({}) and avoids
duplicating filtering logic between execution paths.

Acceptance criteria

  1. An unfiltered call returns the same complete catalogue as before.
  2. workers returns only the named worker rows.
  3. model_ids performs exact ID matching within each returned row.
  4. Selected workers with no matching models remain present with an empty
    models array.
  5. Unknown worker names do not cause unrelated rows to be returned.
  6. The in-process and runner-local paths behave identically.
  7. Tests cover unfiltered, worker-only, model-only, combined, and empty-match
    cases.

Metadata

Metadata

Assignees

Labels

P2-mediumPriority: bug with workaround, important feature requestcomp:runnerComponent: agent runner, execution enginecomp:serverComponent: server, API, session managementenhancementNew feature or requesthelp wantedExtra attention is neededtriagedIssue has been triaged by the bot

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions